1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-30 04:13:01 +00:00

Merge branch 'fix-js-strings' into 'master'

Fix js strings

Closes #85, #84 et #83

See merge request bde/nk20!144
This commit is contained in:
ynerant 2021-01-19 12:24:13 +01:00
commit 002128eed2
46 changed files with 919 additions and 169 deletions

5
.gitignore vendored
View File

@ -47,3 +47,8 @@ backups/
env/ env/
venv/ venv/
db.sqlite3 db.sqlite3
# ansibles customs host
ansible/host_vars/*.yaml
!ansible/host_vars/bde*
ansible/hosts

View File

@ -38,6 +38,21 @@ py38-django22:
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py38-django22 script: tox -e py38-django22
# Debian Bullseye
py39-django22:
stage: test
image: debian:bullseye
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py39-django22
linters: linters:
stage: quality-assurance stage: quality-assurance
image: debian:buster-backports image: debian:buster-backports

View File

@ -69,13 +69,31 @@ accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
de la note sur un téléphone ! de la note sur un téléphone !
## Installation d'une instance de production ## Installation d'une instance de production
Pour déployer facilement la note il est possible d'utiliser le playbook Ansible (sinon vous pouvez toujours le faire a la main, voir plus bas).
### Avec ansible
Il vous faudra un serveur sous debian ou ubuntu connecté à internet et que vous souhaiterez accéder à cette instance de la note sur `note.nomdedomaine.tld`.
0. Installer Ansible sur votre machine personnelle.
0. (bis) cloner le dépot sur votre machine personelle.
1. Copier le fichier `ansible/host_example`
``` bash
$ cp ansible/hosts_example ansible/hosts
```
et ajouter sous [dev] et/ou [prod] les serveurs sur lesquels vous souhaitez installer la note.
2. Créer un fichier `ansible/host_vars/<note.nomdedomaine.tld.yaml>` sur le modèle des fichiers existants dans `ansible/hosts` et compléter les variables nécessaires.
3. lancer `ansible/base.yaml -l <nomdedomaine.tld.yaml>`
4. Aller vous faire un café, ca peux durer un moment.
### Installation manuelle
**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.** **En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.**
Cela permet de mettre à jour facilement les dépendances critiques telles que Django. Cela permet de mettre à jour facilement les dépendances critiques telles que Django.
L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**. L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**.
Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant.
Sinon vous pouvez suivre les étapes décrites ci-dessous. Sinon vous pouvez suivre les étapes décrites ci-dessous.
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports. 0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.

View File

@ -7,7 +7,7 @@
prompt: "Password of the database (leave it blank to skip database init)" prompt: "Password of the database (leave it blank to skip database init)"
private: yes private: yes
vars: vars:
mirror: deb.debian.org mirror: mirror.crans.org
roles: roles:
- 1-apt-basic - 1-apt-basic
- 2-nk20 - 2-nk20

View File

@ -3,3 +3,4 @@ note:
server_name: note-beta.crans.org server_name: note-beta.crans.org
git_branch: beta git_branch: beta
cron_enabled: false cron_enabled: false
email: notekfet2020@lists.crans.org

View File

@ -3,3 +3,4 @@ note:
server_name: note-dev.crans.org server_name: note-dev.crans.org
git_branch: beta git_branch: beta
cron_enabled: false cron_enabled: false
email: notekfet2020@lists.crans.org

View File

@ -3,3 +3,4 @@ note:
server_name: note.crans.org server_name: note.crans.org
git_branch: master git_branch: master
cron_enabled: true cron_enabled: true
email: notekfet2020@lists.crans.org

View File

@ -1,5 +1,5 @@
[dev] [dev]
bde3-virt.adh.crans.org bde-note-dev.adh.crans.org
bde-nk20-beta.adh.crans.org bde-nk20-beta.adh.crans.org
[prod] [prod]

View File

@ -3,11 +3,12 @@
apt_repository: apt_repository:
repo: deb http://{{ mirror }}/debian buster-backports main repo: deb http://{{ mirror }}/debian buster-backports main
state: present state: present
when: ansible_facts['distribution'] == "Debian"
- name: Install note_kfet APT dependencies - name: Install note_kfet APT dependencies
apt: apt:
update_cache: true update_cache: true
default_release: buster-backports default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
install_recommends: false install_recommends: false
name: name:
# Common tools # Common tools

View File

@ -16,7 +16,7 @@
- name: Use default env vars (should be updated!) - name: Use default env vars (should be updated!)
template: template:
src: "env_example" src: "env.j2"
dest: "/var/www/note_kfet/.env" dest: "/var/www/note_kfet/.env"
mode: 0644 mode: 0644
force: false force: false
@ -36,3 +36,13 @@
dest: /etc/cron.d/note dest: /etc/cron.d/note
owner: root owner: root
group: root group: root
- name: Set default directory to /var/www/note_kfet
lineinfile:
path: /etc/skel/.bashrc
line: 'cd /var/www/note_kfet'
- name: Automatically source Python virtual environment
lineinfile:
path: /etc/skel/.bashrc
line: 'source /var/www/note_kfet/env/bin/activate'

View File

@ -0,0 +1,23 @@
DJANGO_APP_STAGE=prod
# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
DJANGO_DEV_STORE_METHOD=sqlite
DJANGO_DB_HOST=localhost
DJANGO_DB_NAME=note_db
DJANGO_DB_USER=note
DJANGO_DB_PASSWORD={{ DB_PASSWORD }}
DJANGO_DB_PORT=
DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL= {{note.server_name}}
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost
EMAIL_HOST=smtp.localhost
EMAIL_PORT=25
EMAIL_USER=notekfet@localhost
EMAIL_PASSWORD=CHANGE_ME
# Wiki configuration
WIKI_USER=NoteKfet2020
WIKI_PASSWORD=

View File

@ -9,6 +9,11 @@
retries: 3 retries: 3
until: pkg_result is succeeded until: pkg_result is succeeded
- name: Check if certificate already exists.
stat:
path: /etc/letsencrypt/live/{{note.server_name}}/cert.pem
register: letsencrypt_cert
- name: Create /etc/letsencrypt/conf.d - name: Create /etc/letsencrypt/conf.d
file: file:
path: /etc/letsencrypt/conf.d path: /etc/letsencrypt/conf.d
@ -19,3 +24,17 @@
src: "letsencrypt/conf.d/nk20.ini.j2" src: "letsencrypt/conf.d/nk20.ini.j2"
dest: "/etc/letsencrypt/conf.d/nk20.ini" dest: "/etc/letsencrypt/conf.d/nk20.ini"
mode: 0644 mode: 0644
- name: Stop services to allow certbot to generate a cert.
service:
name: nginx
state: stopped
- name: Generate new certificate if one doesn't exist.
shell: "certbot certonly --non-interactive --agree-tos --config /etc/letsencrypt/conf.d/nk20.ini -d {{note.server_name}}"
when: letsencrypt_cert.stat.exists == False
- name: Restart services to allow certbot to generate a cert.
service:
name: nginx
state: started

View File

@ -10,7 +10,7 @@ rsa-key-size = 4096
# server = https://acme-staging.api.letsencrypt.org/directory # server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment and update to register with the specified e-mail address # Uncomment and update to register with the specified e-mail address
email = notekfet2020@lists.crans.org email = {{ note.email }}
# Uncomment to use a text interface instead of ncurses # Uncomment to use a text interface instead of ncurses
text = True text = True

View File

@ -11,14 +11,14 @@
until: pkg_result is succeeded until: pkg_result is succeeded
- name: Create role note - name: Create role note
when: "DB_PASSWORD|bool" # If the password is not defined, skip the installation when: DB_PASSWORD|length > 0 # If the password is not defined, skip the installation
postgresql_user: postgresql_user:
name: note name: note
password: "{{ DB_PASSWORD }}" password: "{{ DB_PASSWORD }}"
become_user: postgres become_user: postgres
- name: Create NK20 database - name: Create NK20 database
when: "DB_PASSWORD|bool" when: DB_PASSWORD|length >0
postgresql_db: postgresql_db:
name: note_db name: note_db
owner: note owner: note

View File

@ -15,10 +15,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/type/ then render it on /api/activity/type/
""" """
queryset = ActivityType.objects.all() queryset = ActivityType.objects.order_by('id')
serializer_class = ActivityTypeSerializer serializer_class = ActivityTypeSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'can_invite', ] filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ]
class ActivityViewSet(ReadProtectedModelViewSet): class ActivityViewSet(ReadProtectedModelViewSet):
@ -27,10 +27,16 @@ class ActivityViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/activity/ then render it on /api/activity/activity/
""" """
queryset = Activity.objects.all() queryset = Activity.objects.order_by('id')
serializer_class = ActivitySerializer serializer_class = ActivitySerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'description', 'activity_type', ] filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
'date_start', 'date_end', 'valid', 'open', ]
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
'$creater__email', '$creater__note__alias__name', '$creater__note__alias__normalized_name',
'$organizer__name', '$organizer__email', '$organizer__note__alias__name',
'$organizer__note__alias__normalized_name', '$attendees_club__name', '$attendees_club__email',
'$attendees_club__note__alias__name', '$attendees_club__note__alias__normalized_name', ]
class GuestViewSet(ReadProtectedModelViewSet): class GuestViewSet(ReadProtectedModelViewSet):
@ -39,10 +45,13 @@ class GuestViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/guest/ then render it on /api/activity/guest/
""" """
queryset = Guest.objects.all() queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ]
class EntryViewSet(ReadProtectedModelViewSet): class EntryViewSet(ReadProtectedModelViewSet):
@ -51,7 +60,9 @@ class EntryViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/entry/ then render it on /api/activity/entry/
""" """
queryset = Entry.objects.all() queryset = Entry.objects.order_by('id')
serializer_class = EntrySerializer serializer_class = EntrySerializer
filter_backends = [SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] filterset_fields = ['activity', 'time', 'note', 'guest', ]
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
'$guest__last_name', '$guest__first_name', ]

View File

@ -3,13 +3,16 @@
from datetime import timedelta from datetime import timedelta
from api.tests import TestAPI
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from activity.models import Activity, ActivityType, Guest, Entry
from member.models import Club from member.models import Club
from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
from ..models import Activity, ActivityType, Guest, Entry
class TestActivities(TestCase): class TestActivities(TestCase):
""" """
@ -173,3 +176,58 @@ class TestActivities(TestCase):
""" """
response = self.client.get(reverse("activity:calendar_ics")) response = self.client.get(reverse("activity:calendar_ics"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class TestActivityAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.activity = Activity.objects.create(
name="Activity",
description="This is a test activity\non two very very long lines\nbecause this is very important.",
location="Earth",
activity_type=ActivityType.objects.get(name="Pot"),
creater=self.user,
organizer=Club.objects.get(name="Kfet"),
attendees_club=Club.objects.get(name="Kfet"),
date_start=timezone.now(),
date_end=timezone.now() + timedelta(days=2),
valid=True,
)
self.guest = Guest.objects.create(
activity=self.activity,
inviter=self.user.note,
last_name="GUEST",
first_name="Guest",
)
self.entry = Entry.objects.create(
activity=self.activity,
note=self.user.note,
guest=self.guest,
)
def test_activity_api(self):
"""
Load Activity API page and test all filters and permissions
"""
self.check_viewset(ActivityViewSet, "/api/activity/activity/")
def test_activity_type_api(self):
"""
Load ActivityType API page and test all filters and permissions
"""
self.check_viewset(ActivityTypeViewSet, "/api/activity/type/")
def test_entry_api(self):
"""
Load Entry API page and test all filters and permissions
"""
self.check_viewset(EntryViewSet, "/api/activity/entry/")
def test_guest_api(self):
"""
Load Guest API page and test all filters and permissions
"""
self.check_viewset(GuestViewSet, "/api/activity/guest/")

237
apps/api/tests.py Normal file
View File

@ -0,0 +1,237 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from datetime import datetime, date
from urllib.parse import quote_plus
from warnings import warn
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.files import ImageFieldFile
from django.test import TestCase
from django_filters.rest_framework import DjangoFilterBackend
from member.models import Membership, Club
from note.models import NoteClub, NoteUser, Alias, Note
from permission.models import PermissionMask, Permission, Role
from phonenumbers import PhoneNumber
from rest_framework.filters import SearchFilter, OrderingFilter
from .viewsets import ContentTypeViewSet, UserViewSet
class TestAPI(TestCase):
"""
Load API pages and check that filters are working.
"""
fixtures = ('initial', )
def setUp(self) -> None:
self.user = User.objects.create_superuser(
username="adminapi",
password="adminapi",
email="adminapi@example.com",
last_name="Admin",
first_name="Admin",
)
self.client.force_login(self.user)
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
def check_viewset(self, viewset, url):
"""
This function should be called inside a unit test.
This loads the viewset and for each filter entry, it checks that the filter is running good.
"""
resp = self.client.get(url + "?format=json")
self.assertEqual(resp.status_code, 200)
model = viewset.serializer_class.Meta.model
if not model.objects.exists(): # pragma: no cover
warn(f"Warning: unable to test API filters for the model {model._meta.verbose_name} "
"since there is no instance of it.")
return
if hasattr(viewset, "filter_backends"):
backends = viewset.filter_backends
obj = model.objects.last()
if DjangoFilterBackend in backends:
# Specific search
for field in viewset.filterset_fields:
obj = self.fix_note_object(obj, field)
value = self.get_value(obj, field)
if value is None: # pragma: no cover
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
"has not been tested.")
continue
resp = self.client.get(url + f"?format=json&{field}={quote_plus(str(value))}")
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
content = json.loads(resp.content)
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
if OrderingFilter in backends:
# Ensure that ordering is working well
for field in viewset.ordering_fields:
resp = self.client.get(url + f"?ordering={field}")
self.assertEqual(resp.status_code, 200)
resp = self.client.get(url + f"?ordering=-{field}")
self.assertEqual(resp.status_code, 200)
if SearchFilter in backends:
# Basic search
for field in viewset.search_fields:
obj = self.fix_note_object(obj, field)
if field[0] == '$' or field[0] == '=':
field = field[1:]
value = self.get_value(obj, field)
if value is None: # pragma: no cover
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
"has not been tested.")
continue
resp = self.client.get(url + f"?format=json&search={quote_plus(str(value))}")
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
content = json.loads(resp.content)
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
self.check_permissions(url, obj)
def check_permissions(self, url, obj):
"""
Check that permissions are working
"""
# Drop rights
self.user.is_superuser = False
self.user.save()
sess = self.client.session
sess["permission_mask"] = 0
sess.save()
# Delete user permissions
for m in Membership.objects.filter(user=self.user).all():
m.roles.clear()
m.save()
# Create a new role, which will have the checking permission
role = Role.objects.get_or_create(name="β-tester")[0]
role.permissions.clear()
role.save()
membership = Membership.objects.get_or_create(user=self.user, club=Club.objects.get(name="BDE"))[0]
membership.roles.set([role])
membership.save()
# Ensure that the access to the object is forbidden without permission
resp = self.client.get(url + f"{obj.pk}/")
self.assertEqual(resp.status_code, 404, f"Mysterious access to {url}{obj.pk}/ for {obj}")
obj.refresh_from_db()
# There are problems with polymorphism
if isinstance(obj, Note) and hasattr(obj, "note_ptr"):
obj = obj.note_ptr
mask = PermissionMask.objects.get(rank=0)
for field in obj._meta.fields:
# Build permission query
value = self.get_value(obj, field.name)
if isinstance(value, date) or isinstance(value, datetime):
value = value.isoformat()
elif isinstance(value, ImageFieldFile):
value = value.name
query = json.dumps({field.name: value})
# Create sample permission
permission = Permission.objects.get_or_create(
model=ContentType.objects.get_for_model(obj._meta.model),
query=query,
mask=mask,
type="view",
permanent=False,
description=f"Can view {obj._meta.verbose_name}",
)[0]
role.permissions.set([permission])
role.save()
# Check that the access is possible
resp = self.client.get(url + f"{obj.pk}/")
self.assertEqual(resp.status_code, 200, f"Permission {permission.query} is not working "
f"for the model {obj._meta.verbose_name}")
# Restore rights
self.user.is_superuser = True
self.user.save()
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
@staticmethod
def get_value(obj, key: str):
"""
Resolve the queryset filter to get the Python value of an object.
"""
if hasattr(obj, "all"):
# obj is a RelatedManager
obj = obj.last()
if obj is None: # pragma: no cover
return None
if '__' not in key:
obj = getattr(obj, key)
if hasattr(obj, "pk"):
return obj.pk
elif hasattr(obj, "all"):
if not obj.exists(): # pragma: no cover
return None
return obj.last().pk
elif isinstance(obj, bool):
return int(obj)
elif isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, PhoneNumber):
return obj.raw_input
return obj
key, remaining = key.split('__', 1)
return TestAPI.get_value(getattr(obj, key), remaining)
@staticmethod
def fix_note_object(obj, field):
"""
When querying an object that has a noteclub or a noteuser field,
ensure that the object has a good value.
"""
if isinstance(obj, Alias):
if "noteuser" in field:
return NoteUser.objects.last().alias.last()
elif "noteclub" in field:
return NoteClub.objects.last().alias.last()
elif isinstance(obj, Note):
if "noteuser" in field:
return NoteUser.objects.last()
elif "noteclub" in field:
return NoteClub.objects.last()
return obj
class TestBasicAPI(TestAPI):
def test_user_api(self):
"""
Load the user page.
"""
self.check_viewset(ContentTypeViewSet, "/api/models/")
self.check_viewset(UserViewSet, "/api/user/")

View File

@ -6,6 +6,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_session from note_kfet.middlewares import get_current_session
@ -48,12 +49,13 @@ class UserViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
then render it on /api/users/ then render it on /api/user/
""" """
queryset = User.objects.all() queryset = User.objects
serializer_class = UserSerializer serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active',
'note__alias__name', 'note__alias__normalized_name', ]
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
@ -106,7 +108,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
then render it on /api/users/ then render it on /api/models/
""" """
queryset = ContentType.objects.all() queryset = ContentType.objects.order_by('id')
serializer_class = ContentTypeSerializer serializer_class = ContentTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ]

View File

@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
then render it on /api/logs/ then render it on /api/logs/
""" """
queryset = Changelog.objects.all() queryset = Changelog.objects.order_by('id')
serializer_class = ChangelogSerializer serializer_class = ChangelogSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter] filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ] filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]

View File

@ -1,7 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.filters import SearchFilter from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
@ -14,8 +15,15 @@ class ProfileViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
then render it on /api/members/profile/ then render it on /api/members/profile/
""" """
queryset = Profile.objects.all() queryset = Profile.objects.order_by('id')
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
'ml_art_registration', 'report_frequency', 'email_confirmed', 'registration_valid', ]
search_fields = ['$user__first_name', '$user__last_name', '$user__username', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', ]
class ClubViewSet(ReadProtectedModelViewSet): class ClubViewSet(ReadProtectedModelViewSet):
@ -24,10 +32,13 @@ class ClubViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
then render it on /api/members/club/ then render it on /api/members/club/
""" """
queryset = Club.objects.all() queryset = Club.objects.order_by('id')
serializer_class = ClubSerializer serializer_class = ClubSerializer
filter_backends = [SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$name', ] filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
'membership_duration', 'membership_start', 'membership_end', ]
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
class MembershipViewSet(ReadProtectedModelViewSet): class MembershipViewSet(ReadProtectedModelViewSet):
@ -36,5 +47,14 @@ class MembershipViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
then render it on /api/members/membership/ then render it on /api/members/membership/
""" """
queryset = Membership.objects.all() queryset = Membership.objects.order_by('id')
serializer_class = MembershipSerializer serializer_class = MembershipSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
'user__username', 'user__last_name', 'user__first_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name',
'date_start', 'date_end', 'fee', 'roles', ]
ordering_fields = ['id', 'date_start', 'date_end', ]
search_fields = ['$club__name', '$club__email', '$club__note__alias__name', '$club__note__alias__normalized_name',
'$user__username', '$user__last_name', '$user__first_name', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', '$roles__name', ]

View File

@ -313,6 +313,7 @@ class Membership(models.Model):
roles = models.ManyToManyField( roles = models.ManyToManyField(
"permission.Role", "permission.Role",
related_name="memberships",
verbose_name=_("roles"), verbose_name=_("roles"),
) )

View File

@ -48,7 +48,7 @@
<dd class="col-xl-6"> <dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}"> <a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }}) {% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
</a> </a>
</dd> </dd>

View File

@ -21,7 +21,7 @@
<dd class="col-xl-6"> <dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}"> <a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
{% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }}) {% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
</a> </a>
</dd> </dd>

View File

@ -5,17 +5,20 @@ import hashlib
import os import os
from datetime import date, timedelta from datetime import date, timedelta
from api.tests import TestAPI
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q from django.db.models import Q
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from member.models import Club, Membership, Profile
from note.models import Alias, NoteSpecial from note.models import Alias, NoteSpecial
from permission.models import Role from permission.models import Role
from treasury.models import SogeCredit from treasury.models import SogeCredit
from ..api.views import ClubViewSet, MembershipViewSet, ProfileViewSet
from ..models import Club, Membership, Profile
""" """
Create some users and clubs and test that all pages are rendering properly Create some users and clubs and test that all pages are rendering properly
and that memberships are working. and that memberships are working.
@ -403,3 +406,46 @@ class TestMemberships(TestCase):
self.user.password = "custom_nk15$1$" + salt + "|" + hashed self.user.password = "custom_nk15$1$" + salt + "|" + hashed
self.user.save() self.user.save()
self.assertTrue(self.user.check_password(password)) self.assertTrue(self.user.check_password(password))
class TestMemberAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.user.profile.registration_valid = True
self.user.profile.email_confirmed = True
self.user.profile.phone_number = "0600000000"
self.user.profile.section = "1A0"
self.user.profile.department = "A0"
self.user.profile.address = "Earth"
self.user.profile.save()
self.club = Club.objects.create(
name="totoclub",
parent_club=Club.objects.get(name="BDE"),
membership_start=date(year=1970, month=1, day=1),
membership_end=date(year=2040, month=1, day=1),
membership_duration=365 * 10,
)
self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
self.membership = Membership.objects.create(user=self.user, club=self.club)
self.membership.roles.add(Role.objects.get(name="Bureau de club"))
self.membership.save()
def test_club_api(self):
"""
Load Club API page and test all filters and permissions
"""
self.check_viewset(ClubViewSet, "/api/members/club/")
def test_profile_api(self):
"""
Load Profile API page and test all filters and permissions
"""
self.check_viewset(ProfileViewSet, "/api/members/profile/")
def test_membership_api(self):
"""
Load Membership API page and test all filters and permissions
"""
self.check_viewset(MembershipViewSet, "/api/members/membership/")

View File

@ -256,7 +256,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable( context["aliases"] = AliasTable(
note.alias_set.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all()) 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( context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
@ -458,7 +458,7 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable(note.alias_set.filter( context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all()) PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,

View File

@ -15,29 +15,37 @@ from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\ from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, Alias from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
class NotePolymorphicViewSet(ReadProtectedModelViewSet): class NotePolymorphicViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Note` objects (with polymorhism),
serialize it to JSON with the given serializer,
then render it on /api/note/note/ then render it on /api/note/note/
""" """
queryset = Note.objects.all() queryset = Note.objects.order_by('id')
serializer_class = NotePolymorphicSerializer serializer_class = NotePolymorphicSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['polymorphic_ctype', 'is_active', ] filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ] search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
ordering_fields = ['alias__name', 'alias__normalized_name'] '$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
'$noteuser__user__email', '$noteclub__club__email', ]
ordering_fields = ['alias__name', 'alias__normalized_name', 'balance', 'created_at', ]
def get_queryset(self): def get_queryset(self):
""" """
Parse query and apply filters. Parse query and apply filters.
:return: The filtered set of requested notes :return: The filtered set of requested notes
""" """
queryset = super().get_queryset().distinct() 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()
alias = self.request.query_params.get("alias", ".*") alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter( queryset = queryset.filter(
@ -55,12 +63,12 @@ class AliasViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/aliases/ then render it on /api/aliases/
""" """
queryset = Alias.objects.all() queryset = Alias.objects
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note'] filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name'] ordering_fields = ['name', 'normalized_name', ]
def get_serializer_class(self): def get_serializer_class(self):
serializer_class = self.serializer_class serializer_class = self.serializer_class
@ -106,12 +114,12 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet): class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects.all() queryset = Alias.objects
serializer_class = ConsumerSerializer serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend] filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note'] filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name'] ordering_fields = ['name', 'normalized_name', ]
def get_queryset(self): def get_queryset(self):
""" """
@ -157,10 +165,11 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/category/ then render it on /api/note/transaction/category/
""" """
queryset = TemplateCategory.objects.order_by("name").all() queryset = TemplateCategory.objects.order_by('name')
serializer_class = TemplateCategorySerializer serializer_class = TemplateCategorySerializer
filter_backends = [SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$name', ] filterset_fields = ['name', 'templates', 'templates__name']
search_fields = ['$name', '$templates__name', ]
class TransactionTemplateViewSet(viewsets.ModelViewSet): class TransactionTemplateViewSet(viewsets.ModelViewSet):
@ -169,11 +178,12 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/template/ then render it on /api/note/transaction/template/
""" """
queryset = TransactionTemplate.objects.order_by("name").all() queryset = TransactionTemplate.objects.order_by('name')
serializer_class = TransactionTemplateSerializer serializer_class = TransactionTemplateSerializer
filter_backends = [SearchFilter, DjangoFilterBackend] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['name', 'amount', 'display', 'category', ] filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
search_fields = ['$name', ] search_fields = ['$name', '$category__name', ]
ordering_fields = ['amount', ]
class TransactionViewSet(ReadProtectedModelViewSet): class TransactionViewSet(ReadProtectedModelViewSet):
@ -182,13 +192,17 @@ class TransactionViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/transaction/ then render it on /api/note/transaction/transaction/
""" """
queryset = Transaction.objects.order_by("-created_at").all() queryset = Transaction.objects.order_by('-created_at')
serializer_class = TransactionPolymorphicSerializer serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity", filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
"polymorphic_ctype", "amount", "created_at", ] 'destination', 'destination_alias', 'destination__alias__name',
search_fields = ['$reason', ] 'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
ordering_fields = ['created_at', 'amount'] 'created_at', 'valid', 'invalidity_reason', ]
search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name',
'$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name',
'$invalidity_reason', ]
ordering_fields = ['created_at', 'amount', ]
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user

View File

@ -248,6 +248,7 @@ class Alias(models.Model):
note = models.ForeignKey( note = models.ForeignKey(
Note, Note,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="alias",
) )
class Meta: class Meta:

View File

@ -1,15 +1,20 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from member.models import Club, Membership
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from member.models import Club, Membership from django.utils import timezone
from note.models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias
from permission.models import Role from permission.models import Role
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
TransactionTemplateViewSet, TransactionViewSet
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
class TestTransactions(TestCase): class TestTransactions(TestCase):
fixtures = ('initial', ) fixtures = ('initial', )
@ -297,8 +302,8 @@ class TestTransactions(TestCase):
def test_render_search_transactions(self): def test_render_search_transactions(self):
response = self.client.get(reverse("note:transactions", args=(self.user.note.pk,)), data=dict( response = self.client.get(reverse("note:transactions", args=(self.user.note.pk,)), data=dict(
source=self.second_user.note.alias_set.first().id, source=self.second_user.note.alias.first().id,
destination=self.user.note.alias_set.first().id, destination=self.user.note.alias.first().id,
type=[ContentType.objects.get_for_model(Transaction).id], type=[ContentType.objects.get_for_model(Transaction).id],
reason="test", reason="test",
valid=True, valid=True,
@ -363,3 +368,69 @@ class TestTransactions(TestCase):
self.assertTrue(Alias.objects.filter(name="test_updated_alias").exists()) self.assertTrue(Alias.objects.filter(name="test_updated_alias").exists())
response = self.client.delete("/api/note/alias/" + str(alias.pk) + "/") response = self.client.delete("/api/note/alias/" + str(alias.pk) + "/")
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
class TestNoteAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
membership = Membership.objects.create(club=Club.objects.get(name="BDE"), user=self.user)
membership.roles.add(Role.objects.get(name="Respo info"))
membership.save()
Membership.objects.create(club=Club.objects.get(name="Kfet"), user=self.user)
self.user.note.last_negative = timezone.now()
self.user.note.save()
self.transaction = Transaction.objects.create(
source=Note.objects.first(),
destination=self.user.note,
amount=4200,
reason="Test transaction",
)
self.user.note.refresh_from_db()
Alias.objects.create(note=self.user.note, name="I am a ¢omplex alias")
self.category = TemplateCategory.objects.create(name="Test")
self.template = TransactionTemplate.objects.create(
name="Test",
destination=Club.objects.get(name="BDE").note,
category=self.category,
amount=100,
description="Test template",
)
def test_alias_api(self):
"""
Load Alias API page and test all filters and permissions
"""
self.check_viewset(AliasViewSet, "/api/note/alias/")
def test_consumer_api(self):
"""
Load Consumer API page and test all filters and permissions
"""
self.check_viewset(ConsumerViewSet, "/api/note/consumer/")
def test_note_api(self):
"""
Load Note API page and test all filters and permissions
"""
self.check_viewset(NotePolymorphicViewSet, "/api/note/note/")
def test_template_category_api(self):
"""
Load TemplateCategory API page and test all filters and permissions
"""
self.check_viewset(TemplateCategoryViewSet, "/api/note/transaction/category/")
def test_transaction_template_api(self):
"""
Load TemplateTemplate API page and test all filters and permissions
"""
self.check_viewset(TransactionTemplateViewSet, "/api/note/transaction/template/")
def test_transaction_api(self):
"""
Load Transaction API page and test all filters and permissions
"""
self.check_viewset(TransactionViewSet, "/api/note/transaction/transaction/")

View File

@ -1,8 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet from api.viewsets import ReadOnlyProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from .serializers import PermissionSerializer, RoleSerializer from .serializers import PermissionSerializer, RoleSerializer
from ..models import Permission, Role from ..models import Permission, Role
@ -14,10 +15,11 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer,
then render it on /api/permission/permission/ then render it on /api/permission/permission/
""" """
queryset = Permission.objects.all() queryset = Permission.objects.order_by('id')
serializer_class = PermissionSerializer serializer_class = PermissionSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['model', 'type', ] filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
search_fields = ['$model__name', '$query', '$description', ]
class RoleViewSet(ReadOnlyProtectedModelViewSet): class RoleViewSet(ReadOnlyProtectedModelViewSet):
@ -26,7 +28,8 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
then render it on /api/permission/roles/ then render it on /api/permission/roles/
""" """
queryset = Role.objects.all() queryset = Role.objects.order_by('id')
serializer_class = RoleSerializer serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['role', ] filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
SearchFilter = ['$name', '$for_club__name', ]

View File

@ -1,6 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import sys
from functools import lru_cache from functools import lru_cache
from time import time from time import time
@ -38,6 +38,10 @@ def memoize(f):
nonlocal last_collect nonlocal last_collect
if "test" in sys.argv:
# In a test environment, don't memoize permissions
return f(*args, **kwargs)
if time() - last_collect > 60: if time() - last_collect > 60:
# Clear cache # Clear cache
collect() collect()

View File

@ -819,7 +819,7 @@
"type": "change", "type": "change",
"mask": 1, "mask": 1,
"field": "", "field": "",
"permanent": false, "permanent": true,
"description": "Modifier son profil" "description": "Modifier son profil"
} }
}, },

View File

@ -5,7 +5,6 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django import template from django import template
from note.models import Transaction
from note_kfet.middlewares import get_current_authenticated_user, get_current_session from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -25,21 +24,6 @@ def not_empty_model_list(model_name):
return qs.exists() return qs.exists()
@stringfilter
def not_empty_model_change_list(model_name):
"""
Return True if and only if the current user has right to change any object of the given model.
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
qs = model_list(model_name, "change")
return qs.exists()
@stringfilter @stringfilter
def model_list(model_name, t="view", fetch=True): def model_list(model_name, t="view", fetch=True):
""" """
@ -68,33 +52,8 @@ def has_perm(perm, obj):
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj) return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
def can_create_transaction():
"""
:return: True iff the authenticated user can create a transaction.
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
if session.get("can_create_transaction", None):
return session.get("can_create_transaction", None) == 1
empty_transaction = Transaction(
source=user.note,
destination=user.note,
quantity=1,
amount=0,
reason="Check permissions",
)
session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction)
return session.get("can_create_transaction") == 1
register = template.Library() register = template.Library()
register.filter('not_empty_model_list', not_empty_model_list) register.filter('not_empty_model_list', not_empty_model_list)
register.filter('not_empty_model_change_list', not_empty_model_change_list)
register.filter('model_list', model_list) register.filter('model_list', model_list)
register.filter('model_list_length', model_list_length) register.filter('model_list_length', model_list_length)
register.filter('has_perm', has_perm) register.filter('has_perm', has_perm)

View File

@ -78,7 +78,7 @@ class PermissionQueryTestCase(TestCase):
query = instanced.query query = instanced.query
model = perm.model.model_class() model = perm.model.model_class()
model.objects.filter(query).all() model.objects.filter(query).all()
except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError): except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError): # pragma: no cover
print("Query error for permission", perm) print("Query error for permission", perm)
print("Query:", perm.query) print("Query:", perm.query)
if instanced.query: if instanced.query:

View File

@ -85,7 +85,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
If not, a 403 error is displayed. If not, a 403 error is displayed.
""" """
def get_sample_object(self): def get_sample_object(self): # pragma: no cover
""" """
return a sample instance of the Model. return a sample instance of the Model.
It should be valid (can be stored properly in database), but must not collide with existing data. It should be valid (can be stored properly in database), but must not collide with existing data.

View File

@ -16,10 +16,11 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/invoice/ then render it on /api/treasury/invoice/
""" """
queryset = Invoice.objects.order_by("id").all() queryset = Invoice.objects.order_by('id')
serializer_class = InvoiceSerializer serializer_class = InvoiceSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['bde', ] filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
search_fields = ['$object', '$description', '$name', '$address', ]
class ProductViewSet(ReadProtectedModelViewSet): class ProductViewSet(ReadProtectedModelViewSet):
@ -28,10 +29,11 @@ class ProductViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/product/ then render it on /api/treasury/product/
""" """
queryset = Product.objects.order_by("invoice_id", "id").all() queryset = Product.objects.order_by('invoice_id', 'id')
serializer_class = ProductSerializer serializer_class = ProductSerializer
filter_backends = [SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$designation', ] filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
search_fields = ['$designation', '$invoice__object', ]
class RemittanceTypeViewSet(ReadProtectedModelViewSet): class RemittanceTypeViewSet(ReadProtectedModelViewSet):
@ -40,8 +42,11 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
then render it on /api/treasury/remittance_type/ then render it on /api/treasury/remittance_type/
""" """
queryset = RemittanceType.objects.order_by("id") queryset = RemittanceType.objects.order_by('id')
serializer_class = RemittanceTypeSerializer serializer_class = RemittanceTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['note', ]
search_fields = ['$note__special_type', ]
class RemittanceViewSet(ReadProtectedModelViewSet): class RemittanceViewSet(ReadProtectedModelViewSet):
@ -50,8 +55,11 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/remittance/ then render it on /api/treasury/remittance/
""" """
queryset = Remittance.objects.order_by("id") queryset = Remittance.objects.order_by('id')
serializer_class = RemittanceSerializer serializer_class = RemittanceSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
search_fields = ['$remittance_type__note__special_type', '$comment', ]
class SogeCreditViewSet(ReadProtectedModelViewSet): class SogeCreditViewSet(ReadProtectedModelViewSet):
@ -60,5 +68,10 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/soge_credit/ then render it on /api/treasury/soge_credit/
""" """
queryset = SogeCredit.objects.order_by("id") queryset = SogeCredit.objects.order_by('id')
serializer_class = SogeCreditSerializer serializer_class = SogeCreditSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',
'$user__note__alias__normalized_name', ]

View File

@ -257,6 +257,7 @@ class SpecialTransactionProxy(models.Model):
Remittance, Remittance,
on_delete=models.PROTECT, on_delete=models.PROTECT,
null=True, null=True,
related_name="transaction_proxies",
verbose_name=_("Remittance"), verbose_name=_("Remittance"),
) )

View File

@ -109,9 +109,6 @@ class SpecialTransactionTable(tables.Table):
'a': {'class': 'btn btn-primary btn-danger'} 'a': {'class': 'btn btn-primary btn-danger'}
}, ) }, )
def render_id(self, record):
return record.specialtransactionproxy.pk
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
@ -8,7 +9,10 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from member.models import Membership, Club from member.models import Membership, Club
from note.models import SpecialTransaction, NoteSpecial, Transaction from note.models import SpecialTransaction, NoteSpecial, Transaction
from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit
from ..api.views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, \
SogeCreditViewSet
from ..models import Invoice, Product, Remittance, RemittanceType, SogeCredit
class TestInvoices(TestCase): class TestInvoices(TestCase):
@ -366,11 +370,8 @@ class TestSogeCredits(TestCase):
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,))) response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
try: self.assertRaises(ValidationError, self.client.post,
self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True)) reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
raise AssertionError("It is not possible to delete the soge credit until the note is not credited.")
except ValidationError:
pass
SpecialTransaction.objects.create( SpecialTransaction.objects.create(
source=NoteSpecial.objects.get(special_type="Carte bancaire"), source=NoteSpecial.objects.get(special_type="Carte bancaire"),
@ -399,3 +400,82 @@ class TestSogeCredits(TestCase):
""" """
response = self.client.get("/api/treasury/soge_credit/") response = self.client.get("/api/treasury/soge_credit/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class TestTreasuryAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.invoice = Invoice.objects.create(
id=1,
object="Object",
description="Description",
name="Me",
address="Earth",
acquitted=False,
)
self.product = Product.objects.create(
invoice=self.invoice,
designation="Product",
quantity=3,
amount=3.14,
)
self.credit = SpecialTransaction.objects.create(
source=NoteSpecial.objects.get(special_type="Chèque"),
destination=self.user.note,
amount=4200,
reason="Credit",
last_name="TOTO",
first_name="Toto",
bank="Société générale",
)
self.remittance = Remittance.objects.create(
remittance_type=RemittanceType.objects.get(),
comment="Test remittance",
closed=False,
)
self.credit.specialtransactionproxy.remittance = self.remittance
self.credit.specialtransactionproxy.save()
self.kfet = Club.objects.get(name="Kfet")
self.bde = self.kfet.parent_club
self.kfet_membership = Membership(
user=self.user,
club=self.kfet,
)
self.kfet_membership._force_renew_parent = True
self.kfet_membership._soge = True
self.kfet_membership.save()
def test_invoice_api(self):
"""
Load Invoice API page and test all filters and permissions
"""
self.check_viewset(InvoiceViewSet, "/api/treasury/invoice/")
def test_product_api(self):
"""
Load Product API page and test all filters and permissions
"""
self.check_viewset(ProductViewSet, "/api/treasury/product/")
def test_remittance_api(self):
"""
Load Remittance API page and test all filters and permissions
"""
self.check_viewset(RemittanceViewSet, "/api/treasury/remittance/")
def test_remittance_type_api(self):
"""
Load RemittanceType API page and test all filters and permissions
"""
self.check_viewset(RemittanceTypeViewSet, "/api/treasury/remittance_type/")
def test_sogecredit_api(self):
"""
Load SogeCredit API page and test all filters and permissions
"""
self.check_viewset(SogeCreditViewSet, "/api/treasury/soge_credit/")

View File

@ -1,7 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \ from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
@ -15,11 +16,14 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/club/ then render it on /api/wei/club/
""" """
queryset = WEIClub.objects.all() queryset = WEIClub.objects.order_by('id')
serializer_class = WEIClubSerializer serializer_class = WEIClubSerializer
filter_backends = [SearchFilter, DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$name', ] filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
filterset_fields = ['name', 'year', ] 'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
'membership_end', ]
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
class BusViewSet(ReadProtectedModelViewSet): class BusViewSet(ReadProtectedModelViewSet):
@ -28,11 +32,11 @@ class BusViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/bus/ then render it on /api/wei/bus/
""" """
queryset = Bus.objects queryset = Bus.objects.order_by('id')
serializer_class = BusSerializer serializer_class = BusSerializer
filter_backends = [SearchFilter, DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$name', ] filterset_fields = ['name', 'wei', 'description', ]
filterset_fields = ['name', 'wei', ] search_fields = ['$name', '$wei__name', '$description', ]
class BusTeamViewSet(ReadProtectedModelViewSet): class BusTeamViewSet(ReadProtectedModelViewSet):
@ -41,11 +45,11 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/team/ then render it on /api/wei/team/
""" """
queryset = BusTeam.objects queryset = BusTeam.objects.order_by('id')
serializer_class = BusTeamSerializer serializer_class = BusTeamSerializer
filter_backends = [SearchFilter, DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$name', ] filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
filterset_fields = ['name', 'bus', 'bus__wei', ] search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
class WEIRoleViewSet(ReadProtectedModelViewSet): class WEIRoleViewSet(ReadProtectedModelViewSet):
@ -54,9 +58,10 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/role/ then render it on /api/wei/role/
""" """
queryset = WEIRole.objects queryset = WEIRole.objects.order_by('id')
serializer_class = WEIRoleSerializer serializer_class = WEIRoleSerializer
filter_backends = [SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'permissions', 'memberships', ]
search_fields = ['$name', ] search_fields = ['$name', ]
@ -66,11 +71,17 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
then render it on /api/wei/registration/ then render it on /api/wei/registration/
""" """
queryset = WEIRegistration.objects queryset = WEIRegistration.objects.order_by('id')
serializer_class = WEIRegistrationSerializer serializer_class = WEIRegistrationSerializer
filter_backends = [SearchFilter, DjangoFilterBackend] filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['$user__username', ] filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
filterset_fields = ['user', 'wei', ] 'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name',
'emergency_contact_phone', ]
search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', '$wei__name',
'$wei__email', '$health_issues', '$emergency_contact_name', '$emergency_contact_phone', ]
class WEIMembershipViewSet(ReadProtectedModelViewSet): class WEIMembershipViewSet(ReadProtectedModelViewSet):
@ -79,8 +90,16 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/membership/ then render it on /api/wei/membership/
""" """
queryset = WEIMembership.objects queryset = WEIMembership.objects.order_by('id')
serializer_class = WEIMembershipSerializer serializer_class = WEIMembershipSerializer
filter_backends = [SearchFilter, DjangoFilterBackend] filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ['$user__username', '$bus__name', '$team__name', ] filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
filterset_fields = ['user', 'club', 'bus', 'team', ] 'club__note__alias__normalized_name', 'user__username', 'user__last_name',
'user__first_name', 'user__email', 'user__note__alias__name',
'user__note__alias__normalized_name', 'date_start', 'date_end', 'fee', 'roles', 'bus',
'bus__name', 'team', 'team__name', 'registration', ]
ordering_fields = ['id', 'date_start', 'date_end', ]
search_fields = ['$club__name', '$club__email', '$club__note__alias__name',
'$club__note__alias__normalized_name', '$user__username', '$user__last_name',
'$user__first_name', '$user__email', '$user__note__alias__name',
'$user__note__alias__normalized_name', '$roles__name', '$bus__name', '$team__name', ]

View File

@ -61,10 +61,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>
{% endif %} {% endif %}
{% if "note.change_alias"|has_perm:club.note.alias_set.first %} {% if "note.change_alias"|has_perm:club.note.alias.first %}
<dt class="col-xl-4"><a <dt class="col-xl-4"><a
href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-8 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd> <dd class="col-xl-8 text-truncate">{{ club.note.alias.all|join:", " }}</dd>
{% endif %} {% endif %}
<dt class="col-xl-4">{% trans 'email'|capfirst %}</dt> <dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>

View File

@ -4,16 +4,19 @@
import subprocess import subprocess
from datetime import timedelta, date from datetime import timedelta, date
from api.tests import TestAPI
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from member.models import Membership from member.models import Membership, Club
from note.models import NoteClub, SpecialTransaction from note.models import NoteClub, SpecialTransaction
from treasury.models import SogeCredit from treasury.models import SogeCredit
from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \
WEIRoleViewSet
from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
@ -524,7 +527,7 @@ class TestWEIRegistration(TestCase):
sess["permission_mask"] = 0 sess["permission_mask"] = 0
sess.save() sess.save()
response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk))) response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 403)
sess["permission_mask"] = 42 sess["permission_mask"] = 42
sess.save() sess.save()
@ -807,3 +810,97 @@ class TestWEISurveyAlgorithm(TestCase):
def test_survey_algorithm(self): def test_survey_algorithm(self):
CurrentSurvey.get_algorithm_class()().run_algorithm() CurrentSurvey.get_algorithm_class()().run_algorithm()
class TestWeiAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.year = timezone.now().year
self.wei = WEIClub.objects.create(
name="Test WEI",
email="gc.wei@example.com",
parent_club_id=2,
membership_fee_paid=12500,
membership_fee_unpaid=5500,
membership_start=date(self.year, 1, 1),
membership_end=date(self.year, 12, 31),
membership_duration=396,
year=self.year,
date_start=date.today() + timedelta(days=2),
date_end=date(self.year, 12, 31),
)
NoteClub.objects.create(club=self.wei)
self.bus = Bus.objects.create(
name="Test Bus",
wei=self.wei,
description="Test Bus",
)
self.team = BusTeam.objects.create(
name="Test Team",
bus=self.bus,
color=0xFFFFFF,
description="Test Team",
)
self.registration = WEIRegistration.objects.create(
user_id=self.user.id,
wei_id=self.wei.id,
soge_credit=True,
caution_check=True,
birth_date=date(2000, 1, 1),
gender="nonbinary",
clothing_cut="male",
clothing_size="XL",
health_issues="I am a bot",
emergency_contact_name="Pikachu",
emergency_contact_phone="+33123456789",
first_year=False,
)
Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
Membership.objects.create(user=self.user, club=Club.objects.get(name="Kfet"))
self.membership = WEIMembership.objects.create(
user=self.user,
club=self.wei,
fee=125,
bus=self.bus,
team=self.team,
registration=self.registration,
)
self.membership.roles.add(WEIRole.objects.last())
self.membership.save()
def test_weiclub_api(self):
"""
Load WEI API page and test all filters and permissions
"""
self.check_viewset(WEIClubViewSet, "/api/wei/club/")
def test_wei_bus_api(self):
"""
Load Bus API page and test all filters and permissions
"""
self.check_viewset(BusViewSet, "/api/wei/bus/")
def test_wei_team_api(self):
"""
Load BusTeam API page and test all filters and permissions
"""
self.check_viewset(BusTeamViewSet, "/api/wei/team/")
def test_weirole_api(self):
"""
Load WEIRole API page and test all filters and permissions
"""
self.check_viewset(WEIRoleViewSet, "/api/wei/role/")
def test_weiregistration_api(self):
"""
Load WEIRegistration API page and test all filters and permissions
"""
self.check_viewset(WEIRegistrationViewSet, "/api/wei/registration/")
def test_weimembership_api(self):
"""
Load WEIMembership API page and test all filters and permissions
"""
self.check_viewset(WEIMembershipViewSet, "/api/wei/membership/")

View File

@ -142,3 +142,17 @@ class TurbolinksMiddleware(object):
location = request.session.pop('_turbolinks_redirect_to') location = request.session.pop('_turbolinks_redirect_to')
response['Turbolinks-Location'] = location response['Turbolinks-Location'] = location
return response return response
class ClacksMiddleware(object):
"""
Add Clacks Overhead header on each response.
See https://www.gnuterrypratchett.com/
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['X-Clacks-Overhead'] = 'GNU Terry Pratchett'
return response

View File

@ -82,6 +82,7 @@ MIDDLEWARE = [
'note_kfet.middlewares.SessionMiddleware', 'note_kfet.middlewares.SessionMiddleware',
'note_kfet.middlewares.LoginByIPMiddleware', 'note_kfet.middlewares.LoginByIPMiddleware',
'note_kfet.middlewares.TurbolinksMiddleware', 'note_kfet.middlewares.TurbolinksMiddleware',
'note_kfet.middlewares.ClacksMiddleware',
] ]
ROOT_URLCONF = 'note_kfet.urls' ROOT_URLCONF = 'note_kfet.urls'

View File

@ -1,12 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# CAS
OPTIONAL_APPS = [ OPTIONAL_APPS = [
# 'debug_toolbar' # 'cas_server',
# 'debug_toolbar',
# 'django_extensions',
] ]
# When a server error occured, send an email to these addresses # When a server error occurred, send an email to these addresses
ADMINS = ( ADMINS = (
('Note Kfet', 'notekfet@example.com'), ('Note Kfet', 'notekfet@example.com'),
) )

View File

@ -39,7 +39,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<script src="{% static "js/konami.js" %}"></script> <script src="{% static "js/konami.js" %}"></script>
{# Translation in javascript files #} {# Translation in javascript files #}
<script src="{% static "js/jsi18n/jsi18n."|add:LANGUAGE_CODE|add:".js" %}"></script> <script src="{% static "js/jsi18n/"|add:LANGUAGE_CODE|add:".js" %}"></script>
{# If extra ressources are needed for a form, load here #} {# If extra ressources are needed for a form, load here #}
{% if form.media %} {% if form.media %}

View File

@ -6,6 +6,9 @@ envlist =
# Ubuntu 20.04 Python # Ubuntu 20.04 Python
py38-django22 py38-django22
# Debian Bullseye Python
py39-django22
linters linters
skipsdist = True skipsdist = True
@ -15,7 +18,7 @@ deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
coverage coverage
commands = commands =
coverage run --omit='*migrations*,apps/scripts*' --source=apps,note_kfet ./manage.py test apps/ coverage run --omit='apps/scripts*,*_example.py,note_kfet/wsgi.py' --source=apps,note_kfet ./manage.py test apps/
coverage report -m coverage report -m
[testenv:linters] [testenv:linters]