diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5655c4b937920f37ff5a89b3ee713a76e63ccb9a..97110ecdfdd1a0a7dd8d642c147d0a617d1dcd7e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,6 +39,21 @@ py38-django22:
         python3-bs4 python3-setuptools tox texlive-xetex
   script: tox -e py38-django22
 
+# Debian Bullseye
+py39-django22:
+  stage: test
+  image: debian:bullseye
+  before_script:
+    - >
+        apt-get update &&
+        apt-get install --no-install-recommends -y
+        python3-django python3-django-crispy-forms
+        python3-django-extensions python3-django-filters python3-django-polymorphic
+        python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
+        python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
+        python3-bs4 python3-setuptools tox texlive-xetex
+  script: tox -e py39-django22
+
 linters:
   stage: quality-assurance
   image: debian:buster-backports
diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py
index 8d555b3bd29ad4a2aa56031b76bd32d258d83961..998c3ce4e1e52b2dd6514b35f053b6eecdc7d0dc 100644
--- a/apps/activity/api/views.py
+++ b/apps/activity/api/views.py
@@ -15,10 +15,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
     then render it on /api/activity/type/
     """
-    queryset = ActivityType.objects.all()
+    queryset = ActivityType.objects.order_by('id')
     serializer_class = ActivityTypeSerializer
     filter_backends = [DjangoFilterBackend]
-    filterset_fields = ['name', 'can_invite', ]
+    filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ]
 
 
 class ActivityViewSet(ReadProtectedModelViewSet):
@@ -27,10 +27,16 @@ class ActivityViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
     then render it on /api/activity/activity/
     """
-    queryset = Activity.objects.all()
+    queryset = Activity.objects.order_by('id')
     serializer_class = ActivitySerializer
-    filter_backends = [DjangoFilterBackend]
-    filterset_fields = ['name', 'description', 'activity_type', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
+                        'date_start', 'date_end', 'valid', 'open', ]
+    search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
+                     '$creater__email', '$creater__note__alias__name', '$creater__note__alias__normalized_name',
+                     '$organizer__name', '$organizer__email', '$organizer__note__alias__name',
+                     '$organizer__note__alias__normalized_name', '$attendees_club__name', '$attendees_club__email',
+                     '$attendees_club__note__alias__name', '$attendees_club__note__alias__normalized_name', ]
 
 
 class GuestViewSet(ReadProtectedModelViewSet):
@@ -39,10 +45,13 @@ class GuestViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
     then render it on /api/activity/guest/
     """
-    queryset = Guest.objects.all()
+    queryset = Guest.objects.order_by('id')
     serializer_class = GuestSerializer
-    filter_backends = [SearchFilter]
-    search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
+                        'inviter__alias__normalized_name', ]
+    search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
+                     '$inviter__alias__normalized_name', ]
 
 
 class EntryViewSet(ReadProtectedModelViewSet):
@@ -51,7 +60,9 @@ class EntryViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,
     then render it on /api/activity/entry/
     """
-    queryset = Entry.objects.all()
+    queryset = Entry.objects.order_by('id')
     serializer_class = EntrySerializer
-    filter_backends = [SearchFilter]
-    search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['activity', 'time', 'note', 'guest', ]
+    search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
+                     '$guest__last_name', '$guest__first_name', ]
diff --git a/apps/activity/tests/test_activities.py b/apps/activity/tests/test_activities.py
index 99eb2ffb6108d9ef907860d4adbfd96e9a51a2aa..15635a6b7ef37fd98e410b3b040a9873ee17a73b 100644
--- a/apps/activity/tests/test_activities.py
+++ b/apps/activity/tests/test_activities.py
@@ -3,13 +3,16 @@
 
 from datetime import timedelta
 
+from api.tests import TestAPI
 from django.contrib.auth.models import User
 from django.test import TestCase
 from django.urls import reverse
 from django.utils import timezone
-from activity.models import Activity, ActivityType, Guest, Entry
 from member.models import Club
 
+from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
+from ..models import Activity, ActivityType, Guest, Entry
+
 
 class TestActivities(TestCase):
     """
@@ -173,3 +176,58 @@ class TestActivities(TestCase):
         """
         response = self.client.get(reverse("activity:calendar_ics"))
         self.assertEqual(response.status_code, 200)
+
+
+class TestActivityAPI(TestAPI):
+    def setUp(self) -> None:
+        super().setUp()
+
+        self.activity = Activity.objects.create(
+            name="Activity",
+            description="This is a test activity\non two very very long lines\nbecause this is very important.",
+            location="Earth",
+            activity_type=ActivityType.objects.get(name="Pot"),
+            creater=self.user,
+            organizer=Club.objects.get(name="Kfet"),
+            attendees_club=Club.objects.get(name="Kfet"),
+            date_start=timezone.now(),
+            date_end=timezone.now() + timedelta(days=2),
+            valid=True,
+        )
+
+        self.guest = Guest.objects.create(
+            activity=self.activity,
+            inviter=self.user.note,
+            last_name="GUEST",
+            first_name="Guest",
+        )
+
+        self.entry = Entry.objects.create(
+            activity=self.activity,
+            note=self.user.note,
+            guest=self.guest,
+        )
+
+    def test_activity_api(self):
+        """
+        Load Activity API page and test all filters and permissions
+        """
+        self.check_viewset(ActivityViewSet, "/api/activity/activity/")
+
+    def test_activity_type_api(self):
+        """
+        Load ActivityType API page and test all filters and permissions
+        """
+        self.check_viewset(ActivityTypeViewSet, "/api/activity/type/")
+
+    def test_entry_api(self):
+        """
+        Load Entry API page and test all filters and permissions
+        """
+        self.check_viewset(EntryViewSet, "/api/activity/entry/")
+
+    def test_guest_api(self):
+        """
+        Load Guest API page and test all filters and permissions
+        """
+        self.check_viewset(GuestViewSet, "/api/activity/guest/")
diff --git a/apps/api/tests.py b/apps/api/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..203e592a57bb50d999ceeb35ec40a251f82c963c
--- /dev/null
+++ b/apps/api/tests.py
@@ -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/")
diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py
index fa2fc941054396c174cd7f0a07c98a0b6cb49e16..88ee7f01f33d03dfe183304f55e723d2a3cc1fbf 100644
--- a/apps/api/viewsets.py
+++ b/apps/api/viewsets.py
@@ -6,6 +6,7 @@ from django_filters.rest_framework import DjangoFilterBackend
 from django.db.models import Q
 from django.conf import settings
 from django.contrib.auth.models import User
+from rest_framework.filters import SearchFilter
 from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
 from permission.backends import PermissionBackend
 from note_kfet.middlewares import get_current_session
@@ -48,12 +49,13 @@ class UserViewSet(ReadProtectedModelViewSet):
     """
     REST API View set.
     The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
-    then render it on /api/users/
+    then render it on /api/user/
     """
-    queryset = User.objects.all()
+    queryset = User.objects
     serializer_class = UserSerializer
     filter_backends = [DjangoFilterBackend]
-    filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
+    filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active',
+                        'note__alias__name', 'note__alias__normalized_name', ]
 
     def get_queryset(self):
         queryset = super().get_queryset()
@@ -106,7 +108,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
     """
     REST API View set.
     The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
-    then render it on /api/users/
+    then render it on /api/models/
     """
-    queryset = ContentType.objects.all()
+    queryset = ContentType.objects.order_by('id')
     serializer_class = ContentTypeSerializer
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['id', 'app_label', 'model', ]
+    search_fields = ['$app_label', '$model', ]
diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py
index 4160d609ff45179653b3fcde9609ee6513565585..5f28b71c83ec62485e376f42c34a3ba3d0a7292d 100644
--- a/apps/logs/api/views.py
+++ b/apps/logs/api/views.py
@@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
     The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
     then render it on /api/logs/
     """
-    queryset = Changelog.objects.all()
+    queryset = Changelog.objects.order_by('id')
     serializer_class = ChangelogSerializer
     filter_backends = [DjangoFilterBackend, OrderingFilter]
     filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
diff --git a/apps/member/api/views.py b/apps/member/api/views.py
index 3dc07fe142a8b834cb8b76dc54c6bbfffeec14c8..69943c7f92edfb9178e72455579665cbe0dbf3de 100644
--- a/apps/member/api/views.py
+++ b/apps/member/api/views.py
@@ -1,7 +1,8 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from rest_framework.filters import SearchFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.filters import OrderingFilter, SearchFilter
 from api.viewsets import ReadProtectedModelViewSet
 
 from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
@@ -14,8 +15,15 @@ class ProfileViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
     then render it on /api/members/profile/
     """
-    queryset = Profile.objects.all()
+    queryset = Profile.objects.order_by('id')
     serializer_class = ProfileSerializer
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
+                        'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
+                        'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
+                        'ml_art_registration', 'report_frequency', 'email_confirmed', 'registration_valid', ]
+    search_fields = ['$user__first_name', '$user__last_name', '$user__username', '$user__email',
+                     '$user__note__alias__name', '$user__note__alias__normalized_name', ]
 
 
 class ClubViewSet(ReadProtectedModelViewSet):
@@ -24,10 +32,13 @@ class ClubViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
     then render it on /api/members/club/
     """
-    queryset = Club.objects.all()
+    queryset = Club.objects.order_by('id')
     serializer_class = ClubSerializer
-    filter_backends = [SearchFilter]
-    search_fields = ['$name', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
+                        'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
+                        'membership_duration', 'membership_start', 'membership_end', ]
+    search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
 
 
 class MembershipViewSet(ReadProtectedModelViewSet):
@@ -36,5 +47,14 @@ class MembershipViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
     then render it on /api/members/membership/
     """
-    queryset = Membership.objects.all()
+    queryset = Membership.objects.order_by('id')
     serializer_class = MembershipSerializer
+    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
+    filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
+                        'user__username', 'user__last_name', 'user__first_name', 'user__email',
+                        'user__note__alias__name', 'user__note__alias__normalized_name',
+                        'date_start', 'date_end', 'fee', 'roles', ]
+    ordering_fields = ['id', 'date_start', 'date_end', ]
+    search_fields = ['$club__name', '$club__email', '$club__note__alias__name', '$club__note__alias__normalized_name',
+                     '$user__username', '$user__last_name', '$user__first_name', '$user__email',
+                     '$user__note__alias__name', '$user__note__alias__normalized_name', '$roles__name', ]
diff --git a/apps/member/models.py b/apps/member/models.py
index ff8f2b88082bdfcb0af2fa617f35a85e69d292c6..e5fa23ef4a2df4372d1a2f0965ce58428072a31a 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -313,6 +313,7 @@ class Membership(models.Model):
 
     roles = models.ManyToManyField(
         "permission.Role",
+        related_name="memberships",
         verbose_name=_("roles"),
     )
 
diff --git a/apps/member/templates/member/includes/club_info.html b/apps/member/templates/member/includes/club_info.html
index 51f0ef0333fe344a48310d04c5618c8eed07cb93..0efc71d09526217f523334cb281e23205a6beeeb 100644
--- a/apps/member/templates/member/includes/club_info.html
+++ b/apps/member/templates/member/includes/club_info.html
@@ -48,7 +48,7 @@
     <dd class="col-xl-6">
         <a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
             <i class="fa fa-edit"></i>
-            {% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }})
+            {% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
         </a>
     </dd>
 
diff --git a/apps/member/templates/member/includes/profile_info.html b/apps/member/templates/member/includes/profile_info.html
index e008ec6a378b0485beb11ecdce8ef7a0e0d2975f..e1941d23221e2bbe880b27935844d85a49b6588e 100644
--- a/apps/member/templates/member/includes/profile_info.html
+++ b/apps/member/templates/member/includes/profile_info.html
@@ -21,7 +21,7 @@
     <dd class="col-xl-6">
         <a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
             <i class="fa fa-edit"></i>
-            {% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }})
+            {% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
         </a>
     </dd>
 
diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py
index 80c214f00f42a4e515d00bdfdd52f8d546946d54..1bbae1c7540028fe5fd5166efe5f427369190976 100644
--- a/apps/member/tests/test_memberships.py
+++ b/apps/member/tests/test_memberships.py
@@ -5,17 +5,20 @@ import hashlib
 import os
 from datetime import date, timedelta
 
+from api.tests import TestAPI
 from django.contrib.auth.models import User
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.db.models import Q
 from django.test import TestCase
 from django.urls import reverse
 from django.utils import timezone
-from member.models import Club, Membership, Profile
 from note.models import Alias, NoteSpecial
 from permission.models import Role
 from treasury.models import SogeCredit
 
+from ..api.views import ClubViewSet, MembershipViewSet, ProfileViewSet
+from ..models import Club, Membership, Profile
+
 """
 Create some users and clubs and test that all pages are rendering properly
 and that memberships are working.
@@ -403,3 +406,46 @@ class TestMemberships(TestCase):
         self.user.password = "custom_nk15$1$" + salt + "|" + hashed
         self.user.save()
         self.assertTrue(self.user.check_password(password))
+
+
+class TestMemberAPI(TestAPI):
+    def setUp(self) -> None:
+        super().setUp()
+
+        self.user.profile.registration_valid = True
+        self.user.profile.email_confirmed = True
+        self.user.profile.phone_number = "0600000000"
+        self.user.profile.section = "1A0"
+        self.user.profile.department = "A0"
+        self.user.profile.address = "Earth"
+        self.user.profile.save()
+
+        self.club = Club.objects.create(
+            name="totoclub",
+            parent_club=Club.objects.get(name="BDE"),
+            membership_start=date(year=1970, month=1, day=1),
+            membership_end=date(year=2040, month=1, day=1),
+            membership_duration=365 * 10,
+        )
+        self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
+        self.membership = Membership.objects.create(user=self.user, club=self.club)
+        self.membership.roles.add(Role.objects.get(name="Bureau de club"))
+        self.membership.save()
+
+    def test_club_api(self):
+        """
+        Load Club API page and test all filters and permissions
+        """
+        self.check_viewset(ClubViewSet, "/api/members/club/")
+
+    def test_profile_api(self):
+        """
+        Load Profile API page and test all filters and permissions
+        """
+        self.check_viewset(ProfileViewSet, "/api/members/profile/")
+
+    def test_membership_api(self):
+        """
+        Load Membership API page and test all filters and permissions
+        """
+        self.check_viewset(MembershipViewSet, "/api/members/membership/")
diff --git a/apps/member/views.py b/apps/member/views.py
index 73569c89cbfeb684933900d8124884cce8def507..2ddb35915b683d6ee069e50ea84adcc3e5acd7e6 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -256,7 +256,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context = super().get_context_data(**kwargs)
         note = context['object'].note
         context["aliases"] = AliasTable(
-            note.alias_set.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
+            note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
         context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
             note=context["object"].note,
             name="",
@@ -458,7 +458,7 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         note = context['object'].note
-        context["aliases"] = AliasTable(note.alias_set.filter(
+        context["aliases"] = AliasTable(note.alias.filter(
             PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
         context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
             note=context["object"].note,
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index ae8bc94e126f90e89dd896c75f481b2e886f6fef..517450c7eb51f1dee6eedc156fa4720d627ba737 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -15,29 +15,37 @@ from permission.backends import PermissionBackend
 
 from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
     TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
-from ..models.notes import Note, Alias
+from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
 from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
 
 
 class NotePolymorphicViewSet(ReadProtectedModelViewSet):
     """
     REST API View set.
-    The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
+    The djangorestframework plugin will get all `Note` objects (with polymorhism),
+    serialize it to JSON with the given serializer,
     then render it on /api/note/note/
     """
-    queryset = Note.objects.all()
+    queryset = Note.objects.order_by('id')
     serializer_class = NotePolymorphicSerializer
     filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
-    filterset_fields = ['polymorphic_ctype', 'is_active', ]
-    search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
-    ordering_fields = ['alias__name', 'alias__normalized_name']
+    filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
+    search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
+                     '$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
+                     '$noteuser__user__email', '$noteclub__club__email', ]
+    ordering_fields = ['alias__name', 'alias__normalized_name', 'balance', 'created_at', ]
 
     def get_queryset(self):
         """
         Parse query and apply filters.
         :return: The filtered set of requested notes
         """
-        queryset = super().get_queryset().distinct()
+        user = self.request.user
+        get_current_session().setdefault("permission_mask", 42)
+        queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
+                                        | PermissionBackend.filter_queryset(user, NoteUser, "view")
+                                        | PermissionBackend.filter_queryset(user, NoteClub, "view")
+                                        | PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
 
         alias = self.request.query_params.get("alias", ".*")
         queryset = queryset.filter(
@@ -55,12 +63,12 @@ class AliasViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
     then render it on /api/aliases/
     """
-    queryset = Alias.objects.all()
+    queryset = Alias.objects
     serializer_class = AliasSerializer
     filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
     search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
-    filterset_fields = ['note']
-    ordering_fields = ['name', 'normalized_name']
+    filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
+    ordering_fields = ['name', 'normalized_name', ]
 
     def get_serializer_class(self):
         serializer_class = self.serializer_class
@@ -106,12 +114,12 @@ class AliasViewSet(ReadProtectedModelViewSet):
 
 
 class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
-    queryset = Alias.objects.all()
+    queryset = Alias.objects
     serializer_class = ConsumerSerializer
     filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
     search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
-    filterset_fields = ['note']
-    ordering_fields = ['name', 'normalized_name']
+    filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
+    ordering_fields = ['name', 'normalized_name', ]
 
     def get_queryset(self):
         """
@@ -157,10 +165,11 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
     then render it on /api/note/transaction/category/
     """
-    queryset = TemplateCategory.objects.order_by("name").all()
+    queryset = TemplateCategory.objects.order_by('name')
     serializer_class = TemplateCategorySerializer
-    filter_backends = [SearchFilter]
-    search_fields = ['$name', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'templates', 'templates__name']
+    search_fields = ['$name', '$templates__name', ]
 
 
 class TransactionTemplateViewSet(viewsets.ModelViewSet):
@@ -169,11 +178,12 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
     The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
     then render it on /api/note/transaction/template/
     """
-    queryset = TransactionTemplate.objects.order_by("name").all()
+    queryset = TransactionTemplate.objects.order_by('name')
     serializer_class = TransactionTemplateSerializer
-    filter_backends = [SearchFilter, DjangoFilterBackend]
-    filterset_fields = ['name', 'amount', 'display', 'category', ]
-    search_fields = ['$name', ]
+    filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
+    filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
+    search_fields = ['$name', '$category__name', ]
+    ordering_fields = ['amount', ]
 
 
 class TransactionViewSet(ReadProtectedModelViewSet):
@@ -182,13 +192,17 @@ class TransactionViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
     then render it on /api/note/transaction/transaction/
     """
-    queryset = Transaction.objects.order_by("-created_at").all()
+    queryset = Transaction.objects.order_by('-created_at')
     serializer_class = TransactionPolymorphicSerializer
     filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
-    filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity",
-                        "polymorphic_ctype", "amount", "created_at", ]
-    search_fields = ['$reason', ]
-    ordering_fields = ['created_at', 'amount']
+    filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
+                        'destination', 'destination_alias', 'destination__alias__name',
+                        'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
+                        'created_at', 'valid', 'invalidity_reason', ]
+    search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name',
+                     '$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name',
+                     '$invalidity_reason', ]
+    ordering_fields = ['created_at', 'amount', ]
 
     def get_queryset(self):
         user = self.request.user
diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py
index c649dbc9c9579494c63d79ff0743234b65d24e33..7b034d58c3a943ef4a6cab96bada0c8e1f53e4f9 100644
--- a/apps/note/models/notes.py
+++ b/apps/note/models/notes.py
@@ -248,6 +248,7 @@ class Alias(models.Model):
     note = models.ForeignKey(
         Note,
         on_delete=models.PROTECT,
+        related_name="alias",
     )
 
     class Meta:
diff --git a/apps/note/tests/test_transactions.py b/apps/note/tests/test_transactions.py
index 7192d8edee0f4ba6e85714db22d367e761ec2192..0626c45368efaada9d8bb3d6ff990ae86858454f 100644
--- a/apps/note/tests/test_transactions.py
+++ b/apps/note/tests/test_transactions.py
@@ -1,15 +1,20 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+from api.tests import TestAPI
+from member.models import Club, Membership
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.urls import reverse
-from member.models import Club, Membership
-from note.models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
-    MembershipTransaction, SpecialTransaction, NoteSpecial, Alias
+from django.utils import timezone
 from permission.models import Role
 
+from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
+    TransactionTemplateViewSet, TransactionViewSet
+from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
+    MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
+
 
 class TestTransactions(TestCase):
     fixtures = ('initial', )
@@ -297,8 +302,8 @@ class TestTransactions(TestCase):
 
     def test_render_search_transactions(self):
         response = self.client.get(reverse("note:transactions", args=(self.user.note.pk,)), data=dict(
-            source=self.second_user.note.alias_set.first().id,
-            destination=self.user.note.alias_set.first().id,
+            source=self.second_user.note.alias.first().id,
+            destination=self.user.note.alias.first().id,
             type=[ContentType.objects.get_for_model(Transaction).id],
             reason="test",
             valid=True,
@@ -363,3 +368,69 @@ class TestTransactions(TestCase):
         self.assertTrue(Alias.objects.filter(name="test_updated_alias").exists())
         response = self.client.delete("/api/note/alias/" + str(alias.pk) + "/")
         self.assertEqual(response.status_code, 204)
+
+
+class TestNoteAPI(TestAPI):
+    def setUp(self) -> None:
+        super().setUp()
+
+        membership = Membership.objects.create(club=Club.objects.get(name="BDE"), user=self.user)
+        membership.roles.add(Role.objects.get(name="Respo info"))
+        membership.save()
+        Membership.objects.create(club=Club.objects.get(name="Kfet"), user=self.user)
+        self.user.note.last_negative = timezone.now()
+        self.user.note.save()
+
+        self.transaction = Transaction.objects.create(
+            source=Note.objects.first(),
+            destination=self.user.note,
+            amount=4200,
+            reason="Test transaction",
+        )
+        self.user.note.refresh_from_db()
+        Alias.objects.create(note=self.user.note, name="I am a ¢omplex alias")
+
+        self.category = TemplateCategory.objects.create(name="Test")
+        self.template = TransactionTemplate.objects.create(
+            name="Test",
+            destination=Club.objects.get(name="BDE").note,
+            category=self.category,
+            amount=100,
+            description="Test template",
+        )
+
+    def test_alias_api(self):
+        """
+        Load Alias API page and test all filters and permissions
+        """
+        self.check_viewset(AliasViewSet, "/api/note/alias/")
+
+    def test_consumer_api(self):
+        """
+        Load Consumer API page and test all filters and permissions
+        """
+        self.check_viewset(ConsumerViewSet, "/api/note/consumer/")
+
+    def test_note_api(self):
+        """
+        Load Note API page and test all filters and permissions
+        """
+        self.check_viewset(NotePolymorphicViewSet, "/api/note/note/")
+
+    def test_template_category_api(self):
+        """
+        Load TemplateCategory API page and test all filters and permissions
+        """
+        self.check_viewset(TemplateCategoryViewSet, "/api/note/transaction/category/")
+
+    def test_transaction_template_api(self):
+        """
+        Load TemplateTemplate API page and test all filters and permissions
+        """
+        self.check_viewset(TransactionTemplateViewSet, "/api/note/transaction/template/")
+
+    def test_transaction_api(self):
+        """
+        Load Transaction API page and test all filters and permissions
+        """
+        self.check_viewset(TransactionViewSet, "/api/note/transaction/transaction/")
diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py
index 1ec67aa36d52208055867d98751cf696111582d5..10fd19e3bc0a9e549df85e8a4afcc1816b51b034 100644
--- a/apps/permission/api/views.py
+++ b/apps/permission/api/views.py
@@ -1,8 +1,9 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from django_filters.rest_framework import DjangoFilterBackend
 from api.viewsets import ReadOnlyProtectedModelViewSet
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.filters import SearchFilter
 
 from .serializers import PermissionSerializer, RoleSerializer
 from ..models import Permission, Role
@@ -14,10 +15,11 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
     The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer,
     then render it on /api/permission/permission/
     """
-    queryset = Permission.objects.all()
+    queryset = Permission.objects.order_by('id')
     serializer_class = PermissionSerializer
-    filter_backends = [DjangoFilterBackend]
-    filterset_fields = ['model', 'type', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
+    search_fields = ['$model__name', '$query', '$description', ]
 
 
 class RoleViewSet(ReadOnlyProtectedModelViewSet):
@@ -26,7 +28,8 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
     The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
     then render it on /api/permission/roles/
     """
-    queryset = Role.objects.all()
+    queryset = Role.objects.order_by('id')
     serializer_class = RoleSerializer
-    filter_backends = [DjangoFilterBackend]
-    filterset_fields = ['role', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
+    SearchFilter = ['$name', '$for_club__name', ]
diff --git a/apps/permission/decorators.py b/apps/permission/decorators.py
index 8ab3569791a5f4085c6323bae4432e0dd851558f..11edac430d5e182efbb93d8c592bdd5a65dc5ec1 100644
--- a/apps/permission/decorators.py
+++ b/apps/permission/decorators.py
@@ -1,6 +1,6 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
-
+import sys
 from functools import lru_cache
 from time import time
 
@@ -38,6 +38,10 @@ def memoize(f):
 
         nonlocal last_collect
 
+        if "test" in sys.argv:
+            # In a test environment, don't memoize permissions
+            return f(*args, **kwargs)
+
         if time() - last_collect > 60:
             # Clear cache
             collect()
diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json
index 2f3d064a2fbccacf54639875e2c0e9fa73f8b217..a7e07d89ee84f6393fbd02f7cf6817931e75a78d 100644
--- a/apps/permission/fixtures/initial.json
+++ b/apps/permission/fixtures/initial.json
@@ -819,7 +819,7 @@
 			"type": "change",
 			"mask": 1,
 			"field": "",
-			"permanent": false,
+			"permanent": true,
 			"description": "Modifier son profil"
 		}
 	},
diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py
index 335721a1f683f9bd5a519473b616215eabf0d5d8..7841f4005dd0c833f2bd57b9dbbf48a1c488f0d1 100644
--- a/apps/permission/templatetags/perms.py
+++ b/apps/permission/templatetags/perms.py
@@ -5,7 +5,6 @@ from django.contrib.auth.models import AnonymousUser
 from django.contrib.contenttypes.models import ContentType
 from django.template.defaultfilters import stringfilter
 from django import template
-from note.models import Transaction
 from note_kfet.middlewares import get_current_authenticated_user, get_current_session
 from permission.backends import PermissionBackend
 
@@ -25,21 +24,6 @@ def not_empty_model_list(model_name):
     return qs.exists()
 
 
-@stringfilter
-def not_empty_model_change_list(model_name):
-    """
-    Return True if and only if the current user has right to change any object of the given model.
-    """
-    user = get_current_authenticated_user()
-    session = get_current_session()
-    if user is None or isinstance(user, AnonymousUser):
-        return False
-    elif user.is_superuser and session.get("permission_mask", -1) >= 42:
-        return True
-    qs = model_list(model_name, "change")
-    return qs.exists()
-
-
 @stringfilter
 def model_list(model_name, t="view", fetch=True):
     """
@@ -68,33 +52,8 @@ def has_perm(perm, obj):
     return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
 
 
-def can_create_transaction():
-    """
-    :return: True iff the authenticated user can create a transaction.
-    """
-    user = get_current_authenticated_user()
-    session = get_current_session()
-    if user is None or isinstance(user, AnonymousUser):
-        return False
-    elif user.is_superuser and session.get("permission_mask", -1) >= 42:
-        return True
-    if session.get("can_create_transaction", None):
-        return session.get("can_create_transaction", None) == 1
-
-    empty_transaction = Transaction(
-        source=user.note,
-        destination=user.note,
-        quantity=1,
-        amount=0,
-        reason="Check permissions",
-    )
-    session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction)
-    return session.get("can_create_transaction") == 1
-
-
 register = template.Library()
 register.filter('not_empty_model_list', not_empty_model_list)
-register.filter('not_empty_model_change_list', not_empty_model_change_list)
 register.filter('model_list', model_list)
 register.filter('model_list_length', model_list_length)
 register.filter('has_perm', has_perm)
diff --git a/apps/permission/tests/test_permission_queries.py b/apps/permission/tests/test_permission_queries.py
index fdd530a5c747df181f523f108a0dea754ecd2402..95b8b7b10ac3e5ec6452c577810ae0df0af55f4a 100644
--- a/apps/permission/tests/test_permission_queries.py
+++ b/apps/permission/tests/test_permission_queries.py
@@ -78,7 +78,7 @@ class PermissionQueryTestCase(TestCase):
                 query = instanced.query
                 model = perm.model.model_class()
                 model.objects.filter(query).all()
-            except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError):
+            except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError):  # pragma: no cover
                 print("Query error for permission", perm)
                 print("Query:", perm.query)
                 if instanced.query:
diff --git a/apps/permission/views.py b/apps/permission/views.py
index d77133d627e103b515bc3a1ea6a5c6c70ff9bbad..9ff9b50d629996dd017493a1581c988edb93bdff 100644
--- a/apps/permission/views.py
+++ b/apps/permission/views.py
@@ -85,7 +85,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
     If not, a 403 error is displayed.
     """
 
-    def get_sample_object(self):
+    def get_sample_object(self):  # pragma: no cover
         """
         return a sample instance of the Model.
         It should be valid (can be stored properly in database), but must not collide with existing data.
diff --git a/apps/treasury/api/views.py b/apps/treasury/api/views.py
index 82a0ed1e72815b585045586a25d87b465c13718a..b0a47e0931e9b9a364b1885e54404dfccf53453d 100644
--- a/apps/treasury/api/views.py
+++ b/apps/treasury/api/views.py
@@ -16,10 +16,11 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
     then render it on /api/treasury/invoice/
     """
-    queryset = Invoice.objects.order_by("id").all()
+    queryset = Invoice.objects.order_by('id')
     serializer_class = InvoiceSerializer
-    filter_backends = [DjangoFilterBackend]
-    filterset_fields = ['bde', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
+    search_fields = ['$object', '$description', '$name', '$address', ]
 
 
 class ProductViewSet(ReadProtectedModelViewSet):
@@ -28,10 +29,11 @@ class ProductViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
     then render it on /api/treasury/product/
     """
-    queryset = Product.objects.order_by("invoice_id", "id").all()
+    queryset = Product.objects.order_by('invoice_id', 'id')
     serializer_class = ProductSerializer
-    filter_backends = [SearchFilter]
-    search_fields = ['$designation', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
+    search_fields = ['$designation', '$invoice__object', ]
 
 
 class RemittanceTypeViewSet(ReadProtectedModelViewSet):
@@ -40,8 +42,11 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
     then render it on /api/treasury/remittance_type/
     """
-    queryset = RemittanceType.objects.order_by("id")
+    queryset = RemittanceType.objects.order_by('id')
     serializer_class = RemittanceTypeSerializer
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['note', ]
+    search_fields = ['$note__special_type', ]
 
 
 class RemittanceViewSet(ReadProtectedModelViewSet):
@@ -50,8 +55,11 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
     then render it on /api/treasury/remittance/
     """
-    queryset = Remittance.objects.order_by("id")
+    queryset = Remittance.objects.order_by('id')
     serializer_class = RemittanceSerializer
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
+    search_fields = ['$remittance_type__note__special_type', '$comment', ]
 
 
 class SogeCreditViewSet(ReadProtectedModelViewSet):
@@ -60,5 +68,10 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
     then render it on /api/treasury/soge_credit/
     """
-    queryset = SogeCredit.objects.order_by("id")
+    queryset = SogeCredit.objects.order_by('id')
     serializer_class = SogeCreditSerializer
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
+                        'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
+    search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',
+                     '$user__note__alias__normalized_name', ]
diff --git a/apps/treasury/models.py b/apps/treasury/models.py
index b1ca407f0fbe7ae458fbbd74f162f5f419a11eaf..7782ebec77ca7218109c7aedc27b637045f33acc 100644
--- a/apps/treasury/models.py
+++ b/apps/treasury/models.py
@@ -257,6 +257,7 @@ class SpecialTransactionProxy(models.Model):
         Remittance,
         on_delete=models.PROTECT,
         null=True,
+        related_name="transaction_proxies",
         verbose_name=_("Remittance"),
     )
 
diff --git a/apps/treasury/tables.py b/apps/treasury/tables.py
index 44415061df1031c6af3a26eda6b57561cf156e2a..77d39a0e8fbab475f1e05736def06209b2bde9b1 100644
--- a/apps/treasury/tables.py
+++ b/apps/treasury/tables.py
@@ -109,9 +109,6 @@ class SpecialTransactionTable(tables.Table):
                                               'a': {'class': 'btn btn-primary btn-danger'}
                                           }, )
 
-    def render_id(self, record):
-        return record.specialtransactionproxy.pk
-
     def render_amount(self, value):
         return pretty_money(value)
 
diff --git a/apps/treasury/tests/test_treasury.py b/apps/treasury/tests/test_treasury.py
index 505453b99ef0d8ef7e63eac7c07b0f2673f65d15..98fb22499f0ddd0ec4025f7010e9c42102505dc6 100644
--- a/apps/treasury/tests/test_treasury.py
+++ b/apps/treasury/tests/test_treasury.py
@@ -1,6 +1,7 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+from api.tests import TestAPI
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.db.models import Q
@@ -8,7 +9,10 @@ from django.test import TestCase
 from django.urls import reverse
 from member.models import Membership, Club
 from note.models import SpecialTransaction, NoteSpecial, Transaction
-from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit
+
+from ..api.views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, \
+    SogeCreditViewSet
+from ..models import Invoice, Product, Remittance, RemittanceType, SogeCredit
 
 
 class TestInvoices(TestCase):
@@ -366,11 +370,8 @@ class TestSogeCredits(TestCase):
         response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
         self.assertEqual(response.status_code, 200)
 
-        try:
-            self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
-            raise AssertionError("It is not possible to delete the soge credit until the note is not credited.")
-        except ValidationError:
-            pass
+        self.assertRaises(ValidationError, self.client.post,
+                          reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
 
         SpecialTransaction.objects.create(
             source=NoteSpecial.objects.get(special_type="Carte bancaire"),
@@ -399,3 +400,82 @@ class TestSogeCredits(TestCase):
         """
         response = self.client.get("/api/treasury/soge_credit/")
         self.assertEqual(response.status_code, 200)
+
+
+class TestTreasuryAPI(TestAPI):
+    def setUp(self) -> None:
+        super().setUp()
+
+        self.invoice = Invoice.objects.create(
+            id=1,
+            object="Object",
+            description="Description",
+            name="Me",
+            address="Earth",
+            acquitted=False,
+        )
+        self.product = Product.objects.create(
+            invoice=self.invoice,
+            designation="Product",
+            quantity=3,
+            amount=3.14,
+        )
+
+        self.credit = SpecialTransaction.objects.create(
+            source=NoteSpecial.objects.get(special_type="Chèque"),
+            destination=self.user.note,
+            amount=4200,
+            reason="Credit",
+            last_name="TOTO",
+            first_name="Toto",
+            bank="Société générale",
+        )
+
+        self.remittance = Remittance.objects.create(
+            remittance_type=RemittanceType.objects.get(),
+            comment="Test remittance",
+            closed=False,
+        )
+        self.credit.specialtransactionproxy.remittance = self.remittance
+        self.credit.specialtransactionproxy.save()
+
+        self.kfet = Club.objects.get(name="Kfet")
+        self.bde = self.kfet.parent_club
+
+        self.kfet_membership = Membership(
+            user=self.user,
+            club=self.kfet,
+        )
+        self.kfet_membership._force_renew_parent = True
+        self.kfet_membership._soge = True
+        self.kfet_membership.save()
+
+    def test_invoice_api(self):
+        """
+        Load Invoice API page and test all filters and permissions
+        """
+        self.check_viewset(InvoiceViewSet, "/api/treasury/invoice/")
+
+    def test_product_api(self):
+        """
+        Load Product API page and test all filters and permissions
+        """
+        self.check_viewset(ProductViewSet, "/api/treasury/product/")
+
+    def test_remittance_api(self):
+        """
+        Load Remittance API page and test all filters and permissions
+        """
+        self.check_viewset(RemittanceViewSet, "/api/treasury/remittance/")
+
+    def test_remittance_type_api(self):
+        """
+        Load RemittanceType API page and test all filters and permissions
+        """
+        self.check_viewset(RemittanceTypeViewSet, "/api/treasury/remittance_type/")
+
+    def test_sogecredit_api(self):
+        """
+        Load SogeCredit API page and test all filters and permissions
+        """
+        self.check_viewset(SogeCreditViewSet, "/api/treasury/soge_credit/")
diff --git a/apps/wei/api/views.py b/apps/wei/api/views.py
index aaa1f141685538723d69a8050bfa01faefb43064..f267d09318cfb0273815e840c731dd155a58128f 100644
--- a/apps/wei/api/views.py
+++ b/apps/wei/api/views.py
@@ -1,7 +1,8 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
+
 from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework.filters import SearchFilter
+from rest_framework.filters import OrderingFilter, SearchFilter
 from api.viewsets import ReadProtectedModelViewSet
 
 from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
@@ -15,11 +16,14 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
     then render it on /api/wei/club/
     """
-    queryset = WEIClub.objects.all()
+    queryset = WEIClub.objects.order_by('id')
     serializer_class = WEIClubSerializer
-    filter_backends = [SearchFilter, DjangoFilterBackend]
-    search_fields = ['$name', ]
-    filterset_fields = ['name', 'year', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
+                        'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
+                        'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
+                        'membership_end', ]
+    search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
 
 
 class BusViewSet(ReadProtectedModelViewSet):
@@ -28,11 +32,11 @@ class BusViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
     then render it on /api/wei/bus/
     """
-    queryset = Bus.objects
+    queryset = Bus.objects.order_by('id')
     serializer_class = BusSerializer
-    filter_backends = [SearchFilter, DjangoFilterBackend]
-    search_fields = ['$name', ]
-    filterset_fields = ['name', 'wei', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'wei', 'description', ]
+    search_fields = ['$name', '$wei__name', '$description', ]
 
 
 class BusTeamViewSet(ReadProtectedModelViewSet):
@@ -41,11 +45,11 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
     then render it on /api/wei/team/
     """
-    queryset = BusTeam.objects
+    queryset = BusTeam.objects.order_by('id')
     serializer_class = BusTeamSerializer
-    filter_backends = [SearchFilter, DjangoFilterBackend]
-    search_fields = ['$name', ]
-    filterset_fields = ['name', 'bus', 'bus__wei', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
+    search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
 
 
 class WEIRoleViewSet(ReadProtectedModelViewSet):
@@ -54,9 +58,10 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
     then render it on /api/wei/role/
     """
-    queryset = WEIRole.objects
+    queryset = WEIRole.objects.order_by('id')
     serializer_class = WEIRoleSerializer
-    filter_backends = [SearchFilter]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['name', 'permissions', 'memberships', ]
     search_fields = ['$name', ]
 
 
@@ -66,11 +71,17 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
     then render it on /api/wei/registration/
     """
-    queryset = WEIRegistration.objects
+    queryset = WEIRegistration.objects.order_by('id')
     serializer_class = WEIRegistrationSerializer
-    filter_backends = [SearchFilter, DjangoFilterBackend]
-    search_fields = ['$user__username', ]
-    filterset_fields = ['user', 'wei', ]
+    filter_backends = [DjangoFilterBackend, SearchFilter]
+    filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
+                        'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
+                        'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
+                        'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name',
+                        'emergency_contact_phone', ]
+    search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',
+                     '$user__note__alias__name', '$user__note__alias__normalized_name', '$wei__name',
+                     '$wei__email', '$health_issues', '$emergency_contact_name', '$emergency_contact_phone', ]
 
 
 class WEIMembershipViewSet(ReadProtectedModelViewSet):
@@ -79,8 +90,16 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
     The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
     then render it on /api/wei/membership/
     """
-    queryset = WEIMembership.objects
+    queryset = WEIMembership.objects.order_by('id')
     serializer_class = WEIMembershipSerializer
-    filter_backends = [SearchFilter, DjangoFilterBackend]
-    search_fields = ['$user__username', '$bus__name', '$team__name', ]
-    filterset_fields = ['user', 'club', 'bus', 'team', ]
+    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
+    filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
+                        'club__note__alias__normalized_name', 'user__username', 'user__last_name',
+                        'user__first_name', 'user__email', 'user__note__alias__name',
+                        'user__note__alias__normalized_name', 'date_start', 'date_end', 'fee', 'roles', 'bus',
+                        'bus__name', 'team', 'team__name', 'registration', ]
+    ordering_fields = ['id', 'date_start', 'date_end', ]
+    search_fields = ['$club__name', '$club__email', '$club__note__alias__name',
+                     '$club__note__alias__normalized_name', '$user__username', '$user__last_name',
+                     '$user__first_name', '$user__email', '$user__note__alias__name',
+                     '$user__note__alias__normalized_name', '$roles__name', '$bus__name', '$team__name', ]
diff --git a/apps/wei/templates/wei/base.html b/apps/wei/templates/wei/base.html
index a6521bd2162cf19106e34eb9d5e750d463ce2df2..43d617973cb6a0ebbc914abc6a6025332bef34a3 100644
--- a/apps/wei/templates/wei/base.html
+++ b/apps/wei/templates/wei/base.html
@@ -61,10 +61,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
                     <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>
                     {% endif %}
 
-                    {% if "note.change_alias"|has_perm:club.note.alias_set.first %}
+                    {% if "note.change_alias"|has_perm:club.note.alias.first %}
                     <dt class="col-xl-4"><a
                             href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
-                    <dd class="col-xl-8 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd>
+                    <dd class="col-xl-8 text-truncate">{{ club.note.alias.all|join:", " }}</dd>
                     {% endif %}
 
                     <dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>
diff --git a/apps/wei/tests/test_wei_registration.py b/apps/wei/tests/test_wei_registration.py
index 6d294bddf6fa384f442d038fb60e7f0173ed0f2c..92ceb289569fd8d79a3f2ba184f362a1c2bc76b7 100644
--- a/apps/wei/tests/test_wei_registration.py
+++ b/apps/wei/tests/test_wei_registration.py
@@ -4,16 +4,19 @@
 import subprocess
 from datetime import timedelta, date
 
+from api.tests import TestAPI
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.db.models import Q
 from django.test import TestCase
 from django.urls import reverse
 from django.utils import timezone
-from member.models import Membership
+from member.models import Membership, Club
 from note.models import NoteClub, SpecialTransaction
 from treasury.models import SogeCredit
 
+from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \
+    WEIRoleViewSet
 from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey
 from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
 
@@ -524,7 +527,7 @@ class TestWEIRegistration(TestCase):
         sess["permission_mask"] = 0
         sess.save()
         response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)))
-        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.status_code, 403)
         sess["permission_mask"] = 42
         sess.save()
 
@@ -807,3 +810,97 @@ class TestWEISurveyAlgorithm(TestCase):
 
     def test_survey_algorithm(self):
         CurrentSurvey.get_algorithm_class()().run_algorithm()
+
+
+class TestWeiAPI(TestAPI):
+    def setUp(self) -> None:
+        super().setUp()
+
+        self.year = timezone.now().year
+        self.wei = WEIClub.objects.create(
+            name="Test WEI",
+            email="gc.wei@example.com",
+            parent_club_id=2,
+            membership_fee_paid=12500,
+            membership_fee_unpaid=5500,
+            membership_start=date(self.year, 1, 1),
+            membership_end=date(self.year, 12, 31),
+            membership_duration=396,
+            year=self.year,
+            date_start=date.today() + timedelta(days=2),
+            date_end=date(self.year, 12, 31),
+        )
+        NoteClub.objects.create(club=self.wei)
+        self.bus = Bus.objects.create(
+            name="Test Bus",
+            wei=self.wei,
+            description="Test Bus",
+        )
+        self.team = BusTeam.objects.create(
+            name="Test Team",
+            bus=self.bus,
+            color=0xFFFFFF,
+            description="Test Team",
+        )
+        self.registration = WEIRegistration.objects.create(
+            user_id=self.user.id,
+            wei_id=self.wei.id,
+            soge_credit=True,
+            caution_check=True,
+            birth_date=date(2000, 1, 1),
+            gender="nonbinary",
+            clothing_cut="male",
+            clothing_size="XL",
+            health_issues="I am a bot",
+            emergency_contact_name="Pikachu",
+            emergency_contact_phone="+33123456789",
+            first_year=False,
+        )
+        Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
+        Membership.objects.create(user=self.user, club=Club.objects.get(name="Kfet"))
+        self.membership = WEIMembership.objects.create(
+            user=self.user,
+            club=self.wei,
+            fee=125,
+            bus=self.bus,
+            team=self.team,
+            registration=self.registration,
+        )
+        self.membership.roles.add(WEIRole.objects.last())
+        self.membership.save()
+
+    def test_weiclub_api(self):
+        """
+        Load WEI API page and test all filters and permissions
+        """
+        self.check_viewset(WEIClubViewSet, "/api/wei/club/")
+
+    def test_wei_bus_api(self):
+        """
+        Load Bus API page and test all filters and permissions
+        """
+        self.check_viewset(BusViewSet, "/api/wei/bus/")
+
+    def test_wei_team_api(self):
+        """
+        Load BusTeam API page and test all filters and permissions
+        """
+        self.check_viewset(BusTeamViewSet, "/api/wei/team/")
+
+    def test_weirole_api(self):
+        """
+        Load WEIRole API page and test all filters and permissions
+        """
+        self.check_viewset(WEIRoleViewSet, "/api/wei/role/")
+
+    def test_weiregistration_api(self):
+        """
+        Load WEIRegistration API page and test all filters and permissions
+        """
+        self.check_viewset(WEIRegistrationViewSet, "/api/wei/registration/")
+
+    def test_weimembership_api(self):
+        """
+        Load WEIMembership API page and test all filters and permissions
+        """
+        self.check_viewset(WEIMembershipViewSet, "/api/wei/membership/")
diff --git a/note_kfet/settings/secrets_example.py b/note_kfet/settings/secrets_example.py
index 656e558b4267fb45414282c66a6edd393c43717c..61d92359c3e05702559f28f99a0c50f55436e422 100644
--- a/note_kfet/settings/secrets_example.py
+++ b/note_kfet/settings/secrets_example.py
@@ -1,12 +1,13 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-# CAS
 OPTIONAL_APPS = [
-#    'debug_toolbar'
+    # 'cas_server',
+    # 'debug_toolbar',
+    # 'django_extensions',
 ]
 
-# When a server error occured, send an email to these addresses
+# When a server error occurred, send an email to these addresses
 ADMINS = (
     ('Note Kfet', 'notekfet@example.com'),
 )
diff --git a/tox.ini b/tox.ini
index 1c32a4124fbedbffedfd3ad5dd9d108dfc2022fa..c900d5242a749c7266612ad7ea096d7091b52723 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,6 +6,9 @@ envlist =
     # Ubuntu 20.04 Python
     py38-django22
 
+    # Debian Bullseye Python
+    py39-django22
+
     linters
 skipsdist = True
 
@@ -15,7 +18,7 @@ deps =
     -r{toxinidir}/requirements.txt
     coverage
 commands =
-    coverage run --omit='*migrations*,apps/scripts*' --source=apps,note_kfet ./manage.py test apps/
+    coverage run --omit='apps/scripts*,*_example.py,note_kfet/wsgi.py' --source=apps,note_kfet ./manage.py test apps/
     coverage report -m
 
 [testenv:linters]