254 Commits

Author SHA1 Message Date
858199e476 Modification of the weekmail template 2022-09-25 22:56:39 +02:00
f6ecbd899d Add of some modifications in the weekmail model 2022-09-25 22:55:39 +02:00
e868946fd7 Merge pull request #475 from imperosol/patch-1
update link for poetry install
2022-09-25 17:42:06 +02:00
254044c36b Merge pull request #474 from ae-utbm/lsacienne/change_banner_to_invitation
💄 Modification of banner
2022-09-25 17:26:48 +02:00
c695d6f7a0 update link for poetry install 2022-09-25 12:06:29 +02:00
feef855f01 💄 Modification of banner 2022-09-21 22:12:35 +02:00
b3a48ca5af Merge pull request #471 from ae-utbm/remove-gitlab-files
Added back the **git** .mailmap file
2022-09-11 23:54:03 +02:00
f3a52d094e Merge pull request #472 from ae-utbm/lsacienne/change_banner
💄 Modification of banner
2022-09-11 23:37:43 +02:00
2901bd919f 💄 Modification of banner 2022-09-11 23:17:29 +02:00
0396a5bf2b Added back the **git** .mailmap file 2022-09-09 13:59:01 +02:00
b48ad16f04 Merge pull request #470 from ae-utbm/remove-gitlab-files
Remove old GitLab files
2022-09-02 20:13:20 +02:00
7cc6250860 Delete thank_you.md 2022-09-02 19:53:41 +02:00
ae2e4b518d Removed old GitLab files & may fix auto_assign for reviewers 2022-09-02 19:49:28 +02:00
e9b9f3a62b Merge pull request #469 from ae-utbm/remove-calendar-page
Switched Calendar link to Elections list link (as it was unused)
2022-09-02 19:43:47 +02:00
3321669726 Switched Calendar link to Elections list link (as it was unused) 2022-09-02 19:34:16 +02:00
21fc85670e hot fix: Updated variable names & comments (#461)
- Fixed a wrong condition on the users subscribing history `read` permission.
- The comments are clearer and mentions how to specify clubs by their id.
2022-08-31 20:53:08 +02:00
18a5ad6541 Merge pull request #460 from ae-utbm/integration-subscriptions 2022-08-31 18:51:40 +02:00
71c5456225 Selected club members can now see subscriptions 2022-08-31 18:39:49 +02:00
50e04164a2 Merge pull request #457 from ae-utbm/455-sentry-modal
Fixed some mess done on settings.py
2022-08-27 22:06:13 +02:00
3b1d71f317 Merge pull request #458 from ae-utbm/actions
Editing workflow process
2022-08-27 22:00:54 +02:00
65c2689578 Editing workflow process
Sentry new release only triggers when deployment is successful
2022-08-27 21:56:46 +02:00
b45673f04a Update settings.py 2022-08-27 21:54:20 +02:00
cb6e037f5e Fixed some mess done on settings.py 2022-08-27 21:52:16 +02:00
5e6d60bb3a Merge pull request #456 from ae-utbm/455-sentry-modal
Updated sentry modal SDK
2022-08-27 21:47:59 +02:00
64f8d9bad3 Function name refactor
So the name is clearer
2022-08-27 21:36:45 +02:00
05b86e1f7a Black again 2022-08-27 21:23:49 +02:00
700fed860d Code refactor and comments 2022-08-27 21:22:31 +02:00
820bf6279b Modal window is now autocompleted if user is logged in 2022-08-27 20:14:31 +02:00
b97ce81dd2 Fixed black lint 2022-08-27 19:48:23 +02:00
f4dfd8f99c settings.DEBUG variable sets the sentry env to development
DSN still needs to be specified manually
2022-08-27 19:46:26 +02:00
29139bf360 SENTRY_ENV can now be overriden in settings.py 2022-08-27 18:58:12 +02:00
4f9c2724f5 Updated sentry modal SDK
Specified default environment for issues
2022-08-27 18:46:22 +02:00
7a914f5e94 Merge pull request #451 from ae-utbm/django-3.2-migration
Edited deprecated code
2022-08-27 00:17:36 +02:00
121d04e1d5 Merge branch 'master' into django-3.2-migration 2022-08-27 00:03:58 +02:00
fc6cdba8e2 Merge pull request #452 from ae-utbm/actions
Going back to actions again
2022-08-26 23:33:24 +02:00
7f39ead159 This should work now 2022-08-26 23:32:43 +02:00
1da82ac2dd Another regex 2022-08-26 23:31:33 +02:00
f2dcc39c14 No inspiration 2022-08-26 23:28:08 +02:00
705dc56153 Testing another regex 2022-08-26 23:26:37 +02:00
02047b62d7 Edited random file 2022-08-26 23:21:34 +02:00
895d4b33a6 Going back to actions again 2022-08-26 23:19:29 +02:00
142cb3316e Edited deprecated code
Fixes #449

See : https://docs.djangoproject.com/en/3.2/ref/forms/api/\#notes-on-field-ordering
2022-08-26 22:33:21 +02:00
997fcc9fff Merge pull request #448 from ae-utbm/actions 2022-08-26 22:26:33 +02:00
ec65ca11d6 Added sentry release action (See: #444) 2022-08-26 21:33:18 +02:00
0198027544 I forgot sth 2022-08-26 17:11:20 +02:00
69e0550d4f Merge pull request #447 from ae-utbm/actions
Implemented diff file for CI
2022-08-26 17:09:13 +02:00
9a1a5635e2 Implemented file diff (see: #445) 2022-08-26 17:04:09 +02:00
863f9ff77e Added some safety to deploy script 2022-08-26 16:39:49 +02:00
4146c4c5cb Merge pull request #443 from ae-utbm/actions
Unit tests do not run on master push
2022-08-26 16:24:23 +02:00
b3ad5c5df9 Unit tests do not run on master push
They are only trigerred on PRs
2022-08-26 16:12:17 +02:00
9388e2dc88 Merge pull request #442 from ae-utbm/actions
Actions should work now
2022-08-26 14:54:42 +02:00
56dec9eaa1 Added auto assign for PR 2022-08-26 14:43:51 +02:00
596126f4f4 Actions seem to be operationnal 2022-08-26 14:39:10 +02:00
8646b2c8f7 Rollback to previous version (see: https://github.com/appleboy/ssh-action/issues/174) 2022-08-26 14:10:37 +02:00
c81bb1fb90 Merge pull request #440 from ae-utbm/links-update
Updated links before moving to GitHub
2022-08-26 14:05:12 +02:00
d17a52a8d6 Updated links before moving to GitHub 2022-08-26 14:04:05 +02:00
55e0eecc0b SSH Connection now works 2022-08-26 13:58:45 +02:00
496adc17ea Updated links & moved to a markdown file 2022-08-26 13:58:21 +02:00
ab43d7d2df Testing things 2022-08-26 13:53:17 +02:00
13f0bfe546 Enabled debug 2022-08-26 13:48:40 +02:00
83a384145b Fixed spelling 2022-08-26 13:43:13 +02:00
8a923761a5 Specified environment 2022-08-26 13:26:41 +02:00
6e4a99eba3 Added sample deploy action 2022-08-26 13:20:57 +02:00
0470aa185e Merge pull request #441 from ae-utbm/actions
First try for CI/CD using actions
2022-08-25 22:47:34 +02:00
273371db8b Updated for merging into master 2022-08-25 22:46:30 +02:00
ed3aa0c328 Removed real tests during actions deployment 2022-08-25 22:45:33 +02:00
acfff6b103 Edited master to actions for testing purposes 2022-08-25 22:30:32 +02:00
ada4579193 Created deploy workflow & made a dry run 2022-08-25 22:23:13 +02:00
3a17c3079e 0/10 en dictée 2022-08-25 21:23:48 +02:00
26e46de8e1 Je sais pas écrire 2022-08-25 21:21:12 +02:00
111bcc8e60 Fixed permission issue on apt-get 2022-08-25 21:19:23 +02:00
cdaa204ba2 Added bullshit 2022-08-25 21:17:19 +02:00
e85511fcb9 Initial unit testing action 2022-08-25 21:14:04 +02:00
Sli
35c120a29f Merge branch 'download-all-my-pictures' into 'master'
Fix 'download all my picture button' being displayed in all albums sections

This MR fix the following issue where the download all button is displayed in each album (hence it's for all photos & not only by album)
![image](/uploads/c888e2bf8715d18cd2ea26e63f9fac28/image.png)

See merge request ae-utbm/Sith!320
2022-08-09 17:09:48 +00:00
Sli
7c4c1bc387 Fix permissions on download pictures feature 2022-08-09 18:11:20 +02:00
6e77edcf67 Fix 'download all my picture button' being displayed in all albums sections 2022-08-09 17:57:02 +02:00
effed9c760 Merge branch 'download-all-my-pictures' into 'master'
Add feature to download all of your pictures as a user


See merge request ae-utbm/Sith!319
2022-08-09 13:51:04 +00:00
Sli
0e5c8b53b0 Add missing translations and update doc 2022-08-07 16:45:18 +02:00
Sli
47a332445c Add feature to download all of your pictures as a user 2022-08-07 16:08:56 +02:00
Sli
c904b2d827 Merge branch 'fix/broken-js' into 'master'
Fix broken forms


See merge request ae-utbm/Sith!318
2022-08-06 12:53:30 +00:00
Sli
f56263d6bd Fix broken forms 2022-08-06 14:28:35 +02:00
0c2494cb34 Merge branch 'django-3.2' into 'master'
Upgrade to django 3.2

* Upgrade dependencies
* Fix ugettext
* Fix bad urls

See merge request ae-utbm/Sith!316
2022-08-05 18:46:24 +00:00
9e5743a64c Merge branch 'defer-script-and-font-awesom' into 'master'
Update de base.jinja

Defer des balises script. Ajout de preload sur l'import de fontawesome. Changement de certains commentaires html en commentaires jinja.

Le deux premiers points devraient permettre de gagner un temps non-négligeable au chargement de la page.

See merge request ae-utbm/Sith!317
2022-08-05 18:12:29 +00:00
b5241ec75e Defer des balises script. Ajout de preload sur fa. 2022-08-05 13:22:09 +00:00
Sli
4f00224f0d Update dependencies, apply black and fix wrong default SITH_COUNTER_OFFICES values 2022-08-04 18:42:29 +02:00
Sli
320a896610 Fix tests and broken forms 2022-08-04 17:20:21 +02:00
Sli
08924c5e05 Fix wrong url and set default auto field 2022-08-04 00:38:50 +02:00
Sli
98bfc308a7 Minimal working version
* Upgrade dependencies
* Fix ugettext
* Fix bad urls
2022-08-04 00:28:09 +02:00
Sli
dee24fbc9c Fix deprecation warnings 2022-08-03 21:48:37 +02:00
2556427c7d Merge branch 'lsacienne/invitation_banner2' into 'master'
💄 Change banner to invitation banner

We must set an invitation banner again. For the next one, we should create a new feature with a new button to avoid doing this switch every time.

See merge request ae-utbm/Sith!314
2022-07-04 19:23:28 +00:00
a2b35e5bba 💄 Change banner to invitation banner 2022-07-04 14:03:50 +02:00
3e8f1acb96 Merge branch 'election-css' into 'master'
Improved Elections CSS for the table

- Everything can be seen without scrolling sideways (unless you're on a small screen)
- Each column makes the same size
- Candidate description/program is now below its profile picture
- If the candidate does not have any profile picture, the default one is shown
- The Edit/Delete message has been replaced with their corresponding emojis (they takes fewer spaces and doesn't need to be translated)
- Modified links at the bottom to look like buttons

<details><summary>Before</summary>
![image](/uploads/fd42e2fa027786612582d41c97090277/image.png)
</details>

<details><summary>This MR (root)</summary>
![image](/uploads/8350518422392f971d98f3c7ee48a558/image.png)
</details>

<details><summary>This MR (lambda user)</summary>
![image](/uploads/e6b66730e47556ea21230e89d2d06f83/image.png)
</details>

<details><summary>When a candidate is selected</summary>
![image](/uploads/adde527405fb321ba2023c36e06f4dc3/image.png)
</details>

See merge request ae-utbm/Sith!313
2022-06-15 19:13:47 +00:00
85788977fe Moved file to correct place & improved CSS a bit 2022-06-15 15:32:16 +02:00
066ca5bada This shouldn't be unminified 2022-06-15 01:57:57 +02:00
41369f738e Improved Elections CSS for the table 2022-06-15 01:42:17 +02:00
67377b3cbf Merge branch 'lsacienne/change_weekmail_banner_P22_08_06_2022' into 'master'
Change the invitation banner in weekmail to regular weekmail banner

We now have the weekmail banner and not the invitation banner

See merge request ae-utbm/Sith!312
2022-06-14 09:18:26 +00:00
ac3d668655 💄 CHange the invitation banner in weekmail
We now have the weekmail banner and not the invitation banner
2022-06-08 22:05:24 +02:00
c57b15e159 Merge branch 'lsacienne/change_weekmail_banner_P22' into 'master'
Modification of the banner and footer for the Special General Meeting

There will be a special general meeting next week so we modify the banner to fit with this event.

See merge request ae-utbm/Sith!311
2022-06-01 21:14:38 +00:00
66efb8012e ♻️ Fix black pipeline 2022-06-01 22:46:12 +02:00
cad0c0dadb 💄 Modification of the banner and footer
for the special invitation
2022-06-01 22:40:52 +02:00
b32c90ed5d Add of weekmail footer 2022-06-01 22:39:44 +02:00
4d361dc67b Add of weekmail banner in 2 versions 2022-06-01 22:39:17 +02:00
2b170d91f7 Add of Invitation banner 2022-06-01 22:38:44 +02:00
9e074d6ca6 Merge branch 'service_desk_reply' into 'master'
Update .gitlab/service_desk_templates/thank_you.md


See merge request ae-utbm/Sith!310
2022-05-26 13:46:54 +00:00
b655b2695b Update .gitlab/service_desk_templates/thank_you.md 2022-05-26 08:41:37 +00:00
366aeed2ba Merge branch 'lsacienne/refilling_authorized_for_Bdf_ae' into 'master'
Add authorization to refill to the counters AE & BdF

Since the FIMU is coming, there is a necessity to allow access to physical refilling to the people who will manage the stands.

Therefore, We should authorize the refilling on the BdF and AE counter.

See merge request ae-utbm/Sith!309
2022-05-22 09:56:56 +00:00
454ae5f9e3 Add authorization to refill to the counters AE & BdF 2022-05-22 09:56:53 +00:00
b811114425 fix black pipeline 2022-05-21 21:53:25 +02:00
712e7c8939 Add of verification on the counter 2022-05-21 12:23:34 +02:00
713cd92141 Modification of the settings to fit better with the code 2022-05-21 12:23:23 +02:00
4154b499b1 Add of a new settings for the counters AE & BdF 2022-05-21 09:45:28 +02:00
253f204225 Merge branch '125-fix-family-tree' into 'master'
Ajout de pygraphviz en dépendance

Closes #125

On change également la version minimale de python (`3.7` -> `3.8`)

Closes #125

See merge request ae-utbm/Sith!306
2022-05-08 12:09:38 +00:00
7241f3eb1d Ajout de pygraphviz en dépendance 2022-05-08 12:09:37 +00:00
2422f60898 Merge branch 'lsacienne/refilling_only_for_ae_member' into 'master'
Adds a Restriction for refilling

As it was asked by many members of the AE. I added a restriction applied to the barmens.
In fact, we oftenly loose money due to the physic refilling.
The goal with this change is to only allow **the members of the AE** to refill with physic money.

See merge request ae-utbm/Sith!303
2022-05-05 21:53:57 +00:00
ba6599fa56 Add of tests 2022-05-05 23:24:08 +02:00
f2666f6fb0 Replace the query by a function which already
existed
2022-05-02 00:04:00 +02:00
b33839191d Fix black pipeline 2022-04-28 13:16:03 +02:00
ee3e375dde Post request management 2022-04-28 11:13:07 +02:00
5b0f7ca21b Merge branch 'skia/deploy_in_ci' into 'master'
gitlab-ci: deploy with Gitlab CI/CD

This MR is a proof-of-concept for deploying the Sith using Gitlab CI/CD. It leverage the CI variable to use a private key that is deployed for the `sith` user of `ae-web`. The `prod.sh` script shall do the rest.

TODO before merge:
* [x] Ensure the private key variable is protected (currently done, but may change during development to be used on this branch)
* [x] Remove this branch from the `only:refs` list
* [x] Change `test_prod.sh` for the real script

See merge request ae/Sith!293
2022-04-27 18:21:49 +00:00
f581d91730 gitlab-ci: deploy with Gitlab CI/CD 2022-04-27 18:21:48 +00:00
bbf362691b Change to use settings instead of hardcoding 2022-04-27 15:38:55 +02:00
15e2c8c7b3 Fix the balck pipeline 2022-04-27 15:38:14 +02:00
f838127730 Merge branch 'aile-master-patch-00174' into 'master'
Update badges and links on the readme


See merge request ae/Sith!305
2022-04-27 13:13:08 +00:00
d4c0bb3b0e Fix pipeline
Signed-off-by: Théo DURR <03ht@theodurr.fr>
2022-04-27 14:52:33 +02:00
b81aee3f1c Update badges and links 2022-04-27 09:50:38 +00:00
c6caf5dbce Add of restriction for refilling 2022-04-20 14:01:33 +02:00
7acc59f2cd Merge branch 'lsacienne/refilling_date' into 'master'
Add of date in the counter/refilling_list view

I only add a new field in the counter/refilling_list view which will *normally* display the date of each refilling.

See merge request ae/Sith!302
2022-04-19 10:28:09 +00:00
757ff7ead7 Add of date in the counter/refilling_list view 2022-04-19 12:02:22 +02:00
bc2fe16b74 Merge branch '117-django-2-2-not-compatible-with-psycopg-2-9' into 'master'
Resolve "Django 2.2 not compatible with psycopg 2.9"
Closes #117

See merge request ae/Sith!299
2022-04-18 20:21:19 +00:00
35363d9ee7 Resolve "Django 2.2 not compatible with psycopg 2.9" 2022-04-18 20:21:18 +00:00
52106db6fd Merge branch '118-black-pipeline-is-broken' into 'master'
Resolve "Black pipeline is broken"

Closes #118

Closes #118

See merge request ae/Sith!300
2022-04-18 18:33:39 +00:00
c4b1829e78 Resolve "Black pipeline is broken" 2022-04-18 18:33:36 +00:00
489a9378c5 Merge branch 'poetry' into 'master'
Add missing dependencies and improve pipeline

* Use black version specified in requirements for checking with black
* Check if pyproject.toml file is valid at CI level
* Build documentation in CI
* Add missing postgres dependencie

See merge request ae/Sith!284
2022-03-26 21:27:22 +00:00
Sli
28ae109b32 Add missing dependencies and improve pipeline 2022-03-26 21:27:20 +00:00
e7a6a94ff2 Merge branch 'doc-windows-install' into 'master'
Added WSL Windows doc for the project install

Added steps to install the project on Windows using WSL :)

See merge request ae/Sith!291
2022-03-03 18:18:55 +00:00
234556a172 Merge branch 'skia/fix_eboutic' into 'master'
Multiple fixes

* Bump `black` and fix issues
* `club`: fix tests broken by inclusive translation
* `gitlab-ci`: use `poetry`, as `pip` was broken anyway
* `eboutic`: et_autoanswer: don't require 'Auto' to proceed checking the request: As described in the [doc](https://www.paybox.com/espace-integrateur-documentation/la-solution-paybox-system/gestion-de-la-reponse/), `Auto` may be missing if the payment failed. Thus, it's not required to proceed checking the bank's answer.

See merge request ae/Sith!296
2022-03-02 16:21:10 +00:00
e4ddceabea club: fix tests with inclusive translation 2022-02-28 14:50:24 +01:00
05dd3ad642 gitlab-ci: use poetry 2022-02-28 10:34:15 +01:00
6c5db61a97 eboutic: et_autoanswer: don't require 'Auto' to proceed checking the request 2022-02-28 10:01:32 +01:00
a0e4e9e8e3 Update 'black' version 2022-02-28 10:01:32 +01:00
c66df77d4a Merge branch 'master' of https://ae-dev.utbm.fr/ae/Sith 2022-02-18 16:35:10 +01:00
cfb6b34630 Updated roles to be more inclusive 2022-02-18 16:30:45 +01:00
d8fd0adf47 Merge branch 'skia/et_autoanswer' into 'master'
eboutic: change HTTP return code to avoid blaming the bank's service

See merge request ae/Sith!295
2022-02-10 12:32:43 +00:00
928ae13a8a Merge branch 'bugfix-113-error500' into 'master'
#113: bug fixed

See merge request ae/Sith!294
2022-02-10 12:30:55 +00:00
c2e0ea70e4 eboutic: change HTTP return code to avoid blaming the bank's service 2022-01-04 15:50:36 +01:00
782ce24895 Changed python3 to python 2021-12-02 12:22:34 +01:00
b630742fd4 #113: bug fixed 2021-11-30 17:54:51 +01:00
b20df930a2 Merge branch 'feature-111-fixture_documentation' into 'master'
add fixture documentation

See merge request ae/Sith!292
2021-11-25 22:06:41 +00:00
d60a96fc5c correct populate.rst 2021-11-23 23:44:34 +01:00
05b0a0ab2f Adapted WSL doc to follow recommendation :) 2021-11-23 19:19:24 +01:00
9eb137e503 add fixture documentation 2021-11-22 21:37:10 +01:00
7d797009bb Added WSL windows doc for project install 2021-11-19 13:10:18 +01:00
3c1818f229 Merge branch 'family_rework' into 'master'
Updated text and translations to be more inclusive

See merge request ae/Sith!290
2021-11-18 15:38:13 +00:00
d8b69e9b45 Updated text and translations to be more inclusive 2021-11-18 16:24:14 +01:00
9177c9d4c2 Merge branch 'bugfix-110-ClubSellings' into 'master'
Fix error 500 in club sellings

Closes #110

See merge request ae/Sith!289
2021-11-18 14:32:11 +00:00
5195352975 fixed black pipeline 2021-11-18 15:14:39 +01:00
deb8f865df fix #110 2021-11-18 15:04:25 +01:00
5b2c70e4fb Merge branch 'gender_options' into 'master'
Fix pronouns field being mandatory

See merge request ae/Sith!288
2021-11-18 09:07:21 +00:00
Cel
f66db0859e Fix pronouns field being mandatory 2021-11-18 09:07:19 +00:00
b6488d1d00 Merge branch 'poor_logo_quality' into 'master'
Updated somo logo size where they looked blurry (we love responsive)

See merge request ae/Sith!287
2021-11-10 11:33:05 +00:00
6a4ac336ad Updated somo logo size where they looked blurry (we love responsive) 2021-11-10 12:11:07 +01:00
7ac6dcf8a0 Merge branch 'family_rework' into 'master'
Edited the word "GodFather" to "Family"

See merge request ae/Sith!286
2021-11-10 10:35:40 +00:00
c6a3677cc5 Fixed duplicated translation 2021-11-05 21:11:52 +01:00
707459acd6 Changed word 'Godfather' to 'Family' 2021-11-05 21:01:19 +01:00
6390c3320e Applied black on migration 2021-11-05 20:40:20 +01:00
b8aabc466c Fixed locales
+Pronoun description on the user's profile

Signed-off-by: Ailé <03ht@theodurr.fr>
2021-11-05 20:28:37 +01:00
c66e4232b9 Merge branch 'master' into gender_options
Signed-off-by: Théo DURR <03ht@theodurr.fr>
2021-11-05 17:18:17 +01:00
336450d43f Merge branch 'add-promo-logos' into 'master'
Add missing promo logos

Closes #107

See merge request ae/Sith!285
2021-10-27 10:22:07 +00:00
7e66aadd6f Add missing promo logos 2021-10-27 08:37:58 +02:00
Sli
bf2b796936 Merge branch 'poetry' into 'master'
Using poetry as a dependency system for development

See merge request ae/Sith!281
2021-10-15 16:12:59 +00:00
Sli
85623f48a9 Using poetry as a dependency system for development 2021-10-15 16:12:56 +00:00
4fbee9c3de Make pronouns visible on profile and miniprofile 2021-10-13 08:59:40 +02:00
bfa3b45547 counter_click.jinja: fix error display with Vue 2021-10-11 22:09:45 +02:00
677a9da469 Merge branch 'master' into gender_options 2021-10-11 17:13:06 +02:00
1f7752d457 Add pronouns to profile ; Update gender settings
Add pronouns to option list in profile
Modify "Sex" translation to "Genre"
Added "Other" to sex option list (alongside Man and Woman)

update DB,add default value to Pronouns field

Update views.py
2021-10-06 14:12:34 +02:00
89979dbf61 com: news list: fix UI for admins 2021-10-03 19:08:14 +02:00
8d1abb8f33 Add .mailmap file for cleaner stats 2021-10-03 18:44:47 +02:00
2df3494c3b Merge branch 'skia/weekmail_fix' into 'master'
com: fix weekmail for the case of non-existing email addresses

See merge request ae/Sith!282
2021-10-03 16:35:41 +00:00
39bb490257 com: fix weekmail for the case of non-existing email addresses
If an email address is set as destination for the Weekmail, the SMTP may
refuse it, and `smtplib` will throw a `SMTPRecipientsRefused` error,
containing the list of refused addresses. This commit provides an
interface for the weekmail sender to quickly unsubscribe the faulty
users, so that the next try sending the weekmail can be performed
successfully.
2021-10-03 18:16:51 +02:00
7a7aad0503 style: fix header bar on medium size screens 2021-10-03 16:08:53 +02:00
b157a3fa90 Merge branch 'skia/mobile_ui' into 'master'
Add a first version of a mobile friendly UI

Although not perfect and with many flaws, this should still allow far
easier navigation on mobile devices.

See merge request ae/Sith!280
2021-10-01 17:05:11 +00:00
1b688a8aa5 Add a first version of a mobile friendly UI
Although not perfect and with many flaws, this should still allow far
easier navigation on mobile devices.
2021-10-01 18:44:14 +02:00
e8978cc065 sith/toolbar_debug: don't fail when there is no template 2021-10-01 14:08:57 +02:00
7fd68e4825 Merge branch 'skia/ci_speedup' into 'master'
CI speedup

* Put the Xapian search index in `/dev/shm`, which is an in-memory storage makes the tests go from about 1500s to about 600s.
* Keep the `pip` cache between jobs, to avoid re-downloading all the wheels all the time. This gains about 1min.

See merge request ae/Sith!279
2021-09-30 10:58:49 +00:00
4119eefe37 gitlab-ci: keep pip cache between jobs 2021-09-30 12:07:00 +02:00
aafc2e6e96 gitlab-ci: put search_indexes in shared memory 2021-09-30 12:07:00 +02:00
2cbe6fa11c Merge branch 'genderMatmatroncheV2' into 'master'
Remove gender option of matmatronche & update gender settings

Afin de se mettre à jour il est dorénavant possible de ne pas définir son genre sexué sur l'édit de son profil. D'ailleurs j'ai découvert que de base pour un profil random le sexe était défini sur "Homme" maintenant il est en "-------" !

![image](/uploads/43e9f32dc545b35cbe422a53602b2457/image.png)

De plus afin que personnes n'utilisent l'outil matmatronche à des fins de site de rencontres en cherchant uniquement les "Homme" ou les "Femme" d'une promo etc... Le choix du sexe dans la recherche a été supprimé.

![image](/uploads/e6e75d5661862178acfbe71f3f7efc35/image.png)

C'est la première fois que je fais une modification en solo alors n'hésitez pas à me casser en deux et m'expliquer si j'ai fauté :D

See merge request ae/Sith!264
2021-09-29 15:57:52 +00:00
eec7bcf296 Remove gender option of matmatronche & update gender settings 2021-09-29 17:29:01 +02:00
6c45de34a4 Merge branch 'poster' into 'master'
[com]: add helper_text for resolution and format of poster

See merge request ae/Sith!209
2021-09-29 14:56:30 +00:00
Cyl
61a40c47d2 [com]: add helper_text for resolution and format of poster 2021-09-29 16:09:05 +02:00
007157e2e8 Merge branch 'datetime-hell2' into 'master'
core: create TzAwareDateTimeField to replace forms.DateTimeField

Follow up of !267. I read about Gitlab's slash and merge just after I did my own kind by resetting back to the original commit and creating one commit manually. Sublime merge helps but I still need more practice. :)

What was the right way to group every commit under one?

See merge request ae/Sith!270
2021-09-29 13:53:12 +00:00
49a0ade315 core: create TzAwareDateTimeField to replace forms.DateTimeField 2021-09-29 15:24:06 +02:00
782cd9a45a Merge branch 'sexy-search' into 'master'
Sexy search

The goal of this MR is to solve the search issue #96. Let's assume we have a user with firstname `Jean-François`, lastname `Du Pont` and nickname `Ai'gnan`. Here is a list of search that did not include him previously but now includes him (was and still is case-insensitive):

* `jean françois` (missing -) ;
* `jean-francois` (missing ç) ;
* `jean francois` (both) ;
* `dupont` (space) ;
* `françois` (not the start of his name) ;
* `aignan` (missing ').

You get it, there are a lot of mistakes that humans can do. It also sorts results by `User.last_update` to avoid putting old accounts at the top of common requests (such as firstname-only or lastname-only requests).

### How it works

For those who don't know, the search is handled by Xapian (the search backend) through the haystack library which provides a Django-friendly interface to multiple search backends. Xapian maintains kind of a duplicate of the database (only for models against which we want to search something) which is optimised for search operations. Its "models" are called "indexes" (see `core.search_indexes.UserIndex` for the user model).

Every time a user is created or modified, it is indexed (through a signal handler) so that Xapian knows about it. For the user search, what is indexed is the string outputted by the `core/templates/search/indexes/core/user_auto.txt` template. For our example from above, it looks like this:

```
jean francois
du pont
aignan
jeanfrancois
dupont

jeanfrancoisdupont
```

As you can see, unicode is removed. There also are kind-of duplicates with different spacing as we are using an autocomplete algorithm: it searches from the beginning of words.

The one I am not sure about is the last one. Its goal is to allow searching without putting a space between the firstname and lastname. Is this useful?

The prod will have to do a `./manage.py update_index`, not sure it does it in the upgrade script.

See merge request ae/Sith!269
2021-09-28 00:14:38 +00:00
6382e631b6 search: reduce user index size 2021-09-28 01:44:15 +02:00
12493cffca search: make sure we don't have indexes that are too long 2021-09-28 01:44:15 +02:00
a38ab57ddf search: sort by User.last_update 2021-09-28 01:44:15 +02:00
30091ef69c search: ascii everywhere and unformalized whitespace 2021-09-28 01:44:15 +02:00
1a483bfa2c Merge branch 'och' into 'master'
Settings: Added new subscription for the new CA offer

This year we made a new deal with the CA: if a student open an account, they give us 50€ and the student 80€ with on year of subscription.

See merge request ae/Sith!276
2021-09-27 23:31:46 +00:00
1a091951e8 Added new subscription for the new CA offer 2021-09-28 01:11:23 +02:00
bfb66b352a Merge branch 'dep-hell2' into 'master'
core: add ./manage.py check_front command and call it on runserver

See #92 and !268.

This simplifies checking that front-end dependencies are up to date. It does not allow one to update an outdated dependency. That must be done manually (would otherwise require depending on a CDN or add npm as a dependency). A manual update will make sure changelogs are read and changes will be made appropriately.

We add a `check_front` command to `manage.py` and run it on calls to `runserver`.

This MR does not update any dependency as it is not its goal. MR incoming!

Should doc be added? It seems pretty simple and I don't see what should be documented: if it's red, update it.

~"Review TODO" @sli

See merge request ae/Sith!271
2021-09-27 20:23:35 +00:00
be26e3df7f core: add ./manage.py check_front command and call it on runserver 2021-09-27 22:00:36 +02:00
cb3307509d Merge branch 'skia/counter_rework' into 'master'
counter: make click page dynamic to avoid repetitive loading

See merge request ae/Sith!278
2021-09-27 19:21:28 +00:00
a3158253a7 Black update 2021-09-26 13:58:39 +02:00
406380e4f1 counter: make click page dynamic to avoid repetitive loading
This makes the whole click page load only once for a normal click
workflow. The current basket is now rendered client side with Vue.JS,
and the backend view is able to answer with JSON if asked to.

This should lighten the workflow a lot on the client side, especially
with poor connectivity, and the server should also feel lighter during
big events, due to far less complex Jinja pages to render.
2021-09-26 13:58:39 +02:00
efb70652af counter: redirect to counter main when barman login is timed out 2021-09-26 13:58:39 +02:00
05256bb99a counter: templates: click: JS clean up 2021-09-26 13:58:39 +02:00
64d0cc2fa8 counter: don't display info boxes and navigation menu
This will lighten the pages and make the functionality directly
accessible without ever scrolling the header garbage that is never
needed on those pages.
2021-09-26 13:58:39 +02:00
f5d7267ba7 Merge branch 'skia/fix_ci' into 'master'
Fix CI

See merge request ae/Sith!277
2021-07-21 13:16:02 +00:00
24c0a21cc1 locale: update with latest code version 2021-04-23 12:02:03 +02:00
6a352d642b accounting: fix tests with a computed date instead of hard-coded one 2021-04-23 12:02:03 +02:00
Sli
48ae1f7c1c Merge branch 'och' into 'master'
Edited subscriptions

See merge request ae/Sith!275
2020-09-01 00:21:48 +02:00
aaf1adaaa1 sith: Added a new subscription 2020-08-30 23:53:19 +02:00
f34f5fe693 Upgrade black and format accordingly 2020-08-27 15:59:42 +02:00
Sli
f485178422 Merge branch 'och' into 'master'
settings: Added a new subscription

See merge request ae/Sith!274
2020-06-18 00:23:51 +02:00
Och
797ca0f926 settings: Added a new subscription 2020-06-18 00:23:51 +02:00
Sli
390a4b0064 Merge branch 'bugfix' into 'master'
cache: fix error 500 with new django version

See merge request ae/Sith!273
2020-06-16 19:11:49 +02:00
94b029dc9c cache: fix error 500 with new django version 2020-06-12 20:44:37 +02:00
Sli
45d5728c3e Merge branch 'skia/lazy_load_user_pictures' into 'master'
core: add lazy loading in user pictures page

See merge request ae/Sith!272
2020-06-12 20:19:34 +02:00
6eabbaf209 core: add lazy loading in user pictures page 2020-05-15 12:14:14 +02:00
Sli
03fdd0b947 Merge branch 'trombi' into 'master'
trombi: raw tool for trombi admins to add a club membership to a trombi member

See merge request ae/Sith!266
2020-03-23 21:12:58 +01:00
fb8faacddc trombi: raw tool for trombi admins to add a club membership to a trombi member 2020-03-22 16:14:37 +01:00
Sli
7ee4557ab5 Merge branch 'fix-webcam-error' into 'master'
Front: turn Webcam.js error from an alert to a console log

See merge request ae/Sith!265
2020-03-05 19:20:22 +01:00
5accdbccbb Front: use Webcam.on() for error handling 2020-03-04 07:13:16 +01:00
7fb26f9e45 Front: turn Webcam.js error from an alert to a console log 2020-03-03 09:01:20 +01:00
26a07f722d Remove gender option of matmatronche & update gender settings 2020-02-16 17:51:51 +01:00
Sli
9176a03a8a Merge branch 'bugfix' into 'master'
Fix some SAS and forum errors

Closes #89

See merge request ae/Sith!263
2019-12-17 12:03:28 +01:00
4a1bfc366d sas: fix 500 error when tagging the same user twice or adding a non existing user 2019-12-17 11:25:17 +01:00
ebee8c34e1 forum: fix ForumTopicSubscribeView error 500 with anonymous user 2019-12-16 15:00:33 +01:00
4ecad1c73b Revert "PÈRE 200 !!!!!!!!!!! PÈRE 200 !!!!!!!! TRALALALALÈREEEEUUUU !!!!"
This reverts commit d1b3a4d3f6.
2019-12-10 15:31:37 +01:00
d1b3a4d3f6 PÈRE 200 !!!!!!!!!!! PÈRE 200 !!!!!!!! TRALALALALÈREEEEUUUU !!!! 2019-12-09 03:16:57 +01:00
Sli
40832bb3bf Merge branch 'clubs' into 'master'
Improve Sellings view for clubs

See merge request ae/Sith!262
2019-11-29 16:32:18 +01:00
4a78157f9a club: fix typo on ClubSellingView 2019-11-28 15:14:51 +01:00
bf5fc8750d club: steam CSV download for SellingView 2019-11-28 14:52:33 +01:00
274a7b7137 core/club: allow adding custom js action to pagination link, useful for FormDetailView with pagination 2019-11-28 01:46:41 +01:00
8dd2c02d3e club: add pagination for ClubSellingView 2019-11-28 00:30:51 +01:00
a73f5cb270 club: use sums in bdd for ClubSellingView 2019-11-27 21:37:59 +01:00
7d40e11144 club: ClubSellingView way faster and with multiple selections everywhere 2019-11-27 20:59:32 +01:00
af48553e35 club: separation between archived products and non archived ones 2019-11-27 16:23:14 +01:00
Sli
ad8bcc7282 Merge branch 'bugfix' into 'master'
com: fix 500 error when utbm mail server refuse weekmail

See merge request ae/Sith!260
2019-11-25 14:18:22 +01:00
Sli
22a44415e4 Merge branch 'sli' into 'master'
core: add UserIsRootMixin and an admin delete view for memberships

See merge request ae/Sith!261
2019-11-25 13:32:46 +01:00
6a153719f9 com: fix 500 error when utbm mail server refuse weekmail 2019-11-25 13:30:47 +01:00
5c8fa1b9e7 core: add UserIsRootMixin and an admin delete view for memberships 2019-11-24 19:23:43 +01:00
Sli
d82679e3d7 Merge branch 'documentation' into 'master'
add autoreload/build to documentation server and enhace documentation

See merge request ae/Sith!246
2019-11-21 15:06:13 +01:00
Sli
9cb432a082 doc: correct documentation for groups 2019-11-21 11:11:25 +01:00
Sli
869d29d4a4 doc: corrections for populate documentation 2019-11-21 11:10:31 +01:00
c3d2e64a43 doc: add infos on populate command with group and users available 2019-11-20 18:51:13 +01:00
e1770ec52c doc: add documentation for groups 2019-11-20 17:55:00 +01:00
1256744f1b documentation: add autoreload and build for documentation server 2019-11-20 17:03:18 +01:00
77dddbc581 documentation: add help ressources and update installation instructions 2019-11-20 17:03:18 +01:00
Sli
bfa4000365 Merge branch 'eboutic' into 'master'
eboutic: don't display future account balance if contains refilling item

See merge request ae/Sith!258
2019-11-14 19:31:19 +01:00
Sli
50c2f8164d Merge branch 'deletion_logs' into 'master'
Add generic operation logs and implements it for Sellings and Refilling deletions

See merge request ae/Sith!259
2019-11-14 19:29:55 +01:00
5c30de5f22 core: redesign request middleware with django latest design and better use of threading 2019-11-14 16:32:29 +01:00
1c03ce621f core: remove default value for OperationLog 2019-11-14 16:11:20 +01:00
e634cda318 core/counter: add generic operation logs and implements it for Sellings and Refilling deletions 2019-11-14 01:14:44 +01:00
148 changed files with 5362 additions and 2183 deletions

15
.envrc
View File

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

13
.github/auto_assign.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# Set to true to add reviewers to pull requests
addReviewers: true
# Set to true to add assignees to pull requests
addAssignees: author
# A list of team reviewers to be added to pull requests (GitHub team slug)
reviewers:
- ae-utbm/sith-3-developers
# Number of reviewers has no impact on GitHub teams
# Set 0 to add all the reviewers (default: 0)
numberOfReviewers: 0

64
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Deploy to production
concurrency: production
on:
push:
branches: [master]
workflow_dispatch:
jobs:
deployment:
runs-on: ubuntu-latest
environment: production
timeout-minutes: 30
steps:
- name: SSH Remote Commands
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
with:
# Proxy
proxy_host : ${{secrets.PROXY_HOST}}
proxy_port : ${{secrets.PROXY_PORT}}
proxy_username : ${{secrets.PROXY_USER}}
proxy_passphrase: ${{secrets.PROXY_PASSPHRASE}}
proxy_key: ${{secrets.PROXY_KEY}}
# Serveur web
host: ${{secrets.HOST}}
port : ${{secrets.PORT}}
username : ${{secrets.USER}}
key: ${{secrets.KEY}}
script_stop: true
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
script: |
export PATH="$HOME/.poetry/bin:$PATH"
pushd ${{secrets.SITH_PATH}}
git pull
poetry update
poetry run ./manage.py migrate
echo "yes" | poetry run ./manage.py collectstatic
poetry run ./manage.py compilestatic
poetry run ./manage.py compilemessages
sudo systemctl restart uwsgi
sentry:
runs-on: ubuntu-latest
environment: production
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: production

83
.github/workflows/unittests.yml vendored Normal file
View File

@ -0,0 +1,83 @@
name: Sith3 CI
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
jobs:
unittests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
# Skip unit testing if no diff on .py files
- name: Check file diff
uses: technote-space/get-diff-action@v6
id: git-diff
with:
PATTERNS: |
**/*.py
- name: Set up python
if: steps.git-diff.outputs.diff
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install dependencies
if: steps.git-diff.outputs.diff
run: |
sudo apt-get update
sudo apt-get install gettext libxapian-dev libgraphviz-dev
- name: Install poetry
if: steps.git-diff.outputs.diff
run: |
python -m pip install --upgrade pip
python -m pip install poetry
- name: Checking pyproject.toml syntax
if: steps.git-diff.outputs.diff
run: poetry check
- name: Install project
if: steps.git-diff.outputs.diff
run: poetry install -E testing
- name: Setup xapian index
if: steps.git-diff.outputs.diff
run: |
mkdir -p /dev/shm/search_indexes
ln -s /dev/shm/search_indexes sith/search_indexes
- name: Setup project
if: steps.git-diff.outputs.diff
run: poetry run ./manage.py compilemessages
- name: Launch tests and generate coverage report
if: steps.git-diff.outputs.diff
run: |
poetry run coverage run ./manage.py test
poetry run coverage report
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install black
run: |
python -m pip install --upgrade pip
python -m pip install black==22.6.0
- name: Check linting
run: black --check .

2
.gitignore vendored
View File

@ -4,6 +4,8 @@ db.sqlite3
*.mo
*__pycache__*
.DS_Store
pyrightconfig.json
dist/
.vscode/
env/
doc/html

View File

@ -1,28 +0,0 @@
stages:
- test
test:
stage: test
script:
- apt-get update
- apt-get install -y gettext python3-xapian libgraphviz-dev
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
- python -c 'import xapian' # Fail immediately if there is a problem with xapian
- pip install -r requirements.txt
- pip install coverage
- ./manage.py compilemessages
- coverage run ./manage.py test
- coverage html
- coverage report
- cd doc
- make html # Make documentation
artifacts:
paths:
- coverage_report/
black:
stage: test
script:
- pip install black
- black --check .

18
.mailmap Normal file
View File

@ -0,0 +1,18 @@
Code <gregoire.duvauchelle@utbm.fr>
Cyl <labetowiez@aol.fr>
Juste <maaxleblanc@gmail.com>
Krophil <pierre.brunet@krophil.fr>
Lo-J <renaudg779@gmail.com>
Nabos <gnikwo@hotmail.com>
Och <francescowitz68@gmail.com>
Partoo <joqaste@gmail.com>
Skia <skia@hya.sk> <lordbanana25@mailoo.org>
Skia <skia@hya.sk> <skia@libskia.so>
Sli <klmp200@klmp200.net> <antoine@bartuccio.fr>
Soldat <ryan-68@live.fr>
Terre <jbaptiste.lenglet+git@gmail.com>
Vial <robin.trioux@utbm.fr>
Zar <antoine.charmeau@utbm.fr> <antoine.charmeau@laposte.net>
root <root@localhost.localdomain>
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>

View File

@ -4,6 +4,11 @@
# 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
@ -13,6 +18,9 @@ formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.6
version: 3.8
install:
- requirements: requirements.txt
- method: pip
path: .
extra_requirements:
- docs

41
README.md Normal file
View File

@ -0,0 +1,41 @@
<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/889796155523874847?label=Discord&logo=discord&style=for-the-badge">
</a>
</p>
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3>
<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>
<h4>If you want to contribute, here's how we recommend to read the docs:</h4>
<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>
> This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.

View File

@ -1,37 +0,0 @@
.. image:: https://ae-dev.utbm.fr/ae/Sith/badges/master/pipeline.svg
:target: https://ae-dev.utbm.fr/ae/Sith/commits/master
:alt: pipeline status
.. image:: https://readthedocs.org/projects/sith-ae/badge/?version=latest
:target: https://sith-ae.readthedocs.io/?badge=latest
:alt: documentation Status
.. image:: https://ae-dev.utbm.fr/ae/Sith/badges/master/coverage.svg
:target: https://ae-dev.utbm.fr/ae/Sith/commits/master
:alt: coverage report
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: code style: black
.. image:: https://img.shields.io/badge/zulip-join_chat-brightgreen.svg
:target: https://ae-dev.zulipchat.com
:alt: project chat
This is the source code of the UTBM's student association available at https://ae.utbm.fr/.
All documentation is in the ``docs`` 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.
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 find 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.
To join our team :
* Send a mail at mailto:ae.utbm.fr
* Join our group chat at https://ae-dev.zulipchat.com
* See and join our Trello at https://trello.com/b/YQOaF33m/site-ae.
This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.

View File

@ -27,7 +27,7 @@ from django.core.exceptions import ValidationError
from django.core import validators
from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.template import defaultfilters
from phonenumber_field.modelfields import PhoneNumberField

View File

@ -25,7 +25,7 @@
from django.test import TestCase
from django.urls import reverse
from django.core.management import call_command
from datetime import date
from datetime import date, timedelta
from core.models import User
from accounting.models import (
@ -110,6 +110,9 @@ class JournalTest(TestCase):
class OperationTest(TestCase):
def setUp(self):
call_command("populate")
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
"%d/%m/%Y"
)
self.journal = GeneralJournal.objects.filter(id=1).first()
self.skia = User.objects.filter(username="skia").first()
at = AccountingType(
@ -158,7 +161,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de la nuit",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
@ -191,7 +194,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de la nuit",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
@ -218,7 +221,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome du jour",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
@ -245,7 +248,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de l'aurore",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",

View File

@ -25,7 +25,7 @@
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
from django.urls import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied, ValidationError
from django.forms import HiddenInput
@ -496,7 +496,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
return ret
def get_context_data(self, **kwargs):
""" Add journal to the context """
"""Add journal to the context"""
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
if self.journal:
kwargs["object"] = self.journal
@ -514,7 +514,7 @@ class OperationEditView(CanEditMixin, UpdateView):
template_name = "accounting/operation_edit.jinja"
def get_context_data(self, **kwargs):
""" Add journal to the context """
"""Add journal to the context"""
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
kwargs["object"] = self.object.journal
return kwargs
@ -735,7 +735,7 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return statement
def get_context_data(self, **kwargs):
""" Add infos to the context """
"""Add infos to the context"""
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
kwargs["statement"] = self.big_statement()
return kwargs
@ -774,7 +774,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return sum(self.statement(movement_type).values())
def get_context_data(self, **kwargs):
""" Add journal to the context """
"""Add journal to the context"""
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
kwargs["credit_statement"] = self.statement("CREDIT")
kwargs["debit_statement"] = self.statement("DEBIT")
@ -804,7 +804,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
return statement
def get_context_data(self, **kwargs):
""" Add journal to the context """
"""Add journal to the context"""
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
kwargs["statement"] = self.statement()
return kwargs

View File

@ -22,7 +22,7 @@
#
#
from django.urls import re_path, include
from django.urls import re_path, path, include
from api.views import *
from rest_framework import routers
@ -54,4 +54,5 @@ urlpatterns = [
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

@ -33,8 +33,8 @@ from core.views import can_view, can_edit
def check_if(obj, user, test):
"""
Detect if it's a single object or a queryset
aply a given test on individual object and return global permission
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:
@ -49,7 +49,7 @@ class ManageModelMixin:
@action(detail=True)
def id(self, request, pk=None):
"""
Get by id (api/v1/router/{pk}/id/)
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)
@ -78,3 +78,4 @@ from .club import *
from .group import *
from .launderette import *
from .uv import *
from .sas import *

View File

@ -33,7 +33,7 @@ from core.templatetags.renderer import markdown
@renderer_classes((StaticHTMLRenderer,))
def RenderMarkdown(request):
"""
Render Markdown
Render Markdown
"""
try:
data = markdown(request.POST["text"])

View File

@ -43,7 +43,7 @@ class ClubSerializer(serializers.ModelSerializer):
class ClubViewSet(RightModelViewSet):
"""
Manage Clubs (api/v1/club/)
Manage Clubs (api/v1/club/)
"""
serializer_class = ClubSerializer

View File

@ -45,7 +45,7 @@ class CounterSerializer(serializers.ModelSerializer):
class CounterViewSet(RightModelViewSet):
"""
Manage Counters (api/v1/counter/)
Manage Counters (api/v1/counter/)
"""
serializer_class = CounterSerializer
@ -54,7 +54,7 @@ class CounterViewSet(RightModelViewSet):
@action(detail=False)
def bar(self, request):
"""
Return all bars (api/v1/counter/bar/)
Return all bars (api/v1/counter/bar/)
"""
self.queryset = self.queryset.filter(type="BAR")
serializer = self.get_serializer(self.queryset, many=True)

View File

@ -36,7 +36,7 @@ class GroupSerializer(serializers.ModelSerializer):
class GroupViewSet(RightModelViewSet):
"""
Manage Groups (api/v1/group/)
Manage Groups (api/v1/group/)
"""
serializer_class = GroupSerializer

View File

@ -72,7 +72,7 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
class LaunderettePlaceViewSet(RightModelViewSet):
"""
Manage Launderette (api/v1/launderette/place/)
Manage Launderette (api/v1/launderette/place/)
"""
serializer_class = LaunderettePlaceSerializer
@ -81,7 +81,7 @@ class LaunderettePlaceViewSet(RightModelViewSet):
class LaunderetteMachineViewSet(RightModelViewSet):
"""
Manage Washing Machines (api/v1/launderette/machine/)
Manage Washing Machines (api/v1/launderette/machine/)
"""
serializer_class = LaunderetteMachineSerializer
@ -90,7 +90,7 @@ class LaunderetteMachineViewSet(RightModelViewSet):
class LaunderetteTokenViewSet(RightModelViewSet):
"""
Manage Launderette's tokens (api/v1/launderette/token/)
Manage Launderette's tokens (api/v1/launderette/token/)
"""
serializer_class = LaunderetteTokenSerializer
@ -99,7 +99,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
def washing(self, request):
"""
Return all washing tokens (api/v1/launderette/token/washing)
Return all washing tokens (api/v1/launderette/token/washing)
"""
self.queryset = self.queryset.filter(type="WASHING")
serializer = self.get_serializer(self.queryset, many=True)
@ -108,7 +108,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
def drying(self, request):
"""
Return all drying tokens (api/v1/launderette/token/drying)
Return all drying tokens (api/v1/launderette/token/drying)
"""
self.queryset = self.queryset.filter(type="DRYING")
serializer = self.get_serializer(self.queryset, many=True)
@ -117,7 +117,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
def avaliable(self, request):
"""
Return all avaliable tokens (api/v1/launderette/token/avaliable)
Return all avaliable tokens (api/v1/launderette/token/avaliable)
"""
self.queryset = self.queryset.filter(
borrow_date__isnull=True, user__isnull=True
@ -128,7 +128,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
def unavaliable(self, request):
"""
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
"""
self.queryset = self.queryset.filter(
borrow_date__isnull=False, user__isnull=False

42
api/views/sas.py Normal file
View File

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

View File

@ -50,8 +50,8 @@ class UserSerializer(serializers.ModelSerializer):
class UserViewSet(RightModelViewSet):
"""
Manage Users (api/v1/user/)
Only show active users
Manage Users (api/v1/user/)
Only show active users
"""
serializer_class = UserSerializer
@ -60,7 +60,7 @@ class UserViewSet(RightModelViewSet):
@action(detail=False)
def birthday(self, request):
"""
Return all users born today (api/v1/user/birstdays)
Return all users born today (api/v1/user/birstdays)
"""
date = datetime.datetime.today()
self.queryset = self.queryset.filter(date_of_birth=date)

View File

@ -29,10 +29,10 @@ def uv_endpoint(request):
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.
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)
@ -57,7 +57,7 @@ def find_uv(lang, year, code):
def make_clean_uv(short_uv, full_uv):
"""
Cleans the data up so that it corresponds to our data representation.
Cleans the data up so that it corresponds to our data representation.
"""
res = {}

View File

@ -25,7 +25,7 @@
from django.conf import settings
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
@ -34,6 +34,7 @@ from club.models import Mailing, MailingSubscription, Club, Membership
from core.models import User
from core.views.forms import SelectDate, SelectDateTime
from counter.models import Counter
from core.views.forms import TzAwareDateTimeField
class ClubEditForm(forms.ModelForm):
@ -157,23 +158,28 @@ class MailingForm(forms.Form):
return cleaned_data
class SellingsFormBase(forms.Form):
begin_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
counter = forms.ModelChoiceField(
class SellingsForm(forms.Form):
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), 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)
self.fields["products"] = forms.ModelMultipleChoiceField(
club.products.order_by("name").filter(archived=False).all(),
label=_("Products"),
required=False,
)
self.fields["archived_products"] = forms.ModelMultipleChoiceField(
club.products.order_by("name").filter(archived=True).all(),
label=_("Archived products"),
required=False,
)
class ClubMemberForm(forms.Form):
"""
@ -238,8 +244,8 @@ class ClubMemberForm(forms.Form):
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()
users = []
@ -262,7 +268,7 @@ class ClubMemberForm(forms.Form):
def clean(self):
"""
Check user rights for adding an user
Check user rights for adding an user
"""
cleaned_data = super(ClubMemberForm, self).clean()

View File

@ -26,7 +26,7 @@
from django.db import models
from django.core import validators
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db import transaction
from django.urls import reverse

View File

@ -1,9 +1,9 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% from 'core/macros.jinja' import user_profile_link, paginate %}
{% block content %}
<h3>{% trans %}Sellings{% endtrans %}</h3>
<form action="" method="get">
<form id="form" action="?page=1" method="post">
{% csrf_token %}
{{ form }}
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
@ -28,7 +28,7 @@
</tr>
</thead>
<tbody>
{% for s in 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>
@ -53,6 +53,14 @@
{% endfor %}
</tbody>
</table>
<script type="text/javascript">
function formPagination(link){
$("form").attr("action", link.href);
link.href = "javascript:void(0)"; // block link action
$("form").submit();
}
</script>
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
{% endblock %}

View File

@ -45,7 +45,7 @@
</thead>
<tbody>
{% for widget in form_mailing_removal.subwidgets %}
{% set user = ms[widget.data.value][0] %}
{% set user = ms[widget.data.value.value][0] %}
<tr>
<td>{{ user.get_username }}</td>
<td>{{ user.get_email }}</td>

View File

@ -25,7 +25,7 @@
from django.conf import settings
from django.test import TestCase
from django.utils import timezone, html
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.urls import reverse
from django.core.management import call_command
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
@ -161,10 +161,10 @@ class ClubTest(TestCase):
response = self.client.get(
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
)
self.assertTrue(response.status_code == 200)
self.assertTrue(
"""Richard Batsbak</a></td>\\n <td>Vice-Pr\\xc3\\xa9sident</td>"""
in str(response.content)
self.assertEqual(response.status_code, 200)
self.assertIn(
"""Richard Batsbak</a></td>\n <td>Vice-Président⸱e</td>""",
response.content.decode(),
)
def test_create_add_user_to_club_from_richard_fail(self):
@ -180,7 +180,7 @@ class ClubTest(TestCase):
)
self.assertTrue(response.status_code == 200)
self.assertTrue(
"<li>Vous n&#39;avez pas la permission de faire cela</li>"
"<li>Vous n&#x27;avez pas la permission de faire cela</li>"
in str(response.content)
)
@ -369,14 +369,15 @@ class ClubTest(TestCase):
response = self.client.get(
reverse("club:club_members", kwargs={"club_id": self.bdf.id})
)
self.assertTrue(response.status_code == 200)
content = str(response.content)
self.assertTrue(
"Richard Batsbak</a></td>\\n <td>Curieux</td>" in content
self.assertEqual(response.status_code, 200)
content = response.content.decode()
self.assertIn(
"Richard Batsbak</a></td>\n <td>Curieux⸱euse</td>",
content,
)
self.assertTrue(
"S&#39; Kia</a></td>\\n <td>Responsable info</td>"
in content
self.assertIn(
"S&#39; Kia</a></td>\n <td>Responsable info</td>",
content,
)

View File

@ -91,6 +91,11 @@ urlpatterns = [
MembershipSetOldView.as_view(),
name="membership_set_old",
),
re_path(
r"^membership/(?P<membership_id>[0-9]+)/delete$",
MembershipDeleteView.as_view(),
name="membership_delete",
),
re_path(
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"
),

View File

@ -23,6 +23,7 @@
#
#
import csv
from django.conf import settings
from django import forms
@ -30,19 +31,28 @@ from django.views.generic import ListView, DetailView, TemplateView, View
from django.views.generic.edit import DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView, CreateView
from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.http import (
HttpResponseRedirect,
HttpResponse,
Http404,
StreamingHttpResponse,
)
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _t
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext as _t
from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
from django.core.paginator import Paginator, InvalidPage
from django.shortcuts import get_object_or_404, redirect
from django.db.models import Sum
from core.views import (
CanCreateMixin,
CanViewMixin,
CanEditMixin,
CanEditPropMixin,
UserIsRootMixin,
TabedViewMixin,
PageEditViewBase,
DetailFormView,
@ -59,7 +69,7 @@ from com.views import (
)
from club.models import Club, Membership, Mailing, MailingSubscription
from club.forms import MailingForm, ClubEditForm, ClubMemberForm, SellingsFormBase
from club.forms import MailingForm, ClubEditForm, ClubMemberForm, SellingsForm
class ClubTabsMixin(TabedViewMixin):
@ -280,7 +290,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
def form_valid(self, form):
"""
Check user rights
Check user rights
"""
resp = super(ClubMembersView, self).form_valid(form)
@ -318,7 +328,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
current_tab = "elderlies"
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
"""
Sellings of a club
"""
@ -327,21 +337,35 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
pk_url_kwarg = "club_id"
template_name = "club/club_sellings.jinja"
current_tab = "sellings"
form_class = SellingsForm
paginate_by = 70
def get_form_class(self):
kwargs = {
"product": forms.ModelChoiceField(
self.object.products.order_by("name").all(),
label=_("Product"),
required=False,
)
}
return type("SellingsForm", (SellingsFormBase,), kwargs)
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)
def get_form_kwargs(self):
kwargs = super(ClubSellingView, self).get_form_kwargs()
kwargs["club"] = self.object
return kwargs
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs = super(ClubSellingView, self).get_context_data(**kwargs)
form = self.get_form_class()(self.request.GET)
qs = Selling.objects.filter(club=self.object)
kwargs["result"] = qs[:0]
kwargs["paginated_result"] = kwargs["result"]
kwargs["total"] = 0
kwargs["total_quantity"] = 0
kwargs["benefit"] = 0
form = self.get_form()
if form.is_valid():
if not len([v for v in form.cleaned_data.values() if v is not None]):
qs = Selling.objects.filter(id=-1)
@ -349,19 +373,36 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
qs = qs.filter(date__gte=form.cleaned_data["begin_date"])
if form.cleaned_data["end_date"]:
qs = qs.filter(date__lte=form.cleaned_data["end_date"])
if form.cleaned_data["counter"]:
qs = qs.filter(counter=form.cleaned_data["counter"])
if form.cleaned_data["product"]:
qs = qs.filter(product__id=form.cleaned_data["product"].id)
if form.cleaned_data["counters"]:
qs = qs.filter(counter__in=form.cleaned_data["counters"])
selected_products = []
if form.cleaned_data["products"]:
selected_products.extend(form.cleaned_data["products"])
if form.cleaned_data["archived_products"]:
selected_products.extend(form.cleaned_data["archived_products"])
if len(selected_products) > 0:
qs = qs.filter(product__in=selected_products)
kwargs["result"] = qs.all().order_by("-id")
kwargs["total"] = sum([s.quantity * s.unit_price for s in qs.all()])
kwargs["total_quantity"] = sum([s.quantity for s in qs.all()])
kwargs["benefit"] = kwargs["total"] - sum(
[s.product.purchase_price for s in qs.exclude(product=None)]
kwargs["total"] = sum([s.quantity * s.unit_price for s in kwargs["result"]])
total_quantity = qs.all().aggregate(Sum("quantity"))
if total_quantity["quantity__sum"]:
kwargs["total_quantity"] = total_quantity["quantity__sum"]
benefit = (
qs.exclude(product=None).all().aggregate(Sum("product__purchase_price"))
)
else:
kwargs["result"] = qs[:0]
kwargs["form"] = form
if benefit["product__purchase_price__sum"]:
kwargs["benefit"] = benefit["product__purchase_price__sum"]
kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by)
try:
kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page)
except InvalidPage:
raise Http404
return kwargs
@ -370,16 +411,46 @@ class ClubSellingCSVView(ClubSellingView):
Generate sellings in csv for a given period
"""
def get(self, request, *args, **kwargs):
import csv
class StreamWriter:
"""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."""
return value
def write_selling(self, selling):
row = [selling.date, selling.counter]
if selling.seller:
row.append(selling.seller.get_display_name())
else:
row.append("")
if selling.customer:
row.append(selling.customer.user.get_display_name())
else:
row.append("")
row = row + [
selling.label,
selling.quantity,
selling.quantity * selling.unit_price,
selling.get_payment_method_display(),
]
if selling.product:
row.append(selling.product.selling_price)
row.append(selling.product.purchase_price)
row.append(selling.product.selling_price - selling.product.purchase_price)
else:
row = row + ["", "", ""]
return row
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="text/csv")
self.object = self.get_object()
name = _("Sellings") + "_" + self.object.name + ".csv"
response["Content-Disposition"] = "filename=" + name
kwargs = self.get_context_data(**kwargs)
# Use the StreamWriter class instead of request for streaming
pseudo_buffer = self.StreamWriter()
writer = csv.writer(
response, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL
pseudo_buffer, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL
)
writer.writerow([_t("Quantity"), kwargs["total_quantity"]])
@ -400,29 +471,17 @@ class ClubSellingCSVView(ClubSellingView):
_t("Benefit"),
]
)
for o in kwargs["result"]:
row = [o.date, o.counter]
if o.seller:
row.append(o.seller.get_display_name())
else:
row.append("")
if o.customer:
row.append(o.customer.user.get_display_name())
else:
row.append("")
row = row + [
o.label,
o.quantity,
o.quantity * o.unit_price,
o.get_payment_method_display(),
]
if o.product:
row.append(o.product.selling_price)
row.append(o.product.purchase_price)
row.append(o.product.selling_price - o.product.purchase_price)
else:
row = row + ["", "", ""]
writer.writerow(row)
# Stream response
response = StreamingHttpResponse(
(
writer.writerow(self.write_selling(selling))
for selling in kwargs["result"]
),
content_type="text/csv",
)
name = _("Sellings") + "_" + self.object.name + ".csv"
response["Content-Disposition"] = "filename=" + name
return response
@ -493,6 +552,19 @@ class MembershipSetOldView(CanEditMixin, DetailView):
)
class MembershipDeleteView(UserIsRootMixin, DeleteView):
"""
Delete a membership (for admins only)
"""
model = Membership
pk_url_kwarg = "membership_id"
template_name = "core/delete_confirm.jinja"
def get_success_url(self):
return reverse_lazy("core:user_clubs", kwargs={"user_id": self.object.user.id})
class ClubStatView(TemplateView):
template_name = "club/stats.jinja"

View File

@ -26,11 +26,11 @@
from django.shortcuts import render
from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.urls import reverse
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.templatetags.static import static
from django.core.mail import EmailMultiAlternatives
from django.core.exceptions import ValidationError
@ -61,6 +61,11 @@ NEWS_TYPES = [
("CALL", _("Call")),
]
WEEKMAIL_TYPE = [
("WEEKMAIL", _("Weekmail")),
("INVITATION", _("Invitation")),
]
class News(models.Model):
"""The news class"""
@ -178,6 +183,9 @@ class Weekmail(models.Model):
protip = models.TextField(_("protip"), blank=True)
conclusion = models.TextField(_("conclusion"), blank=True)
sent = models.BooleanField(_("sent"), default=False)
type = models.CharField(
_("type"), max_length=16, choices=WEEKMAIL_TYPE, default="WEEKMAIL"
)
class Meta:
ordering = ["-id"]
@ -215,6 +223,17 @@ class Weekmail(models.Model):
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
).content.decode("utf-8")
def switch_type(self):
"""
Switch the type of weekmail we are sending :
- a simple weekmail
- or an invitation
"""
if self.type == "INVITATION":
self.type = "WEEKMAIL"
else:
self.type = "INVITATION"
def render_html(self):
"""
Renders an HTML version of the mail with images and fancy CSS.
@ -227,13 +246,20 @@ class Weekmail(models.Model):
"""
Return an absolute link to the banner.
"""
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA19.jpg")
if self.type == "INVITATION":
return (
"http://" + settings.SITH_URL + static("com/img/invitation_bannerP22.png")
)
return (
"http://" + settings.SITH_URL + static("com/img/weekmail_bannerV2P22.png")
)
def get_footer(self):
"""
Return an absolute link to the footer.
"""
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerA19.jpg")
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerP22.png")
def __str__(self):
return "Weekmail %s (sent: %s) - %s" % (self.id, self.sent, self.title)

View File

@ -6,152 +6,150 @@
{% endblock %}
{% block content %}
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<div id="news_admin">
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
</div>
{% endif %}
<div id="news">
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<div id="news_admin">
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
</div>
{% endif %}
<div id="right_column" class="news_column">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 birthdays %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
<div id="left_column" class="news_column">
{% for news in object_list.filter(type="NOTICE") %}
<section class="news_notice">
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(type="NOTICE") %}
<section class="news_notice">
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
dates__end_date__gte=timezone.now(), type="CALL") %}
<section class="news_call">
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
<section class="news_call">
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5),
news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
{% if events_dates %}
{% for d in events_dates %}
<div class="news_events_group">
<div class="news_events_group_date">
<div>
<div>{{ d|localtime|date('D') }}</div>
<div class="day">{{ d|localtime|date('d') }}</div>
<div>{{ d|localtime|date('b') }}</div>
</div>
</div>
<div class="news_events_group_items">
{% for news in object_list.filter(dates__start_date__gte=d,
dates__start_date__lte=d+timedelta(days=1),
type="EVENT").exclude(dates__end_date__lt=timezone.now())
.order_by('dates__start_date') %}
<section class="news_event">
<div class="club_logo">
{% if news.club.logo %}
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
{% else %}
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
{% endif %}
</div>
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}
<div class="button_bar">
{{ fb_quick(news) }}
{{ tweet_quick(news) }}
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="news_empty">
<em>{% trans %}Nothing to come...{% endtrans %}</em>
</div>
{% endif %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
{% if events_dates %}
{% for d in events_dates %}
<div class="news_events_group">
<div class="news_events_group_date">
<div>
<div>{{ d|localtime|date('D') }}</div>
<div class="day">{{ d|localtime|date('d') }}</div>
<div>{{ d|localtime|date('b') }}</div>
</div>
</div>
<div class="news_events_group_items">
{% for news in object_list.filter(dates__start_date__gte=d,
dates__start_date__lte=d+timedelta(days=1),
type="EVENT").exclude(dates__end_date__lt=timezone.now())
.order_by('dates__start_date') %}
<section class="news_event">
<div class="club_logo">
{% if news.club.logo %}
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
{% else %}
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
{% endif %}
</div>
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}
<div class="button_bar">
{{ fb_quick(news) }}
{{ tweet_quick(news) }}
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="news_empty">
<em>{% trans %}Nothing to come...{% endtrans %}</em>
</div>
{% endif %}
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
type="EVENT").order_by('dates__start_date') %}
{% if coming_soon %}
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
{% for news in coming_soon %}
<section class="news_coming_soon">
<a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a>
<span class="news_date">{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</section>
{% endfor %}
{% endif %}
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
type="EVENT").order_by('dates__start_date') %}
{% if coming_soon %}
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
{% for news in coming_soon %}
<section class="news_coming_soon">
<a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a>
<span class="news_date">{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</section>
{% endfor %}
{% endif %}
</div>
<div id="right_column" class="news_column">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 "birthdays" %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -36,8 +36,8 @@
<div class="name">{{ poster.name }}</div>
<div class="image"><img src="{{ poster.file.url }}"></img></div>
<div class="dates">
<div class="begin">{{ poster.date_begin | date("d/M/Y H:m") }}</div>
<div class="end">{{ poster.date_end | date("d/M/Y H:m") }}</div>
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
<div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
</div>
{% if app == "com" %}
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>

View File

@ -10,6 +10,7 @@
<p><a href="{{ url('com:weekmail_preview') }}">{% trans %}Preview{% endtrans %}</a></p>
<p><a href="{{ url('com:weekmail_preview') }}?send=true">{% trans %}Send{% endtrans %}</a></p>
<p><a href="{{ url('com:weekmail_article') }}">{% trans %}New article{% endtrans %}</a></p>
<p><a href="{{ url('com:weekmail') }}" onclick="{{weekmail.switch_type()}}">{% trans %}Switch invitation/weekmail{% endtrans %}</a></p>
<h4>{% trans %}Articles in no weekmail yet{% endtrans %}</h4>
<table>
<thead>

View File

@ -7,15 +7,33 @@
{% block content %}
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
{% if request.GET['send'] %}
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
{% endif %}
<form method="post" action="">
{% csrf_token %}
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
</form>
{% if bad_recipients %}
<p>
<span class="important">
{% trans %}The following recipients were refused by the SMTP:{% endtrans %}
</span>
<ul>
{% for r in bad_recipients.keys() %}
<li>{{ r }}</li>
{% endfor %}
</ul>
</p>
<form method="post" action="">
{% csrf_token %}
<button type="submit" name="send" value="clean">{% trans %}Clean subscribers{% endtrans %}</button>
</form>
{% else %}
{% if request.GET['send'] %}
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
{% endif %}
<form method="post" action="">
{% csrf_token %}
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
</form>
{% endif %}
{% endif %}
<hr>
{{ weekmail_rendered|safe }}

View File

@ -27,7 +27,7 @@ from django.conf import settings
from django.urls import reverse
from django.core.management import call_command
from django.utils import html
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from core.models import User, RealGroup
@ -40,8 +40,8 @@ class ComAlertTest(TestCase):
def test_page_is_working(self):
self.client.login(username="comunity", password="plop")
response = self.client.get(reverse("com:alert_edit"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
self.assertNotEqual(response.status_code, 500)
self.assertEqual(response.status_code, 200)
class ComInfoTest(TestCase):
@ -51,8 +51,8 @@ class ComInfoTest(TestCase):
def test_page_is_working(self):
self.client.login(username="comunity", password="plop")
response = self.client.get(reverse("com:info_edit"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
self.assertNotEqual(response.status_code, 500)
self.assertEqual(response.status_code, 200)
class ComTest(TestCase):
@ -79,9 +79,11 @@ class ComTest(TestCase):
)
r = self.client.get(reverse("core:index"))
self.assertTrue(r.status_code == 200)
self.assertTrue(
"""<div id="alert_box">\\n <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>"""
in str(r.content)
self.assertContains(
r,
"""<div id="alert_box">
<div class="markdown"><h3>ALERTE!</h3>
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
)
def test_info_msg(self):
@ -95,9 +97,10 @@ class ComTest(TestCase):
)
r = self.client.get(reverse("core:index"))
self.assertTrue(r.status_code == 200)
self.assertTrue(
"""<div id="info_box">\\n <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>"""
in str(r.content)
self.assertContains(
r,
"""<div id="info_box">
<div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
)
def test_birthday_non_subscribed_user(self):

View File

@ -28,7 +28,7 @@ from django.http import HttpResponseRedirect
from django.views.generic import ListView, DetailView, View
from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
from django.core.exceptions import ValidationError
from django.utils import timezone
@ -39,6 +39,7 @@ from django.core.exceptions import PermissionDenied
from django import forms
from datetime import timedelta
from smtplib import SMTPRecipientsRefused
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
from core.views import (
@ -52,6 +53,7 @@ from core.views import (
from core.views.forms import SelectDateTime, MarkdownInput
from core.models import Notification, RealGroup, User
from club.models import Club, Mailing
from core.views.forms import TzAwareDateTimeField
# Sith object
@ -72,20 +74,14 @@ class PosterForm(forms.ModelForm):
"display_time",
]
widgets = {"screens": forms.CheckboxSelectMultiple}
help_texts = {"file": _("Format: 16:9 | Resolution: 1920x1080")}
date_begin = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
date_begin = TzAwareDateTimeField(
label=_("Start date"),
widget=SelectDateTime,
required=True,
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
)
date_end = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=False,
)
date_end = TzAwareDateTimeField(label=_("End date"), required=False)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
@ -199,24 +195,10 @@ class NewsForm(forms.ModelForm):
"content": MarkdownInput,
}
start_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"),
widget=SelectDateTime,
required=False,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=False,
)
until = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Until"),
widget=SelectDateTime,
required=False,
)
start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
until = TzAwareDateTimeField(label=_("Until"), required=False)
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
def clean(self):
@ -433,23 +415,36 @@ class NewsDetailView(CanViewMixin, DetailView):
# Weekmail
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
model = Weekmail
template_name = "com/weekmail_preview.jinja"
success_url = reverse_lazy("com:weekmail")
current_tab = "weekmail"
def dispatch(self, request, *args, **kwargs):
self.bad_recipients = []
return super(WeekmailPreviewView, self).dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
try:
if request.POST["send"] == "validate":
if request.POST["send"] == "validate":
try:
self.object.send()
return HttpResponseRedirect(
reverse("com:weekmail") + "?qn_weekmail_send_success"
)
except:
pass
return super(WeekmailEditView, self).get(request, *args, **kwargs)
except SMTPRecipientsRefused as e:
self.bad_recipients = e.recipients
elif request.POST["send"] == "clean":
try:
self.object.send() # This should fail
except SMTPRecipientsRefused as e:
users = User.objects.filter(email__in=e.recipients.keys())
for u in users:
u.preferences.receive_weekmail = False
u.preferences.save()
self.quick_notif_list += ["qn_success"]
return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
def get_object(self, queryset=None):
return self.model.objects.filter(sent=False).order_by("-id").first()
@ -458,6 +453,7 @@ class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
"""Add rendered weekmail"""
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
kwargs["weekmail_rendered"] = self.object.render_html()
kwargs["bad_recipients"] = self.bad_recipients
return kwargs
@ -534,7 +530,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
return super(WeekmailEditView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""Add orphan articles """
"""Add orphan articles"""
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
return kwargs

View File

@ -21,5 +21,3 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
default_app_config = "core.apps.SithConfig"

View File

@ -0,0 +1,107 @@
import re
from subprocess import PIPE, Popen, TimeoutExpired
from django.conf import settings
from django.core.management.base import BaseCommand
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
# added "v?"
semver_regex = re.compile(
"""^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
)
class Command(BaseCommand):
help = "Checks the front dependencies are up to date."
def handle(self, *args, **options):
deps = settings.SITH_FRONT_DEP_VERSIONS
processes = dict(
(url, create_process(url))
for url in deps.keys()
if parse_semver(deps[url]) is not None
)
for url, process in processes.items():
try:
stdout, stderr = process.communicate(timeout=15)
except TimeoutExpired:
process.kill()
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
continue
# error, notice, warning
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if stderr != "":
self.stderr.write(self.style.WARNING(stderr.strip()))
continue
# get all tags, parse them as semvers and find the biggest
tags = list_tags(stdout)
tags = map(parse_semver, tags)
tags = filter(lambda tag: tag is not None, tags)
latest_version = max(tags)
# cannot fail as those which fail are filtered in the processes dict creation
current_version = parse_semver(deps[url])
assert current_version is not None
if latest_version == current_version:
msg = "{}: {}".format(url, semver_to_s(current_version))
self.stdout.write(self.style.SUCCESS(msg))
else:
msg = "{}: {} < {}".format(
url, semver_to_s(current_version), semver_to_s(latest_version)
)
self.stdout.write(self.style.ERROR(msg))
def create_process(url):
"""Spawn a "git ls-remote --tags" child process."""
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
def list_tags(s):
"""Parses "git ls-remote --tags" output. Takes a string."""
tag_prefix = "refs/tags/"
for line in s.strip().split("\n"):
# an example line could be:
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
parts = line.split("\t")
# check we have a commit ID (SHA-1 hash) and a tag name
assert len(parts) == 2
assert len(parts[0]) == 40
assert parts[1].startswith(tag_prefix)
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
if not parts[1].endswith("^{}"):
yield parts[1][len(tag_prefix) :]
def parse_semver(s):
"""
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
prerelease or it has build metadata.
See https://semver.org
"""
m = semver_regex.match(s)
if (
m is None
or m.group("prerelease") is not None
or m.group("buildmetadata") is not None
):
return None
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
def semver_to_s(t):
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
return "{}.{}.{}".format(t[0], t[1], t[2])

View File

@ -31,7 +31,7 @@ from django.conf import settings
class Command(BaseCommand):
"""
Compiles scss in static folder for production
Compiles scss in static folder for production
"""
help = "Compile scss files from static folder"

View File

@ -25,13 +25,12 @@
import os
import sys
import signal
from http.server import test, CGIHTTPRequestHandler
from django.core.management.base import BaseCommand
# TODO Django 2.2 : implement autoreload following
# https://stackoverflow.com/questions/42907285/django-autoreload-add-watched-file
from django.utils import autoreload
class Command(BaseCommand):
@ -45,15 +44,15 @@ class Command(BaseCommand):
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
)
def handle(self, *args, **kwargs):
os.chdir("doc")
def build_documentation(self):
os.chdir(os.path.join(self.project_dir, "doc"))
err = os.system("make html")
if err != 0:
self.stdout.write("A build error occured, exiting")
sys.exit(err)
self.stdout.write("A build error occured")
os.chdir("_build/html")
def start_server(self, **kwargs):
os.chdir(os.path.join(self.project_dir, "doc", "_build/html"))
addr = self.default_addr
port = self.default_port
if kwargs["addrport"]:
@ -69,3 +68,25 @@ class Command(BaseCommand):
sys.exit(0)
test(HandlerClass=CGIHTTPRequestHandler, port=int(port), bind=addr)
def build_and_start_server(self, **kwargs):
self.build_documentation()
self.start_server(**kwargs)
def handle(self, *args, **kwargs):
self.project_dir = os.getcwd()
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get(autoreload.DJANGO_AUTORELOAD_ENV) == "true":
reloader = autoreload.get_reloader()
reloader.watch_dir(os.path.join(self.project_dir, "doc"), "**/*.rst")
autoreload.logger.info(
"Watching for file changes with %s", reloader.__class__.__name__
)
autoreload.start_django(reloader, self.build_and_start_server, **kwargs)
else:
exit_code = autoreload.restart_with_reloader()
sys.exit(exit_code)
except KeyboardInterrupt:
pass

View File

@ -611,6 +611,7 @@ Welcome to the wiki page!
mde.products.add(cons)
mde.products.add(dcons)
mde.sellers.add(skia)
mde.save()
eboutic = Counter.objects.filter(name="Eboutic").first()
@ -935,6 +936,7 @@ Welcome to the wiki page!
# Add barman to counter
c = Counter.objects.get(id=2)
c.sellers.add(User.objects.get(pk=krophil.pk))
mde.sellers.add(sli)
c.save()
# Create an election

View File

@ -23,6 +23,7 @@
#
import importlib
import threading
from django.conf import settings
from django.utils.functional import SimpleLazyObject
from django.contrib.auth import get_user
@ -49,8 +50,31 @@ class AuthenticationMiddleware(DjangoAuthenticationMiddleware):
def process_request(self, request):
assert hasattr(request, "session"), (
"The Django authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert "
"to be installed. Edit your MIDDLEWARE setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'account.middleware.AuthenticationMiddleware'."
)
request.user = SimpleLazyObject(lambda: get_cached_user(request))
_threadlocal = threading.local()
def get_signal_request():
"""
!!! Do not use if your operation is asynchronus !!!
Allow to access current request in signals
This is a hack that looks into the thread
Mainly used for log purpose
"""
return getattr(_threadlocal, "request", None)
class SignalRequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
setattr(_threadlocal, "request", request)
return self.get_response(request)

View File

@ -0,0 +1,51 @@
# Generated by Django 2.2.6 on 2019-11-14 15:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("core", "0033_auto_20191006_0049"),
]
operations = [
migrations.CreateModel(
name="OperationLog",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True, verbose_name="date")),
("label", models.CharField(max_length=255, verbose_name="label")),
(
"operation_type",
models.CharField(
choices=[
("SELLING_DELETION", "Selling deletion"),
("REFILLING_DELETION", "Refilling deletion"),
],
max_length=40,
verbose_name="operation type",
),
),
(
"operator",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="logs",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.10 on 2020-02-16 16:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0034_operationlog"),
]
operations = [
migrations.AlterField(
model_name="user",
name="sex",
field=models.CharField(
blank=True,
choices=[("MAN", "Man"), ("WOMAN", "Woman")],
max_length=10,
null=True,
verbose_name="sex",
),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.24 on 2021-10-01 00:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0035_auto_20200216_1743")]
operations = [
migrations.AddField(
model_name="user",
name="pronouns",
field=models.CharField(default="", max_length=64, verbose_name="pronouns"),
preserve_default=False,
),
migrations.AlterField(
model_name="user",
name="sex",
field=models.CharField(
blank=True,
choices=[("MAN", "Man"), ("WOMAN", "Woman"), ("OTHER", "Other")],
max_length=10,
null=True,
verbose_name="sex",
),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-11-05 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0036_auto_20211001_0248")]
operations = [
migrations.AlterField(
model_name="user",
name="pronouns",
field=models.CharField(
blank=True, default="", max_length=64, verbose_name="pronouns"
),
)
]

View File

@ -34,7 +34,7 @@ from django.contrib.auth.models import (
GroupManager as AuthGroupManager,
AnonymousUser as AuthAnonymousUser,
)
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.core import validators
from django.core.exceptions import ValidationError, PermissionDenied
@ -65,11 +65,19 @@ class MetaGroupManager(AuthGroupManager):
class Group(AuthGroup):
"""
Implement both RealGroups and Meta groups
Groups are sorted by their is_meta property
"""
#: If False, this is a RealGroup
is_meta = models.BooleanField(
_("meta group status"),
default=False,
help_text=_("Whether a group is a meta group or not"),
)
#: Description of the group
description = models.CharField(_("description"), max_length=60)
class Meta:
@ -83,6 +91,15 @@ class Group(AuthGroup):
class MetaGroup(Group):
"""
MetaGroups are dynamically created groups.
Generaly used with clubs where creating a club creates two groups:
* club-SITH_BOARD_SUFFIX
* club-SITH_MEMBER_SUFFIX
"""
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=False
objects = MetaGroupManager()
class Meta:
@ -94,6 +111,12 @@ class MetaGroup(Group):
class RealGroup(Group):
"""
RealGroups are created by the developer.
Most of the time they match a number in settings to be easily used for permissions.
"""
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=True
objects = RealGroupManager()
class Meta:
@ -202,9 +225,11 @@ class User(AbstractBaseUser):
sex = models.CharField(
_("sex"),
max_length=10,
choices=[("MAN", _("Man")), ("WOMAN", _("Woman"))],
default="MAN",
null=True,
blank=True,
choices=[("MAN", _("Man")), ("WOMAN", _("Woman")), ("OTHER", _("Other"))],
)
pronouns = models.CharField(_("pronouns"), max_length=64, blank=True, default="")
tshirt_size = models.CharField(
_("tshirt size"),
max_length=5,
@ -389,6 +414,20 @@ class User(AbstractBaseUser):
.has_rights_in_club(self)
)
@cached_property
def can_read_subscription_history(self):
if self.is_root or self.is_board_member:
return True
from club.models import Club
for club in Club.objects.filter(
id__in=settings.SITH_CAN_READ_SUBSCRIPTION_HISTORY
).all():
if club.has_rights_in_club(self):
return True
return False
@cached_property
def can_create_subscription(self):
from club.models import Club
@ -666,6 +705,10 @@ class AnonymousUser(AuthAnonymousUser):
def can_create_subscription(self):
return False
@property
def can_read_subscription_history(self):
return False
@property
def was_subscribed(self):
return False
@ -1454,3 +1497,24 @@ class Gift(models.Model):
def is_owned_by(self, user):
return user.is_board_member or user.is_root
class OperationLog(models.Model):
"""
General purpose log object to register operations
"""
date = models.DateTimeField(_("date"), auto_now_add=True)
label = models.CharField(_("label"), max_length=255)
operator = models.ForeignKey(
User, related_name="logs", on_delete=models.SET_NULL, null=True
)
operation_type = models.CharField(
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE
)
def is_owned_by(self, user):
return user.is_root
def __str__(self):
return "%s - %s - %s" % (self.operation_type, self.label, self.operator)

View File

@ -33,16 +33,16 @@ from django.db import connection, migrations
class PsqlRunOnly(migrations.RunSQL):
"""
This is an SQL runner that will launch the given command only if
the used DBMS is PostgreSQL.
It may be useful to run Postgres' specific SQL, or to take actions
that would be non-senses with backends other than Postgre, such
as disabling particular constraints that would prevent the migration
to run successfully.
This is an SQL runner that will launch the given command only if
the used DBMS is PostgreSQL.
It may be useful to run Postgres' specific SQL, or to take actions
that would be non-senses with backends other than Postgre, such
as disabling particular constraints that would prevent the migration
to run successfully.
See `club/migrations/0010_auto_20170912_2028.py` as an example.
Some explanations can be found here too:
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
See `club/migrations/0010_auto_20170912_2028.py` as an example.
Some explanations can be found here too:
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
"""
def _run_sql(self, schema_editor, sqls):

View File

@ -25,9 +25,9 @@
import os
import sass
from urllib.parse import urljoin
from django.utils.encoding import force_bytes, iri_to_uri
from django.core.files.base import ContentFile
from django.utils.six.moves.urllib.parse import urljoin
from django.templatetags.static import static
from django.conf import settings
from core.scss.storage import ScssFileStorage, find_file
@ -35,9 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
class ScssProcessor(object):
"""
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production
"""
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))

View File

@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
class UserIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True)
last_update = indexes.DateTimeField(model_attr="last_update")
def get_model(self):
return User
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
def get_updated_field(self):
return "last_update"
def prepare_auto(self, obj):
return self.prepared_data["auto"].strip()[:245]
class IndexSignalProcessor(signals.BaseSignalProcessor):
def setup(self):

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because one or more lines are too long

View File

@ -28,7 +28,7 @@ $twitblue: hsl(206, 82%, 63%);
$shadow-color: rgb(223, 223, 223);
$background-bouton-color: hsl(0, 0%, 90%);
$background-button-color: hsl(0, 0%, 95%);
/*--------------------------MEDIA QUERY HELPERS------------------------*/
$small-devices: 576px;
@ -47,10 +47,11 @@ body {
input[type=button], input[type=submit], input[type=reset],input[type=file] {
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 10px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-weight: bold;
font-size: 16px;
font-size: 1.2em;
border-radius: 5px;
cursor: pointer;
box-shadow: $shadow-color 0px 0px 1px;
@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
button{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 10px;
font-size: 14px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.18em;
border-radius: 5px;
box-shadow: $shadow-color 0px 0px 1px;
cursor: pointer;
@ -75,24 +77,26 @@ button{
input,textarea[type=text],[type=number]{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 7px;
font-size: 16px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.2em;
border-radius: 5px;
max-width: 95%;
}
textarea{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
background-color: $background-button-color;
padding: 7px;
font-size: 16px;
font-size: 1.2em;
border-radius: 5px;
}
select{
border: none;
text-decoration: none;
font-size: 15px;
background-color: $background-bouton-color;
font-size: 1.2em;
background-color: $background-button-color;
padding: 10px;
border-radius: 5px;
cursor: pointer;
@ -130,9 +134,10 @@ a {
#header_language_chooser {
position: absolute;
top: 0.2em;
right: 0.5em;
top: 2em;
left: 0.5em;
width: 3%;
min-width: 2.2em;
text-align: center;
input {
display: block;
@ -157,9 +162,6 @@ header {
border-radius: 0px 0px 10px 10px;
#header_logo {
display: inline-block;
flex: none;
background-size: 100% 100%;
background-color: $white-color;
padding: 0.2em;
border-radius: 0px 0px 0px 9px;
@ -169,11 +171,19 @@ header {
margin: 0px;
width: 100%;
height: 100%;
img {
max-width: 70%;
max-height: 100%;
margin: auto;
display: block;
}
}
}
#header_connect_links {
margin: 0.6em 0.6em 0em auto;
padding: 0.2em;
color: $white-color;
form {
display: inline;
@ -190,6 +200,7 @@ header {
#header_bar {
display: flex;
flex: auto;
flex-wrap: wrap;
width: 80%;
a {
@ -203,7 +214,6 @@ header {
}
#header_bars_infos {
width: 35ch;
flex: initial;
list-style-type: none;
margin: 0.2em 0.2em;
@ -213,12 +223,15 @@ header {
display: inline-block;
flex: auto;
margin: 0.8em 0em;
input {
width: 14ch;
}
}
#header_user_links {
display: flex;
width: 120ch;
flex: initial;
flex-wrap: wrap;
text-align: right;
margin: 0em;
div {
@ -287,42 +300,34 @@ header {
#info_boxes {
display: flex;
flex-wrap: wrap;
width: 90%;
margin: 1em auto;
p {
margin: 0px;
padding: 7px;
}
#alert_box, #info_box {
font-size: 14px;
display: inline-block;
flex: auto;
padding: 2px;
margin: 0.2em 1.5%;
min-width: 10%;
max-width: 46%;
min-height: 20px;
flex: 49%;
font-size: 0.9em;
margin: 0.2em;
border-radius: 0.6em;
.markdown {
margin: 0.5em;
}
&:before {
float: left;
font-family: FontAwesome;
font-size: 4em;
float: right;
margin: 0.2em;
}
}
#info_box {
border-radius: 10px;
background: $primary-neutral-light-color;
&:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f05a";
color: hsl(210, 100%, 56%);
}
}
#alert_box {
border-radius: 10px;
background: $second-color;
&:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f06a";
color: $white-color;
}
@ -345,7 +350,7 @@ header {
a {
flex: auto;
text-align: center;
padding: 20px;
padding: 1.5em;
color: $white-color;
font-style: normal;
font-weight: bolder;
@ -458,6 +463,8 @@ header {
/*---------------------------------NEWS--------------------------------*/
#news {
display: flex;
flex-wrap: wrap;
.news_column {
display: inline-block;
margin: 0px;
@ -467,11 +474,13 @@ header {
margin-bottom: 1em;
}
#right_column {
width: 20%;
flex: 20%;
float: right;
margin: 0.2em;
}
#left_column {
width: 79%;
flex: 79%;
margin: 0.2em;
h3 {
background: $second-color;
box-shadow: $shadow-color 1px 1px 1px;
@ -484,6 +493,11 @@ header {
}
}
}
@media screen and (max-width: $small-devices){
#left_column, #right_column {
flex: 100%;
}
}
/* AGENDA/BIRTHDAYS */
#agenda,#birthdays {
@ -691,6 +705,12 @@ header {
}
}
@media screen and (max-width: $small-devices){
#page {
width: 98%;
}
}
#news_details {
display: inline-block;
margin-top: 20px;
@ -723,7 +743,7 @@ header {
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
font-size: 1.2em;
border-radius: 2px;
float: right;
display: block;
@ -1111,33 +1131,36 @@ u, .underline {
text-decoration: underline;
}
#basket {
width: 40%;
background: $primary-neutral-light-color;
float: right;
padding: 10px;
border-radius: 10px;
}
#products {
width: 90%;
margin: 0px auto;
overflow: auto;
}
#bar_ui {
float: left;
min-width: 57%;
}
padding: 0.4em;
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
#user_info_container {}
#products {
flex-basis: 100%;
margin: 0.2em;
overflow: auto;
}
#user_info {
float: right;
padding: 5px;
width: 40%;
margin: 0px auto;
background: $secondary-neutral-light-color;
#click_form {
flex: auto;
margin: 0.2em;
}
#user_info {
flex: auto;
padding: 0.5em;
margin: 0.2em;
height: 100%;
background: $secondary-neutral-light-color;
img {
max-width: 70%;
}
input {
background: white;
}
}
}
/*-----------------------------USER PROFILE----------------------------*/
@ -1169,6 +1192,7 @@ u, .underline {
display: flex;
align-items: center;
img {
width: 5em;
margin: 0.5em;
}
}
@ -1212,6 +1236,11 @@ u, .underline {
}
}
}
@media screen and (max-width: $small-devices){
#user_profile_infos, #user_profile_pictures {
flex-basis: 50%;
}
}
}
}
@ -1412,6 +1441,7 @@ textarea {
.search_bar {
margin: 10px 0px;
display: flex;
flex-wrap: wrap;
height: 20p;
align-items: center;
}
@ -1551,6 +1581,7 @@ footer {
color: $white-color;
border-radius: 5px;
display: flex;
flex-wrap: wrap;
background-color: $primary-neutral-dark-color;
box-shadow: $shadow-color 0px 0px 15px;
a {

View File

@ -0,0 +1,281 @@
$padding: 1.5rem;
$padding_smaller: .5rem;
$gap: .25rem;
$border: .01rem solid black;
$min_col_width: 100px;
.error {
color: red !important;
}
.radio-btn {
display: flex;
flex-direction: row;
gap: $gap;
> input,
> label {
margin: 0;
}
&:hover {
cursor: pointer;
}
}
.election_vote {
overflow-x: scroll !important;
}
.election_table {
width: 100%;
>.lists {
display: flex;
flex-direction: row;
>tr {
display: flex;
flex-direction: row;
width: 100%;
>.column {
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: center;
padding: $padding;
border: $border;
border-collapse: collapse;
position: relative;
min-width: $min_col_width;
>a{
margin-left: $padding;
width: 20px;
height: 20px;
text-align: center;
padding: 5px;
border-radius: 25%;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: $gap;
top: $gap;
&:hover {
background-color: #ddd;
}
}
}
}
}
>.role {
display: flex;
flex-direction: column;
>tr {
display: flex;
flex-direction: row;
background-color: lightgrey;
&:hover {
background-color: lightgrey;
}
>.role_title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0;
padding: $padding;
width: 100%;
>.role_text {
>h4 {
margin: 0;
}
>p {
margin-top: .5em;
}
}
>.role_buttons {
display: flex;
flex-direction: row;
align-items: center;
gap: $gap;
> button,
> button > i,
> a {
width: 20px;
height: 20px;
background-color: #e9e9e9;
text-align: center;
padding: 5px;
border-radius: 25%;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover,
&:hover > i {
background-color: #fff;
}
}
> button {
width: 30px;
height: 30px;
}
> button[disabled] {
background-color: #eee;
cursor: not-allowed;
>i,
&:hover,
&:hover > i {
background-color: #eee;
}
}
}
}
>.list_per_role {
display: flex;
flex-direction: row;
justify-content: center;
border: $border;
border-collapse: collapse;
background-color: #fff;
padding: $padding_smaller;
margin: 0;
min-width: $min_col_width;
>.candidates {
margin: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: $gap;
>.candidate {
display: flex;
flex-direction: column;
align-items: center;
list-style-type: none;
gap: $gap;
>input[type="radio"]:checked + label,
>input[type="checkbox"]:checked + label {
background-color: lightgray;
border-radius: 10px;
>figure>.edit_btns>a:hover{
background-color: #fff;
}
}
>label>figure,
>figure {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: $gap;
padding: 10px;
>img {
max-width: 100%;
}
>figcaption {
h5 {
margin: 0;
text-align: center;
}
q {
margin: 5px 0;
}
}
>.edit_btns {
position: absolute;
display: flex;
flex-direction: column;
top: $gap;
right: $gap;
gap: $gap;
> a {
width: 20px;
height: 20px;
background-color: #e9e9e9;
text-align: center;
padding: 5px;
border-radius: 25%;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #d8d8d8;
}
}
}
}
}
}
}
}
}
}
.election_details {
margin: .5em 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: $gap;
}
.button {
border: none;
color: black;
text-decoration: none;
background-color: #f2f2f2;
padding: 0.4em;
margin: 0.1em;
font-size: 1.18em;
border-radius: 5px;
box-shadow: #dfdfdf 0px 0px 1px;
cursor: pointer;
&:hover {
color: black;
background: #d4d4d4;
}
&_send {
background-color: #59aee2;
&:hover {
background-color: rgb(130, 186, 235);
}
}
}

View File

@ -1,7 +1,11 @@
{% extends "core/base.jinja" %}
{% block head %}
{{ super() }}
<script src="{{ static('core/js/sentry/bundle.min.js') }}" crossorigin="anonymous"></script>
<script
src="https://browser.sentry-cdn.com/7.11.1/bundle.min.js"
integrity="sha384-qcYSo5+/E8hEkPmHFa79GRDsGT84SRhBJHRw3+dbQyh0UwueiFP1jCsRBClEREcs"
crossorigin="anonymous"
></script>
{% endblock head %}
{% block content %}
@ -9,7 +13,15 @@
{% if settings.SENTRY_DSN %}
<script>
Sentry.init({ dsn: '{{ settings.SENTRY_DSN }}' });
Sentry.showReportDialog({ eventId: '{{ request.sentry_last_event_id() }}' })
Sentry.showReportDialog({
eventId: '{{ request.sentry_last_event_id() }}',
{% if user.is_authenticated %}
user: {
'name': '{{user.first_name}} {{user.last_name}}',
'email': '{{user.email}}'
}
{% endif %}
})
</script>
{% endif %}
{% endblock content %}

View File

@ -3,14 +3,16 @@
<head>
{% block head %}
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
<link rel="stylesheet" href="{{ static('core/js/ui/jquery-ui.min.css') }}">
<link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}">
<script href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
<link rel="preload" as="style" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}"></noscript>
<script defer href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
<!-- Jquery declared here to be accessible in every django widgets -->
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
@ -27,6 +29,7 @@
<!-- BEGIN HEADER -->
{% block header %}
{% if not popup %}
<header>
<div id="header_language_chooser">
{% for language in LANGUAGES %}
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
@ -37,10 +40,11 @@
{% endfor %}
</div>
<header>
{% if not user.is_authenticated %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;">
<a href="{{ url('core:index') }}"></a>
<div id="header_logo">
<a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div>
<div id="header_connect_links">
<form method="post" action="{{ url('core:login') }}">
@ -54,12 +58,14 @@
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
</div>
{% else %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 92px; height: 52px;">
<a href="{{ url('core:index') }}"></a>
<div id="header_logo">
<a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div>
<div id="header_bar">
<ul id="header_bars_infos">
{% cache 100 counters_activity %}
{% cache 100 "counters_activity" %}
{% for bar in Counter.objects.filter(type="BAR").all() %}
<li>
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
@ -85,7 +91,7 @@
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
</div>
<div>
<a href="#" onclick="display_notif()"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
<a href="#" onclick="display_notif()" style="white-space: nowrap;"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
<ul id="header_notif">
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
<li>
@ -126,17 +132,19 @@
</header>
<div id="info_boxes">
{% set sith = get_sith() %}
{% if sith.alert_msg %}
<div id="alert_box">
{{ sith.alert_msg|markdown }}
</div>
{% endif %}
{% if sith.info_msg %}
<div id="info_box">
{{ sith.info_msg|markdown }}
</div>
{% endif %}
{% block info_boxes %}
{% set sith = get_sith() %}
{% if sith.alert_msg %}
<div id="alert_box">
{{ sith.alert_msg|markdown }}
</div>
{% endif %}
{% if sith.info_msg %}
<div id="info_box">
{{ sith.info_msg|markdown }}
</div>
{% endif %}
{% endblock %}
</div>
{% else %}{# if not popup #}
@ -171,7 +179,7 @@
<i class="fa fa-caret-down"></i>
</button>
<div class="dropdown-content">
<a href="{{ url('core:page', page_name='Index/calendrier_evenements') }}">{% trans %}Calendar{% endtrans %}</a>
<a href="{{ url('election:list') }}">{% trans %}Elections{% endtrans %}</a>
<a href="{{ url('core:page', page_name='ga') }}">{% trans %}Big event{% endtrans %}</a>
</div>
</div>

View File

@ -4,6 +4,12 @@
{% trans %}Delete confirmation{% endtrans %}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
<form action="" method="post">{% csrf_token %}

View File

@ -55,6 +55,9 @@
{% if user.nick_name %}
<div class="user_mini_profile_nick">&laquo; {{ user.nick_name }} &raquo;</div>
{% endif %}
{% if user.pronouns %}
<div class="user_mini_profile_pronouns">{{ user.pronouns }}</div>
{% endif %}
{% if user.date_of_birth %}
<div class="user_mini_profile_dob">
{{ user.date_of_birth|date("d/m/Y") }} ({{ user.get_age() }})
@ -113,10 +116,11 @@
{% endif %}
{% endmacro %}
{% macro paginate(page_obj, paginator) %}
{% 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 href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
<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 %}
@ -124,11 +128,11 @@
{% if page_obj.number == i %}
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
{% else %}
<a href="?page={{ i }}">{{ i }}</a>
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next() %}
<a href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
<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 %}

View File

@ -63,7 +63,7 @@
<td>{{ i.amount }} €</td>
<td>{{ i.get_payment_method_display() }}</td>
{% if i.is_owned_by(user) %}
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">Delete</a></td>
<td><a href="{{ url('counter:refilling_delete', refilling_id=i.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %}
</tr>
{% endfor %}

View File

@ -28,6 +28,9 @@
{% if m.can_be_edited_by(user) %}
<td><a href="{{ url('club:membership_set_old', membership_id=m.id) }}">{% trans %}Mark as old{% endtrans %}</a></td>
{% endif %}
{% if user.is_root %}
<td><a href="{{ url('club:membership_delete', membership_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
@ -54,6 +57,9 @@
<td>{{ m.description }}</td>
<td>{{ m.start_date }}</td>
<td>{{ m.end_date }}</td>
{% if user.is_root %}
<td><a href="{{ url('club:membership_delete', membership_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>

View File

@ -15,6 +15,8 @@
<div id="user_profile_infos_nick">&laquo; {{ profile.nick_name }} &raquo;</div>
{% endif %}
{% if profile.quote %}
<div id="user_profile_infos_quote">
{{ profile.quote }}
@ -22,6 +24,12 @@
{% endif %}
<div id="user_profile_infos_items">
{% if profile.pronouns %}
<div>
<span class="user_profile_infos_item">{% trans %}Pronouns: {% endtrans %}</span>
<span class="user_profile_infos_item_value">{{ profile.pronouns }}</span>
</div>
{% endif %}
{% if profile.date_of_birth %}
<div>
<span class="user_profile_infos_item">{% trans %}Born: {% endtrans %}</span>
@ -137,7 +145,7 @@
{% endif %}
</div>
{% endif %}
{% if profile.was_subscribed and (user == profile or user.is_root or user.is_board_member)%}
{% if profile.was_subscribed and (user == profile or user.can_read_subscription_history)%}
<div id="drop_subscriptions">
<h5>{% trans %}Subscription history{% endtrans %}</h5>
<table>
@ -230,4 +238,3 @@ $(function(){
});
</script>
{% endblock %}

View File

@ -52,6 +52,7 @@
{% if not form.instance.profile_pict %}
<script src="{{ static('core/js/webcam.js') }}"></script>
<script language="JavaScript">
Webcam.on('error', function(msg) { console.log('Webcam.js error: ' + msg) })
Webcam.set({
width: 320,
height: 240,

View File

@ -2,14 +2,14 @@
{% from "core/macros.jinja" import user_link_with_pict, delete_godfather %}
{% block title %}
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s godfathers{% endtrans %}
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s family{% endtrans %}
{% endblock %}
{% block content %}
<p><a href="{{ url("core:user_godfathers_tree_pict", user_id=profile.id) }}?family">
{% trans %}Show family picture{% endtrans %}</a></p>
{% if profile.godfathers.exists() %}
<h4>{% trans %}Godfathers{% endtrans %}</h4>
<h4>{% trans %}Godfathers / Godmothers{% endtrans %}</h4>
<ul>
{% for u in profile.godfathers.all() %}
<li> <a href="{{ url("core:user_godfathers", user_id=u.id) }}" class="mini_profile_link" >
@ -19,7 +19,7 @@
<p><a href="{{ url("core:user_godfathers_tree", user_id=profile.id) }}">
{% trans %}Show ancestors tree{% endtrans %}</a></p>
{% else %}
<p>{% trans %}No godfathers{% endtrans %}
<p>{% trans %}No godfathers / godmothers{% endtrans %}
{% endif %}
{% if profile.godchildren.exists() %}
<h4>{% trans %}Godchildren{% endtrans %}</h4>

View File

@ -47,7 +47,7 @@
{% if param == "godchildren" %}
<p>{% trans %}No godchildren{% endtrans %}
{% else %}
<p>{% trans %}No godfathers{% endtrans %}
<p>{% trans %}No godfathers / godmothers{% endtrans %}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -5,6 +5,9 @@
{% endblock %}
{% block content %}
{% if can_edit(profile, user) %}
<button id="download_all_pictures", onclick=download_pictures()>{% trans %}Download all my pictures{% endtrans %}</button>
{% endif %}
{% for a in albums %}
<div style="padding: 10px">
<h4>{{ a.name }}</h4>
@ -12,7 +15,7 @@
{% for picture in pictures[a.id] %}
<div class="picture">
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%"/>
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%" loading="lazy"/>
</a>
</div>
{% endfor %}
@ -20,3 +23,67 @@
{% endfor %}
{% endblock %}
{% block script %}
{{ super() }}
<script type="text/javascript">
/**
* Download a list of files.
* @author speedplane
*/
function download_files(files) {
function download_next(i) {
if (i >= files.length) {
return;
}
var a = document.createElement('a');
a.href = files[i].download;
a.target = '_parent';
// Use a.download if available, it prevents plugins from opening.
if ('download' in a) {
a.download = files[i].filename;
}
// Add a to the doc for click to work.
(document.body || document.documentElement).appendChild(a);
if (a.click) {
a.click(); // The click method is supported by most browsers.
} else {
$(a).click(); // Backup using jquery
}
// Delete the temporary link.
a.parentNode.removeChild(a);
// Download the next file with a small timeout. The timeout is necessary
// for IE, which will otherwise only download the first file.
setTimeout(function() {
download_next(i + 1);
}, 500);
}
// Initiate the first download.
download_next(0);
}
function download_pictures() {
$("#download_all_pictures").prop("disabled", true);
var xhr = new XMLHttpRequest();
$.ajax({
type: "GET",
url: "{{ url('api:all_pictures_of_user', user=object.id) }}",
tryCount: 0,
xhr: function(){
return xhr;
},
success: function(data){
$("#download_all_pictures").prop("disabled", false);
to_download = [];
data.forEach(picture =>
to_download.push({ download: picture["full_size_url"], filename: picture["name"] })
);
download_files(to_download);
},
error: function(data){
console.log("Error retrieving data from url: " + data);
$("#download_all_pictures").text("{% trans %}Error downloading your pictures{% endtrans %}");
}
});
}
</script>
{% endblock %}

View File

@ -13,6 +13,7 @@
{% if user.is_root %}
<li><a href="{{ url('core:group_list') }}">{% trans %}Groups{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:merge') }}">{% trans %}Merge users{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
<li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
{% endif %}
{% if user.can_create_subscription or user.is_root %}

View File

@ -1,3 +1,13 @@
{{ object.first_name }}
{{ object.last_name }}
{{ object.nick_name }}
{% load search_helpers %}
{% with first=object.first_name|safe|slugify last=object.last_name|safe|slugify nick=object.nick_name|default_if_none:""|safe|slugify %}
{{ first|replace:"|-| " }}
{{ last|replace:"|-| " }}
{{ nick|replace:"|-| " }}
{% if first|count:"-" != 0 %}{{ first|cut:"-" }}{% endif %}
{% if last|count:"-" != 0 %}{{ last|cut:"-" }}{% endif %}
{% if nick|count:"-" != 0 %}{{ nick|cut:"-" }}{% endif %}
{{ first|cut:"-" }}{{ last|cut:"-" }}
{% endwith %}

View File

@ -94,7 +94,7 @@ def datetime_format_python_to_PHP(python_format_string):
@register.simple_tag()
def scss(path):
"""
Return path of the corresponding css file after compilation
Return path of the corresponding css file after compilation
"""
processor = ScssProcessor(path)
return processor.get_converted_scss()

View File

@ -0,0 +1,27 @@
from django.template.exceptions import TemplateSyntaxError
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
# arg should be of the form "|foo|bar" where the first character is the
# separator between old and new in value.replace(old, new)
@register.filter
@stringfilter
def replace(value, arg):
# s.replace('', '') == s so len(arg) == 2 is fine
if len(arg) < 2:
raise TemplateSyntaxError("badly formatted argument")
arg = arg.split(arg[0])
if len(arg) != 3:
raise TemplateSyntaxError("badly formatted argument")
return value.replace(arg[1], arg[2])
@register.filter
def count(value, arg):
return value.count(arg)

View File

@ -402,32 +402,32 @@ class UserToolsTest(TestCase):
def test_anonymous_user_unauthorized(self):
response = self.client.get(reverse("core:user_tools"))
self.assertEquals(response.status_code, 403)
self.assertEqual(response.status_code, 403)
def test_page_is_working(self):
# Test for simple user
self.client.login(username="guy", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
self.assertNotEqual(response.status_code, 500)
self.assertEqual(response.status_code, 200)
# Test for root
self.client.login(username="root", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
self.assertNotEqual(response.status_code, 500)
self.assertEqual(response.status_code, 200)
# Test for skia
self.client.login(username="skia", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
self.assertNotEqual(response.status_code, 500)
self.assertEqual(response.status_code, 200)
# Test for comunity
self.client.login(username="comunity", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
self.assertNotEqual(response.status_code, 500)
self.assertEqual(response.status_code, 200)
# TODO: many tests on the pages:

View File

@ -23,7 +23,7 @@
#
#
from django.urls import re_path
from django.urls import re_path, path
from core.views import *
@ -60,8 +60,8 @@ urlpatterns = [
SithPasswordResetDoneView.as_view(),
name="password_reset_done",
),
re_path(
r"^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
path(
r"reset/<str:uidb64>/<str:token>/",
SithPasswordResetConfirmView.as_view(),
name="password_reset_confirm",
),
@ -73,7 +73,7 @@ urlpatterns = [
re_path(r"^register$", register, name="register"),
# Group handling
re_path(r"^group/$", GroupListView.as_view(), name="group_list"),
re_path(r"^group/new$", GroupCreateView.as_view(), name="group_new"),
re_path(r"^group/new/$", GroupCreateView.as_view(), name="group_new"),
re_path(
r"^group/(?P<group_id>[0-9]+)/$", GroupEditView.as_view(), name="group_edit"
),

View File

@ -242,6 +242,16 @@ class CanViewMixin(GenericContentPermissionMixinBuilder):
permission_function = can_view
class UserIsRootMixin(GenericContentPermissionMixinBuilder):
"""
This view check if the user is root
:raises: PermissionDenied
"""
permission_function = lambda obj, user: user.is_root
class FormerSubscriberMixin(View):
"""
This view check if the user was at least an old subscriber
@ -334,19 +344,19 @@ class QuickNotifMixin:
class DetailFormView(SingleObjectMixin, FormView):
"""
Class that allow both a detail view and a form view
Class that allow both a detail view and a form view
"""
def get_object(self):
"""
Get current group from id in url
Get current group from id in url
"""
return self.cached_object
@cached_property
def cached_object(self):
"""
Optimisation on group retrieval
Optimisation on group retrieval
"""
return super(DetailFormView, self).get_object()

View File

@ -24,12 +24,13 @@
# This file contains all the views that concern the page model
from django.shortcuts import redirect, get_object_or_404
from django.utils.http import http_date
from django.views.generic import ListView, DetailView, TemplateView
from django.views.generic.edit import UpdateView, FormMixin, DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.forms.models import modelform_factory
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponse
from wsgiref.util import FileWrapper
from django.urls import reverse
@ -68,6 +69,7 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"):
with open(filepath.encode("utf-8"), "rb") as filename:
wrapper = FileWrapper(filename)
response = HttpResponse(wrapper, content_type=f.mime_type)
response["Last-Modified"] = http_date(f.date.timestamp())
response["Content-Length"] = os.path.getsize(filepath.encode("utf-8"))
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
"utf-8"
@ -147,7 +149,7 @@ class FileListView(ListView):
def get_context_data(self, **kwargs):
kwargs = super(FileListView, self).get_context_data(**kwargs)
kwargs["popup"] = ""
if self.kwargs["popup"]:
if self.kwargs.get("popup") is not None:
kwargs["popup"] = "popup"
return kwargs
@ -165,7 +167,7 @@ class FileEditView(CanEditMixin, UpdateView):
return modelform_factory(SithFile, fields=fields)
def get_success_url(self):
if self.kwargs["popup"]:
if self.kwargs.get("popup") is not None:
return reverse(
"core:file_detail", kwargs={"file_id": self.object.id, "popup": "popup"}
)
@ -176,7 +178,7 @@ class FileEditView(CanEditMixin, UpdateView):
def get_context_data(self, **kwargs):
kwargs = super(FileEditView, self).get_context_data(**kwargs)
kwargs["popup"] = ""
if self.kwargs["popup"]:
if self.kwargs.get("popup") is not None:
kwargs["popup"] = "popup"
return kwargs
@ -217,13 +219,13 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
def get_success_url(self):
return reverse(
"core:file_detail",
kwargs={"file_id": self.object.id, "popup": self.kwargs["popup"] or ""},
kwargs={"file_id": self.object.id, "popup": self.kwargs.get("popup", "")},
)
def get_context_data(self, **kwargs):
kwargs = super(FileEditPropView, self).get_context_data(**kwargs)
kwargs["popup"] = ""
if self.kwargs["popup"]:
if self.kwargs.get("popup") is not None:
kwargs["popup"] = "popup"
return kwargs
@ -301,14 +303,14 @@ class FileView(CanViewMixin, DetailView, FormMixin):
def get_success_url(self):
return reverse(
"core:file_detail",
kwargs={"file_id": self.object.id, "popup": self.kwargs["popup"] or ""},
kwargs={"file_id": self.object.id, "popup": self.kwargs.get("popup", "")},
)
def get_context_data(self, **kwargs):
kwargs = super(FileView, self).get_context_data(**kwargs)
kwargs["popup"] = ""
kwargs["form"] = self.form
if self.kwargs["popup"]:
if self.kwargs.get("popup") is not None:
kwargs["popup"] = "popup"
kwargs["clipboard"] = SithFile.objects.filter(
id__in=self.request.session["clipboard"]
@ -328,20 +330,20 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
return self.request.GET["next"]
if self.object.parent is None:
return reverse(
"core:file_list", kwargs={"popup": self.kwargs["popup"] or ""}
"core:file_list", kwargs={"popup": self.kwargs.get("popup", "")}
)
return reverse(
"core:file_detail",
kwargs={
"file_id": self.object.parent.id,
"popup": self.kwargs["popup"] or "",
"popup": self.kwargs.get("popup", ""),
},
)
def get_context_data(self, **kwargs):
kwargs = super(FileDeleteView, self).get_context_data(**kwargs)
kwargs["popup"] = ""
if self.kwargs["popup"]:
if self.kwargs.get("popup") is not None:
kwargs["popup"] = "popup"
return kwargs

View File

@ -37,11 +37,15 @@ from django.forms import (
DateTimeInput,
Textarea,
)
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
from ajax_select.fields import AutoCompleteSelectField
from ajax_select import make_ajax_field
from django.utils.dateparse import parse_datetime
from django.utils import timezone
import datetime
from django.forms.utils import to_current_timezone
import re
@ -126,7 +130,7 @@ class SelectFile(TextInput):
'<span name="'
+ name
+ '" class="choose_file_button">'
+ ugettext("Choose file")
+ gettext("Choose file")
+ "</span>"
)
return output
@ -150,7 +154,7 @@ class SelectUser(TextInput):
'<span name="'
+ name
+ '" class="choose_user_button">'
+ ugettext("Choose user")
+ gettext("Choose user")
+ "</span>"
)
return output
@ -222,6 +226,7 @@ class UserProfileForm(forms.ModelForm):
"avatar_pict",
"scrub_pict",
"sex",
"pronouns",
"second_email",
"address",
"parent_address",
@ -333,7 +338,10 @@ class UserPropForm(forms.ModelForm):
class UserGodfathersForm(forms.Form):
type = forms.ChoiceField(
choices=[("godfather", _("Godfather")), ("godchild", _("Godchild"))],
choices=[
("godfather", _("Godfather / Godmother")),
("godchild", _("Godchild")),
],
label=_("Add"),
)
user = AutoCompleteSelectField(
@ -398,3 +406,27 @@ class GiftForm(forms.ModelForm):
id=user_id
)
self.fields["user"].widget = forms.HiddenInput()
class TzAwareDateTimeField(forms.DateTimeField):
def __init__(
self, input_formats=["%Y-%m-%d %H:%M:%S"], widget=SelectDateTime, **kwargs
):
super().__init__(input_formats=input_formats, widget=widget, **kwargs)
def prepare_value(self, value):
# the db value is a datetime as a string in UTC
if isinstance(value, str):
# convert it into a naive datetime (no timezone attached)
value = parse_datetime(value)
# attach it to the UTC timezone (so that to_current_timezone()) if not None
# converts it to the local timezone)
if value is not None:
value = timezone.make_aware(value, timezone.utc)
if isinstance(value, datetime.datetime):
value = to_current_timezone(value)
# otherwise it is formatted according to locale (in french)
value = str(value)
return value

View File

@ -31,20 +31,20 @@ from django.views.generic import ListView
from django.views.generic.edit import FormView
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django import forms
from ajax_select.fields import AutoCompleteSelectMultipleField
from core.models import RealGroup, User
from core.views import CanEditMixin, DetailFormView
from core.views import CanCreateMixin, CanEditMixin, DetailFormView
# Forms
class EditMembersForm(forms.Form):
"""
Add and remove members from a Group
Add and remove members from a Group
"""
def __init__(self, *args, **kwargs):
@ -66,7 +66,7 @@ class EditMembersForm(forms.Form):
def clean_users_added(self):
"""
Check that the user is not trying to add an user already in the group
Check that the user is not trying to add an user already in the group
"""
cleaned_data = super(EditMembersForm, self).clean()
users_added = cleaned_data.get("users_added", None)
@ -100,7 +100,7 @@ class GroupListView(CanEditMixin, ListView):
class GroupEditView(CanEditMixin, UpdateView):
"""
Edit infos of a Group
Edit infos of a Group
"""
model = RealGroup
@ -109,20 +109,20 @@ class GroupEditView(CanEditMixin, UpdateView):
fields = ["name", "description"]
class GroupCreateView(CanEditMixin, CreateView):
class GroupCreateView(CanCreateMixin, CreateView):
"""
Add a new Group
Add a new Group
"""
model = RealGroup
template_name = "core/group_edit.jinja"
template_name = "core/create.jinja"
fields = ["name", "description"]
class GroupTemplateView(CanEditMixin, DetailFormView):
"""
Display all users in a given Group
Allow adding and removing users from it
Display all users in a given Group
Allow adding and removing users from it
"""
model = RealGroup
@ -156,7 +156,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
class GroupDeleteView(CanEditMixin, DeleteView):
"""
Delete a Group
Delete a Group
"""
model = RealGroup

View File

@ -30,6 +30,7 @@ from django.contrib.auth.decorators import login_required
from django.utils import html
from django.views.generic import ListView, TemplateView
from django.conf import settings
from django.utils.text import slugify
import json
@ -73,7 +74,18 @@ def notification(request, notif_id):
def search_user(query, as_json=False):
try:
res = SearchQuerySet().models(User).autocomplete(auto=html.escape(query))[:20]
# slugify turns everything into ascii and every whitespace into -
# it ends by removing duplicate - (so ' - ' will turn into '-')
# replace('-', ' ') because search is whitespace based
query = slugify(query).replace("-", " ")
# TODO: is this necessary?
query = html.escape(query)
res = (
SearchQuerySet()
.models(User)
.autocomplete(auto=query)
.order_by("-last_update")[:20]
)
return [r.object for r in res]
except TypeError:
return []

View File

@ -27,7 +27,7 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import views
from django.contrib.auth.forms import PasswordChangeForm
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.urls import reverse
from django.core.exceptions import PermissionDenied, ValidationError
from django.http import Http404, HttpResponse
@ -48,6 +48,7 @@ from django.views.generic.dates import YearMixin, MonthMixin
from datetime import timedelta, date
import logging
from api.views.sas import all_pictures_of_user
from core.views import (
CanViewMixin,
@ -202,7 +203,7 @@ class UserTabsMixin(TabedViewMixin):
"core:user_godfathers", kwargs={"user_id": self.object.id}
),
"slug": "godfathers",
"name": _("Godfathers"),
"name": _("Family"),
}
)
tab_list.append(
@ -325,19 +326,15 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
kwargs = super(UserPicturesView, self).get_context_data(**kwargs)
kwargs["albums"] = []
kwargs["pictures"] = {}
picture_qs = (
self.object.pictures.exclude(picture=None)
.order_by("-picture__parent__date", "id")
.select_related("picture__parent")
)
picture_qs = all_pictures_of_user(self.object)
last_album = None
for pict_relation in picture_qs:
album = pict_relation.picture.parent
for picture in picture_qs:
album = picture.parent
if album.id != last_album:
kwargs["albums"].append(album)
kwargs["pictures"][album.id] = []
last_album = album.id
kwargs["pictures"][album.id].append(pict_relation.picture)
kwargs["pictures"][album.id].append(picture)
return kwargs
@ -474,7 +471,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView):
if self.param == "godchildren":
self.graph.graph_attr["label"] = _("Godchildren")
elif self.param == "godfathers":
self.graph.graph_attr["label"] = _("Godfathers")
self.graph.graph_attr["label"] = _("Family")
else:
self.graph.graph_attr["label"] = _("Family")
img = self.graph.draw(format="png", prog="dot")

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# Copyright 2016,2017,2019
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.

34
counter/app.py Normal file
View File

@ -0,0 +1,34 @@
# -*- coding:utf-8 -*
#
# Copyright 2019
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# 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.
#
# 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.
#
#
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class CounterConfig(AppConfig):
name = "counter"
verbose_name = _("counter")
def ready(self):
import counter.signals

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.db import migrations, models
from django.conf import settings

View File

@ -22,8 +22,9 @@
#
#
from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.conf import settings
from django.urls import reverse
@ -39,7 +40,7 @@ import os
import base64
import datetime
from club.models import Club
from club.models import Club, Membership
from accounting.models import CurrencyField
from core.models import Group, User, Notification
from subscription.models import Subscription
@ -89,9 +90,9 @@ class Customer(models.Model):
def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
"""
is_selling : tell if the current action is a selling
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
Those two parameters avoid blocking the save method of a customer if his account is negative
is_selling : tell if the current action is a selling
allow_negative : ignored if not a selling. Allow a selling to put the account in negative
Those two parameters avoid blocking the save method of a customer if his account is negative
"""
if self.amount < 0 and (is_selling and not allow_negative):
raise ValidationError(_("Not enough money"))
@ -342,6 +343,23 @@ class Counter(models.Model):
"""
return [b.id for b in self.get_barmen_list()]
def can_refill(self):
"""
Show if the counter authorize the refilling with physic money
"""
if (
self.id in SITH_COUNTER_OFFICES
): # If the counter is the counters 'AE' or 'BdF', the refiling are authorized
return True
is_ae_member = False
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
for barman in self.get_barmen_list():
if ae.get_membership_for(barman):
is_ae_member = True
return is_ae_member
class Refilling(models.Model):
"""
@ -527,7 +545,7 @@ class Selling(models.Model):
def save(self, allow_negative=False, *args, **kwargs):
"""
allow_negative : Allow this selling to use more money than available for this user
allow_negative : Allow this selling to use more money than available for this user
"""
if not self.date:
self.date = timezone.now()

71
counter/signals.py Normal file
View File

@ -0,0 +1,71 @@
# -*- coding:utf-8 -*
#
# Copyright 2019
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# 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.
#
# 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.
#
#
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.conf import settings
from core.middleware import get_signal_request
from core.models import OperationLog
from counter.models import Selling, Refilling, Counter
def write_log(instance, operation_type):
def get_user():
request = get_signal_request()
if not request:
return None
# Get a random barmen if deletion is from a counter
session = getattr(request, "session", {})
session_token = session.get("counter_token", None)
if session_token:
counter = Counter.objects.filter(token=session_token).first()
if counter and len(counter.get_barmen_list()) > 0:
return counter.get_random_barman()
# Get the current logged user if not from a counter
if request.user and not request.user.is_anonymous:
return request.user
# Return None by default
return None
log = OperationLog(
label=str(instance),
operator=get_user(),
operation_type=operation_type,
).save()
@receiver(pre_delete, sender=Refilling, dispatch_uid="write_log_refilling_deletion")
def write_log_refilling_deletion(sender, instance, **kwargs):
write_log(instance, "REFILLING_DELETION")
@receiver(pre_delete, sender=Selling, dispatch_uid="write_log_refilling_deletion")
def write_log_selling_deletion(sender, instance, **kwargs):
write_log(instance, "SELLING_DELETION")

View File

@ -4,6 +4,12 @@
{% trans obj=object %}Edit {{ obj }}{% endtrans %}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
<form action="" method="post" id="cash_summary_form">

View File

@ -1,163 +1,224 @@
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
{% macro add_product(id, content, class="") %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
</form>
{% endmacro %}
{% macro del_product(id, content, class="") %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
{% csrf_token %}
<input type="hidden" name="action" value="del_product">
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
</form>
{% endmacro %}
{% block title %}
{{ counter }}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h4 id="click_interface">{{ counter }}</h4>
<div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
<input type="input" name="student_card_uid" />
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %}
<ul>
{% for card in customer.student_cards.all() %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>
<div id="bar_ui">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
<div class="important">
{% if request.session['too_young'] %}
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_allowed'] %}
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['no_age'] %}
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_enough'] %}
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
{% endif %}
</div>
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="code">
<input type="input" name="code" value="" class="focus" id="code_field"/>
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
<input type="input" name="student_card_uid" />
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<p>{% trans %}Basket: {% endtrans %}</p>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %}
<ul>
{% for card in customer.student_cards.all() %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>
<div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
{% raw %}
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
{% endraw %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code">
{% csrf_token %}
<input type="hidden" name="action" value="code">
<input type="input" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<p>{% trans %}Basket: {% endtrans %}</p>
{% raw %}
<ul>
<li v-for="p_info,p_id in basket">
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> - </button>
</form>
{{ p_info["qty"] + p_info["bonus_qty"] }}
<form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> + </button>
</form>
{{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span>
</li>
</ul>
<p>
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
</p>
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
{% endraw %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
</form>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
</form>
</div>
{% if (counter.type == 'BAR' and barmens_can_refill) %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
</div>
{% endif %}
</div>
<div id="products">
<ul>
{% for id,infos in request.session['basket']|dictsort %}
{% set product = counter.products.filter(id=id).first() %}
{% set s = infos['qty'] * infos['price'] / 100 %}
<li>{{ del_product(id, '-', "inline") }} {{ infos['qty'] + infos['bonus_qty'] }} {{ add_product(id, '+', "inline") }}
{{ product.name }}: {{ "%0.2f"|format(s) }}
{% if infos['bonus_qty'] %}
P
{% endif %}
</li>
{% endfor %}
{% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</ul>
<p><strong>{% trans %}Total: {% endtrans %}{{ "%0.2f"|format(basket_total) }} €</strong></p>
<div class="important">
{% if request.session['too_young'] %}
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_allowed'] %}
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['no_age'] %}
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_enough'] %}
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
{% endif %}
</div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
</form>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
</form>
</div>
{% if counter.type == 'BAR' %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
</div>
{% endif %}
</div>
<div id="products">
<ul>
{% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</ul>
{% for category in categories.keys() -%}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
{% else %}
{% set file = static('core/img/na.gif') %}
{% endif %}
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %}
{{ add_product(p.id, prod, "form_button") }}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
{% else %}
{% set file = static('core/img/na.gif') %}
{% endif %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
<button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
</form>
{%- endfor %}
</div>
{%- endfor %}
</div>
{%- endfor %}
</div>
{% endblock %}
{% block script %}
<script>
document.getElementById("click_interface").scrollIntoView();
</script>
{{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
<script>
$( function() {
var products = [
/* Vue.JS dynamic form */
const click_form_vue = Vue.createApp({
data() {
return {
js_csrf_token: "{{ csrf_token }}",
products: {
{% for p in products -%}
{{ p.id }}: {
code: "{{ p.code }}",
name: "{{ p.name }}",
selling_price: "{{ p.selling_price }}",
special_selling_price: "{{ p.special_selling_price }}",
},
{%- endfor %}
},
basket: {{ request.session["basket"]|tojson }},
errors: [],
}
},
methods: {
sum_basket() {
var vm = this;
var total = 0;
for(idx in vm.basket) {
var item = vm.basket[idx];
console.log(item);
total += item["qty"] * item["price"];
}
return total / 100;
},
handle_code(event) {
var vm = this;
var code = $(event.target).find("#code_field").val().toUpperCase();
console.log("Code:");
console.log(code);
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
$(event.target).submit();
} else {
vm.handle_action(event);
}
},
handle_action(event) {
var vm = this;
var payload = $(event.target).serialize();
$.ajax({
type: 'post',
dataType: 'json',
data: payload,
success: function(response) {
vm.basket = response.basket;
vm.errors = [];
},
error: function(error) {
vm.basket = error.responseJSON.basket;
vm.errors = error.responseJSON.errors;
}
});
$('form.code_form #code_field').val("").focus();
}
}
}).mount('#bar_ui');
/* Autocompletion in the code field */
var products_autocomplete = [
{% for p in products -%}
{
value: "{{ p.code }}",
@ -166,6 +227,7 @@ $( function() {
},
{%- endfor %}
];
var quantity = "";
var search = "";
var pattern = /^(\d+x)?(.*)/i;
@ -183,21 +245,22 @@ $( function() {
quantity = res[1] || "";
search = res[2];
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
response($.grep( products, function( value ) {
response($.grep( products_autocomplete, function( value ) {
value = value.tags;
return matcher.test( value );
}));
},
});
});
$( function() {
$("#bar_ui").accordion({
/* Accordion UI between basket and refills */
$("#click_form").accordion({
heightStyle: "content",
activate: function(event, ui){
$(".focus").focus();
}
});
$("#products").tabs();
$("#code_field").focus();
});
</script>

View File

@ -12,6 +12,12 @@
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>

View File

@ -5,6 +5,12 @@
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
<h4>{% trans %}Refillings{% endtrans %}</h4>

View File

@ -13,6 +13,7 @@
<th>{% trans %}Amount{% endtrans %}</th>
<th>{% trans %}Payment method{% endtrans %}</th>
<th>{% trans %}Seller{% endtrans %}</th>
<th>{% trans %}Date{% endtrans %}
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
{%- for refilling in object_list %}
@ -21,6 +22,7 @@
<td>{{ refilling.amount }}</td>
<td>{{ refilling.payment_method }}</td>
<td>{{ refilling.operator }}</td>
<td>{{ refilling.date }}</td>
<td><a href="{{ url('counter:refilling_delete', refilling_id=refilling.id)}}">Delete</a></td>
</tr>
{%- endfor %}

View File

@ -36,7 +36,10 @@ class CounterTest(TestCase):
def setUp(self):
call_command("populate")
self.skia = User.objects.filter(username="skia").first()
self.sli = User.objects.filter(username="sli").first()
self.krophil = User.objects.filter(username="krophil").first()
self.mde = Counter.objects.filter(name="MDE").first()
self.foyer = Counter.objects.get(id=2)
def test_full_click(self):
response = self.client.post(
@ -68,20 +71,77 @@ class CounterTest(TestCase):
location,
{
"action": "refill",
"amount": "10",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
)
response = self.client.post(location, {"action": "code", "code": "BARB"})
response = self.client.post(
location, {"action": "add_product", "product_id": "4"}
)
response = self.client.post(
location, {"action": "del_product", "product_id": "4"}
)
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
response = self.client.post(location, {"action": "code", "code": "fin"})
response_get = self.client.get(response.get("location"))
response_content = response_get.content.decode("utf-8")
self.assertTrue("<li>2 x Barbar" in str(response_content))
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
self.assertTrue(
"<p>Client : Richard Batsbak - Nouveau montant : 8.30"
in str(response_get.content)
"<p>Client : Richard Batsbak - Nouveau montant : 3.60"
in str(response_content)
)
response = self.client.post(
reverse("counter:login", kwargs={"counter_id": self.mde.id}),
{"username": self.sli.username, "password": "plop"},
)
response = self.client.post(
location,
{
"action": "refill",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
)
self.assertTrue(response.status_code == 200)
response = self.client.post(
reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
{"username": self.krophil.username, "password": "plop"},
)
response = self.client.get(
reverse("counter:details", kwargs={"counter_id": self.foyer.id})
)
counter_token = re.search(
r'name="counter_token" value="([^"]*)"', str(response.content)
).group(1)
response = self.client.post(
reverse("counter:details", kwargs={"counter_id": self.foyer.id}),
{"code": "4000k", "counter_token": counter_token},
)
location = response.get("location")
response = self.client.post(
location,
{
"action": "refill",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
)
self.assertTrue(response.status_code == 200)
class CounterStatsTest(TestCase):
def setUp(self):

View File

@ -38,16 +38,17 @@ from django.views.generic.edit import (
from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple
from django.urls import reverse_lazy, reverse
from django.http import HttpResponseRedirect, HttpResponse
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
from django.utils import timezone
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.db import DataError, transaction, models
import re
import pytz
from datetime import date, timedelta, datetime
from http import HTTPStatus
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_field
@ -69,6 +70,7 @@ from counter.models import (
Permanency,
)
from accounting.models import CurrencyField
from core.views.forms import TzAwareDateTimeField
class CounterAdminMixin(View):
@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
pk_url_kwarg = "counter_id"
current_tab = "counter"
def render_to_response(self, *args, **kwargs):
if self.request.is_ajax(): # JSON response for AJAX requests
response = {"errors": []}
status = HTTPStatus.OK
if self.request.session["too_young"]:
response["errors"].append(_("Too young for that product"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_allowed"]:
response["errors"].append(_("Not allowed for that product"))
status = HTTPStatus.FORBIDDEN
if self.request.session["no_age"]:
response["errors"].append(_("No date of birth provided"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_enough"]:
response["errors"].append(_("Not enough money"))
status = HTTPStatus.PAYMENT_REQUIRED
if len(response["errors"]) > 1:
status = HTTPStatus.BAD_REQUEST
response["basket"] = self.request.session["basket"]
return JsonResponse(response, status=status)
else: # Standard HTML page
return super().render_to_response(*args, **kwargs)
def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
obj = self.get_object()
@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
or len(obj.get_barmen_list()) < 1
):
raise PermissionDenied
return HttpResponseRedirect(
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
)
else:
if not request.user.is_authenticated:
raise PermissionDenied
@ -394,7 +426,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return ret
def post(self, request, *args, **kwargs):
""" Handle the many possibilities of the post request """
"""Handle the many possibilities of the post request"""
self.object = self.get_object()
self.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
@ -590,7 +622,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return True
def del_product(self, request):
""" Delete a product from the basket """
"""Delete a product from the basket"""
pid = str(request.POST["product_id"])
product = self.get_product(pid)
if pid in request.session["basket"]:
@ -632,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return self.render_to_response(context)
def finish(self, request):
""" Finish the click session, and validate the basket """
"""Finish the click session, and validate the basket"""
with transaction.atomic():
request.session["last_basket"] = []
if self.sum_basket(request) > self.customer.amount:
@ -684,7 +716,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
def cancel(self, request):
""" Cancel the click session """
"""Cancel the click session"""
kwargs = {"counter_id": self.object.id}
request.session.pop("basket", None)
return HttpResponseRedirect(
@ -693,7 +725,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def refill(self, request):
"""Refill the customer's account"""
if self.get_object().type == "BAR":
if self.get_object().type == "BAR" and self.object.can_refill():
form = RefillForm(request.POST)
if form.is_valid():
form.instance.counter = self.object
@ -706,7 +738,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
raise PermissionDenied
def get_context_data(self, **kwargs):
""" Add customer to the context """
"""Add customer to the context"""
kwargs = super(CounterClick, self).get_context_data(**kwargs)
kwargs["products"] = self.object.products.select_related("product_type")
kwargs["categories"] = {}
@ -719,6 +751,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
kwargs["basket_total"] = self.sum_basket(self.request)
kwargs["refill_form"] = self.refill_form or RefillForm()
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
kwargs["barmens_can_refill"] = self.object.can_refill()
return kwargs
@ -1360,7 +1393,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
)
def get_context_data(self, **kwargs):
"""Add form to the context """
"""Add form to the context"""
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
threshold = timezone.now() - timedelta(
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
@ -1422,7 +1455,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
def get_context_data(self, **kwargs):
""" Add form to the context """
"""Add form to the context"""
kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
kwargs["form"] = self.form
return kwargs
@ -1448,7 +1481,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
template_name = "counter/stats.jinja"
def get_context_data(self, **kwargs):
""" Add stats to the context """
"""Add stats to the context"""
from django.db.models import Sum, Case, When, F, DecimalField
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
@ -1553,18 +1586,8 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class CashSummaryFormBase(forms.Form):
begin_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
@ -1578,7 +1601,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
def get_context_data(self, **kwargs):
""" Add sums to the context """
"""Add sums to the context"""
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
form = CashSummaryFormBase(self.request.GET)
kwargs["form"] = form
@ -1629,7 +1652,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
current_tab = "invoices_call"
def get_context_data(self, **kwargs):
""" Add sums to the context """
"""Add sums to the context"""
kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
start_date = None

View File

@ -8,7 +8,7 @@ C'est un projet bénévole qui tire ses origines des années 2000. Il s'agit de
Pourquoi réécrire le site
-------------------------
L'ancienne version du site, sobrement baptisée `ae2 <https://ae-dev.utbm.fr/ae/ae2>`_ présentait un nombre impressionnant de fonctionnalités. Il avait été écrit en PHP et se basait sur son propre framework maison.
L'ancienne version du site, sobrement baptisée `ae2 <https://github.com/ae-utbm/sith2>`_ présentait un nombre impressionnant de fonctionnalités. Il avait été écrit en PHP et se basait sur son propre framework maison.
Malheureusement, son entretiens était plus ou moins hasardeux et son framework reposait sur des principes assez différents de ce qui se fait aujourd'hui, rendant la maintenance difficile. De plus, la version de PHP qu'il utilisait était plus que déprécié et à l'heure de l'arrivée de PHP 7 et de sa non rétrocompatibilité il était vital de faire quelque chose. Il a donc été décidé de le réécrire.

View File

@ -146,7 +146,7 @@ GitLab
~~~~~~
| `Site officiel <https://about.gitlab.com/>`__
| `Instance de l'AE <https://ae-dev.utbm.fr/>`__
| `Instance de l'AE <https://github.com/ae-utbm/>`__
GitLab est une alternative libre à GitHub. C'est une plate-forme avec interface web permettant de déposer du code géré avec Git offrant également de l'intégration continue et du déploiement automatique.
@ -160,12 +160,13 @@ Sentry
Sentry est une plate-forme libre qui permet de se tenir informer des bugs qui ont lieu sur le site. À chaque crash du logiciel (erreur 500), une erreur est envoyée sur la plate-forme et est indiqué précisément à quelle ligne de code celle-ci a eu lieu, à quelle heure, combien de fois, avec quel navigateur la page a été visitée et même éventuellement un commentaire de l'utilisateur qui a rencontré le bug.
Virtualenv
Poetry
~~~~~~~~~~
`Utiliser virtualenv <https://python-guide-pt-br.readthedocs.io/fr/latest/dev/virtualenvs.html>`__
`Utiliser Poetry <https://python-poetry.org/docs/basic-usage/>`__
Virtualenv est un utilitaire permettant d'installer un environnement Python de manière locale sans avoir besoin des droits root pour y installer des dépendances. Il est très utilisé pour gérer plusieurs projets différents en parallèles puisqu'il permet d'avoir sur sa machine plusieurs environnements différents et donc plusieurs versions d'une même dépendance dans plusieurs projets différent sans impacter le système sur lequel le tout est installé.
Poetry est un utilitaire qui permet de créer et gérer des environements Python de manière simple et intuitive. Il permet également de gérer et mettre à jour le fichier de dépendances.
L'avantage d'utiliser poetry (et les environnements virtuels en général) est de pouvoir gérer plusieurs projets différents en parallèles puisqu'il permet d'avoir sur sa machine plusieurs environnements différents et donc plusieurs versions d'une même dépendance dans plusieurs projets différent sans impacter le système sur lequel le tout est installé.
Black
~~~~~

87
doc/devenv/populate.rst Normal file
View File

@ -0,0 +1,87 @@
Générer l'environnement avec populate
=====================================
Lors de l'installation du site en local (via la commande `setup`), la commande **populate** est appelée.
Cette commande génère entièrement la base de données de développement. Elle se situe dans `core/management/commands/populate.py`.
Utilisations :
.. code-block:: shell
./manage.py setup # Génère la base de test
./manage.py setup --prod # Ne génère que le schéma de base et les données strictement nécessaires au fonctionnement
Les données générées du site dev
================================
Par défaut, la base de données du site de prod contient des données nécessaires au fonctionnement du site comme les groupes (voir :ref:`groups-list`), un utilisateur root, les clubs de base et quelques autres instances indispensables. En plus de ces données par défaut, la base de données du site de dev contient des données de test (*fixtures*) pour remplir le site et le rendre exploitable.
**Voici les clubs générés pour le site de dev :**
* AE
- Bibo'UT
- Carte AE
- Guy'UT
+ Woenzel'UT
- Troll Penché
* BdF
* Laverie
**Voici utilisateurs générés pour le site de dev :**
Le mot de passe de tous les utilisateurs est **plop**.
* **root** -> Dans le groupe Root et cotisant
* **skia** -> responsable info AE et cotisant, barmen MDE
* **public** -> utilisateur non cotisant et sans groupe
* **subscriber** -> utilisateur cotisant et sans groupe
* **old_subscriber** -> utilisateur anciennement cotisant et sans groupe
* **counter** -> administrateur comptoir
* **comptable** -> administrateur comptabilité
* **guy** -> utilisateur non cotisant et sans groupe
* **rbatsbak** -> utilisateur non cotisant et sans groupe
* **sli** -> cotisant avec carte étudiante attachée au compte, barmen MDE
* **krophil** -> cotisant avec des plein d'écocups, barmen foyer
* **comunity** -> administrateur communication
* **tutu** -> administrateur pédagogie
Ajouter des fixtures
====================
.. role:: python(code)
:language: python
Les fixtures sont contenus dans *core/management/commands/populate.py* après la ligne 205 : :python:`if not options["prod"]:`.
Pour ajouter une fixtures, il faut :
* importer la classe à instancier en début de fichier
* créer un objet avec les attributs nécessaires en fin de fichier
* enregistrer l'objet dans la base de données
.. code-block:: python
# Exemple pour ajouter un utilisateur
# Importation de la classe
import core.models import User
# [...]
# Création de l'objet
jesus = User(
username="jc",
last_name="Jesus",
first_name="Christ",
email="son@god.cloud",
date_of_birth="2020-24-12",
is_superuser=False,
is_staff=True,
)
jesus.set_password("plop")
# Enregistrement dans la base de donnée
jesus.save()

View File

@ -12,7 +12,7 @@ Pour modifier les cotisations disponnibles, tout se gère dans la configuration
.. code-block:: python
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
SITH_SUBSCRIPTIONS = {
# Voici un échantillon de la véritable configuration à l'heure de l'écriture.

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