Merge branch 'api' into 'beta'

API Filters

See merge request bde/nk20!143
This commit is contained in:
ynerant 2020-12-23 19:02:59 +01:00
commit c8f7986d5a
29 changed files with 815 additions and 159 deletions

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

@ -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

@ -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

@ -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

@ -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]