1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-12-28 02:12:20 +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/
venv/
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
script: tox -e py38-django22
# Debian Bullseye
py39-django22:
stage: test
image: debian:bullseye
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py39-django22
linters:
stage: quality-assurance
image: debian:buster-backports

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

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,
then render it on /api/logs/
"""
queryset = Changelog.objects.all()
queryset = Changelog.objects.order_by('id')
serializer_class = ChangelogSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from .serializers import PermissionSerializer, RoleSerializer
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,
then render it on /api/permission/permission/
"""
queryset = Permission.objects.all()
queryset = Permission.objects.order_by('id')
serializer_class = PermissionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['model', 'type', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
search_fields = ['$model__name', '$query', '$description', ]
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
then render it on /api/permission/roles/
"""
queryset = Role.objects.all()
queryset = Role.objects.order_by('id')
serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['role', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
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
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
from functools import lru_cache
from time import time
@ -38,6 +38,10 @@ def memoize(f):
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:
# Clear cache
collect()

View File

@ -819,7 +819,7 @@
"type": "change",
"mask": 1,
"field": "",
"permanent": false,
"permanent": true,
"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.template.defaultfilters import stringfilter
from django import template
from note.models import Transaction
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend
@ -25,21 +24,6 @@ def not_empty_model_list(model_name):
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
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)
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.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_length', model_list_length)
register.filter('has_perm', has_perm)

View File

@ -78,7 +78,7 @@ class PermissionQueryTestCase(TestCase):
query = instanced.query
model = perm.model.model_class()
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:", perm.query)
if instanced.query:

View File

@ -85,7 +85,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
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.
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,
then render it on /api/treasury/invoice/
"""
queryset = Invoice.objects.order_by("id").all()
queryset = Invoice.objects.order_by('id')
serializer_class = InvoiceSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['bde', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
search_fields = ['$object', '$description', '$name', '$address', ]
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,
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
filter_backends = [SearchFilter]
search_fields = ['$designation', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
search_fields = ['$designation', '$invoice__object', ]
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
then render it on /api/treasury/remittance_type/
"""
queryset = RemittanceType.objects.order_by("id")
queryset = RemittanceType.objects.order_by('id')
serializer_class = RemittanceTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['note', ]
search_fields = ['$note__special_type', ]
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,
then render it on /api/treasury/remittance/
"""
queryset = Remittance.objects.order_by("id")
queryset = Remittance.objects.order_by('id')
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):
@ -60,5 +68,10 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/soge_credit/
"""
queryset = SogeCredit.objects.order_by("id")
queryset = SogeCredit.objects.order_by('id')
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,
on_delete=models.PROTECT,
null=True,
related_name="transaction_proxies",
verbose_name=_("Remittance"),
)

View File

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

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.models import Q
@ -8,7 +9,10 @@ from django.test import TestCase
from django.urls import reverse
from member.models import Membership, Club
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):
@ -366,11 +370,8 @@ class TestSogeCredits(TestCase):
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
self.assertEqual(response.status_code, 200)
try:
self.client.post(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
self.assertRaises(ValidationError, self.client.post,
reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
SpecialTransaction.objects.create(
source=NoteSpecial.objects.get(special_type="Carte bancaire"),
@ -399,3 +400,82 @@ class TestSogeCredits(TestCase):
"""
response = self.client.get("/api/treasury/soge_credit/")
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
# SPDX-License-Identifier: GPL-3.0-or-later
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 .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,
then render it on /api/wei/club/
"""
queryset = WEIClub.objects.all()
queryset = WEIClub.objects.order_by('id')
serializer_class = WEIClubSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'year', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'year', 'date_start', 'date_end', '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 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,
then render it on /api/wei/bus/
"""
queryset = Bus.objects
queryset = Bus.objects.order_by('id')
serializer_class = BusSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'wei', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'wei', 'description', ]
search_fields = ['$name', '$wei__name', '$description', ]
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,
then render it on /api/wei/team/
"""
queryset = BusTeam.objects
queryset = BusTeam.objects.order_by('id')
serializer_class = BusTeamSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'bus', 'bus__wei', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
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,
then render it on /api/wei/role/
"""
queryset = WEIRole.objects
queryset = WEIRole.objects.order_by('id')
serializer_class = WEIRoleSerializer
filter_backends = [SearchFilter]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'permissions', 'memberships', ]
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,
then render it on /api/wei/registration/
"""
queryset = WEIRegistration.objects
queryset = WEIRegistration.objects.order_by('id')
serializer_class = WEIRegistrationSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$user__username', ]
filterset_fields = ['user', 'wei', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
'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):
@ -79,8 +90,16 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/membership/
"""
queryset = WEIMembership.objects
queryset = WEIMembership.objects.order_by('id')
serializer_class = WEIMembershipSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$user__username', '$bus__name', '$team__name', ]
filterset_fields = ['user', 'club', 'bus', 'team', ]
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', '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>
{% 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
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 %}
<dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>

View File

@ -4,16 +4,19 @@
import subprocess
from datetime import timedelta, date
from api.tests import TestAPI
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Q
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from member.models import Membership
from member.models import Membership, Club
from note.models import NoteClub, SpecialTransaction
from treasury.models import SogeCredit
from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \
WEIRoleViewSet
from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
@ -524,7 +527,7 @@ class TestWEIRegistration(TestCase):
sess["permission_mask"] = 0
sess.save()
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.save()
@ -807,3 +810,97 @@ class TestWEISurveyAlgorithm(TestCase):
def test_survey_algorithm(self):
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')
response['Turbolinks-Location'] = location
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.LoginByIPMiddleware',
'note_kfet.middlewares.TurbolinksMiddleware',
'note_kfet.middlewares.ClacksMiddleware',
]
ROOT_URLCONF = 'note_kfet.urls'

View File

@ -1,12 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
# CAS
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 = (
('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>
{# 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 form.media %}

View File

@ -6,6 +6,9 @@ envlist =
# Ubuntu 20.04 Python
py38-django22
# Debian Bullseye Python
py39-django22
linters
skipsdist = True
@ -15,7 +18,7 @@ deps =
-r{toxinidir}/requirements.txt
coverage
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
[testenv:linters]