mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 01:48:21 +02:00
Compare commits
287 Commits
faster_ci
...
update_per
Author | SHA1 | Date | |
---|---|---|---|
03d2d5f03e | |||
d2057a9f45 | |||
6b1cd3ba7a | |||
9f114b8ca2 | |||
e0132b6dc8 | |||
31e67ae3f6 | |||
2839d3de1e | |||
30afa6da0a | |||
84fc77696f | |||
19fc620d1f | |||
6bceb394c5 | |||
62cf8f9d84 | |||
2dd1c3fb89 | |||
c8665c5798 | |||
e9f1b6f52d | |||
1d95ae4810 | |||
84b16ab603 | |||
6a1b51dbbf | |||
c441a43a8b | |||
87f3b51b04 | |||
0a853fd3e6 | |||
c429734810 | |||
5d759111b6 | |||
70baf7566c | |||
eb355f547c | |||
7068170f18 | |||
45ee9a8941 | |||
454ea19603 | |||
5a77a66391 | |||
761fc170eb
|
|||
ac23d7eb54
|
|||
40e7415062
|
|||
319405d2b1
|
|||
633ab88b04
|
|||
e29b42eecc
|
|||
dc69faaf1d
|
|||
442a5c5e36
|
|||
7ab0fec3bc
|
|||
bd4fb23351 | |||
ee22e9b3b6 | |||
19ae616fb4 | |||
b7657ec362 | |||
4d03d9460d | |||
3633f66a87 | |||
d43fbe7ac6 | |||
df5f9b5f1e | |||
4161248bff
|
|||
58136f3c48
|
|||
d9b4e0a9a9
|
|||
8563a8d235
|
|||
5f69232560 | |||
d3273e9ee2
|
|||
4e30f805a7 | |||
546e422e64
|
|||
9048a416df
|
|||
8578bd743c
|
|||
45a10dad00
|
|||
18a1282773
|
|||
132afc3d15
|
|||
6bf16a181a
|
|||
e20df82346
|
|||
1eb72044c2 | |||
f88eae924c
|
|||
4b6e3ba546
|
|||
bf0fe3479f | |||
45ba4f9537
|
|||
b204805ce2
|
|||
2f28e34cec
|
|||
9c8ea2cd41
|
|||
41289857b2 | |||
28a8792c9f
|
|||
58cafad032
|
|||
7848cd9cc2
|
|||
d18ccfac23
|
|||
e479e1e3a4 | |||
82b0c83b1f | |||
38ca414ef6
|
|||
fd811053c7
|
|||
9d386d1ecf
|
|||
0bd447b608 | |||
3f3c93d928 | |||
340c90f5d3 | |||
ca2b9f061c | |||
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
|
@ -1,3 +0,0 @@
|
||||
skip_list:
|
||||
- command-instead-of-shell # Use shell only when shell functionality is required
|
||||
- experimental # all rules tagged as experimental
|
@ -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
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,6 +47,7 @@ backups/
|
||||
env/
|
||||
venv/
|
||||
db.sqlite3
|
||||
shell.nix
|
||||
|
||||
# ansibles customs host
|
||||
ansible/host_vars/*.yaml
|
||||
|
@ -10,22 +10,50 @@ variables:
|
||||
# Debian Buster
|
||||
py37-django22:
|
||||
stage: test
|
||||
image: otthorn/nk20_ci_37
|
||||
image: debian:buster-backports
|
||||
before_script:
|
||||
- >
|
||||
apt-get update &&
|
||||
apt-get install --no-install-recommends -t buster-backports -y
|
||||
python3-django python3-django-crispy-forms
|
||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py37-django22
|
||||
|
||||
# Ubuntu 20.04
|
||||
py38-django22:
|
||||
stage: test
|
||||
image: otthorn/nk20_ci_38
|
||||
image: ubuntu:20.04
|
||||
before_script:
|
||||
# Fix tzdata prompt
|
||||
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
||||
- >
|
||||
apt-get update &&
|
||||
apt-get install --no-install-recommends -y
|
||||
python3-django python3-django-crispy-forms
|
||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||
python3-bs4 python3-setuptools tox texlive-xetex
|
||||
script: tox -e py38-django22
|
||||
|
||||
# Debian Bullseye
|
||||
py39-django22:
|
||||
stage: test
|
||||
image: otthorn/nk20_ci_39
|
||||
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
|
||||
|
||||
# Tox linter
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: debian:buster-backports
|
||||
@ -36,20 +64,6 @@ linters:
|
||||
# Be nice to new contributors, but please use `tox`
|
||||
allow_failure: true
|
||||
|
||||
# Ansible linter
|
||||
ansible-linter:
|
||||
stage: quality-assurance
|
||||
image: otthorn/nk20_ci_ansiblelint
|
||||
script: ansible-lint ansible/
|
||||
|
||||
# Docker linter
|
||||
docker-linter:
|
||||
stage: quality-assurance
|
||||
image: hadolint/hadolint
|
||||
script:
|
||||
- hadolint -c .hadolint Dockerfile
|
||||
- hadolint -c .hadolint docker_ci/Dockerfile.*
|
||||
|
||||
# Compile documentation
|
||||
documentation:
|
||||
stage: docs
|
||||
|
@ -1,4 +0,0 @@
|
||||
ignored:
|
||||
- DL3008 # Do not force to pin version in apt (Debian)
|
||||
- DL3013 # Do not force to pin version in pip (PyPI)
|
||||
- DL3018 # Do not force to pin version in apk (Alpine)
|
@ -1,8 +1,8 @@
|
||||
# NoteKfet 2020
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||
|
||||
## Table des matières
|
||||
|
||||
@ -279,7 +279,8 @@ 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
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
prompt: "Password of the database (leave it blank to skip database init)"
|
||||
private: yes
|
||||
vars:
|
||||
mirror: mirror.crans.org
|
||||
mirror: eclats.crans.org
|
||||
roles:
|
||||
- 1-apt-basic
|
||||
- 2-nk20
|
||||
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
note:
|
||||
server_name: note-beta.crans.org
|
||||
git_branch: beta
|
||||
cron_enabled: false
|
||||
email: notekfet2020@lists.crans.org
|
@ -2,5 +2,6 @@
|
||||
note:
|
||||
server_name: note-dev.crans.org
|
||||
git_branch: beta
|
||||
serve_static: false
|
||||
cron_enabled: false
|
||||
email: notekfet2020@lists.crans.org
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
note:
|
||||
server_name: note.crans.org
|
||||
git_branch: master
|
||||
git_branch: main
|
||||
serve_static: true
|
||||
cron_enabled: true
|
||||
email: notekfet2020@lists.crans.org
|
||||
|
@ -1,6 +1,5 @@
|
||||
[dev]
|
||||
bde-note-dev.adh.crans.org
|
||||
bde-nk20-beta.adh.crans.org
|
||||
|
||||
[prod]
|
||||
bde-note.adh.crans.org
|
||||
|
@ -1,14 +1,15 @@
|
||||
---
|
||||
- name: Add buster-backports to apt sources
|
||||
- name: Add buster-backports to apt sources if needed
|
||||
apt_repository:
|
||||
repo: deb http://{{ mirror }}/debian buster-backports main
|
||||
state: present
|
||||
when: ansible_facts['distribution'] == "Debian"
|
||||
when:
|
||||
- ansible_distribution == "Debian"
|
||||
- ansible_distribution_major_version | int == 10
|
||||
|
||||
- name: Install note_kfet APT dependencies
|
||||
apt:
|
||||
update_cache: true
|
||||
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
|
||||
install_recommends: false
|
||||
name:
|
||||
# Common tools
|
||||
|
@ -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,7 @@ 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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -6,7 +6,7 @@
|
||||
"name": "Pot",
|
||||
"manage_entries": true,
|
||||
"can_invite": true,
|
||||
"guest_entry_fee": 500
|
||||
"guest_entry_fee": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -28,5 +28,25 @@
|
||||
"can_invite": false,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "activity.activitytype",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Soir\u00e9e avec entrées",
|
||||
"manage_entries": true,
|
||||
"can_invite": false,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "activity.activitytype",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Soir\u00e9e avec invitations",
|
||||
"manage_entries": true,
|
||||
"can_invite": true,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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):
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
@ -170,10 +168,13 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
||||
it is closed or doesn't manage entries.
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
|
||||
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 +192,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 +207,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 +231,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 +257,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 +282,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
|
||||
|
5
apps/api/pagination.py
Normal file
5
apps/api/pagination.py
Normal file
@ -0,0 +1,5 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
class CustomPagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
||||
|
@ -1,13 +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 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 member.models import Membership
|
||||
from note.api.serializers import NoteSerializer
|
||||
from note.models import Alias
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
|
||||
class UserSerializer(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 +29,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 +38,54 @@ 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 = serializers.SerializerMethodField()
|
||||
|
||||
note = serializers.SerializerMethodField()
|
||||
|
||||
memberships = serializers.SerializerMethodField()
|
||||
|
||||
def get_normalized_name(self, obj):
|
||||
return Alias.normalize(obj.username)
|
||||
|
||||
def get_profile(self, obj):
|
||||
# Display the profile of the user only if we have rights to see it.
|
||||
return ProfileSerializer().to_representation(obj.profile) \
|
||||
if PermissionBackend.check_perm(get_current_request(), 'member.view_profile', obj.profile) else None
|
||||
|
||||
def get_note(self, obj):
|
||||
# Display the note of the user only if we have rights to see it.
|
||||
return NoteSerializer().to_representation(obj.note) \
|
||||
if PermissionBackend.check_perm(get_current_request(), 'note.view_note', obj.note) else None
|
||||
|
||||
def get_memberships(self, obj):
|
||||
# Display only memberships that we are allowed to see.
|
||||
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
|
||||
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'id',
|
||||
'username',
|
||||
'normalized_name',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'is_superuser',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'profile',
|
||||
'note',
|
||||
'memberships',
|
||||
)
|
||||
|
@ -1,8 +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 json
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote_plus
|
||||
from warnings import warn
|
||||
|
||||
@ -152,6 +153,8 @@ class TestAPI(TestCase):
|
||||
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
|
||||
|
@ -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
|
||||
@ -9,7 +9,6 @@ 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
|
||||
@ -25,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):
|
||||
@ -40,9 +37,7 @@ 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):
|
||||
|
@ -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
|
||||
|
@ -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,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
|
||||
|
@ -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(
|
||||
|
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'),
|
||||
),
|
||||
]
|
18
apps/member/migrations/0008_auto_20211005_1544.py
Normal file
18
apps/member/migrations/0008_auto_20211005_1544.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.24 on 2021-10-05 13:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0007_auto_20210313_1235'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='department',
|
||||
field=models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ("A''2", "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department'),
|
||||
),
|
||||
]
|
@ -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)"),
|
||||
)
|
||||
@ -258,16 +258,18 @@ class Club(models.Model):
|
||||
This function is called each time the club detail view is displayed.
|
||||
Update the year of the membership dates.
|
||||
"""
|
||||
if not self.membership_start:
|
||||
if not self.membership_start or not self.membership_end:
|
||||
return
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
if (today - self.membership_start).days >= 365:
|
||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||
self.membership_end.month, self.membership_end.day)
|
||||
if self.membership_start:
|
||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
if self.membership_end:
|
||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||
self.membership_end.month, self.membership_end.day)
|
||||
self._force_save = True
|
||||
self.save(force_update=True)
|
||||
|
||||
@ -413,6 +415,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
|
||||
|
||||
|
||||
|
53
apps/member/static/member/js/trust.js
Normal file
53
apps/member/static/member/js/trust.js
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* On form submit, create a new friendship
|
||||
*/
|
||||
function create_trust (e) {
|
||||
// Do not submit HTML form
|
||||
e.preventDefault()
|
||||
|
||||
// Get data and send to API
|
||||
const formData = new FormData(e.target)
|
||||
$.getJSON('/api/note/alias/'+formData.get('trusted') + '/',
|
||||
function (trusted_alias) {
|
||||
if ((trusted_alias.note == formData.get('trusting')))
|
||||
{
|
||||
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
||||
return
|
||||
}
|
||||
$.post('/api/note/trust/', {
|
||||
csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'),
|
||||
trusting: formData.get('trusting'),
|
||||
trusted: trusted_alias.note
|
||||
}).done(function () {
|
||||
// Reload table
|
||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||
addMsg(gettext('Friendship successfully added'), 'success')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On click of "delete", delete the alias
|
||||
* @param button_id:Integer Alias id to remove
|
||||
*/
|
||||
function delete_button (button_id) {
|
||||
$.ajax({
|
||||
url: '/api/note/trust/' + button_id + '/',
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||
}).done(function () {
|
||||
addMsg(gettext('Friendship successfully deleted'), 'success')
|
||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Attach event
|
||||
document.getElementById('form_trust').addEventListener('submit', create_trust)
|
||||
})
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
@ -51,19 +52,19 @@ class UserTable(tables.Table):
|
||||
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_authenticated_user(), "member.view_profile", record.profile):
|
||||
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_authenticated_user(), "member.view_profile", record.profile) \
|
||||
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 = {
|
||||
@ -74,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',
|
||||
}
|
||||
|
||||
|
||||
@ -93,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)
|
||||
|
||||
@ -102,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)
|
||||
|
||||
@ -118,7 +120,7 @@ class MembershipTable(tables.Table):
|
||||
club=record.club,
|
||||
user=record.user,
|
||||
date_start__gte=record.club.membership_start,
|
||||
date_end__lte=record.club.membership_end,
|
||||
date_end__lte=record.club.membership_end or date(9999, 12, 31),
|
||||
).exists(): # If the renew is not yet performed
|
||||
empty_membership = Membership(
|
||||
club=record.club,
|
||||
@ -127,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})
|
||||
@ -142,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
|
||||
@ -165,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)
|
||||
|
||||
|
@ -25,6 +25,14 @@
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
|
||||
</a>
|
||||
</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>
|
||||
@ -39,13 +47,13 @@
|
||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
|
@ -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 %}
|
41
apps/member/templates/member/profile_trust.html
Normal file
41
apps/member/templates/member/profile_trust.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends "member/base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load static django_tables2 i18n %}
|
||||
|
||||
{% block profile_content %}
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "Note friendships" %}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
{% if can_create %}
|
||||
<form class="input-group" method="POST" id="form_trust">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
|
||||
{%include "autocomplete_model.html" %}
|
||||
<div class="input-group-append">
|
||||
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% render_table trusting %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning card">
|
||||
{% blocktrans trimmed %}
|
||||
Adding someone as a friend enables them to initiate transactions coming
|
||||
from your account (while keeping your balance positive). This is
|
||||
designed to simplify using note kfet transfers to transfer money between
|
||||
users. The intent is that one person can make all transfers for a group of
|
||||
friends without needing additional rights among them.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script src="{% static "member/js/trust.js" %}"></script>
|
||||
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
||||
{% endblock%}
|
@ -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,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 hashlib
|
||||
|
@ -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
|
||||
@ -23,5 +23,6 @@ urlpatterns = [
|
||||
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
|
||||
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
||||
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
||||
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
|
||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||
]
|
||||
|
@ -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
|
||||
@ -8,6 +8,7 @@ from django.contrib.auth import logout
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, F
|
||||
from django.shortcuts import redirect
|
||||
@ -18,10 +19,10 @@ from django.views.generic import DetailView, UpdateView, TemplateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from note.models import Alias, NoteUser
|
||||
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||
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.tables import HistoryTable, AliasTable, TrustTable
|
||||
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 +42,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,7 +72,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form.fields['email'].required = True
|
||||
form.fields['email'].help_text = _("This address must be valid.")
|
||||
|
||||
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
|
||||
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:
|
||||
@ -153,13 +155,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")\
|
||||
@ -173,24 +175,23 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
modified_note = NoteUser.objects.get(pk=user.note.pk)
|
||||
# Don't log these tests
|
||||
modified_note._no_signal = True
|
||||
modified_note.is_active = True
|
||||
modified_note.is_active = False
|
||||
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_noteuser_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_noteuser_is_active", modified_note)
|
||||
|
||||
return context
|
||||
|
||||
@ -237,12 +238,45 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
|
||||
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 ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View and manage user trust relationships
|
||||
"""
|
||||
model = User
|
||||
template_name = 'member/profile_trust.html'
|
||||
context_object_name = 'user_object'
|
||||
extra_context = {"title": _("Note friendships")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["trusting"] = TrustTable(
|
||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
||||
trusting=context["object"].note,
|
||||
trusted=context["object"].note
|
||||
))
|
||||
context["widget"] = {
|
||||
"name": "trusted",
|
||||
"attrs": {
|
||||
"model_pk": ContentType.objects.get_for_model(Alias).pk,
|
||||
"class": "autocomplete form-control",
|
||||
"id": "trusted",
|
||||
"resetable": True,
|
||||
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||
"name_field": "name",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View and manage user aliases.
|
||||
@ -256,8 +290,9 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["aliases"] = AliasTable(
|
||||
note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
||||
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||
.order_by('normalized_name').all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||
note=context["object"].note,
|
||||
name="",
|
||||
normalized_name="",
|
||||
@ -382,7 +417,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",
|
||||
))
|
||||
@ -403,9 +438,12 @@ 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):
|
||||
club = self.object
|
||||
context["note"] = club.note
|
||||
|
||||
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",
|
||||
date_start__lte=date.today(), date_end__gte=date.today())\
|
||||
@ -413,7 +451,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))
|
||||
@ -422,7 +460,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")\
|
||||
@ -443,6 +481,29 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context["can_add_members"] = PermissionBackend()\
|
||||
.has_perm(self.request.user, "member.add_membership", empty_membership)
|
||||
|
||||
# Check permissions to see if the authenticated user can lock/unlock the note
|
||||
with transaction.atomic():
|
||||
modified_note = NoteClub.objects.get(pk=club.note.pk)
|
||||
# Don't log these tests
|
||||
modified_note._no_signal = True
|
||||
modified_note.is_active = False
|
||||
modified_note.inactivity_reason = 'manual'
|
||||
context["can_lock_note"] = club.note.is_active and PermissionBackend \
|
||||
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
|
||||
modified_note.inactivity_reason = 'forced'
|
||||
modified_note._force_save = True
|
||||
modified_note.save()
|
||||
context["can_force_lock"] = club.note.is_active and PermissionBackend \
|
||||
.check_perm(self.request, "note.change_noteclub_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 club.note.is_active and PermissionBackend \
|
||||
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@ -459,8 +520,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["aliases"] = AliasTable(note.alias.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
||||
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="",
|
||||
@ -535,7 +596,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.
|
||||
@ -625,9 +686,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:
|
||||
@ -658,8 +716,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
|
||||
user=form.instance.user,
|
||||
club=club.parent_club,
|
||||
date_start__lte=timezone.now(),
|
||||
date_end__gte=club.parent_club.membership_end,
|
||||
date_start__gte=club.parent_club.membership_start,
|
||||
).exists():
|
||||
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
|
||||
error = True
|
||||
@ -674,17 +731,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."))
|
||||
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
|
||||
if credit_amount and not SpecialTransaction.validate_payment_form(form):
|
||||
# Check that special information for payment are filled
|
||||
error = True
|
||||
|
||||
return not error
|
||||
|
||||
@ -695,7 +744,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
|
||||
@ -746,6 +795,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.
|
||||
@ -878,7 +928,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,11 +8,11 @@ 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
|
||||
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||
RecurrentTransaction, SpecialTransaction
|
||||
|
||||
@ -77,6 +77,22 @@ class NoteUserSerializer(serializers.ModelSerializer):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class TrustSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Trusts.
|
||||
The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Trust
|
||||
fields = '__all__'
|
||||
|
||||
def validate(self, attrs):
|
||||
instance = Trust(**attrs)
|
||||
instance.clean()
|
||||
return attrs
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Aliases.
|
||||
@ -126,7 +142,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 +158,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,8 +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 .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
|
||||
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \
|
||||
TrustViewSet
|
||||
|
||||
|
||||
def register_note_urls(router, path):
|
||||
@ -11,6 +12,7 @@ def register_note_urls(router, path):
|
||||
"""
|
||||
router.register(path + '/note', NotePolymorphicViewSet)
|
||||
router.register(path + '/alias', AliasViewSet)
|
||||
router.register(path + '/trust', TrustViewSet)
|
||||
router.register(path + '/consumer', ConsumerViewSet)
|
||||
|
||||
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
||||
|
@ -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,12 +11,12 @@ 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, NoteUser, NoteClub, NoteSpecial
|
||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
|
||||
TrustSerializer
|
||||
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
|
||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||
|
||||
|
||||
@ -40,12 +41,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
Parse query and apply filters.
|
||||
:return: The filtered set of requested notes
|
||||
"""
|
||||
user = self.request.user
|
||||
get_current_session().setdefault("permission_mask", 42)
|
||||
queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
|
||||
| PermissionBackend.filter_queryset(user, NoteUser, "view")
|
||||
| PermissionBackend.filter_queryset(user, NoteClub, "view")
|
||||
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).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(
|
||||
@ -57,24 +57,25 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
return queryset.order_by("id")
|
||||
|
||||
|
||||
class AliasViewSet(ReadProtectedModelViewSet):
|
||||
class TrustViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/aliases/
|
||||
REST Trust View set.
|
||||
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/trust/
|
||||
"""
|
||||
queryset = Alias.objects
|
||||
serializer_class = AliasSerializer
|
||||
queryset = Trust.objects
|
||||
serializer_class = TrustSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
ordering_fields = ['name', 'normalized_name', ]
|
||||
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
||||
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
||||
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
||||
ordering_fields = ['trusting', 'trusted', ]
|
||||
|
||||
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',))
|
||||
# trust relationship can't change people involved
|
||||
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
|
||||
return serializer_class
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
@ -82,7 +83,37 @@ 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)
|
||||
|
||||
|
||||
class AliasViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/aliases/
|
||||
"""
|
||||
queryset = Alias.objects
|
||||
serializer_class = AliasSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
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
|
||||
serializer_class.Meta.read_only_fields = ('note',)
|
||||
return serializer_class
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
try:
|
||||
self.perform_destroy(instance)
|
||||
except ValidationError as e:
|
||||
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def get_queryset(self):
|
||||
@ -118,7 +149,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
serializer_class = ConsumerSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
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):
|
||||
@ -133,23 +165,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)
|
||||
|
||||
@ -205,7 +245,5 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
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'),
|
||||
),
|
||||
]
|
27
apps/note/migrations/0006_trust.py
Normal file
27
apps/note/migrations/0006_trust.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.2.24 on 2021-09-05 19:16
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('note', '0005_auto_20210313_1235'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Trust',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')),
|
||||
('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'frienship',
|
||||
'verbose_name_plural': 'friendships',
|
||||
'unique_together': {('trusting', 'trusted')},
|
||||
},
|
||||
),
|
||||
]
|
@ -1,13 +1,13 @@
|
||||
# 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
|
||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
||||
from .transactions import MembershipTransaction, Transaction, \
|
||||
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
|
||||
|
||||
__all__ = [
|
||||
# Notes
|
||||
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
||||
'Alias', 'Trust', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
||||
# Transactions
|
||||
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
|
||||
'RecurrentTransaction', 'SpecialTransaction',
|
||||
|
@ -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):
|
||||
@ -218,6 +217,38 @@ class NoteSpecial(Note):
|
||||
return self.special_type
|
||||
|
||||
|
||||
class Trust(models.Model):
|
||||
"""
|
||||
A one-sided trust relationship bertween two users
|
||||
|
||||
If another user considers you as your friend, you can transfer money from
|
||||
them
|
||||
"""
|
||||
|
||||
trusting = models.ForeignKey(
|
||||
Note,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='trusting',
|
||||
verbose_name=_('trusting')
|
||||
)
|
||||
|
||||
trusted = models.ForeignKey(
|
||||
Note,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='trusted',
|
||||
verbose_name=_('trusted')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("frienship")
|
||||
verbose_name_plural = _("friendships")
|
||||
unique_together = ("trusting", "trusted")
|
||||
|
||||
def __str__(self):
|
||||
return _("Friendship between {trusting} and {trusted}").format(
|
||||
trusting=str(self.trusting), trusted=str(self.trusted))
|
||||
|
||||
|
||||
class Alias(models.Model):
|
||||
"""
|
||||
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
|
||||
|
@ -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
|
||||
@ -333,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
|
||||
|
@ -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
|
||||
}
|
||||
@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
||||
.done(function () {
|
||||
if (!isNaN(source.balance)) {
|
||||
const newBalance = source.balance - quantity * amount
|
||||
if (newBalance <= -5000) {
|
||||
if (newBalance <= -2000) {
|
||||
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) {
|
||||
|
@ -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 }
|
||||
|
||||
@ -243,7 +250,7 @@ $('#btn_transfer').click(function () {
|
||||
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>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
|
||||
@ -307,7 +314,7 @@ $('#btn_transfer').click(function () {
|
||||
|
||||
if (!isNaN(source.note.balance)) {
|
||||
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
||||
if (newBalance <= -5000) {
|
||||
if (newBalance <= -2000) {
|
||||
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()
|
||||
@ -348,14 +355,14 @@ $('#btn_transfer').click(function () {
|
||||
destination_alias: dest.name
|
||||
}).done(function () {
|
||||
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)
|
||||
[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(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger')
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, error]), 'danger')
|
||||
LOCK = false
|
||||
})
|
||||
})
|
||||
|
@ -1,16 +1,16 @@
|
||||
# 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
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.utils.html import format_html
|
||||
from django.utils.html import format_html, mark_safe
|
||||
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
|
||||
from .models.notes import Alias, Trust
|
||||
from .models.transactions import Transaction, TransactionTemplate
|
||||
from .templatetags.pretty_money import pretty_money
|
||||
|
||||
@ -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 "✖"
|
||||
|
||||
@ -148,6 +148,31 @@ DELETE_TEMPLATE = """
|
||||
"""
|
||||
|
||||
|
||||
class TrustTable(tables.Table):
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table condensed table-striped',
|
||||
'id': "trust_table"
|
||||
}
|
||||
model = Trust
|
||||
fields = ("trusted",)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
show_header = False
|
||||
trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
|
||||
|
||||
delete_col = tables.TemplateColumn(
|
||||
template_code=DELETE_TEMPLATE,
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={
|
||||
'td': {
|
||||
'class': lambda record: 'col-sm-1'
|
||||
+ (' d-none' if not PermissionBackend.check_perm(
|
||||
get_current_request(), "note.delete_trust", record)
|
||||
else '')}},
|
||||
verbose_name=_("Delete"),)
|
||||
|
||||
|
||||
class AliasTable(tables.Table):
|
||||
class Meta:
|
||||
attrs = {
|
||||
@ -165,7 +190,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"), )
|
||||
|
||||
|
||||
@ -197,6 +222,17 @@ class ButtonTable(tables.Table):
|
||||
verbose_name=_("Edit"),
|
||||
)
|
||||
|
||||
hideshow = tables.Column(
|
||||
verbose_name=_("Hide/Show"),
|
||||
accessor="pk",
|
||||
attrs={
|
||||
'td': {
|
||||
'class': 'col-sm-1',
|
||||
'id': lambda record: "hideshow_" + str(record.pk),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={'td': {'class': 'col-sm-1'}},
|
||||
@ -204,3 +240,16 @@ class ButtonTable(tables.Table):
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
||||
def order_category(self, queryset, is_descending):
|
||||
return queryset.order_by(f"{'-' if is_descending else ''}category__name"), True
|
||||
|
||||
def render_hideshow(self, record):
|
||||
val = '<button id="'
|
||||
val += str(record.pk)
|
||||
val += '" class="btn btn-secondary btn-sm" \
|
||||
onclick="hideshow(' + str(record.id) + ',' + \
|
||||
str(record.display).lower() + ')">'
|
||||
val += str(_("Hide/Show"))
|
||||
val += '</button>'
|
||||
return mark_safe(val)
|
||||
|
@ -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 %}
|
||||
|
@ -31,29 +31,29 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script type="text/javascript">
|
||||
function refreshMatchedWords() {
|
||||
$("tr").each(function() {
|
||||
let pattern = $('#search_field').val();
|
||||
if (pattern) {
|
||||
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
|
||||
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
let pattern = $('#search_field').val();
|
||||
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
let searchbar_obj = $("#search_field");
|
||||
let timer_on = false;
|
||||
let timer;
|
||||
|
||||
function refreshMatchedWords() {
|
||||
$("tr").each(function() {
|
||||
let pattern = searchbar_obj.val();
|
||||
if (pattern) {
|
||||
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
|
||||
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshMatchedWords();
|
||||
|
||||
function reloadTable() {
|
||||
let pattern = searchbar_obj.val();
|
||||
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
|
||||
}
|
||||
|
||||
searchbar_obj.keyup(function() {
|
||||
if (timer_on)
|
||||
clearTimeout(timer);
|
||||
@ -77,5 +77,28 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
|
||||
});
|
||||
}
|
||||
|
||||
// on click of button "hide/show", call the API
|
||||
function hideshow(id, displayed) {
|
||||
$.ajax({
|
||||
url: '/api/note/transaction/template/' + id + '/',
|
||||
type: 'PATCH',
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-CSRFTOKEN': CSRF_TOKEN
|
||||
},
|
||||
data: {
|
||||
display: !displayed
|
||||
},
|
||||
success: function() {
|
||||
if(displayed)
|
||||
addMsg("{% trans "Button hidden"%}", 'success', 1000)
|
||||
else addMsg("{% trans "Button displayed"%}", 'success', 1000)
|
||||
reloadTable()
|
||||
},
|
||||
error: function (err) {
|
||||
addMsg("{% trans "An error occured"%}", 'danger')
|
||||
}})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -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,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.tests import TestAPI
|
||||
|
@ -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, ))]
|
||||
@ -90,9 +90,9 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
qs = qs.filter(
|
||||
Q(name__iregex="^" + pattern)
|
||||
| Q(destination__club__name__iregex="^" + pattern)
|
||||
| Q(category__name__iregex="^" + pattern)
|
||||
Q(name__iregex=pattern)
|
||||
| Q(destination__club__name__iregex=pattern)
|
||||
| Q(category__name__iregex=pattern)
|
||||
| Q(description__iregex=pattern)
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
@ -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 PermissionViewSet, RoleViewSet
|
||||
|
@ -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 ReadOnlyProtectedModelViewSet
|
||||
|
@ -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,15 +1,15 @@
|
||||
# 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
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q, F
|
||||
from django.utils import timezone
|
||||
from note.models import Note, NoteUser, NoteClub, NoteSpecial
|
||||
from note_kfet.middlewares import get_current_session
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from member.models import Membership, Club
|
||||
|
||||
from .decorators import memoize
|
||||
@ -26,14 +26,31 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
@staticmethod
|
||||
@memoize
|
||||
def get_raw_permissions(user, t):
|
||||
def get_raw_permissions(request, t):
|
||||
"""
|
||||
Query permissions of a certain type for a user, then memoize it.
|
||||
:param user: The owner of the permissions
|
||||
:param request: The current request
|
||||
:param t: The type of the permissions: view, change, add or delete
|
||||
:return: The queryset of the permissions of the user (memoized) grouped by clubs
|
||||
"""
|
||||
if isinstance(user, AnonymousUser):
|
||||
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||
# OAuth2 Authentication
|
||||
user = request.auth.user
|
||||
|
||||
def permission_filter(membership_obj):
|
||||
query = Q(pk=-1)
|
||||
for scope in request.auth.scope.split(' '):
|
||||
permission_id, club_id = scope.split('_')
|
||||
if int(club_id) == membership_obj.club_id:
|
||||
query |= Q(pk=permission_id)
|
||||
return query
|
||||
else:
|
||||
user = request.user
|
||||
|
||||
def permission_filter(membership_obj):
|
||||
return Q(mask__rank__lte=request.session.get("permission_mask", 42))
|
||||
|
||||
if user.is_anonymous:
|
||||
# Unauthenticated users have no permissions
|
||||
return Permission.objects.none()
|
||||
|
||||
@ -43,7 +60,7 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
for membership in memberships:
|
||||
for role in membership.roles.all():
|
||||
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
|
||||
for perm in role.permissions.filter(permission_filter(membership), type=t).all():
|
||||
if not perm.permanent:
|
||||
if membership.date_start > date.today() or membership.date_end < date.today():
|
||||
continue
|
||||
@ -52,16 +69,22 @@ class PermissionBackend(ModelBackend):
|
||||
return perms
|
||||
|
||||
@staticmethod
|
||||
def permissions(user, model, type):
|
||||
def permissions(request, model, type):
|
||||
"""
|
||||
List all permissions of the given user that applies to a given model and a give type
|
||||
:param user: The owner of the permissions
|
||||
:param request: The current request
|
||||
:param model: The model that the permissions shoud apply
|
||||
:param type: The type of the permissions: view, change, add or delete
|
||||
:return: A generator of the requested permissions
|
||||
"""
|
||||
|
||||
for permission in PermissionBackend.get_raw_permissions(user, type):
|
||||
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||
# OAuth2 Authentication
|
||||
user = request.auth.user
|
||||
else:
|
||||
user = request.user
|
||||
|
||||
for permission in PermissionBackend.get_raw_permissions(request, type):
|
||||
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
|
||||
continue
|
||||
|
||||
@ -88,20 +111,26 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
@staticmethod
|
||||
@memoize
|
||||
def filter_queryset(user, model, t, field=None):
|
||||
def filter_queryset(request, model, t, field=None):
|
||||
"""
|
||||
Filter a queryset by considering the permissions of a given user.
|
||||
:param user: The owner of the permissions that are fetched
|
||||
:param request: The current request
|
||||
:param model: The concerned model of the queryset
|
||||
:param t: The type of modification (view, add, change, delete)
|
||||
:param field: The field of the model to test, if concerned
|
||||
:return: A query that corresponds to the filter to give to a queryset
|
||||
"""
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||
# OAuth2 Authentication
|
||||
user = request.auth.user
|
||||
else:
|
||||
user = request.user
|
||||
|
||||
if user is None or user.is_anonymous:
|
||||
# Anonymous users can't do anything
|
||||
return Q(pk=-1)
|
||||
|
||||
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
|
||||
if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
|
||||
# Superusers have all rights
|
||||
return Q()
|
||||
|
||||
@ -110,7 +139,7 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
# Never satisfied
|
||||
query = Q(pk=-1)
|
||||
perms = PermissionBackend.permissions(user, model, t)
|
||||
perms = PermissionBackend.permissions(request, model, t)
|
||||
for perm in perms:
|
||||
if perm.field and field != perm.field:
|
||||
continue
|
||||
@ -122,7 +151,7 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
@staticmethod
|
||||
@memoize
|
||||
def check_perm(user_obj, perm, obj=None):
|
||||
def check_perm(request, perm, obj=None):
|
||||
"""
|
||||
Check is the given user has the permission over a given object.
|
||||
The result is then memoized.
|
||||
@ -130,11 +159,18 @@ class PermissionBackend(ModelBackend):
|
||||
primary key, the result is not memoized. Moreover, the right could change
|
||||
(e.g. for a transaction, the balance of the user could change)
|
||||
"""
|
||||
if user_obj is None or isinstance(user_obj, AnonymousUser):
|
||||
# Requested by a shell
|
||||
if request is None:
|
||||
return False
|
||||
|
||||
sess = get_current_session()
|
||||
if sess is not None and sess.session_key is None:
|
||||
user_obj = request.user
|
||||
sess = request.session
|
||||
|
||||
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||
# OAuth2 Authentication
|
||||
user_obj = request.auth.user
|
||||
|
||||
if user_obj is None or user_obj.is_anonymous:
|
||||
return False
|
||||
|
||||
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
||||
@ -149,16 +185,19 @@ class PermissionBackend(ModelBackend):
|
||||
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
if any(permission.applies(obj, perm_type, perm_field)
|
||||
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
|
||||
for permission in PermissionBackend.permissions(request, ct, perm_type)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_perm(self, user_obj, perm, obj=None):
|
||||
return PermissionBackend.check_perm(user_obj, perm, obj)
|
||||
# Warning: this does not check that user_obj has the permission,
|
||||
# but if the current request has the permission.
|
||||
# This function is implemented for backward compatibility, and should not be used.
|
||||
return PermissionBackend.check_perm(get_current_request(), perm, obj)
|
||||
|
||||
def has_module_perms(self, user_obj, app_label):
|
||||
return False
|
||||
|
||||
def get_all_permissions(self, user_obj, obj=None):
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
return list(self.permissions(user_obj, ct, "view"))
|
||||
return list(self.permissions(get_current_request(), ct, "view"))
|
||||
|
@ -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
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from time import time
|
||||
|
||||
from django.contrib.sessions.models import Session
|
||||
from note_kfet.middlewares import get_current_session
|
||||
from note_kfet.middlewares import get_current_request
|
||||
|
||||
|
||||
def memoize(f):
|
||||
@ -48,11 +48,11 @@ def memoize(f):
|
||||
last_collect = time()
|
||||
|
||||
# If there is no session, then we don't memoize anything.
|
||||
sess = get_current_session()
|
||||
if sess is None or sess.session_key is None:
|
||||
request = get_current_request()
|
||||
if request is None or request.session is None or request.session.session_key is None:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
sess_key = sess.session_key
|
||||
sess_key = request.session.session_key
|
||||
if sess_key not in sess_funs:
|
||||
# lru_cache makes the job of memoization
|
||||
# We store only the 512 latest data per session. It has to be enough.
|
||||
|
File diff suppressed because it is too large
Load Diff
19
apps/permission/migrations/0002_club_not_required.py
Normal file
19
apps/permission/migrations/0002_club_not_required.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.28 on 2023-07-24 10:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('permission', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='role',
|
||||
name='for_club',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for 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
|
||||
|
||||
import functools
|
||||
@ -339,6 +339,7 @@ class Role(models.Model):
|
||||
"member.Club",
|
||||
verbose_name=_("for club"),
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user