mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 18:08:21 +02:00
Compare commits
302 Commits
Author | SHA1 | Date | |
---|---|---|---|
466cbd9878 | |||
0bd447b608 | |||
3f3c93d928 | |||
340c90f5d3 | |||
a05dfcbf3d
|
|||
ba3c0fb18d
|
|||
ab69963ea1 | |||
654c01631a
|
|||
d94cc2a7ad
|
|||
69bb38297f
|
|||
9628560d64
|
|||
df3bb71357
|
|||
2a216fd994
|
|||
8dd2619013
|
|||
62431a4910
|
|||
946bc1e497 | |||
d4896bfd76
|
|||
23f46cc598
|
|||
d1a9f21b56 | |||
d809b2595a
|
|||
97803ac983 | |||
b951c4aa05 | |||
69b3d2ac9c
|
|||
f29054558a
|
|||
11dd8adbb7 | |||
d437f2bdbd
|
|||
ac8453b04c
|
|||
6b4d18f4b3 | |||
668cfa71a7 | |||
161db0b00b
|
|||
8638c16b34
|
|||
9583cec3ff
|
|||
1ef25924a0
|
|||
e89383e3f4
|
|||
79a116d9c6
|
|||
aa75ce5c7a
|
|||
a3a9dfc812
|
|||
76531595ad
|
|||
a0b920ac94
|
|||
ab2e580e68
|
|||
0234f19a33
|
|||
1a4b7c83e8
|
|||
4c17e2a92b
|
|||
e68afc7d0a
|
|||
c6e3b54f94
|
|||
7e6a14296a | |||
780f78b385 | |||
4e3c32eb5e
|
|||
ef118c2445
|
|||
600ba15faa
|
|||
944bb127e2
|
|||
f6d042c998
|
|||
bb9a0a2593
|
|||
61feac13c7
|
|||
81e708a7e3
|
|||
3532846c87
|
|||
49551e88f8
|
|||
db936bf75a
|
|||
5828a20383 | |||
cea3138daf | |||
fb98d9cd8b
|
|||
0dd3da5c01
|
|||
af4be98b5b
|
|||
be6059eba6
|
|||
5793b83de7
|
|||
2c02c747f4
|
|||
a78f3b7caa
|
|||
1ee40cb94e
|
|||
bd035744a4
|
|||
7edd622755
|
|||
8fd5b6ee01
|
|||
03411ac9bd
|
|||
d965732b65
|
|||
048266ed61
|
|||
b27341009e
|
|||
da1e15c5e6
|
|||
4b03a78ad6
|
|||
fb6e3c3de0
|
|||
391f3bde8f
|
|||
ad04e45992
|
|||
4e1ba1447a
|
|||
b646f549d6
|
|||
ba9ef0371a
|
|||
881cd88f48
|
|||
b4ed354b73 | |||
e5051ab018
|
|||
bb69627ac5
|
|||
ffaa020310
|
|||
6d2b7054e2
|
|||
d888d5863a
|
|||
dbc7b3444b
|
|||
f25eb1d2c5
|
|||
a2a749e1ca
|
|||
5bf6a5501d
|
|||
9523b5f05f
|
|||
5eb3ffca66 | |||
9930c48253 | |||
d902e63a0c
|
|||
48b0bade51
|
|||
f75dbc4525
|
|||
fbf64db16e
|
|||
a3fd8ba063
|
|||
9b26207515
|
|||
7ea36a5415
|
|||
898f6d52bf
|
|||
8be16e7b58
|
|||
ea092803d7
|
|||
5e9f36ef1a
|
|||
b4d87bc6b5
|
|||
dd639d829e
|
|||
7b809ff3a6 | |||
d36edfc063
|
|||
cf87da096f
|
|||
e452b7acbf
|
|||
74ab4df9fe
|
|||
451851c955
|
|||
789ca149af | |||
7d3f1930b8 | |||
e8f4ca1e09
|
|||
733f145be3
|
|||
48c37353ea
|
|||
8056dc096d
|
|||
6d5b69cd26
|
|||
a7bdffd71a
|
|||
0887e4bbde
|
|||
199f4ca1f2
|
|||
802a6c68cb
|
|||
41a0b3a1c1
|
|||
aa35724be2
|
|||
9086d33158
|
|||
43d214b982
|
|||
b93e4a8d11
|
|||
b9a9704061
|
|||
fee52f326a
|
|||
317966d5c1 | |||
9f0a22d3d1
|
|||
a5ecdd100c | |||
f60691846b
|
|||
d5ecb72a71
|
|||
8cf9dfb9b9
|
|||
c3ab61bd04
|
|||
0b4b6dcb3e | |||
0d5f6c0332 | |||
7b28938cde
|
|||
35ffb36fbd
|
|||
08ba0b263a | |||
c4c4e9594f | |||
4166823d55 | |||
dc0f3dbcef | |||
4583958f50 | |||
b3abe9ab18 | |||
27f23b48b6 | |||
67e170d4a6 | |||
8f895dc4d7 | |||
1187577728 | |||
8a58af3b31 | |||
0c23625147 | |||
21219b9c62 | |||
5ab8beecef | |||
1ca5133026 | |||
93bc6bb245 | |||
952c4383e7 | |||
15dd2b8f0c
|
|||
c540b6334c
|
|||
bab394908d | |||
0b93968b9e | |||
97375ef6c0
|
|||
36cfcd533f
|
|||
21dbc53615
|
|||
e6f10ebdac
|
|||
47968844ce
|
|||
a435460e29
|
|||
b7c4360108
|
|||
8d8c417c50
|
|||
2b189af25b
|
|||
5a07c8a94f
|
|||
6cc1857eb6
|
|||
601534d610
|
|||
c271593839
|
|||
f351794aa0
|
|||
2793fee58c | |||
7a715df121
|
|||
9308878054
|
|||
b5ccf5b800
|
|||
5e63254439 | |||
da96506218
|
|||
b4714b896a | |||
cdb2647a4d
|
|||
cc12e3ec63
|
|||
be168c5ada
|
|||
b46ae6f856
|
|||
ec0bcbf015
|
|||
81303b8ef8 | |||
910b98fefc
|
|||
5a7a219ba8
|
|||
116451603c | |||
b2437ef9b5
|
|||
d8c9618772
|
|||
c825dee95a
|
|||
73d27e820b
|
|||
40e1b42078
|
|||
72806f0ace
|
|||
b244e01231
|
|||
76d1784aea
|
|||
56c5fa4057
|
|||
b5ef937a03
|
|||
e95a8b6e18
|
|||
635adf1360
|
|||
d5a9bf175f
|
|||
b597a6ac5b
|
|||
a704b92c3d | |||
53090b1a21 | |||
c49af0b83a | |||
5a05997d9d
|
|||
c109cd3ddd
|
|||
84304971d7
|
|||
b8b781f9a2 | |||
002128eed2 | |||
8d71783c42 | |||
a6f23df7d5
|
|||
d9c97628e2
|
|||
893534955d
|
|||
dfbf9972c2
|
|||
b5f3b3ffc1
|
|||
3aad4e7398
|
|||
b4a1b513cc
|
|||
c0c64f225c | |||
9d8f47115c
|
|||
f4156f1b94
|
|||
e60994e065
|
|||
801f711994
|
|||
e4568b410f
|
|||
c8f7986d5a | |||
d3a9c442a5
|
|||
016ab5a9c9
|
|||
7866ab7ec0
|
|||
f570ff3cd5
|
|||
6b2638c271
|
|||
5cb4183e9f
|
|||
3a20555663
|
|||
95be0042e9
|
|||
48880e7fd3
|
|||
e0030771e4
|
|||
d47799e6ee
|
|||
eae091625a
|
|||
aceb77ffb9
|
|||
338c94ed05
|
|||
290848f904 | |||
72dca54bbf | |||
117d9da3ba | |||
37efebe85b | |||
3af2ec71b6 | |||
0b4a95525b | |||
af664e481f | |||
0171f16311 | |||
296b94d237 | |||
4942553335 | |||
c1efb87180 | |||
72eead8595 | |||
ade7e583e5 | |||
4a8a101822 | |||
dd2cfa6327 | |||
2adf84b7fc | |||
2f54e64ea2 | |||
8434c0062c | |||
6d976f32bf | |||
b9d49d53f2 | |||
23243e09bb | |||
2682e9a610 | |||
5635598bbc | |||
b58a0c43cd | |||
e1f647bd02 | |||
39fd3a2471 | |||
1072e227b8 | |||
cbf7e6fe6c | |||
950922d041 | |||
78fe070cd3 | |||
51d5733578 | |||
7bd895c1df | |||
e5e94c52f2 | |||
051591cb7a | |||
0e7390b669 | |||
fe4363b83d | |||
6e80016b38 | |||
08e50ffc22 | |||
9cb65277f3 | |||
224a0fdd8c | |||
6dc7604e90 | |||
cb7f3c9f18 | |||
f910feca9e | |||
91f784872c | |||
b655135a42 | |||
58aa4983e3 | |||
6cc3cf4174 | |||
2097e67321 | |||
d773303d18 | |||
3cabcf40e7 | |||
bf29efda0a | |||
ceccba0d71 | |||
3eced33082 | |||
acb3fb4a91 | |||
420a24ebac |
@ -10,7 +10,6 @@ DJANGO_SECRET_KEY=CHANGE_ME
|
||||
DJANGO_SETTINGS_MODULE=note_kfet.settings
|
||||
CONTACT_EMAIL=tresorerie.bde@localhost
|
||||
NOTE_URL=localhost
|
||||
DOMAIN=localhost
|
||||
|
||||
# Config for mails. Only used in production
|
||||
NOTE_MAIL=notekfet@localhost
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -47,3 +47,9 @@ backups/
|
||||
env/
|
||||
venv/
|
||||
db.sqlite3
|
||||
shell.nix
|
||||
|
||||
# ansibles customs host
|
||||
ansible/host_vars/*.yaml
|
||||
!ansible/host_vars/bde*
|
||||
ansible/hosts
|
||||
|
@ -1,6 +1,7 @@
|
||||
stages:
|
||||
- test
|
||||
- quality-assurance
|
||||
- docs
|
||||
|
||||
# Also fetch submodules
|
||||
variables:
|
||||
@ -38,6 +39,21 @@ py38-django22:
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py38-django22
|
||||
|
||||
# Debian Bullseye
|
||||
py39-django22:
|
||||
stage: test
|
||||
image: debian:bullseye
|
||||
before_script:
|
||||
- >
|
||||
apt-get update &&
|
||||
apt-get install --no-install-recommends -y
|
||||
python3-django python3-django-crispy-forms
|
||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py39-django22
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: debian:buster-backports
|
||||
@ -47,3 +63,17 @@ linters:
|
||||
|
||||
# Be nice to new contributors, but please use `tox`
|
||||
allow_failure: true
|
||||
|
||||
# Compile documentation
|
||||
documentation:
|
||||
stage: docs
|
||||
image: sphinxdoc/sphinx
|
||||
before_script:
|
||||
- pip install sphinx-rtd-theme
|
||||
- cd docs
|
||||
script:
|
||||
- make dirhtml
|
||||
artifacts:
|
||||
paths:
|
||||
- docs/_build
|
||||
expire_in: 1 day
|
||||
|
@ -12,7 +12,7 @@ RUN apt-get update && \
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
||||
python3-bs4 python3-setuptools \
|
||||
uwsgi uwsgi-plugin-python3 \
|
||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \
|
||||
texlive-xetex gettext libjs-bootstrap4 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instal PyPI requirements
|
||||
|
37
README.md
37
README.md
@ -23,7 +23,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
||||
$ sudo apt update
|
||||
$ sudo apt install --no-install-recommends -y \
|
||||
ipython3 python3-setuptools python3-venv python3-dev \
|
||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
|
||||
texlive-xetex gettext libjs-bootstrap4 git
|
||||
```
|
||||
|
||||
2. **Clonage du dépot** là où vous voulez :
|
||||
@ -69,13 +69,31 @@ accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
|
||||
de la note sur un téléphone !
|
||||
|
||||
## Installation d'une instance de production
|
||||
Pour déployer facilement la note il est possible d'utiliser le playbook Ansible (sinon vous pouvez toujours le faire a la main, voir plus bas).
|
||||
### Avec ansible
|
||||
Il vous faudra un serveur sous debian ou ubuntu connecté à internet et que vous souhaiterez accéder à cette instance de la note sur `note.nomdedomaine.tld`.
|
||||
|
||||
0. Installer Ansible sur votre machine personnelle.
|
||||
|
||||
0. (bis) cloner le dépot sur votre machine personelle.
|
||||
|
||||
1. Copier le fichier `ansible/host_example`
|
||||
``` bash
|
||||
$ cp ansible/hosts_example ansible/hosts
|
||||
```
|
||||
et ajouter sous [dev] et/ou [prod] les serveurs sur lesquels vous souhaitez installer la note.
|
||||
2. Créer un fichier `ansible/host_vars/<note.nomdedomaine.tld.yaml>` sur le modèle des fichiers existants dans `ansible/hosts` et compléter les variables nécessaires.
|
||||
|
||||
3. lancer `ansible/base.yaml -l <nomdedomaine.tld.yaml>`
|
||||
4. Aller vous faire un café, ca peux durer un moment.
|
||||
|
||||
### Installation manuelle
|
||||
|
||||
**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.**
|
||||
Cela permet de mettre à jour facilement les dépendances critiques telles que Django.
|
||||
|
||||
L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**.
|
||||
|
||||
Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant.
|
||||
Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
||||
|
||||
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
||||
@ -97,7 +115,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
||||
python3-bs4 python3-setuptools python3-docutils \
|
||||
memcached uwsgi uwsgi-plugin-python3 \
|
||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
|
||||
texlive-xetex gettext libjs-bootstrap4 \
|
||||
nginx python3-venv git acl
|
||||
```
|
||||
|
||||
@ -261,20 +279,25 @@ Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.cr
|
||||
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
|
||||
**Commentez votre code !**
|
||||
|
||||
La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
|
||||
La documentation plus haut niveau sur le développement et sur l'utilisation
|
||||
est disponible sur <https://note.crans.org/doc> et également dans le dossier `docs`.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Regénérer les fichiers de traduction
|
||||
|
||||
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
|
||||
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`.
|
||||
Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
|
||||
De plus, il faut aussi extraire les variables des fichiers JavaScript.
|
||||
|
||||
```bash
|
||||
django-admin makemessages -i env
|
||||
python3 manage.py makemessages -i env
|
||||
python3 manage.py makemessages -i env -e js -d djangojs
|
||||
```
|
||||
|
||||
Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec
|
||||
|
||||
```bash
|
||||
django-admin compilemessages
|
||||
python3 manage.py compilemessages
|
||||
python3 manage.py compilejsmessages
|
||||
```
|
||||
|
@ -7,7 +7,7 @@
|
||||
prompt: "Password of the database (leave it blank to skip database init)"
|
||||
private: yes
|
||||
vars:
|
||||
mirror: deb.debian.org
|
||||
mirror: mirror.crans.org
|
||||
roles:
|
||||
- 1-apt-basic
|
||||
- 2-nk20
|
||||
@ -16,3 +16,4 @@
|
||||
- 5-nginx
|
||||
- 6-psql
|
||||
- 7-postinstall
|
||||
- 8-docs
|
||||
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
note:
|
||||
server_name: note-beta.crans.org
|
||||
git_branch: beta
|
||||
cron_enabled: false
|
@ -2,4 +2,6 @@
|
||||
note:
|
||||
server_name: note-dev.crans.org
|
||||
git_branch: beta
|
||||
serve_static: false
|
||||
cron_enabled: false
|
||||
email: notekfet2020@lists.crans.org
|
@ -2,4 +2,6 @@
|
||||
note:
|
||||
server_name: note.crans.org
|
||||
git_branch: master
|
||||
serve_static: true
|
||||
cron_enabled: true
|
||||
email: notekfet2020@lists.crans.org
|
||||
|
@ -1,6 +1,5 @@
|
||||
[dev]
|
||||
bde3-virt.adh.crans.org
|
||||
bde-nk20-beta.adh.crans.org
|
||||
bde-note-dev.adh.crans.org
|
||||
|
||||
[prod]
|
||||
bde-note.adh.crans.org
|
@ -3,11 +3,12 @@
|
||||
apt_repository:
|
||||
repo: deb http://{{ mirror }}/debian buster-backports main
|
||||
state: present
|
||||
when: ansible_facts['distribution'] == "Debian"
|
||||
|
||||
- name: Install note_kfet APT dependencies
|
||||
apt:
|
||||
update_cache: true
|
||||
default_release: buster-backports
|
||||
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
|
||||
install_recommends: false
|
||||
name:
|
||||
# Common tools
|
||||
@ -16,7 +17,6 @@
|
||||
- ipython3
|
||||
|
||||
# Front-end dependencies
|
||||
- fonts-font-awesome
|
||||
- libjs-bootstrap4
|
||||
|
||||
# Python dependencies
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
- name: Use default env vars (should be updated!)
|
||||
template:
|
||||
src: "env_example"
|
||||
src: "env.j2"
|
||||
dest: "/var/www/note_kfet/.env"
|
||||
mode: 0644
|
||||
force: false
|
||||
@ -36,3 +36,13 @@
|
||||
dest: /etc/cron.d/note
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Set default directory to /var/www/note_kfet
|
||||
lineinfile:
|
||||
path: /etc/skel/.bashrc
|
||||
line: 'cd /var/www/note_kfet'
|
||||
|
||||
- name: Automatically source Python virtual environment
|
||||
lineinfile:
|
||||
path: /etc/skel/.bashrc
|
||||
line: 'source /var/www/note_kfet/env/bin/activate'
|
||||
|
23
ansible/roles/2-nk20/templates/env.j2
Normal file
23
ansible/roles/2-nk20/templates/env.j2
Normal file
@ -0,0 +1,23 @@
|
||||
DJANGO_APP_STAGE=prod
|
||||
# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
|
||||
DJANGO_DEV_STORE_METHOD=sqlite
|
||||
DJANGO_DB_HOST=localhost
|
||||
DJANGO_DB_NAME=note_db
|
||||
DJANGO_DB_USER=note
|
||||
DJANGO_DB_PASSWORD={{ DB_PASSWORD }}
|
||||
DJANGO_DB_PORT=
|
||||
DJANGO_SECRET_KEY=CHANGE_ME
|
||||
DJANGO_SETTINGS_MODULE=note_kfet.settings
|
||||
CONTACT_EMAIL=tresorerie.bde@localhost
|
||||
NOTE_URL= {{note.server_name}}
|
||||
|
||||
# Config for mails. Only used in production
|
||||
NOTE_MAIL=notekfet@localhost
|
||||
EMAIL_HOST=smtp.localhost
|
||||
EMAIL_PORT=25
|
||||
EMAIL_USER=notekfet@localhost
|
||||
EMAIL_PASSWORD=CHANGE_ME
|
||||
|
||||
# Wiki configuration
|
||||
WIKI_USER=NoteKfet2020
|
||||
WIKI_PASSWORD=
|
@ -9,6 +9,11 @@
|
||||
retries: 3
|
||||
until: pkg_result is succeeded
|
||||
|
||||
- name: Check if certificate already exists.
|
||||
stat:
|
||||
path: /etc/letsencrypt/live/{{note.server_name}}/cert.pem
|
||||
register: letsencrypt_cert
|
||||
|
||||
- name: Create /etc/letsencrypt/conf.d
|
||||
file:
|
||||
path: /etc/letsencrypt/conf.d
|
||||
@ -19,3 +24,17 @@
|
||||
src: "letsencrypt/conf.d/nk20.ini.j2"
|
||||
dest: "/etc/letsencrypt/conf.d/nk20.ini"
|
||||
mode: 0644
|
||||
|
||||
- name: Stop services to allow certbot to generate a cert.
|
||||
service:
|
||||
name: nginx
|
||||
state: stopped
|
||||
|
||||
- name: Generate new certificate if one doesn't exist.
|
||||
shell: "certbot certonly --non-interactive --agree-tos --config /etc/letsencrypt/conf.d/nk20.ini -d {{note.server_name}}"
|
||||
when: letsencrypt_cert.stat.exists == False
|
||||
|
||||
- name: Restart services to allow certbot to generate a cert.
|
||||
service:
|
||||
name: nginx
|
||||
state: started
|
||||
|
@ -10,7 +10,7 @@ rsa-key-size = 4096
|
||||
# server = https://acme-staging.api.letsencrypt.org/directory
|
||||
|
||||
# Uncomment and update to register with the specified e-mail address
|
||||
email = notekfet2020@lists.crans.org
|
||||
email = {{ note.email }}
|
||||
|
||||
# Uncomment to use a text interface instead of ncurses
|
||||
text = True
|
||||
|
@ -1,5 +1,5 @@
|
||||
# the upstream component nginx needs to connect to
|
||||
upstream note{
|
||||
upstream note {
|
||||
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ server {
|
||||
# max upload size
|
||||
client_max_body_size 75M; # adjust to taste
|
||||
|
||||
{% if note.serve_static %}
|
||||
# Django media
|
||||
location /media {
|
||||
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
|
||||
@ -50,6 +51,11 @@ server {
|
||||
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
location /doc {
|
||||
alias /var/www/documentation; # The documentation of the project
|
||||
}
|
||||
|
||||
# Finally, send all non-media requests to the Django server.
|
||||
location / {
|
||||
uwsgi_pass note;
|
||||
|
@ -11,14 +11,14 @@
|
||||
until: pkg_result is succeeded
|
||||
|
||||
- name: Create role note
|
||||
when: "DB_PASSWORD|bool" # If the password is not defined, skip the installation
|
||||
when: DB_PASSWORD|length > 0 # If the password is not defined, skip the installation
|
||||
postgresql_user:
|
||||
name: note
|
||||
password: "{{ DB_PASSWORD }}"
|
||||
become_user: postgres
|
||||
|
||||
- name: Create NK20 database
|
||||
when: "DB_PASSWORD|bool"
|
||||
when: DB_PASSWORD|length >0
|
||||
postgresql_db:
|
||||
name: note_db
|
||||
owner: note
|
||||
|
@ -1,4 +1,10 @@
|
||||
---
|
||||
- name: Collect static files
|
||||
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
|
||||
args:
|
||||
chdir: /var/www/note_kfet
|
||||
become_user: www-data
|
||||
|
||||
- name: Migrate Django database
|
||||
command: /var/www/note_kfet/env/bin/python manage.py migrate
|
||||
args:
|
||||
@ -11,14 +17,14 @@
|
||||
chdir: /var/www/note_kfet
|
||||
become_user: www-data
|
||||
|
||||
- name: Compile JavaScript messages
|
||||
command: /var/www/note_kfet/env/bin/python manage.py compilejsmessages
|
||||
args:
|
||||
chdir: /var/www/note_kfet
|
||||
become_user: www-data
|
||||
|
||||
- name: Install initial fixtures
|
||||
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
|
||||
args:
|
||||
chdir: /var/www/note_kfet
|
||||
become_user: postgres
|
||||
|
||||
- name: Collect static files
|
||||
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
|
||||
args:
|
||||
chdir: /var/www/note_kfet
|
||||
become_user: www-data
|
||||
|
20
ansible/roles/8-docs/tasks/main.yml
Normal file
20
ansible/roles/8-docs/tasks/main.yml
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
- name: Install Sphinx and RTD theme
|
||||
pip:
|
||||
requirements: /var/www/note_kfet/docs/requirements.txt
|
||||
virtualenv: /var/www/note_kfet/env
|
||||
virtualenv_command: /usr/bin/python3 -m venv
|
||||
virtualenv_site_packages: true
|
||||
become_user: www-data
|
||||
|
||||
- name: Create documentation directory with good permissions
|
||||
file:
|
||||
path: /var/www/documentation
|
||||
state: directory
|
||||
owner: www-data
|
||||
group: www-data
|
||||
mode: u=rwx,g=rwxs,o=rx
|
||||
|
||||
- name: Build HTML documentation
|
||||
command: /var/www/note_kfet/env/bin/sphinx-build -b dirhtml /var/www/note_kfet/docs/ /var/www/documentation/
|
||||
become_user: www-data
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'activity.apps.ActivityConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
@ -15,10 +15,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/type/
|
||||
"""
|
||||
queryset = ActivityType.objects.all()
|
||||
queryset = ActivityType.objects.order_by('id')
|
||||
serializer_class = ActivityTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['name', 'can_invite', ]
|
||||
filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ]
|
||||
|
||||
|
||||
class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
@ -27,10 +27,16 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/activity/
|
||||
"""
|
||||
queryset = Activity.objects.all()
|
||||
queryset = Activity.objects.order_by('id')
|
||||
serializer_class = ActivitySerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['name', 'description', 'activity_type', ]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||
'date_start', 'date_end', 'valid', 'open', ]
|
||||
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||
'$creater__email', '$creater__note__alias__name', '$creater__note__alias__normalized_name',
|
||||
'$organizer__name', '$organizer__email', '$organizer__note__alias__name',
|
||||
'$organizer__note__alias__normalized_name', '$attendees_club__name', '$attendees_club__email',
|
||||
'$attendees_club__note__alias__name', '$attendees_club__note__alias__normalized_name', ]
|
||||
|
||||
|
||||
class GuestViewSet(ReadProtectedModelViewSet):
|
||||
@ -39,10 +45,13 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/guest/
|
||||
"""
|
||||
queryset = Guest.objects.all()
|
||||
queryset = Guest.objects.order_by('id')
|
||||
serializer_class = GuestSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||
'inviter__alias__normalized_name', ]
|
||||
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
|
||||
'$inviter__alias__normalized_name', ]
|
||||
|
||||
|
||||
class EntryViewSet(ReadProtectedModelViewSet):
|
||||
@ -51,7 +60,9 @@ class EntryViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/entry/
|
||||
"""
|
||||
queryset = Entry.objects.all()
|
||||
queryset = Entry.objects.order_by('id')
|
||||
serializer_class = EntrySerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
||||
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
||||
'$guest__last_name', '$guest__first_name', ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Club
|
||||
from note.models import Note, NoteUser
|
||||
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import Activity, Guest
|
||||
@ -24,10 +24,16 @@ class ActivityForm(forms.ModelForm):
|
||||
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
|
||||
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
|
||||
clubs = list(Club.objects.filter(PermissionBackend
|
||||
.filter_queryset(get_current_authenticated_user(), Club, "view")).all())
|
||||
.filter_queryset(get_current_request(), Club, "view")).all())
|
||||
shuffle(clubs)
|
||||
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||
|
||||
def clean_organizer(self):
|
||||
organizer = self.cleaned_data['organizer']
|
||||
if not organizer.note.is_active:
|
||||
self.add_error('organiser', _('The note of this club is inactive.'))
|
||||
return organizer
|
||||
|
||||
def clean_date_end(self):
|
||||
date_end = self.cleaned_data["date_end"]
|
||||
date_start = self.cleaned_data["date_start"]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
|
@ -1,7 +1,9 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
@ -52,8 +54,8 @@ class GuestTable(tables.Table):
|
||||
def render_entry(self, record):
|
||||
if record.has_entry:
|
||||
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
|
||||
return format_html('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
|
||||
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
|
||||
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
|
||||
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
|
||||
|
||||
|
||||
def get_row_class(record):
|
||||
@ -91,7 +93,7 @@ class EntryTable(tables.Table):
|
||||
if hasattr(record, 'username'):
|
||||
username = record.username
|
||||
if username != value:
|
||||
return format_html(value + " <em>aka.</em> " + username)
|
||||
return mark_safe(escape(value) + " <em>aka.</em> " + escape(username))
|
||||
return value
|
||||
|
||||
def render_balance(self, value):
|
||||
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
|
||||
})
|
||||
.done(function() {
|
||||
addMsg('Invité supprimé','success');
|
||||
addMsg('{% trans "Guest deleted" %}', 'success');
|
||||
$("#guests_table").load(location.pathname + " #guests_table");
|
||||
})
|
||||
.fail(function(xhr, textStatus, error) {
|
||||
|
@ -63,7 +63,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
refreshBalance();
|
||||
}
|
||||
|
||||
alias_obj.keyup(reloadTable);
|
||||
alias_obj.keyup(function(event) {
|
||||
let code = event.originalEvent.keyCode
|
||||
if (65 <= code <= 122 || code === 13) {
|
||||
debounce(reloadTable)()
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(init);
|
||||
|
||||
@ -86,10 +91,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
}).done(function () {
|
||||
if (target.hasClass("table-info"))
|
||||
addMsg(
|
||||
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.",
|
||||
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
|
||||
"warning", 10000);
|
||||
else
|
||||
addMsg("Entrée effectuée !", "success", 4000);
|
||||
addMsg("Entry made!", "success", 4000);
|
||||
reloadTable(true);
|
||||
}).fail(function (xhr) {
|
||||
errMsg(xhr.responseJSON, 4000);
|
||||
@ -121,10 +126,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
}).done(function () {
|
||||
if (target.hasClass("table-info"))
|
||||
addMsg(
|
||||
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.",
|
||||
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
|
||||
"warning", 10000);
|
||||
else
|
||||
addMsg("Entrée effectuée !", "success", 4000);
|
||||
addMsg("{% trans "Entry done!" %}", "success", 4000);
|
||||
reloadTable(true);
|
||||
}).fail(function (xhr) {
|
||||
errMsg(xhr.responseJSON, 4000);
|
||||
|
@ -34,7 +34,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endif %}
|
||||
<div class="card-footer">
|
||||
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
|
||||
<i class="fa fa-calendar-plus-o" aria-hidden="true"></i>
|
||||
<svg class="bi bi-calendar-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM8.5 8.5V10H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V11H6a.5.5 0 0 1 0-1h1.5V8.5a.5.5 0 0 1 1 0z"/>
|
||||
</svg>
|
||||
{% trans 'New activity' %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -1,15 +1,18 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from api.tests import TestAPI
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from activity.models import Activity, ActivityType, Guest, Entry
|
||||
from member.models import Club
|
||||
|
||||
from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
|
||||
from ..models import Activity, ActivityType, Guest, Entry
|
||||
|
||||
|
||||
class TestActivities(TestCase):
|
||||
"""
|
||||
@ -173,3 +176,58 @@ class TestActivities(TestCase):
|
||||
"""
|
||||
response = self.client.get(reverse("activity:calendar_ics"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestActivityAPI(TestAPI):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
self.activity = Activity.objects.create(
|
||||
name="Activity",
|
||||
description="This is a test activity\non two very very long lines\nbecause this is very important.",
|
||||
location="Earth",
|
||||
activity_type=ActivityType.objects.get(name="Pot"),
|
||||
creater=self.user,
|
||||
organizer=Club.objects.get(name="Kfet"),
|
||||
attendees_club=Club.objects.get(name="Kfet"),
|
||||
date_start=timezone.now(),
|
||||
date_end=timezone.now() + timedelta(days=2),
|
||||
valid=True,
|
||||
)
|
||||
|
||||
self.guest = Guest.objects.create(
|
||||
activity=self.activity,
|
||||
inviter=self.user.note,
|
||||
last_name="GUEST",
|
||||
first_name="Guest",
|
||||
)
|
||||
|
||||
self.entry = Entry.objects.create(
|
||||
activity=self.activity,
|
||||
note=self.user.note,
|
||||
guest=self.guest,
|
||||
)
|
||||
|
||||
def test_activity_api(self):
|
||||
"""
|
||||
Load Activity API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(ActivityViewSet, "/api/activity/activity/")
|
||||
|
||||
def test_activity_type_api(self):
|
||||
"""
|
||||
Load ActivityType API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(ActivityTypeViewSet, "/api/activity/type/")
|
||||
|
||||
def test_entry_api(self):
|
||||
"""
|
||||
Load Entry API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(EntryViewSet, "/api/activity/entry/")
|
||||
|
||||
def test_guest_api(self):
|
||||
"""
|
||||
Load Guest API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(GuestViewSet, "/api/activity/guest/")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from hashlib import md5
|
||||
@ -66,21 +66,19 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||
ordering = ('-date_start',)
|
||||
extra_context = {"title": _("Activities")}
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().distinct()
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
||||
context['upcoming'] = ActivityTable(
|
||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
||||
prefix='upcoming-',
|
||||
)
|
||||
|
||||
started_activities = Activity.objects\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
||||
.filter(open=True, valid=True).all()
|
||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||
context["started_activities"] = started_activities
|
||||
|
||||
return context
|
||||
@ -98,7 +96,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data()
|
||||
|
||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
||||
context["guests"] = table
|
||||
|
||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
||||
@ -144,15 +142,15 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
||||
.get(pk=self.kwargs["pk"])
|
||||
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||
.filter(pk=self.kwargs["pk"]).first()
|
||||
form.fields["inviter"].initial = self.request.user.note
|
||||
return form
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.activity = Activity.objects\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
@ -173,7 +171,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
||||
|
||||
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
||||
if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
|
||||
if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry):
|
||||
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
|
||||
|
||||
if not activity.activity_type.manage_entries:
|
||||
@ -191,8 +189,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
guest_qs = Guest.objects\
|
||||
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
||||
.filter(activity=activity)\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
|
||||
.order_by('last_name', 'first_name').distinct()
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
|
||||
.order_by('last_name', 'first_name')
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
@ -206,7 +204,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
)
|
||||
else:
|
||||
guest_qs = guest_qs.none()
|
||||
return guest_qs
|
||||
return guest_qs.distinct()
|
||||
|
||||
def get_invited_note(self, activity):
|
||||
"""
|
||||
@ -230,7 +228,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
)
|
||||
|
||||
# Filter with permission backend
|
||||
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
|
||||
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
@ -256,7 +254,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||
.distinct().get(pk=self.kwargs["pk"])
|
||||
context["activity"] = activity
|
||||
|
||||
@ -281,9 +279,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
||||
|
||||
activities_open = Activity.objects.filter(open=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
|
||||
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||
context["activities_open"] = [a for a in activities_open
|
||||
if PermissionBackend.check_perm(self.request.user,
|
||||
if PermissionBackend.check_perm(self.request,
|
||||
"activity.add_entry",
|
||||
Entry(activity=a, note=self.request.user.note,))]
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'api.apps.APIConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,13 +1,17 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
||||
from note.api.serializers import NoteSerializer
|
||||
from note.models import Alias
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
@ -22,7 +26,7 @@ class UserSerializer(ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class ContentTypeSerializer(ModelSerializer):
|
||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
@ -31,3 +35,42 @@ class ContentTypeSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class OAuthSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Informations that are transmitted by OAuth.
|
||||
For now, this includes user, profile and valid memberships.
|
||||
This should be better managed later.
|
||||
"""
|
||||
normalized_name = serializers.SerializerMethodField()
|
||||
|
||||
profile = ProfileSerializer()
|
||||
|
||||
note = NoteSerializer()
|
||||
|
||||
memberships = serializers.SerializerMethodField()
|
||||
|
||||
def get_normalized_name(self, obj):
|
||||
return Alias.normalize(obj.username)
|
||||
|
||||
def get_memberships(self, obj):
|
||||
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'id',
|
||||
'username',
|
||||
'normalized_name',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'is_superuser',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'profile',
|
||||
'note',
|
||||
'memberships',
|
||||
)
|
||||
|
240
apps/api/tests.py
Normal file
240
apps/api/tests.py
Normal file
@ -0,0 +1,240 @@
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote_plus
|
||||
from warnings import warn
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from django.test import TestCase
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from member.models import Membership, Club
|
||||
from note.models import NoteClub, NoteUser, Alias, Note
|
||||
from permission.models import PermissionMask, Permission, Role
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
|
||||
class TestAPI(TestCase):
|
||||
"""
|
||||
Load API pages and check that filters are working.
|
||||
"""
|
||||
fixtures = ('initial', )
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_superuser(
|
||||
username="adminapi",
|
||||
password="adminapi",
|
||||
email="adminapi@example.com",
|
||||
last_name="Admin",
|
||||
first_name="Admin",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
sess = self.client.session
|
||||
sess["permission_mask"] = 42
|
||||
sess.save()
|
||||
|
||||
def check_viewset(self, viewset, url):
|
||||
"""
|
||||
This function should be called inside a unit test.
|
||||
This loads the viewset and for each filter entry, it checks that the filter is running good.
|
||||
"""
|
||||
resp = self.client.get(url + "?format=json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
model = viewset.serializer_class.Meta.model
|
||||
|
||||
if not model.objects.exists(): # pragma: no cover
|
||||
warn(f"Warning: unable to test API filters for the model {model._meta.verbose_name} "
|
||||
"since there is no instance of it.")
|
||||
return
|
||||
|
||||
if hasattr(viewset, "filter_backends"):
|
||||
backends = viewset.filter_backends
|
||||
obj = model.objects.last()
|
||||
|
||||
if DjangoFilterBackend in backends:
|
||||
# Specific search
|
||||
for field in viewset.filterset_fields:
|
||||
obj = self.fix_note_object(obj, field)
|
||||
|
||||
value = self.get_value(obj, field)
|
||||
if value is None: # pragma: no cover
|
||||
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
|
||||
"has not been tested.")
|
||||
continue
|
||||
resp = self.client.get(url + f"?format=json&{field}={quote_plus(str(value))}")
|
||||
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
|
||||
f"{model._meta.verbose_name} does not work. "
|
||||
f"Given parameter: {value}")
|
||||
content = json.loads(resp.content)
|
||||
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
|
||||
f"{model._meta.verbose_name} does not work. "
|
||||
f"Given parameter: {value}")
|
||||
|
||||
if OrderingFilter in backends:
|
||||
# Ensure that ordering is working well
|
||||
for field in viewset.ordering_fields:
|
||||
resp = self.client.get(url + f"?ordering={field}")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp = self.client.get(url + f"?ordering=-{field}")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
if SearchFilter in backends:
|
||||
# Basic search
|
||||
for field in viewset.search_fields:
|
||||
obj = self.fix_note_object(obj, field)
|
||||
|
||||
if field[0] == '$' or field[0] == '=':
|
||||
field = field[1:]
|
||||
value = self.get_value(obj, field)
|
||||
if value is None: # pragma: no cover
|
||||
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
|
||||
"has not been tested.")
|
||||
continue
|
||||
resp = self.client.get(url + f"?format=json&search={quote_plus(str(value))}")
|
||||
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
|
||||
f"{model._meta.verbose_name} does not work. "
|
||||
f"Given parameter: {value}")
|
||||
content = json.loads(resp.content)
|
||||
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
|
||||
f"{model._meta.verbose_name} does not work. "
|
||||
f"Given parameter: {value}")
|
||||
|
||||
self.check_permissions(url, obj)
|
||||
|
||||
def check_permissions(self, url, obj):
|
||||
"""
|
||||
Check that permissions are working
|
||||
"""
|
||||
# Drop rights
|
||||
self.user.is_superuser = False
|
||||
self.user.save()
|
||||
sess = self.client.session
|
||||
sess["permission_mask"] = 0
|
||||
sess.save()
|
||||
|
||||
# Delete user permissions
|
||||
for m in Membership.objects.filter(user=self.user).all():
|
||||
m.roles.clear()
|
||||
m.save()
|
||||
|
||||
# Create a new role, which will have the checking permission
|
||||
role = Role.objects.get_or_create(name="β-tester")[0]
|
||||
role.permissions.clear()
|
||||
role.save()
|
||||
membership = Membership.objects.get_or_create(user=self.user, club=Club.objects.get(name="BDE"))[0]
|
||||
membership.roles.set([role])
|
||||
membership.save()
|
||||
|
||||
# Ensure that the access to the object is forbidden without permission
|
||||
resp = self.client.get(url + f"{obj.pk}/")
|
||||
self.assertEqual(resp.status_code, 404, f"Mysterious access to {url}{obj.pk}/ for {obj}")
|
||||
|
||||
obj.refresh_from_db()
|
||||
|
||||
# There are problems with polymorphism
|
||||
if isinstance(obj, Note) and hasattr(obj, "note_ptr"):
|
||||
obj = obj.note_ptr
|
||||
|
||||
mask = PermissionMask.objects.get(rank=0)
|
||||
|
||||
for field in obj._meta.fields:
|
||||
# Build permission query
|
||||
value = self.get_value(obj, field.name)
|
||||
if isinstance(value, date) or isinstance(value, datetime):
|
||||
value = value.isoformat()
|
||||
elif isinstance(value, ImageFieldFile):
|
||||
value = value.name
|
||||
elif isinstance(value, Decimal):
|
||||
value = str(value)
|
||||
query = json.dumps({field.name: value})
|
||||
|
||||
# Create sample permission
|
||||
permission = Permission.objects.get_or_create(
|
||||
model=ContentType.objects.get_for_model(obj._meta.model),
|
||||
query=query,
|
||||
mask=mask,
|
||||
type="view",
|
||||
permanent=False,
|
||||
description=f"Can view {obj._meta.verbose_name}",
|
||||
)[0]
|
||||
role.permissions.set([permission])
|
||||
role.save()
|
||||
|
||||
# Check that the access is possible
|
||||
resp = self.client.get(url + f"{obj.pk}/")
|
||||
self.assertEqual(resp.status_code, 200, f"Permission {permission.query} is not working "
|
||||
f"for the model {obj._meta.verbose_name}")
|
||||
|
||||
# Restore rights
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
sess = self.client.session
|
||||
sess["permission_mask"] = 42
|
||||
sess.save()
|
||||
|
||||
@staticmethod
|
||||
def get_value(obj, key: str):
|
||||
"""
|
||||
Resolve the queryset filter to get the Python value of an object.
|
||||
"""
|
||||
if hasattr(obj, "all"):
|
||||
# obj is a RelatedManager
|
||||
obj = obj.last()
|
||||
|
||||
if obj is None: # pragma: no cover
|
||||
return None
|
||||
|
||||
if '__' not in key:
|
||||
obj = getattr(obj, key)
|
||||
if hasattr(obj, "pk"):
|
||||
return obj.pk
|
||||
elif hasattr(obj, "all"):
|
||||
if not obj.exists(): # pragma: no cover
|
||||
return None
|
||||
return obj.last().pk
|
||||
elif isinstance(obj, bool):
|
||||
return int(obj)
|
||||
elif isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, PhoneNumber):
|
||||
return obj.raw_input
|
||||
return obj
|
||||
|
||||
key, remaining = key.split('__', 1)
|
||||
return TestAPI.get_value(getattr(obj, key), remaining)
|
||||
|
||||
@staticmethod
|
||||
def fix_note_object(obj, field):
|
||||
"""
|
||||
When querying an object that has a noteclub or a noteuser field,
|
||||
ensure that the object has a good value.
|
||||
"""
|
||||
if isinstance(obj, Alias):
|
||||
if "noteuser" in field:
|
||||
return NoteUser.objects.last().alias.last()
|
||||
elif "noteclub" in field:
|
||||
return NoteClub.objects.last().alias.last()
|
||||
elif isinstance(obj, Note):
|
||||
if "noteuser" in field:
|
||||
return NoteUser.objects.last()
|
||||
elif "noteclub" in field:
|
||||
return NoteClub.objects.last()
|
||||
return obj
|
||||
|
||||
|
||||
class TestBasicAPI(TestAPI):
|
||||
def test_user_api(self):
|
||||
"""
|
||||
Load the user page.
|
||||
"""
|
||||
self.check_viewset(ContentTypeViewSet, "/api/models/")
|
||||
self.check_viewset(UserViewSet, "/api/user/")
|
@ -1,10 +1,11 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import UserInformationView
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
@ -47,5 +48,6 @@ app_name = 'api'
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^me/', UserInformationView.as_view()),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
||||
|
20
apps/api/views.py
Normal file
20
apps/api/views.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
|
||||
from .serializers import OAuthSerializer
|
||||
|
||||
|
||||
class UserInformationView(RetrieveAPIView):
|
||||
"""
|
||||
These fields are give to OAuth authenticators.
|
||||
"""
|
||||
serializer_class = OAuthSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(pk=self.request.user.pk)
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -6,9 +6,9 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||
from permission.backends import PermissionBackend
|
||||
from note_kfet.middlewares import get_current_session
|
||||
from note.models import Alias
|
||||
|
||||
from .serializers import UserSerializer, ContentTypeSerializer
|
||||
@ -24,9 +24,7 @@ class ReadProtectedModelViewSet(ModelViewSet):
|
||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
get_current_session().setdefault("permission_mask", 42)
|
||||
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
||||
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
|
||||
|
||||
|
||||
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
||||
@ -39,21 +37,20 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
get_current_session().setdefault("permission_mask", 42)
|
||||
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
||||
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
|
||||
|
||||
|
||||
class UserViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/users/
|
||||
then render it on /api/user/
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
queryset = User.objects
|
||||
serializer_class = UserSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
|
||||
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active',
|
||||
'note__alias__name', 'note__alias__normalized_name', ]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
@ -106,7 +103,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/users/
|
||||
then render it on /api/models/
|
||||
"""
|
||||
queryset = ContentType.objects.all()
|
||||
queryset = ContentType.objects.order_by('id')
|
||||
serializer_class = ContentTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['id', 'app_label', 'model', ]
|
||||
search_fields = ['$app_label', '$model', ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'logs.apps.LogsConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ChangelogViewSet
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/logs/
|
||||
"""
|
||||
queryset = Changelog.objects.all()
|
||||
queryset = Changelog.objects.order_by('id')
|
||||
serializer_class = ChangelogSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from note.models import NoteUser, Alias
|
||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
|
||||
from note_kfet.middlewares import get_current_request
|
||||
|
||||
from .models import Changelog
|
||||
|
||||
@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
|
||||
previous = instance._previous
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
request = get_current_request()
|
||||
|
||||
if user is None:
|
||||
if request is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
@ -71,9 +71,23 @@ def save_object(sender, instance, **kwargs):
|
||||
# else:
|
||||
if note.exists():
|
||||
user = note.get().user
|
||||
else:
|
||||
user = None
|
||||
else:
|
||||
user = request.user
|
||||
if 'HTTP_X_REAL_IP' in request.META:
|
||||
ip = request.META.get('HTTP_X_REAL_IP')
|
||||
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
|
||||
if not user.is_authenticated:
|
||||
# For registration and OAuth2 purposes
|
||||
user = None
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
if user is not None and instance._meta.label_lower == "auth.user" and previous:
|
||||
if request is not None and instance._meta.label_lower == "auth.user" and previous:
|
||||
# On n'enregistre pas les connexions
|
||||
if instance.last_login != previous.last_login:
|
||||
return
|
||||
@ -121,9 +135,9 @@ def delete_object(sender, instance, **kwargs):
|
||||
return
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
request = get_current_request()
|
||||
|
||||
if user is None:
|
||||
if request is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
@ -135,6 +149,20 @@ def delete_object(sender, instance, **kwargs):
|
||||
# else:
|
||||
if note.exists():
|
||||
user = note.get().user
|
||||
else:
|
||||
user = None
|
||||
else:
|
||||
user = request.user
|
||||
if 'HTTP_X_REAL_IP' in request.META:
|
||||
ip = request.META.get('HTTP_X_REAL_IP')
|
||||
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
|
||||
if not user.is_authenticated:
|
||||
# For registration and OAuth2 purposes
|
||||
user = None
|
||||
|
||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
||||
class CustomSerializer(ModelSerializer):
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'member.apps.MemberConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||
|
@ -1,7 +1,8 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework.filters import SearchFilter
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||
@ -14,8 +15,15 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/profile/
|
||||
"""
|
||||
queryset = Profile.objects.all()
|
||||
queryset = Profile.objects.order_by('id')
|
||||
serializer_class = ProfileSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||
'ml_art_registration', 'report_frequency', 'email_confirmed', 'registration_valid', ]
|
||||
search_fields = ['$user__first_name', '$user__last_name', '$user__username', '$user__email',
|
||||
'$user__note__alias__name', '$user__note__alias__normalized_name', ]
|
||||
|
||||
|
||||
class ClubViewSet(ReadProtectedModelViewSet):
|
||||
@ -24,10 +32,13 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/club/
|
||||
"""
|
||||
queryset = Club.objects.all()
|
||||
queryset = Club.objects.order_by('id')
|
||||
serializer_class = ClubSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||
'membership_duration', 'membership_start', 'membership_end', ]
|
||||
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
|
||||
|
||||
|
||||
class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
@ -36,5 +47,14 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/membership/
|
||||
"""
|
||||
queryset = Membership.objects.all()
|
||||
queryset = Membership.objects.order_by('id')
|
||||
serializer_class = MembershipSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
||||
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||
'date_start', 'date_end', 'fee', 'roles', ]
|
||||
ordering_fields = ['id', 'date_start', 'date_end', ]
|
||||
search_fields = ['$club__name', '$club__email', '$club__note__alias__name', '$club__note__alias__normalized_name',
|
||||
'$user__username', '$user__last_name', '$user__first_name', '$user__email',
|
||||
'$user__note__alias__name', '$user__note__alias__normalized_name', '$roles__name', ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
17
apps/member/auth.py
Normal file
17
apps/member/auth.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||
from note.models import Alias
|
||||
|
||||
|
||||
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
|
||||
"""
|
||||
Override Django Auth User model to define a custom Matrix username.
|
||||
"""
|
||||
|
||||
def attributs(self):
|
||||
d = super().attributs()
|
||||
if self.user:
|
||||
d["normalized_name"] = Alias.normalize(self.user.username)
|
||||
return d
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import io
|
||||
|
@ -1,12 +1,14 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher, mask_hash
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_request
|
||||
|
||||
|
||||
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||
@ -24,16 +26,22 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||
|
||||
def must_update(self, encoded):
|
||||
if settings.DEBUG:
|
||||
current_user = get_current_authenticated_user()
|
||||
# Small hack to let superusers to impersonate people.
|
||||
# Don't change their password.
|
||||
request = get_current_request()
|
||||
current_user = request.user
|
||||
if current_user is not None and current_user.is_superuser:
|
||||
return False
|
||||
return True
|
||||
|
||||
def verify(self, password, encoded):
|
||||
if settings.DEBUG:
|
||||
current_user = get_current_authenticated_user()
|
||||
# Small hack to let superusers to impersonate people.
|
||||
# If a superuser is already connected, let him/her log in as another person.
|
||||
request = get_current_request()
|
||||
current_user = request.user
|
||||
if current_user is not None and current_user.is_superuser\
|
||||
and get_current_session().get("permission_mask", -1) >= 42:
|
||||
and request.session.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
|
||||
if '|' in encoded:
|
||||
@ -41,6 +49,18 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
||||
return super().verify(password, encoded)
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
# Displayed information in Django Admin.
|
||||
if '|' in encoded:
|
||||
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
||||
return OrderedDict([
|
||||
(_('algorithm'), 'custom_nk15'),
|
||||
(_('iterations'), '1'),
|
||||
(_('salt'), mask_hash(salt)),
|
||||
(_('hash'), mask_hash(db_hashed_pass)),
|
||||
])
|
||||
return super().safe_summary(encoded)
|
||||
|
||||
|
||||
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
||||
"""
|
||||
@ -51,8 +71,11 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
||||
|
||||
def verify(self, password, encoded):
|
||||
if settings.DEBUG:
|
||||
current_user = get_current_authenticated_user()
|
||||
# Small hack to let superusers to impersonate people.
|
||||
# If a superuser is already connected, let him/her log in as another person.
|
||||
request = get_current_request()
|
||||
current_user = request.user
|
||||
if current_user is not None and current_user.is_superuser\
|
||||
and get_current_session().get("permission_mask", -1) >= 42:
|
||||
and request.session.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
return super().verify(password, encoded)
|
||||
|
@ -19,8 +19,8 @@ def create_bde_and_kfet(apps, schema_editor):
|
||||
membership_fee_paid=500,
|
||||
membership_fee_unpaid=500,
|
||||
membership_duration=396,
|
||||
membership_start="2020-08-01",
|
||||
membership_end="2021-09-30",
|
||||
membership_start="2021-08-01",
|
||||
membership_end="2022-09-30",
|
||||
)
|
||||
Club.objects.get_or_create(
|
||||
id=2,
|
||||
@ -31,8 +31,8 @@ def create_bde_and_kfet(apps, schema_editor):
|
||||
membership_fee_paid=3500,
|
||||
membership_fee_unpaid=3500,
|
||||
membership_duration=396,
|
||||
membership_start="2020-08-01",
|
||||
membership_end="2021-09-30",
|
||||
membership_start="2021-08-01",
|
||||
membership_end="2022-09-30",
|
||||
)
|
||||
|
||||
NoteClub.objects.get_or_create(
|
||||
|
@ -0,0 +1,50 @@
|
||||
import sys
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def give_note_account_permissions(apps, schema_editor):
|
||||
"""
|
||||
Automatically manage the membership of the Note account.
|
||||
"""
|
||||
User = apps.get_model("auth", "user")
|
||||
Membership = apps.get_model("member", "membership")
|
||||
Role = apps.get_model("permission", "role")
|
||||
|
||||
note = User.objects.filter(username="note")
|
||||
if not note.exists():
|
||||
# We are in a test environment, don't log error message
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'test':
|
||||
return
|
||||
print("Warning: Note account was not found. The note account was not imported.")
|
||||
print("Make sure you have imported the NK15 database. The new import script handles correctly the permissions.")
|
||||
print("This migration will be ignored, you can re-run it if you forgot the note account or ignore it if you "
|
||||
"don't want this account.")
|
||||
return
|
||||
|
||||
note = note.get()
|
||||
|
||||
# Set for the two clubs a large expiration date and the correct role.
|
||||
for m in Membership.objects.filter(user_id=note.id).all():
|
||||
m.date_end = "3142-12-12"
|
||||
m.roles.set(Role.objects.filter(name="PC Kfet").all())
|
||||
m.save()
|
||||
# By default, the note account is only authorized to be logged from localhost.
|
||||
note.password = "ipbased$127.0.0.1"
|
||||
note.is_active = True
|
||||
note.save()
|
||||
# Ensure that the note of the account is disabled
|
||||
note.note.inactivity_reason = 'forced'
|
||||
note.note.is_active = False
|
||||
note.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('member', '0005_remove_null_tag_on_charfields'),
|
||||
('permission', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(give_note_account_permissions),
|
||||
]
|
23
apps/member/migrations/0007_auto_20210313_1235.py
Normal file
23
apps/member/migrations/0007_auto_20210313_1235.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.19 on 2021-03-13 11:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0006_create_note_account_bde_membership'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='membership',
|
||||
name='roles',
|
||||
field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='promotion',
|
||||
field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import datetime
|
||||
@ -57,7 +57,7 @@ class Profile(models.Model):
|
||||
('A1', _("Mathematics (A1)")),
|
||||
('A2', _("Physics (A2)")),
|
||||
("A'2", _("Applied physics (A'2)")),
|
||||
('A''2', _("Chemistry (A''2)")),
|
||||
("A''2", _("Chemistry (A''2)")),
|
||||
('A3', _("Biology (A3)")),
|
||||
('B1234', _("SAPHIRE (B1234)")),
|
||||
('B1', _("Mechanics (B1)")),
|
||||
@ -74,7 +74,7 @@ class Profile(models.Model):
|
||||
|
||||
promotion = models.PositiveSmallIntegerField(
|
||||
null=True,
|
||||
default=datetime.date.today().year,
|
||||
default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1,
|
||||
verbose_name=_("promotion"),
|
||||
help_text=_("Year of entry to the school (None if not ENS student)"),
|
||||
)
|
||||
@ -313,6 +313,7 @@ class Membership(models.Model):
|
||||
|
||||
roles = models.ManyToManyField(
|
||||
"permission.Role",
|
||||
related_name="memberships",
|
||||
verbose_name=_("roles"),
|
||||
)
|
||||
|
||||
@ -412,6 +413,12 @@ class Membership(models.Model):
|
||||
"""
|
||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||
"""
|
||||
# Ensure that club membership dates are valid
|
||||
old_membership_start = self.club.membership_start
|
||||
self.club.update_membership_dates()
|
||||
if self.club.membership_start != old_membership_start:
|
||||
self.club.save()
|
||||
|
||||
created = not self.pk
|
||||
if not created:
|
||||
for role in self.roles.all():
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ function create_alias (e) {
|
||||
}).done(function () {
|
||||
// Reload table
|
||||
$('#alias_table').load(location.pathname + ' #alias_table')
|
||||
addMsg('Alias ajouté', 'success')
|
||||
addMsg(gettext('Alias successfully added'), 'success')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
@ -22,7 +22,7 @@ function create_alias (e) {
|
||||
|
||||
/**
|
||||
* On click of "delete", delete the alias
|
||||
* @param Integer button_id Alias id to remove
|
||||
* @param button_id:Integer Alias id to remove
|
||||
*/
|
||||
function delete_button (button_id) {
|
||||
$.ajax({
|
||||
@ -30,7 +30,7 @@ function delete_button (button_id) {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||
}).done(function () {
|
||||
addMsg('Alias supprimé', 'success')
|
||||
addMsg(gettext('Alias successfully deleted'), 'success')
|
||||
$('#alias_table').load(location.pathname + ' #alias_table')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import Club, Membership
|
||||
@ -31,7 +31,8 @@ class ClubTable(tables.Table):
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
'data-href': lambda record: record.pk,
|
||||
'style': 'cursor:pointer',
|
||||
}
|
||||
|
||||
|
||||
@ -43,11 +44,27 @@ class UserTable(tables.Table):
|
||||
|
||||
section = tables.Column(accessor='profile__section')
|
||||
|
||||
# Override the column to let replace the URL
|
||||
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
|
||||
|
||||
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
|
||||
|
||||
def render_email(self, record, value):
|
||||
# Replace the email by a dash if the user can't see the profile detail
|
||||
# Replace also the URL
|
||||
if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile):
|
||||
value = "—"
|
||||
record.email = value
|
||||
return value
|
||||
|
||||
def render_section(self, record, value):
|
||||
return value \
|
||||
if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \
|
||||
else "—"
|
||||
|
||||
def render_balance(self, record, value):
|
||||
return pretty_money(value)\
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—"
|
||||
if PermissionBackend.check_perm(get_current_request(), "note.view_note", record.note) else "—"
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
@ -58,7 +75,8 @@ class UserTable(tables.Table):
|
||||
model = User
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'data-href': lambda record: record.pk
|
||||
'data-href': lambda record: record.pk,
|
||||
'style': 'cursor:pointer',
|
||||
}
|
||||
|
||||
|
||||
@ -77,7 +95,7 @@ class MembershipTable(tables.Table):
|
||||
def render_user(self, value):
|
||||
# If the user has the right, link the displayed user with the page of its detail.
|
||||
s = value.username
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
||||
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||
s = format_html("<a href={url}>{name}</a>",
|
||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||
|
||||
@ -86,7 +104,7 @@ class MembershipTable(tables.Table):
|
||||
def render_club(self, value):
|
||||
# If the user has the right, link the displayed club with the page of its detail.
|
||||
s = value.name
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
|
||||
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
|
||||
s = format_html("<a href={url}>{name}</a>",
|
||||
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
||||
|
||||
@ -111,7 +129,7 @@ class MembershipTable(tables.Table):
|
||||
date_end=date.today(),
|
||||
fee=0,
|
||||
)
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
if PermissionBackend.check_perm(get_current_request(),
|
||||
"member.add_membership", empty_membership): # If the user has right
|
||||
renew_url = reverse_lazy('member:club_renew_membership',
|
||||
kwargs={"pk": record.pk})
|
||||
@ -126,7 +144,7 @@ class MembershipTable(tables.Table):
|
||||
# If the user has the right to manage the roles, display the link to manage them
|
||||
roles = record.roles.all()
|
||||
s = ", ".join(str(role) for role in roles)
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
|
||||
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
|
||||
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
||||
+ "'>" + s + "</a>")
|
||||
return s
|
||||
@ -149,7 +167,7 @@ class ClubManagerTable(tables.Table):
|
||||
def render_user(self, value):
|
||||
# If the user has the right, link the displayed user with the page of its detail.
|
||||
s = value.username
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
||||
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||
s = format_html("<a href={url}>{name}</a>",
|
||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||
|
||||
|
@ -45,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card-footer">
|
||||
{% if user_object %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}">
|
||||
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Update Profile' %}
|
||||
</a>
|
||||
{% url 'member:user_detail' user_object.pk as user_profile_url %}
|
||||
{% if request.path_info != user_profile_url %}
|
||||
@ -59,7 +62,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% if ".change_"|has_perm:club %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}"
|
||||
data-turbolinks="false">
|
||||
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Update Profile' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% url 'member:club_detail' club.pk as club_detail_url %}
|
||||
|
@ -10,7 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="font-weight-bold">
|
||||
<i class="fa fa-users"></i> {% trans "Club managers" %}
|
||||
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
{% trans "Club managers" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table managers %}
|
||||
@ -23,7 +26,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
|
||||
<i class="fa fa-users"></i> {% trans "Club members" %}
|
||||
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
|
||||
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||
</svg>
|
||||
{% trans "Club members" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table member_list %}
|
||||
@ -37,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card-header position-relative" id="historyListHeading">
|
||||
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %}
|
||||
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
|
||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
||||
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||
</svg>
|
||||
{% trans "Transaction history" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="history_list">
|
||||
|
@ -47,8 +47,10 @@
|
||||
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }})
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
|
@ -11,7 +11,9 @@
|
||||
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'password_change' %}">
|
||||
<i class="fa fa-lock"></i>
|
||||
<svg class="bi bi-lock" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
{% trans 'Change password' %}
|
||||
</a>
|
||||
</dd>
|
||||
@ -20,37 +22,44 @@
|
||||
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }})
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
||||
{% if "member.view_profile"|has_perm:user_object.profile %}
|
||||
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
|
||||
<dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd>
|
||||
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
|
||||
<dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
|
||||
<dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a>
|
||||
</dd>
|
||||
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
|
||||
<dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
{% if user_object.pk == user.pk %}
|
||||
<div class="text-center">
|
||||
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
||||
<svg class="bi bi-cogs" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
||||
</svg>
|
||||
{% trans 'API token' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -5,32 +5,98 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
<h4>À quoi sert un jeton d'authentification ?</h4>
|
||||
<div class="row mt-4">
|
||||
<div class="col-xl-6">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h3>{% trans "Token authentication" %}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<h4>À quoi sert un jeton d'authentification ?</h4>
|
||||
|
||||
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
|
||||
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code>
|
||||
pour pouvoir vous identifier.<br /><br />
|
||||
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte
|
||||
depuis un client externe.<br />
|
||||
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code>
|
||||
pour pouvoir vous identifier.<br /><br />
|
||||
|
||||
Une documentation de l'API arrivera ultérieurement.
|
||||
La documentation de l'API est disponible ici :
|
||||
<a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>{%trans 'Token' %} :</strong>
|
||||
{% if 'show' in request.GET %}
|
||||
{{ token.key }} (<a href="?">cacher</a>)
|
||||
{% else %}
|
||||
<em>caché</em> (<a href="?show">montrer</a>)
|
||||
{% endif %}
|
||||
<br />
|
||||
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>{% trans "Warning" %} :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="?regenerate">
|
||||
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h3>{% trans "OAuth2 authentication" %}</h3>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
La Note Kfet implémente également le protocole <a href="https://oauth.net/2/">OAuth2</a>, afin de
|
||||
permettre à des applications tierces d'interagir avec la Note en récoltant des informations
|
||||
(de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il
|
||||
s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L'usage de ce protocole est recommandé pour tout usage non personnel, car permet de mieux cibler
|
||||
les droits dont on a besoin, en restreignant leur usage par jeton généré.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
La documentation vis-à-vis de l'usage de ce protocole est disponible ici :
|
||||
<a href="/doc/external_services/oauth2/">{{ request.scheme }}://{{ request.get_host }}/doc/external_services/oauth2/</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Liste des URL à communiquer à votre application :
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{% trans "Authorization:" %}
|
||||
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Token:" %}
|
||||
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:token' %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Revoke Token:" %}
|
||||
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:revoke-token' %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Introspect Token:" %}
|
||||
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:introspect' %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a class="btn btn-primary" href="{% url 'oauth2_provider:list' %}">{% trans "Show my applications" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>{%trans 'Token' %} :</strong>
|
||||
{% if 'show' in request.GET %}
|
||||
{{ token.key }} (<a href="?">cacher</a>)
|
||||
{% else %}
|
||||
<em>caché</em> (<a href="?show">montrer</a>)
|
||||
{% endif %}
|
||||
<br />
|
||||
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
|
||||
</div>
|
||||
|
||||
<a href="?regenerate">
|
||||
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
|
||||
</a>
|
||||
{% endblock %}
|
@ -18,7 +18,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="font-weight-bold">
|
||||
<i class="fa fa-users"></i> {% trans "View my memberships" %}
|
||||
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
{% trans "View my memberships" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table club_list %}
|
||||
@ -29,7 +32,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<a class="stretched-link font-weight-bold text-decoration-none"
|
||||
{% if "note.view_note"|has_perm:user_object.note %}
|
||||
href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}>
|
||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
||||
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||
</svg>
|
||||
{% trans "Transaction history" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="history_list">
|
||||
|
@ -5,9 +5,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% load i18n perms %}
|
||||
|
||||
{% block content %}
|
||||
{% if "member.change_profile_registration_valid"|has_perm:user %}
|
||||
{% if can_manage_registrations %}
|
||||
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
|
||||
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
|
||||
<svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||
</svg>
|
||||
{% trans "Registrations" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -1,21 +1,24 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from datetime import date, timedelta
|
||||
|
||||
from api.tests import TestAPI
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db.models import Q
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from member.models import Club, Membership, Profile
|
||||
from note.models import Alias, NoteSpecial
|
||||
from permission.models import Role
|
||||
from treasury.models import SogeCredit
|
||||
|
||||
from ..api.views import ClubViewSet, MembershipViewSet, ProfileViewSet
|
||||
from ..models import Club, Membership, Profile
|
||||
|
||||
"""
|
||||
Create some users and clubs and test that all pages are rendering properly
|
||||
and that memberships are working.
|
||||
@ -403,3 +406,46 @@ class TestMemberships(TestCase):
|
||||
self.user.password = "custom_nk15$1$" + salt + "|" + hashed
|
||||
self.user.save()
|
||||
self.assertTrue(self.user.check_password(password))
|
||||
|
||||
|
||||
class TestMemberAPI(TestAPI):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
self.user.profile.registration_valid = True
|
||||
self.user.profile.email_confirmed = True
|
||||
self.user.profile.phone_number = "0600000000"
|
||||
self.user.profile.section = "1A0"
|
||||
self.user.profile.department = "A0"
|
||||
self.user.profile.address = "Earth"
|
||||
self.user.profile.save()
|
||||
|
||||
self.club = Club.objects.create(
|
||||
name="totoclub",
|
||||
parent_club=Club.objects.get(name="BDE"),
|
||||
membership_start=date(year=1970, month=1, day=1),
|
||||
membership_end=date(year=2040, month=1, day=1),
|
||||
membership_duration=365 * 10,
|
||||
)
|
||||
self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
|
||||
self.membership = Membership.objects.create(user=self.user, club=self.club)
|
||||
self.membership.roles.add(Role.objects.get(name="Bureau de club"))
|
||||
self.membership.save()
|
||||
|
||||
def test_club_api(self):
|
||||
"""
|
||||
Load Club API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(ClubViewSet, "/api/members/club/")
|
||||
|
||||
def test_profile_api(self):
|
||||
"""
|
||||
Load Profile API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(ProfileViewSet, "/api/members/profile/")
|
||||
|
||||
def test_membership_api(self):
|
||||
"""
|
||||
Load Membership API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(MembershipViewSet, "/api/members/membership/")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta, date
|
||||
@ -21,7 +21,7 @@ from rest_framework.authtoken.models import Token
|
||||
from note.models import Alias, NoteUser
|
||||
from note.models.transactions import Transaction, SpecialTransaction
|
||||
from note.tables import HistoryTable, AliasTable
|
||||
from note_kfet.middlewares import _set_current_user_and_ip
|
||||
from note_kfet.middlewares import _set_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
@ -41,7 +41,8 @@ class CustomLoginView(LoginView):
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
logout(self.request)
|
||||
_set_current_user_and_ip(form.get_user(), self.request.session, None)
|
||||
self.request.user = form.get_user()
|
||||
_set_current_request(self.request)
|
||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||
return super().form_valid(form)
|
||||
|
||||
@ -70,10 +71,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form.fields['email'].required = True
|
||||
form.fields['email'].help_text = _("This address must be valid.")
|
||||
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||
data=self.request.POST if self.request.POST else None)
|
||||
if not self.object.profile.report_frequency:
|
||||
del context['profile_form'].fields["last_report"]
|
||||
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||
data=self.request.POST if self.request.POST else None)
|
||||
if not self.object.profile.report_frequency:
|
||||
del context['profile_form'].fields["last_report"]
|
||||
|
||||
return context
|
||||
|
||||
@ -152,13 +154,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||
.order_by("-created_at")\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
|
||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||
context['history_list'] = history_table
|
||||
|
||||
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
|
||||
.order_by("club__name", "-date_start")
|
||||
# Display only the most recent membership
|
||||
club_list = club_list.distinct("club__name")\
|
||||
@ -175,21 +177,20 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
modified_note.is_active = True
|
||||
modified_note.inactivity_reason = 'manual'
|
||||
context["can_lock_note"] = user.note.is_active and PermissionBackend\
|
||||
.check_perm(self.request.user, "note.change_noteuser_is_active",
|
||||
modified_note)
|
||||
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
|
||||
modified_note.inactivity_reason = 'forced'
|
||||
modified_note._force_save = True
|
||||
modified_note.save()
|
||||
context["can_force_lock"] = user.note.is_active and PermissionBackend\
|
||||
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
|
||||
.check_perm(self.request, "note.change_note_is_active", modified_note)
|
||||
old_note._force_save = True
|
||||
old_note._no_signal = True
|
||||
old_note.save()
|
||||
modified_note.refresh_from_db()
|
||||
modified_note.is_active = True
|
||||
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
|
||||
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
|
||||
.check_perm(self.request, "note.change_note_is_active", modified_note)
|
||||
|
||||
return context
|
||||
|
||||
@ -234,6 +235,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
|
||||
.filter(profile__registration_valid=False)
|
||||
context["can_manage_registrations"] = pre_registered_users.exists()
|
||||
return context
|
||||
|
||||
|
||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
@ -247,9 +255,9 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
|
||||
.filter_queryset(self.request.user, Alias, "view")).all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
||||
context["aliases"] = AliasTable(
|
||||
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||
note=context["object"].note,
|
||||
name="",
|
||||
normalized_name="",
|
||||
@ -374,7 +382,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club(
|
||||
context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
|
||||
name="",
|
||||
email="club@example.com",
|
||||
))
|
||||
@ -396,7 +404,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
club = context["club"]
|
||||
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
||||
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
|
||||
club.update_membership_dates()
|
||||
# managers list
|
||||
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
|
||||
@ -405,7 +413,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
||||
# transaction history
|
||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||
.order_by('-created_at')
|
||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||
@ -414,7 +422,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
club_member = Membership.objects.filter(
|
||||
club=club,
|
||||
date_end__gte=date.today() - timedelta(days=15),
|
||||
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
|
||||
).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
|
||||
.order_by("user__username", "-date_start")
|
||||
# Display only the most recent membership
|
||||
club_member = club_member.distinct("user__username")\
|
||||
@ -450,9 +458,9 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
|
||||
.filter_queryset(self.request.user, Alias, "view")).all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
||||
context["aliases"] = AliasTable(note.alias.filter(
|
||||
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||
note=context["object"].note,
|
||||
name="",
|
||||
normalized_name="",
|
||||
@ -527,7 +535,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
form = context['form']
|
||||
|
||||
if "club_pk" in self.kwargs: # We create a new membership.
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
|
||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||
# Ensure that the user is member of the parent club and all its the family tree.
|
||||
@ -617,9 +625,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
# Retrieve form data
|
||||
credit_type = form.cleaned_data["credit_type"]
|
||||
credit_amount = form.cleaned_data["credit_amount"]
|
||||
last_name = form.cleaned_data["last_name"]
|
||||
first_name = form.cleaned_data["first_name"]
|
||||
bank = form.cleaned_data["bank"]
|
||||
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
|
||||
|
||||
if not credit_type:
|
||||
@ -651,7 +656,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
user=form.instance.user,
|
||||
club=club.parent_club,
|
||||
date_start__gte=club.parent_club.membership_start,
|
||||
date_end__lte=club.parent_club.membership_end,
|
||||
).exists():
|
||||
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
|
||||
error = True
|
||||
@ -666,15 +670,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
.format(form.instance.club.membership_end))
|
||||
error = True
|
||||
|
||||
if credit_amount:
|
||||
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
|
||||
if not last_name:
|
||||
form.add_error('last_name', _("This field is required."))
|
||||
if not first_name:
|
||||
form.add_error('first_name', _("This field is required."))
|
||||
if not bank and credit_type.special_type == "Chèque":
|
||||
form.add_error('bank', _("This field is required."))
|
||||
return self.form_invalid(form)
|
||||
if credit_amount and not SpecialTransaction.validate_payment_form(form):
|
||||
# Check that special information for payment are filled
|
||||
error = True
|
||||
|
||||
return not error
|
||||
|
||||
@ -685,7 +683,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
# Get the club that is concerned by the membership
|
||||
if "club_pk" in self.kwargs: # get from url of new membership
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
|
||||
.get(pk=self.kwargs["club_pk"])
|
||||
user = form.instance.user
|
||||
old_membership = None
|
||||
@ -736,6 +734,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
# When we renew the BDE membership, we update the profile section
|
||||
# that should happens at least once a year.
|
||||
user.profile.section = user.profile.section_generated
|
||||
user.profile._force_save = True
|
||||
user.profile.save()
|
||||
|
||||
# Credit note before the membership is created.
|
||||
@ -868,7 +867,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
club = Club.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Club, "view")
|
||||
PermissionBackend.filter_queryset(self.request, Club, "view")
|
||||
).get(pk=self.kwargs["pk"])
|
||||
context["club"] = club
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'note.apps.NoteConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||
from member.api.serializers import MembershipSerializer
|
||||
from member.models import Membership
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
from rest_framework.utils import model_meta
|
||||
|
||||
@ -126,7 +126,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
# If the user has no right to see the note, then we only display the note identifier
|
||||
return NotePolymorphicSerializer().to_representation(obj.note)\
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\
|
||||
if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\
|
||||
else dict(
|
||||
id=obj.note.id,
|
||||
name=str(obj.note),
|
||||
@ -142,7 +142,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
||||
def get_membership(self, obj):
|
||||
if isinstance(obj.note, NoteUser):
|
||||
memberships = Membership.objects.filter(
|
||||
PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
|
||||
PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter(
|
||||
user=obj.note.user,
|
||||
club=2, # Kfet
|
||||
).order_by("-date_start")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
@ -10,34 +11,40 @@ from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||
from note_kfet.middlewares import get_current_session
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
||||
from ..models.notes import Note, Alias
|
||||
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
|
||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||
|
||||
|
||||
class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
|
||||
The djangorestframework plugin will get all `Note` objects (with polymorhism),
|
||||
serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/note/
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
queryset = Note.objects.order_by('id')
|
||||
serializer_class = NotePolymorphicSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ['polymorphic_ctype', 'is_active', ]
|
||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
|
||||
ordering_fields = ['alias__name', 'alias__normalized_name']
|
||||
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
||||
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
||||
'$noteuser__user__email', '$noteclub__club__email', ]
|
||||
ordering_fields = ['alias__name', 'alias__normalized_name', 'balance', 'created_at', ]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Parse query and apply filters.
|
||||
:return: The filtered set of requested notes
|
||||
"""
|
||||
queryset = super().get_queryset().distinct()
|
||||
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
|
||||
| PermissionBackend.filter_queryset(self.request, NoteUser, "view")
|
||||
| PermissionBackend.filter_queryset(self.request, NoteClub, "view")
|
||||
| PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||
.distinct()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(
|
||||
@ -55,18 +62,19 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/aliases/
|
||||
"""
|
||||
queryset = Alias.objects.all()
|
||||
queryset = Alias.objects
|
||||
serializer_class = AliasSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['note']
|
||||
ordering_fields = ['name', 'normalized_name']
|
||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
ordering_fields = ['name', 'normalized_name', ]
|
||||
|
||||
def get_serializer_class(self):
|
||||
serializer_class = self.serializer_class
|
||||
if self.request.method in ['PUT', 'PATCH']:
|
||||
# alias owner cannot be change once establish
|
||||
setattr(serializer_class.Meta, 'read_only_fields', ('note',))
|
||||
serializer_class.Meta.read_only_fields = ('note',)
|
||||
return serializer_class
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
@ -74,7 +82,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
try:
|
||||
self.perform_destroy(instance)
|
||||
except ValidationError as e:
|
||||
return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST)
|
||||
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def get_queryset(self):
|
||||
@ -106,12 +114,13 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
|
||||
|
||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
queryset = Alias.objects.all()
|
||||
queryset = Alias.objects
|
||||
serializer_class = ConsumerSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['note']
|
||||
ordering_fields = ['name', 'normalized_name']
|
||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
ordering_fields = ['name', 'normalized_name', ]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
@ -125,23 +134,31 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
||||
|
||||
alias = self.request.query_params.get("alias", None)
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
try:
|
||||
re.compile(alias)
|
||||
valid_regex = True
|
||||
except (re.error, TypeError):
|
||||
valid_regex = False
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.prefetch_related('note')
|
||||
|
||||
if alias:
|
||||
# We match first an alias if it is matched without normalization,
|
||||
# then if the normalized pattern matches a normalized alias.
|
||||
queryset = queryset.filter(
|
||||
name__iregex="^" + alias
|
||||
**{f'name{suffix}': alias_prefix + alias}
|
||||
).union(
|
||||
queryset.filter(
|
||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
& ~Q(name__iregex="^" + alias)
|
||||
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||
),
|
||||
all=True).union(
|
||||
queryset.filter(
|
||||
Q(normalized_name__iregex="^" + alias.lower())
|
||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
& ~Q(name__iregex="^" + alias)
|
||||
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
||||
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||
),
|
||||
all=True)
|
||||
|
||||
@ -157,10 +174,11 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/category/
|
||||
"""
|
||||
queryset = TemplateCategory.objects.order_by("name").all()
|
||||
queryset = TemplateCategory.objects.order_by('name')
|
||||
serializer_class = TemplateCategorySerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'templates', 'templates__name']
|
||||
search_fields = ['$name', '$templates__name', ]
|
||||
|
||||
|
||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
@ -169,11 +187,12 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/template/
|
||||
"""
|
||||
queryset = TransactionTemplate.objects.order_by("name").all()
|
||||
queryset = TransactionTemplate.objects.order_by('name')
|
||||
serializer_class = TransactionTemplateSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
||||
filterset_fields = ['name', 'amount', 'display', 'category', ]
|
||||
search_fields = ['$name', ]
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
||||
search_fields = ['$name', '$category__name', ]
|
||||
ordering_fields = ['amount', ]
|
||||
|
||||
|
||||
class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
@ -182,16 +201,18 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/transaction/
|
||||
"""
|
||||
queryset = Transaction.objects.order_by("-created_at").all()
|
||||
queryset = Transaction.objects.order_by('-created_at')
|
||||
serializer_class = TransactionPolymorphicSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity",
|
||||
"polymorphic_ctype", "amount", "created_at", ]
|
||||
search_fields = ['$reason', ]
|
||||
ordering_fields = ['created_at', 'amount']
|
||||
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
||||
'destination', 'destination_alias', 'destination__alias__name',
|
||||
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
||||
'created_at', 'valid', 'invalidity_reason', ]
|
||||
search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name',
|
||||
'$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name',
|
||||
'$invalidity_reason', ]
|
||||
ordering_fields = ['created_at', 'amount', ]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
get_current_session().setdefault("permission_mask", 42)
|
||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
|
||||
return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
|
||||
.order_by("created_at", "id")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import datetime
|
||||
|
||||
|
19
apps/note/migrations/0005_auto_20210313_1235.py
Normal file
19
apps/note/migrations/0005_auto_20210313_1235.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.19 on 2021-03-13 11:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('note', '0004_remove_null_tag_on_charfields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='alias',
|
||||
name='note',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||
|
@ -1,10 +1,9 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import unicodedata
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.global_settings import DEFAULT_FROM_EMAIL
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.mail import send_mail
|
||||
from django.core.validators import RegexValidator
|
||||
@ -190,8 +189,8 @@ class NoteClub(Note):
|
||||
def send_mail_negative_balance(self):
|
||||
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
|
||||
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
|
||||
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text, DEFAULT_FROM_EMAIL,
|
||||
[self.club.email], html_message=html)
|
||||
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text,
|
||||
settings.DEFAULT_FROM_EMAIL, [self.club.email], html_message=html)
|
||||
|
||||
|
||||
class NoteSpecial(Note):
|
||||
@ -248,6 +247,7 @@ class Alias(models.Model):
|
||||
note = models.ForeignKey(
|
||||
Note,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="alias",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -223,7 +223,8 @@ class Transaction(PolymorphicModel):
|
||||
# Check that the amounts stay between big integer bounds
|
||||
diff_source, diff_dest = self.validate()
|
||||
|
||||
if not self.source.is_active or not self.destination.is_active:
|
||||
if not (hasattr(self, '_force_save') and self._force_save) \
|
||||
and (not self.source.is_active or not self.destination.is_active):
|
||||
raise ValidationError(_("The transaction can't be saved since the source note "
|
||||
"or the destination note is not active."))
|
||||
|
||||
@ -271,7 +272,7 @@ class RecurrentTransaction(Transaction):
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.template.destination != self.destination:
|
||||
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
|
||||
raise ValidationError(
|
||||
_("The destination of this transaction must equal to the destination of the template."))
|
||||
return super().clean()
|
||||
@ -332,6 +333,36 @@ class SpecialTransaction(Transaction):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def validate_payment_form(form):
|
||||
"""
|
||||
Ensure that last name and first name are filled for a form that creates a SpecialTransaction,
|
||||
and check that if the user pays with a check, then the bank field is filled.
|
||||
|
||||
Return True iff there is no error.
|
||||
Whenever there is an error, they are inserted in the form errors.
|
||||
"""
|
||||
|
||||
credit_type = form.cleaned_data["credit_type"]
|
||||
last_name = form.cleaned_data["last_name"]
|
||||
first_name = form.cleaned_data["first_name"]
|
||||
bank = form.cleaned_data["bank"]
|
||||
|
||||
error = False
|
||||
|
||||
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
|
||||
if not last_name:
|
||||
form.add_error('last_name', _("This field is required."))
|
||||
error = True
|
||||
if not first_name:
|
||||
form.add_error('first_name', _("This field is required."))
|
||||
error = True
|
||||
if not bank and credit_type.special_type == "Chèque":
|
||||
form.add_error('bank', _("This field is required."))
|
||||
error = True
|
||||
|
||||
return not error
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Special transaction")
|
||||
verbose_name_plural = _("Special transactions")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import timezone
|
||||
@ -43,4 +43,5 @@ def delete_transaction(instance, **_kwargs):
|
||||
"""
|
||||
if not hasattr(instance, "_no_signal"):
|
||||
instance.valid = False
|
||||
instance._force_save = True
|
||||
instance.save()
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
// Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// When a transaction is performed, lock the interface to prevent spam clicks.
|
||||
@ -28,7 +28,7 @@ $(document).ready(function () {
|
||||
|
||||
// Switching in double consumptions mode should update the layout
|
||||
$('#double_conso').change(function () {
|
||||
$('#consos_list_div').removeClass('d-none')
|
||||
document.getElementById('consos_list_div').classList.remove('d-none')
|
||||
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
|
||||
|
||||
const note_list_obj = $('#note_list')
|
||||
@ -37,7 +37,7 @@ $(document).ready(function () {
|
||||
note_list_obj.html('')
|
||||
|
||||
buttons.forEach(function (button) {
|
||||
$('#conso_button_' + button.id).click(function () {
|
||||
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
|
||||
if (LOCK) { return }
|
||||
removeNote(button, 'conso_button', buttons, 'consos_list')()
|
||||
})
|
||||
@ -46,7 +46,7 @@ $(document).ready(function () {
|
||||
})
|
||||
|
||||
$('#single_conso').change(function () {
|
||||
$('#consos_list_div').addClass('d-none')
|
||||
document.getElementById('consos_list_div').classList.add('d-none')
|
||||
$('#infos_div').attr('class', 'col-sm-5 col-md-4')
|
||||
|
||||
const consos_list_obj = $('#consos_list')
|
||||
@ -68,9 +68,9 @@ $(document).ready(function () {
|
||||
})
|
||||
|
||||
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
|
||||
$("label[for='double_conso']").removeClass('active')
|
||||
document.querySelector("label[for='double_conso']").classList.remove('active')
|
||||
|
||||
$('#consume_all').click(consumeAll)
|
||||
document.getElementById("consume_all").addEventListener('click', consumeAll)
|
||||
})
|
||||
|
||||
notes = []
|
||||
@ -127,11 +127,10 @@ function addConso (dest, amount, type, category_id, category_name, template_id,
|
||||
html += li('conso_button_' + button.id, button.name +
|
||||
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
|
||||
})
|
||||
document.getElementById(list).innerHTML = html
|
||||
|
||||
$('#' + list).html(html)
|
||||
|
||||
buttons.forEach(function (button) {
|
||||
$('#conso_button_' + button.id).click(function () {
|
||||
buttons.forEach((button) => {
|
||||
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
|
||||
if (LOCK) { return }
|
||||
removeNote(button, 'conso_button', buttons, list)()
|
||||
})
|
||||
@ -146,12 +145,13 @@ function reset () {
|
||||
notes_display.length = 0
|
||||
notes.length = 0
|
||||
buttons.length = 0
|
||||
$('#note_list').html('')
|
||||
$('#consos_list').html('')
|
||||
$('#note').val('')
|
||||
$('#note').attr('data-original-title', '').tooltip('hide')
|
||||
$('#profile_pic').attr('src', '/static/member/img/default_picture.png')
|
||||
$('#profile_pic_link').attr('href', '#')
|
||||
document.getElementById('note_list').innerHTML = ''
|
||||
document.getElementById('consos_list').innerHTML = ''
|
||||
document.getElementById('note').value = ''
|
||||
document.getElementById('note').dataset.originTitle = ''
|
||||
$('#note').tooltip('hide')
|
||||
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png'
|
||||
document.getElementById('profile_pic_link').href = '#'
|
||||
refreshHistory()
|
||||
refreshBalance()
|
||||
LOCK = false
|
||||
@ -168,7 +168,7 @@ function consumeAll () {
|
||||
let error = false
|
||||
|
||||
if (notes_display.length === 0) {
|
||||
$('#note').addClass('is-invalid')
|
||||
document.getElementById('note').classList.add('is-invalid')
|
||||
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
|
||||
error = true
|
||||
}
|
||||
@ -222,17 +222,15 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
||||
if (!isNaN(source.balance)) {
|
||||
const newBalance = source.balance - quantity * amount
|
||||
if (newBalance <= -5000) {
|
||||
addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' +
|
||||
'succès, mais la note émettrice ' + source_alias + ' est en négatif sévère.',
|
||||
'danger', 30000)
|
||||
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
||||
} else if (newBalance < 0) {
|
||||
addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' +
|
||||
'succès, mais la note émettrice ' + source_alias + ' est en négatif.',
|
||||
'warning', 30000)
|
||||
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||
'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000)
|
||||
}
|
||||
if (source.membership && source.membership.date_end < new Date().toISOString()) {
|
||||
addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.",
|
||||
'danger', 30000)
|
||||
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]),
|
||||
'danger', 30000)
|
||||
}
|
||||
}
|
||||
reset()
|
||||
@ -253,7 +251,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
||||
template: template
|
||||
}).done(function () {
|
||||
reset()
|
||||
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", 'danger', 10000)
|
||||
addMsg(gettext("The transaction couldn't be validated because of insufficient balance."), 'danger', 10000)
|
||||
}).fail(function () {
|
||||
reset()
|
||||
errMsg(e.responseJSON)
|
||||
|
@ -222,6 +222,13 @@ $(document).ready(function () {
|
||||
})
|
||||
})
|
||||
|
||||
// Make transfer when pressing Enter on the amount section
|
||||
$('#amount, #reason, #last_name, #first_name, #bank').keypress((event) => {
|
||||
if (event.originalEvent.charCode === 13) {
|
||||
$('#btn_transfer').click()
|
||||
}
|
||||
})
|
||||
|
||||
$('#btn_transfer').click(function () {
|
||||
if (LOCK) { return }
|
||||
|
||||
@ -239,20 +246,20 @@ $('#btn_transfer').click(function () {
|
||||
|
||||
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
|
||||
amount_field.addClass('is-invalid')
|
||||
$('#amount-required').html('<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>')
|
||||
$('#amount-required').html('<strong>' + gettext('This field is required and must contain a decimal positive number.') + '</strong>')
|
||||
error = true
|
||||
}
|
||||
|
||||
const amount = Math.floor(100 * amount_field.val())
|
||||
const amount = Math.round(100 * amount_field.val())
|
||||
if (amount > 2147483647) {
|
||||
amount_field.addClass('is-invalid')
|
||||
$('#amount-required').html('<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>')
|
||||
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
|
||||
error = true
|
||||
}
|
||||
|
||||
if (!reason_field.val() && $('#type_transfer').is(':checked')) {
|
||||
reason_field.addClass('is-invalid')
|
||||
$('#reason-required').html('<strong>Ce champ est requis.</strong>')
|
||||
$('#reason-required').html('<strong>' + gettext('This field is required.') + '</strong>')
|
||||
error = true
|
||||
}
|
||||
|
||||
@ -278,9 +285,8 @@ $('#btn_transfer').click(function () {
|
||||
[...sources_notes_display].forEach(function (source) {
|
||||
[...dests_notes_display].forEach(function (dest) {
|
||||
if (source.note.id === dest.note.id) {
|
||||
addMsg('Attention : la transaction de ' + pretty_money(amount) + ' de la note ' + source.name +
|
||||
' vers la note ' + dest.name + " n'a pas été faite car il s'agit de la même note au départ" +
|
||||
" et à l'arrivée.", 'warning', 10000)
|
||||
addMsg(interpolate(gettext('Warning: the transaction of %s from %s to %s was not made because ' +
|
||||
'it is the same source and destination note.'), [pretty_money(amount), source.name, dest.name]), 'warning', 10000)
|
||||
LOCK = false
|
||||
return
|
||||
}
|
||||
@ -300,43 +306,35 @@ $('#btn_transfer').click(function () {
|
||||
destination_alias: dest.name
|
||||
}).done(function () {
|
||||
if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) {
|
||||
addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.",
|
||||
'danger', 30000)
|
||||
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000)
|
||||
}
|
||||
if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) {
|
||||
addMsg('Attention : la note destination ' + dest.name + " n'est plus adhérente.",
|
||||
'danger', 30000)
|
||||
addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000)
|
||||
}
|
||||
|
||||
if (!isNaN(source.note.balance)) {
|
||||
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
||||
if (newBalance <= -5000) {
|
||||
addMsg('Le transfert de ' +
|
||||
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' +
|
||||
source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' +
|
||||
'mais la note émettrice est en négatif sévère.', 'danger', 10000)
|
||||
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
||||
reset()
|
||||
return
|
||||
} else if (newBalance < 0) {
|
||||
addMsg('Le transfert de ' +
|
||||
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' +
|
||||
source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' +
|
||||
'mais la note émettrice est en négatif.', 'warning', 10000)
|
||||
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is negative.'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
||||
reset()
|
||||
return
|
||||
}
|
||||
}
|
||||
addMsg('Le transfert de ' +
|
||||
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
|
||||
' vers la note ' + dest.name + ' a été fait avec succès !', 'success', 10000)
|
||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s succeed!'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name]), 'success', 10000)
|
||||
|
||||
reset()
|
||||
}).fail(function (err) { // do it again but valid = false
|
||||
const errObj = JSON.parse(err.responseText)
|
||||
if (errObj.non_field_errors) {
|
||||
addMsg('Le transfert de ' +
|
||||
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
|
||||
' vers la note ' + dest.name + ' a échoué : ' + errObj.non_field_errors, 'danger')
|
||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, errObj.non_field_errors]), 'danger')
|
||||
LOCK = false
|
||||
return
|
||||
}
|
||||
@ -356,17 +354,15 @@ $('#btn_transfer').click(function () {
|
||||
destination: dest.note.id,
|
||||
destination_alias: dest.name
|
||||
}).done(function () {
|
||||
addMsg('Le transfert de ' +
|
||||
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
|
||||
' vers la note ' + dest.name + ' a échoué : Solde insuffisant', 'danger', 10000)
|
||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, gettext('insufficient funds')]), 'danger', 10000)
|
||||
reset()
|
||||
}).fail(function (err) {
|
||||
const errObj = JSON.parse(err.responseText)
|
||||
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
|
||||
if (!error) { error = err.responseText }
|
||||
addMsg('Le transfert de ' +
|
||||
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
|
||||
' vers la note ' + dest.name + ' a échoué : ' + error, 'danger')
|
||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, error]), 'danger')
|
||||
LOCK = false
|
||||
})
|
||||
})
|
||||
@ -412,14 +408,14 @@ $('#btn_transfer').click(function () {
|
||||
first_name: $('#first_name').val(),
|
||||
bank: $('#bank').val()
|
||||
}).done(function () {
|
||||
addMsg('Le crédit/retrait a bien été effectué !', 'success', 10000)
|
||||
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg('Attention : la note ' + alias + " n'est plus adhérente.", 'danger', 10000) }
|
||||
addMsg(gettext('Credit/debit succeed!'), 'success', 10000)
|
||||
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) }
|
||||
reset()
|
||||
}).fail(function (err) {
|
||||
const errObj = JSON.parse(err.responseText)
|
||||
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
|
||||
if (!error) { error = err.responseText }
|
||||
addMsg('Le crédit/retrait a échoué : ' + error, 'danger', 10000)
|
||||
addMsg(interpolate(gettext('Credit/debit failed: %s'), [error]), 'danger', 10000)
|
||||
LOCK = false
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import html
|
||||
@ -7,7 +7,7 @@ import django_tables2 as tables
|
||||
from django.utils.html import format_html
|
||||
from django_tables2.utils import A
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models.notes import Alias
|
||||
@ -88,16 +88,16 @@ class HistoryTable(tables.Table):
|
||||
"class": lambda record:
|
||||
str(record.valid).lower()
|
||||
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
|
||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
|
||||
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
|
||||
else ''),
|
||||
"data-toggle": "tooltip",
|
||||
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
if PermissionBackend.check_perm(get_current_request(),
|
||||
"note.change_transaction_invalidity_reason", record)
|
||||
and record.source.is_active and record.destination.is_active else None,
|
||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
|
||||
+ ', "' + str(record.__class__.__name__) + '")'
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
if PermissionBackend.check_perm(get_current_request(),
|
||||
"note.change_transaction_invalidity_reason", record)
|
||||
and record.source.is_active and record.destination.is_active else None,
|
||||
"onmouseover": lambda record: '$("#invalidity_reason_'
|
||||
@ -126,7 +126,7 @@ class HistoryTable(tables.Table):
|
||||
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
||||
"""
|
||||
has_perm = PermissionBackend \
|
||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
|
||||
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
|
||||
|
||||
val = "✔" if value else "✖"
|
||||
|
||||
@ -165,7 +165,7 @@ class AliasTable(tables.Table):
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={'td': {'class': lambda record: 'col-sm-1' + (
|
||||
' d-none' if not PermissionBackend.check_perm(
|
||||
get_current_authenticated_user(), "note.delete_alias",
|
||||
get_current_request(), "note.delete_alias",
|
||||
record) else '')}}, verbose_name=_("Delete"), )
|
||||
|
||||
|
||||
|
@ -129,7 +129,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{# Mode switch #}
|
||||
<div class="card-footer border-primary">
|
||||
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
|
||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
|
||||
<label for="single_conso" class="btn btn-sm btn-outline-primary active">
|
||||
|
@ -10,21 +10,25 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
{# bandeau transfert/crédit/débit/activité #}
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
|
||||
<label for="type_transfer" class="btn btn-sm btn-outline-primary active">
|
||||
<input type="radio" name="transaction_type" id="type_transfer">
|
||||
{% trans "Transfer" %}
|
||||
</label>
|
||||
{% if "note.notespecial"|not_empty_model_list %}
|
||||
<label for="type_credit" class="btn btn-sm btn-outline-primary">
|
||||
<input type="radio" name="transaction_type" id="type_credit">
|
||||
{% trans "Credit" %}
|
||||
<div class="btn-group btn-block">
|
||||
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
|
||||
<label for="type_transfer" class="btn btn-sm btn-outline-primary active">
|
||||
<input type="radio" name="transaction_type" id="type_transfer">
|
||||
{% trans "Transfer" %}
|
||||
</label>
|
||||
<label for="type_debit" class="btn btn-sm btn-outline-primary">
|
||||
<input type="radio" name="transaction_type" id="type_debit">
|
||||
{% trans "Debit" %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if "note.notespecial"|not_empty_model_list %}
|
||||
<label for="type_credit" class="btn btn-sm btn-outline-primary">
|
||||
<input type="radio" name="transaction_type" id="type_credit">
|
||||
{% trans "Credit" %}
|
||||
</label>
|
||||
<label for="type_debit" class="btn btn-sm btn-outline-primary">
|
||||
<input type="radio" name="transaction_type" id="type_debit">
|
||||
{% trans "Debit" %}
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Add shortcuts for opened activites if necessary #}
|
||||
{% for activity in activities_open %}
|
||||
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
{% trans "Entries" %} {{ activity.name }}
|
||||
@ -57,7 +61,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ul class="list-group list-group-flush" id="source_note_list">
|
||||
</ul>
|
||||
<div class="card-body">
|
||||
<select id="credit_type" class="custom-select d-none">
|
||||
<select id="credit_type" class="form-control custom-select d-none">
|
||||
{% for special_type in special_types %}
|
||||
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
||||
{% endfor %}
|
||||
@ -84,7 +88,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ul class="list-group list-group-flush" id="dest_note_list">
|
||||
</ul>
|
||||
<div class="card-body">
|
||||
<select id="debit_type" class="custom-select d-none">
|
||||
<select id="debit_type" class="form-control custom-select d-none">
|
||||
{% for special_type in special_types %}
|
||||
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
||||
{% endfor %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import template
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import template
|
||||
|
@ -1,15 +1,20 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.tests import TestAPI
|
||||
from member.models import Club, Membership
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from member.models import Club, Membership
|
||||
from note.models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
|
||||
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias
|
||||
from django.utils import timezone
|
||||
from permission.models import Role
|
||||
|
||||
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
|
||||
TransactionTemplateViewSet, TransactionViewSet
|
||||
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
|
||||
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
|
||||
|
||||
|
||||
class TestTransactions(TestCase):
|
||||
fixtures = ('initial', )
|
||||
@ -297,8 +302,8 @@ class TestTransactions(TestCase):
|
||||
|
||||
def test_render_search_transactions(self):
|
||||
response = self.client.get(reverse("note:transactions", args=(self.user.note.pk,)), data=dict(
|
||||
source=self.second_user.note.alias_set.first().id,
|
||||
destination=self.user.note.alias_set.first().id,
|
||||
source=self.second_user.note.alias.first().id,
|
||||
destination=self.user.note.alias.first().id,
|
||||
type=[ContentType.objects.get_for_model(Transaction).id],
|
||||
reason="test",
|
||||
valid=True,
|
||||
@ -363,3 +368,69 @@ class TestTransactions(TestCase):
|
||||
self.assertTrue(Alias.objects.filter(name="test_updated_alias").exists())
|
||||
response = self.client.delete("/api/note/alias/" + str(alias.pk) + "/")
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
|
||||
class TestNoteAPI(TestAPI):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
membership = Membership.objects.create(club=Club.objects.get(name="BDE"), user=self.user)
|
||||
membership.roles.add(Role.objects.get(name="Respo info"))
|
||||
membership.save()
|
||||
Membership.objects.create(club=Club.objects.get(name="Kfet"), user=self.user)
|
||||
self.user.note.last_negative = timezone.now()
|
||||
self.user.note.save()
|
||||
|
||||
self.transaction = Transaction.objects.create(
|
||||
source=Note.objects.first(),
|
||||
destination=self.user.note,
|
||||
amount=4200,
|
||||
reason="Test transaction",
|
||||
)
|
||||
self.user.note.refresh_from_db()
|
||||
Alias.objects.create(note=self.user.note, name="I am a ¢omplex alias")
|
||||
|
||||
self.category = TemplateCategory.objects.create(name="Test")
|
||||
self.template = TransactionTemplate.objects.create(
|
||||
name="Test",
|
||||
destination=Club.objects.get(name="BDE").note,
|
||||
category=self.category,
|
||||
amount=100,
|
||||
description="Test template",
|
||||
)
|
||||
|
||||
def test_alias_api(self):
|
||||
"""
|
||||
Load Alias API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(AliasViewSet, "/api/note/alias/")
|
||||
|
||||
def test_consumer_api(self):
|
||||
"""
|
||||
Load Consumer API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(ConsumerViewSet, "/api/note/consumer/")
|
||||
|
||||
def test_note_api(self):
|
||||
"""
|
||||
Load Note API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(NotePolymorphicViewSet, "/api/note/note/")
|
||||
|
||||
def test_template_category_api(self):
|
||||
"""
|
||||
Load TemplateCategory API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(TemplateCategoryViewSet, "/api/note/transaction/category/")
|
||||
|
||||
def test_transaction_template_api(self):
|
||||
"""
|
||||
Load TemplateTemplate API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(TransactionTemplateViewSet, "/api/note/transaction/template/")
|
||||
|
||||
def test_transaction_api(self):
|
||||
"""
|
||||
Load Transaction API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(TransactionViewSet, "/api/note/transaction/transaction/")
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
@ -38,7 +38,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
||||
def get_queryset(self, **kwargs):
|
||||
# retrieves only Transaction that user has the right to see.
|
||||
return Transaction.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
||||
PermissionBackend.filter_queryset(self.request, Transaction, "view")
|
||||
).order_by("-created_at").all()[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -47,16 +47,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||
context['special_types'] = NoteSpecial.objects\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||
.order_by("special_type").all()
|
||||
|
||||
# Add a shortcut for entry page for open activities
|
||||
if "activity" in settings.INSTALLED_APPS:
|
||||
from activity.models import Activity
|
||||
activities_open = Activity.objects.filter(open=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
|
||||
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||
context["activities_open"] = [a for a in activities_open
|
||||
if PermissionBackend.check_perm(self.request.user,
|
||||
if PermissionBackend.check_perm(self.request,
|
||||
"activity.add_entry",
|
||||
Entry(activity=a,
|
||||
note=self.request.user.note, ))]
|
||||
@ -159,7 +159,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
return self.handle_no_permission()
|
||||
|
||||
templates = TransactionTemplate.objects.filter(
|
||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||
)
|
||||
if not templates.exists():
|
||||
raise PermissionDenied(_("You can't see any button."))
|
||||
@ -170,7 +170,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
restrict to the transaction history the user can see.
|
||||
"""
|
||||
return Transaction.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
||||
PermissionBackend.filter_queryset(self.request, Transaction, "view")
|
||||
).order_by("-created_at").all()[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -180,13 +180,13 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
# for each category, find which transaction templates the user can see.
|
||||
for category in categories:
|
||||
category.templates_filtered = category.templates.filter(
|
||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||
).filter(display=True).order_by('name').all()
|
||||
|
||||
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
||||
# some transactiontemplate are put forward to find them easily
|
||||
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||
).order_by('name').all()
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||
|
||||
@ -209,7 +209,7 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
|
||||
data = form.cleaned_data if form.is_valid() else {}
|
||||
|
||||
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
||||
PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
|
||||
|
||||
if "source" in data and data["source"]:
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'permission.apps.PermissionConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||
|
||||
from django.contrib import admin
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user