mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Merge branch 'beta' into docs
This commit is contained in:
		| @@ -39,6 +39,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 | ||||||
|   | |||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										237
									
								
								apps/api/tests.py
									
									
									
									
									
										Normal 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/") | ||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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"), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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/") | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
| @@ -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/") | ||||||
|   | |||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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() | ||||||
|   | |||||||
| @@ -819,7 +819,7 @@ | |||||||
| 			"type": "change", | 			"type": "change", | ||||||
| 			"mask": 1, | 			"mask": 1, | ||||||
| 			"field": "", | 			"field": "", | ||||||
| 			"permanent": false, | 			"permanent": true, | ||||||
| 			"description": "Modifier son profil" | 			"description": "Modifier son profil" | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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"), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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/") | ||||||
|   | |||||||
| @@ -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', ] | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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/") | ||||||
|   | |||||||
| @@ -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'), | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -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] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user