diff --git a/apps/activity/models.py b/apps/activity/models.py
index e3ff0c2f32745de9e8acc2b06193c3e4a9b78983..ed2d94c9a4394a5314d5e90cd94a382389de9017 100644
--- a/apps/activity/models.py
+++ b/apps/activity/models.py
@@ -73,15 +73,6 @@ class Activity(models.Model):
         verbose_name=_('organizer'),
     )
 
-    note = models.ForeignKey(
-        'note.Note',
-        on_delete=models.PROTECT,
-        blank=True,
-        null=True,
-        related_name='+',
-        verbose_name=_('note'),
-    )
-
     attendees_club = models.ForeignKey(
         'member.Club',
         on_delete=models.PROTECT,
@@ -160,9 +151,7 @@ class Entry(models.Model):
         if insert and self.guest:
             GuestTransaction.objects.create(
                 source=self.note,
-                source_alias=self.note.user.username,
-                destination=self.note,
-                destination_alias=self.activity.organizer.name,
+                destination=self.activity.organizer.note,
                 quantity=1,
                 amount=self.activity.activity_type.guest_entry_fee,
                 reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
diff --git a/apps/activity/views.py b/apps/activity/views.py
index feb7591d9f8cdf98ebc0a6a844ef95e8a6c94988..51e2ebf535a8ba762e8da65ee91c749ceeec556b 100644
--- a/apps/activity/views.py
+++ b/apps/activity/views.py
@@ -1,5 +1,6 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
+
 from datetime import datetime, timezone
 
 from django.contrib.auth.mixins import LoginRequiredMixin
@@ -11,13 +12,14 @@ from django.utils.translation import gettext_lazy as _
 from django_tables2.views import SingleTableView
 from note.models import NoteUser, Alias, NoteSpecial
 from permission.backends import PermissionBackend
+from permission.views import ProtectQuerysetMixin
 
 from .forms import ActivityForm, GuestForm
 from .models import Activity, Guest, Entry
 from .tables import ActivityTable, GuestTable, EntryTable
 
 
-class ActivityCreateView(LoginRequiredMixin, CreateView):
+class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     model = Activity
     form_class = ActivityForm
 
@@ -30,13 +32,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView):
         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
 
 
-class ActivityListView(LoginRequiredMixin, SingleTableView):
+class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     model = Activity
     table_class = ActivityTable
 
     def get_queryset(self):
-        return super().get_queryset()\
-            .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).reverse()
+        return super().get_queryset().reverse()
 
     def get_context_data(self, **kwargs):
         ctx = super().get_context_data(**kwargs)
@@ -50,7 +51,7 @@ class ActivityListView(LoginRequiredMixin, SingleTableView):
         return ctx
 
 
-class ActivityDetailView(LoginRequiredMixin, DetailView):
+class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
     model = Activity
     context_object_name = "activity"
 
@@ -66,7 +67,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView):
         return ctx
 
 
-class ActivityUpdateView(LoginRequiredMixin, UpdateView):
+class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     model = Activity
     form_class = ActivityForm
 
@@ -74,18 +75,20 @@ class ActivityUpdateView(LoginRequiredMixin, UpdateView):
         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
 
 
-class ActivityInviteView(LoginRequiredMixin, CreateView):
+class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     model = Guest
     form_class = GuestForm
     template_name = "activity/activity_invite.html"
 
     def get_form(self, form_class=None):
         form = super().get_form(form_class)
-        form.activity = Activity.objects.get(pk=self.kwargs["pk"])
+        form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
+            .get(pk=self.kwargs["pk"])
         return form
 
     def form_valid(self, form):
-        form.instance.activity = Activity.objects.get(pk=self.kwargs["pk"])
+        form.instance.activity = Activity.objects\
+            .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
         return super().form_valid(form)
 
     def get_success_url(self, **kwargs):
@@ -98,7 +101,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
     def get_context_data(self, **kwargs):
         ctx = super().get_context_data(**kwargs)
 
-        activity = Activity.objects.get(pk=self.kwargs["pk"])
+        activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
+            .get(pk=self.kwargs["pk"])
         ctx["activity"] = activity
 
         matched = []
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index 43fc1e13a92d23e746fbb5cab92bbbdaa6d70cf4..68bf95c0fc7d52d232093983d55ef9615d30093e 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_no_log"):
+        return
+
     # noinspection PyProtectedMember
     previous = instance._previous
 
@@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_no_log"):
+        return
+
     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
     user, ip = get_current_authenticated_user(), get_current_ip()
 
diff --git a/apps/member/filters.py b/apps/member/filters.py
deleted file mode 100644
index 951723e86b6d0fecd3c6c728836cef43331c42fc..0000000000000000000000000000000000000000
--- a/apps/member/filters.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Layout, Submit
-from django.contrib.auth.models import User
-from django.db.models import CharField
-from django_filters import FilterSet, CharFilter
-
-
-class UserFilter(FilterSet):
-    class Meta:
-        model = User
-        fields = ['last_name', 'first_name', 'username', 'profile__section']
-        filter_overrides = {
-            CharField: {
-                'filter_class': CharFilter,
-                'extra': lambda f: {
-                    'lookup_expr': 'icontains'
-                }
-            }
-        }
-
-
-class UserFilterFormHelper(FormHelper):
-    form_method = 'GET'
-    layout = Layout(
-        'last_name',
-        'first_name',
-        'username',
-        'profile__section',
-        Submit('Submit', 'Apply Filter'),
-    )
diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json
index bba1e7ac8de61ef1028e2eb99d306bc3032f62f2..e27eb72d65a27dd10ca0b0faf6ea37bb73bed7ec 100644
--- a/apps/member/fixtures/initial.json
+++ b/apps/member/fixtures/initial.json
@@ -5,10 +5,12 @@
         "fields": {
             "name": "BDE",
             "email": "tresorerie.bde@example.com",
-            "membership_fee": 500,
-            "membership_duration": "396 00:00:00",
-            "membership_start": "213 00:00:00",
-            "membership_end": "273 00:00:00"
+            "require_memberships":  true,
+            "membership_fee_paid": 500,
+            "membership_fee_unpaid": 500,
+            "membership_duration": 396,
+            "membership_start": "2019-08-31",
+            "membership_end": "2020-09-30"
         }
     },
     {
@@ -17,10 +19,13 @@
         "fields": {
             "name": "Kfet",
             "email": "tresorerie.bde@example.com",
-            "membership_fee": 3500,
-            "membership_duration": "396 00:00:00",
-            "membership_start": "213 00:00:00",
-            "membership_end": "273 00:00:00"
+            "parent_club": 1,
+            "require_memberships":  true,
+            "membership_fee_paid": 3500,
+            "membership_fee_unpaid": 3500,
+            "membership_duration": 396,
+            "membership_start": "2019-08-31",
+            "membership_end": "2020-09-30"
         }
     }
 ]
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 20f0acfe28119264390f246224067f2fa3e42467..a37d143ef4d75d3e4481385bef27f45fada5865b 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -1,13 +1,10 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from crispy_forms.bootstrap import Div
-from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Layout
 from django import forms
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 from django.contrib.auth.models import User
-from note_kfet.inputs import Autocomplete
+from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
 from permission.models import PermissionMask
 
 from .models import Profile, Club, Membership
@@ -47,11 +44,18 @@ class ClubForm(forms.ModelForm):
     class Meta:
         model = Club
         fields = '__all__'
-
-
-class AddMembersForm(forms.Form):
-    class Meta:
-        fields = ('',)
+        widgets = {
+            "membership_fee_paid": AmountInput(),
+            "membership_fee_unpaid": AmountInput(),
+            "parent_club": Autocomplete(
+                Club,
+                attrs={
+                    'api_url': '/api/members/club/',
+                }
+            ),
+            "membership_start": DatePickerInput(),
+            "membership_end": DatePickerInput(),
+        }
 
 
 class MembershipForm(forms.ModelForm):
@@ -71,28 +75,5 @@ class MembershipForm(forms.ModelForm):
                         'placeholder': 'Nom ...',
                     },
                 ),
+            'date_start': DatePickerInput(),
         }
-
-
-MemberFormSet = forms.modelformset_factory(
-    Membership,
-    form=MembershipForm,
-    extra=2,
-    can_delete=True,
-)
-
-
-class FormSetHelper(FormHelper):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.form_tag = False
-        self.form_method = 'POST'
-        self.form_class = 'form-inline'
-        # self.template = 'bootstrap/table_inline_formset.html'
-        self.layout = Layout(
-            Div(
-                Div('user', css_class='col-sm-2'),
-                Div('roles', css_class='col-sm-2'),
-                Div('date_start', css_class='col-sm-2'),
-                css_class="row formset-row",
-            ))
diff --git a/apps/member/models.py b/apps/member/models.py
index d0051e59d612019df3f4cb104857ba743c09e713..693854afa046ff17d3668ff774b16521f9781254 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -4,10 +4,12 @@
 import datetime
 
 from django.conf import settings
+from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse, reverse_lazy
 from django.utils.translation import gettext_lazy as _
+from note.models import MembershipTransaction
 
 
 class Profile(models.Model):
@@ -77,22 +79,43 @@ class Club(models.Model):
     )
 
     # Memberships
-    membership_fee = models.PositiveIntegerField(
-        verbose_name=_('membership fee'),
+
+    # When set to False, the membership system won't be used.
+    # Useful to create notes for activities or departments.
+    require_memberships = models.BooleanField(
+        default=True,
+        verbose_name=_("require memberships"),
+        help_text=_("Uncheck if this club don't require memberships."),
+    )
+
+    membership_fee_paid = models.PositiveIntegerField(
+        default=0,
+        verbose_name=_('membership fee (paid students)'),
+    )
+
+    membership_fee_unpaid = models.PositiveIntegerField(
+        default=0,
+        verbose_name=_('membership fee (unpaid students)'),
     )
-    membership_duration = models.DurationField(
+
+    membership_duration = models.PositiveIntegerField(
+        blank=True,
         null=True,
         verbose_name=_('membership duration'),
-        help_text=_('The longest time a membership can last '
+        help_text=_('The longest time (in days) a membership can last '
                     '(NULL = infinite).'),
     )
-    membership_start = models.DurationField(
+
+    membership_start = models.DateField(
+        blank=True,
         null=True,
         verbose_name=_('membership start'),
         help_text=_('How long after January 1st the members can renew '
                     'their membership.'),
     )
-    membership_end = models.DurationField(
+
+    membership_end = models.DateField(
+        blank=True,
         null=True,
         verbose_name=_('membership end'),
         help_text=_('How long the membership can last after January 1st '
@@ -100,6 +123,33 @@ class Club(models.Model):
                     'membership.'),
     )
 
+    def update_membership_dates(self):
+        """
+        This function is called each time the club detail view is displayed.
+        Update the year of the membership dates.
+        """
+        if not self.membership_start:
+            return
+
+        today = datetime.date.today()
+
+        if (today - self.membership_start).days >= 365:
+            self.membership_start = datetime.date(self.membership_start.year + 1,
+                                                  self.membership_start.month, self.membership_start.day)
+            self.membership_end = datetime.date(self.membership_end.year + 1,
+                                                self.membership_end.month, self.membership_end.day)
+            self.save(force_update=True)
+
+    def save(self, force_insert=False, force_update=False, using=None,
+             update_fields=None):
+        if not self.require_memberships:
+            self.membership_fee_paid = 0
+            self.membership_fee_unpaid = 0
+            self.membership_duration = None
+            self.membership_start = None
+            self.membership_end = None
+        super().save(force_insert, force_update, update_fields)
+
     class Meta:
         verbose_name = _("club")
         verbose_name_plural = _("clubs")
@@ -114,9 +164,6 @@ class Club(models.Model):
 class Role(models.Model):
     """
     Role that an :model:`auth.User` can have in a :model:`member.Club`
-
-    TODO: Integrate the right management, and create some standard Roles at the
-    creation of the club.
     """
     name = models.CharField(
         verbose_name=_('name'),
@@ -138,24 +185,31 @@ class Membership(models.Model):
 
     """
     user = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
+        User,
         on_delete=models.PROTECT,
+        verbose_name=_("user"),
     )
+
     club = models.ForeignKey(
         Club,
         on_delete=models.PROTECT,
+        verbose_name=_("club"),
     )
-    roles = models.ForeignKey(
+
+    roles = models.ManyToManyField(
         Role,
-        on_delete=models.PROTECT,
+        verbose_name=_("roles"),
     )
+
     date_start = models.DateField(
         verbose_name=_('membership starts on'),
     )
+
     date_end = models.DateField(
         verbose_name=_('membership ends on'),
         null=True,
     )
+
     fee = models.PositiveIntegerField(
         verbose_name=_('fee'),
     )
@@ -168,10 +222,54 @@ class Membership(models.Model):
 
     def save(self, *args, **kwargs):
         if self.club.parent_club is not None:
-            if not Membership.objects.filter(user=self.user, club=self.club.parent_club):
-                raise ValidationError(_('User is not a member of the parent club'))
+            if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists():
+                raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
+
+        created = not self.pk
+        if created:
+            if Membership.objects.filter(
+                    user=self.user,
+                    club=self.club,
+                    date_start__lte=self.date_start,
+                    date_end__gte=self.date_start,
+            ).exists():
+                raise ValidationError(_('User is already a member of the club'))
+
+            if self.user.profile.paid:
+                self.fee = self.club.membership_fee_paid
+            else:
+                self.fee = self.club.membership_fee_unpaid
+
+            if self.club.membership_duration is not None:
+                self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
+            else:
+                self.date_end = self.date_start + datetime.timedelta(days=424242)
+            if self.club.membership_end is not None and self.date_end > self.club.membership_end:
+                self.date_end = self.club.membership_end
+
         super().save(*args, **kwargs)
 
+        self.make_transaction()
+
+    def make_transaction(self):
+        if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
+            return
+
+        if self.fee:
+            transaction = MembershipTransaction(
+                membership=self,
+                source=self.user.note,
+                destination=self.club.note,
+                quantity=1,
+                amount=self.fee,
+                reason="Adhésion " + self.club.name,
+            )
+            transaction._force_save = True
+            transaction.save(force_insert=True)
+
+    def __str__(self):
+        return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
+
     class Meta:
         verbose_name = _('membership')
         verbose_name_plural = _('memberships')
diff --git a/apps/member/tables.py b/apps/member/tables.py
index d0c37a6e2e47baaa3e4b2f3eef1eebe152fdc5d1..c8a510ff8d5a85c6e7a7195b85c8426d5436a64c 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -1,10 +1,17 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
+from datetime import datetime
 
 import django_tables2 as tables
 from django.contrib.auth.models import User
+from django.utils.translation import gettext_lazy as _
+from django.urls import reverse_lazy
+from django.utils.html import format_html
+from note.templatetags.pretty_money import pretty_money
+from note_kfet.middlewares import get_current_authenticated_user
+from permission.backends import PermissionBackend
 
-from .models import Club
+from .models import Club, Membership
 
 
 class ClubTable(tables.Table):
@@ -24,7 +31,11 @@ class ClubTable(tables.Table):
 
 class UserTable(tables.Table):
     section = tables.Column(accessor='profile.section')
-    solde = tables.Column(accessor='note.balance')
+
+    balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
+
+    def render_balance(self, value):
+        return pretty_money(value)
 
     class Meta:
         attrs = {
@@ -33,3 +44,68 @@ class UserTable(tables.Table):
         template_name = 'django_tables2/bootstrap4.html'
         fields = ('last_name', 'first_name', 'username', 'email')
         model = User
+        row_attrs = {
+            'class': 'table-row',
+            'data-href': lambda record: record.pk
+        }
+
+
+class MembershipTable(tables.Table):
+    roles = tables.Column(
+        attrs={
+            "td": {
+                "class": "text-truncate",
+            }
+        }
+    )
+
+    def render_club(self, value):
+        s = value.name
+        if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
+            s = format_html("<a href={url}>{name}</a>",
+                            url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
+
+        return s
+
+    def render_fee(self, value, record):
+        t = pretty_money(value)
+
+        # If it is required and if the user has the right, the renew button is displayed.
+        if record.club.membership_start is not None:
+            if record.date_start < record.club.membership_start:  # If the renew is available
+                if not Membership.objects.filter(
+                        club=record.club,
+                        user=record.user,
+                        date_start__gte=record.club.membership_start,
+                        date_end__lte=record.club.membership_end,
+                ).exists():  # If the renew is not yet performed
+                    empty_membership = Membership(
+                        club=record.club,
+                        user=record.user,
+                        date_start=datetime.now().date(),
+                        date_end=datetime.now().date(),
+                        fee=0,
+                    )
+                    if PermissionBackend.check_perm(get_current_authenticated_user(),
+                                                    "member:add_membership", empty_membership):  # If the user has right
+                        t = format_html(t + ' <a class="btn btn-warning" href="{url}">{text}</a>',
+                                        url=reverse_lazy('member:club_renew_membership',
+                                                         kwargs={"pk": record.pk}), text=_("Renew"))
+        return t
+
+    def render_roles(self, record):
+        roles = record.roles.all()
+        s = ", ".join(str(role) for role in roles)
+        if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
+            s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+                            + "'>" + s + "</a>")
+        return s
+
+    class Meta:
+        attrs = {
+            'class': 'table table-condensed table-striped table-hover',
+            'style': 'table-layout: fixed;'
+        }
+        template_name = 'django_tables2/bootstrap4.html'
+        fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
+        model = Membership
diff --git a/apps/member/urls.py b/apps/member/urls.py
index 085a3fecf69b1bc6c2df208d5b746689a9f5e95b..1214f0243c5aebe98e3b2da5469eda3240e659cb 100644
--- a/apps/member/urls.py
+++ b/apps/member/urls.py
@@ -8,17 +8,21 @@ from . import views
 app_name = 'member'
 urlpatterns = [
     path('signup/', views.UserCreateView.as_view(), name="signup"),
+
     path('club/', views.ClubListView.as_view(), name="club_list"),
+    path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
     path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
     path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
-    path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
-    path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"),
-    path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
-    path('club/<int:pk>/aliases', views.ClubAliasView.as_view(), name="club_alias"),
+    path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
+    path('club/renew_membership/<int:pk>/', views.ClubRenewMembershipView.as_view(), name="club_renew_membership"),
+    path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
+    path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
+    path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
+
     path('user/', views.UserListView.as_view(), name="user_list"),
-    path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
-    path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),
-    path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
-    path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
+    path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
+    path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
+    path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
+    path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
     path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
 ]
diff --git a/apps/member/views.py b/apps/member/views.py
index 8145b5e9346548d351ddbf30903a7df65cfacb2b..f695002f9d141afe31d645b149bb8fa8ed160a22 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -2,17 +2,21 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 import io
+from datetime import datetime, timedelta
 
 from PIL import Image
 from django.conf import settings
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import User
 from django.contrib.auth.views import LoginView
+from django.core.exceptions import ValidationError
 from django.db.models import Q
+from django.forms import HiddenInput
 from django.shortcuts import redirect
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
+from django.views.generic.base import View
 from django.views.generic.edit import FormMixin
 from django_tables2.views import SingleTableView
 from rest_framework.authtoken.models import Token
@@ -21,12 +25,11 @@ from note.models import Alias, NoteUser
 from note.models.transactions import Transaction
 from note.tables import HistoryTable, AliasTable
 from permission.backends import PermissionBackend
+from permission.views import ProtectQuerysetMixin
 
-from .filters import UserFilter, UserFilterFormHelper
-from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \
-    CustomAuthenticationForm
+from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
 from .models import Club, Membership
-from .tables import ClubTable, UserTable
+from .tables import ClubTable, UserTable, MembershipTable
 
 
 class CustomLoginView(LoginView):
@@ -63,7 +66,7 @@ class UserCreateView(CreateView):
         return super().form_valid(form)
 
 
-class UserUpdateView(LoginRequiredMixin, UpdateView):
+class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     model = User
     fields = ['first_name', 'last_name', 'username', 'email']
     template_name = 'member/profile_update.html'
@@ -97,7 +100,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
         if form.is_valid() and profile_form.is_valid():
             new_username = form.data['username']
             alias = Alias.objects.filter(name=new_username)
-            # Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer
+            # Si le nouveau pseudo n'est pas un de nos alias,
+            # on supprime éventuellement un alias similaire pour le remplacer
             if not alias.exists():
                 similar = Alias.objects.filter(
                     normalized_name=Alias.normalize(new_username))
@@ -119,7 +123,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
             return reverse_lazy('member:user_detail', args=(self.object.id,))
 
 
-class UserDetailView(LoginRequiredMixin, DetailView):
+class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
     """
     Affiche les informations sur un utilisateur, sa note, ses clubs...
     """
@@ -127,44 +131,56 @@ class UserDetailView(LoginRequiredMixin, DetailView):
     context_object_name = "user_object"
     template_name = "member/profile_detail.html"
 
-    def get_queryset(self, **kwargs):
-        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
-
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         user = context['user_object']
         history_list = \
-            Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")
+            Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\
+            .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
         context['history_list'] = HistoryTable(history_list)
-        club_list = \
-            Membership.objects.all().filter(user=user).only("club")
-        context['club_list'] = ClubTable(club_list)
+        club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\
+            .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
+        context['club_list'] = MembershipTable(data=club_list)
         return context
 
 
-class UserListView(LoginRequiredMixin, SingleTableView):
+class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     """
     Affiche la liste des utilisateurs, avec une fonction de recherche statique
     """
     model = User
     table_class = UserTable
     template_name = 'member/user_list.html'
-    filter_class = UserFilter
-    formhelper_class = UserFilterFormHelper
 
     def get_queryset(self, **kwargs):
-        qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
-        self.filter = self.filter_class(self.request.GET, queryset=qs)
-        self.filter.form.helper = self.formhelper_class()
-        return self.filter.qs
+        qs = super().get_queryset()
+        if "search" in self.request.GET:
+            pattern = self.request.GET["search"]
+
+            if not pattern:
+                return qs.none()
+
+            qs = qs.filter(
+                Q(first_name__iregex=pattern)
+                | Q(last_name__iregex=pattern)
+                | Q(profile__section__iregex=pattern)
+                | Q(note__alias__name__iregex="^" + pattern)
+                | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
+            )
+        else:
+            qs = qs.none()
+
+        return qs[:20]
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context["filter"] = self.filter
+
+        context["title"] = _("Search user")
+
         return context
 
 
-class ProfileAliasView(LoginRequiredMixin, DetailView):
+class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
     model = User
     template_name = 'member/profile_alias.html'
     context_object_name = 'user_object'
@@ -176,11 +192,11 @@ class ProfileAliasView(LoginRequiredMixin, DetailView):
         return context
 
 
-class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
+class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
     form_class = ImageForm
 
-    def get_context_data(self, *args, **kwargs):
-        context = super().get_context_data(*args, **kwargs)
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
         context['form'] = self.form_class(self.request.POST, self.request.FILES)
         return context
 
@@ -237,8 +253,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
     template_name = "member/manage_auth_tokens.html"
 
     def get(self, request, *args, **kwargs):
-        if 'regenerate' in request.GET and Token.objects.filter(
-                user=request.user).exists():
+        if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
             Token.objects.get(user=self.request.user).delete()
             return redirect(reverse_lazy('member:auth_token') + "?show",
                             permanent=True)
@@ -247,8 +262,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context['token'] = Token.objects.get_or_create(
-            user=self.request.user)[0]
+        context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
         return context
 
 
@@ -257,7 +271,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
 # ******************************* #
 
 
-class ClubCreateView(LoginRequiredMixin, CreateView):
+class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     """
     Create Club
     """
@@ -269,38 +283,49 @@ class ClubCreateView(LoginRequiredMixin, CreateView):
         return super().form_valid(form)
 
 
-class ClubListView(LoginRequiredMixin, SingleTableView):
+class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     """
     List existing Clubs
     """
     model = Club
     table_class = ClubTable
 
-    def get_queryset(self, **kwargs):
-        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
-
 
-class ClubDetailView(LoginRequiredMixin, DetailView):
+class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
     model = Club
     context_object_name = "club"
 
-    def get_queryset(self, **kwargs):
-        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
-
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
+
         club = context["club"]
-        club_transactions = \
-            Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))
+        if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
+            club.update_membership_dates()
+
+        club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
+            .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id')
         context['history_list'] = HistoryTable(club_transactions)
-        club_member = \
-            Membership.objects.all().filter(club=club)
-        # TODO: consider only valid Membership
-        context['member_list'] = club_member
+        club_member = Membership.objects.filter(
+            club=club,
+            date_end__gte=datetime.today(),
+        ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
+
+        context['member_list'] = MembershipTable(data=club_member)
+
+        empty_membership = Membership(
+            club=club,
+            user=User.objects.first(),
+            date_start=datetime.now().date(),
+            date_end=datetime.now().date(),
+            fee=0,
+        )
+        context["can_add_members"] = PermissionBackend()\
+            .has_perm(self.request.user, "member.add_membership", empty_membership)
+
         return context
 
 
-class ClubAliasView(LoginRequiredMixin, DetailView):
+class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
     model = Club
     template_name = 'member/club_alias.html'
     context_object_name = 'club'
@@ -312,12 +337,14 @@ class ClubAliasView(LoginRequiredMixin, DetailView):
         return context
 
 
-class ClubUpdateView(LoginRequiredMixin, UpdateView):
+class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     model = Club
     context_object_name = "club"
     form_class = ClubForm
     template_name = "member/club_form.html"
-    success_url = reverse_lazy("member:club_detail")
+
+    def get_success_url(self):
+        return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
 
 
 class ClubPictureUpdateView(PictureUpdateView):
@@ -329,35 +356,123 @@ class ClubPictureUpdateView(PictureUpdateView):
         return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})
 
 
-class ClubAddMemberView(LoginRequiredMixin, CreateView):
+class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     model = Membership
     form_class = MembershipForm
     template_name = 'member/add_members.html'
 
-    def get_queryset(self, **kwargs):
-        return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")
-                                             | PermissionBackend.filter_queryset(self.request.user, Membership,
-                                                                                 "change"))
+    def get_context_data(self, **kwargs):
+        club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
+            .get(pk=self.kwargs["pk"])
+        context = super().get_context_data(**kwargs)
+        context['club'] = club
+
+        return context
+
+    def form_valid(self, form):
+        club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
+            .get(pk=self.kwargs["pk"])
+        user = self.request.user
+        form.instance.club = club
+
+        if user.profile.paid:
+            fee = club.membership_fee_paid
+        else:
+            fee = club.membership_fee_unpaid
+        if user.note.balance < fee and not Membership.objects.filter(
+                club__name="Kfet",
+                user=user,
+                date_start__lte=datetime.now().date(),
+                date_end__gte=datetime.now().date(),
+        ).exists():
+            # Users without a valid Kfet membership can't have a negative balance.
+            # Club 2 = Kfet (hard-code :'( )
+            # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
+            form.add_error('user',
+                           _("This user don't have enough money to join this club, and can't have a negative balance."))
+
+        if club.parent_club is not None:
+            if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
+                form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
+                return super().form_invalid(form)
+
+        if Membership.objects.filter(
+                user=form.instance.user,
+                club=club,
+                date_start__lte=form.instance.date_start,
+                date_end__gte=form.instance.date_start,
+        ).exists():
+            form.add_error('user', _('User is already a member of the club'))
+            return super().form_invalid(form)
+
+        if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start:
+            form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end:
+            form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
+
+
+class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
+    model = Membership
+    form_class = MembershipForm
+    template_name = 'member/add_members.html'
 
     def get_context_data(self, **kwargs):
-        club = Club.objects.get(pk=self.kwargs["pk"])
         context = super().get_context_data(**kwargs)
-        context['formset'] = MemberFormSet()
-        context['helper'] = FormSetHelper()
+        club = self.object.club
         context['club'] = club
-        context['no_cache'] = True
+        form = context['form']
+        form.fields['user'].disabled = True
+        form.fields['date_start'].widget = HiddenInput()
 
         return context
 
-    def post(self, request, *args, **kwargs):
-        return
-        # TODO: Implement POST
-        # formset = MembershipFormset(request.POST)
-        # if formset.is_valid():
-        #     return self.form_valid(formset)
-        # else:
-        #     return self.form_invalid(formset)
-
-    def form_valid(self, formset):
-        formset.save()
-        return super().form_valid(formset)
+    def form_valid(self, form):
+        if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start:
+            form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end:
+            form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
+
+
+class ClubRenewMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, View):
+    def get(self, *args, **kwargs):
+        user = self.request.user
+        membership = Membership.objects.filter(PermissionBackend.filter_queryset(user, Membership, "change"))\
+            .filter(pk=self.kwargs["pk"]).get()
+
+        if Membership.objects.filter(
+            club=membership.club,
+            user=membership.user,
+            date_start__gte=membership.club.membership_start,
+            date_end__lte=membership.club.membership_end,
+        ).exists():
+            raise ValidationError(_("This membership is already renewed"))
+
+        new_membership = Membership.objects.create(
+            user=user,
+            club=membership.club,
+            date_start=membership.date_end + timedelta(days=1),
+        )
+        new_membership.roles.set(membership.roles.all())
+        new_membership.save()
+
+        return redirect(reverse_lazy('member:club_detail', kwargs={'pk': membership.club.pk}))
diff --git a/apps/note/admin.py b/apps/note/admin.py
index 702d3350e7933ac7141b80bdd1ab884ce515e1b7..7b4ba870c64331eb10501ba13ef68df493418127 100644
--- a/apps/note/admin.py
+++ b/apps/note/admin.py
@@ -138,6 +138,13 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
         return []
 
 
+@admin.register(MembershipTransaction)
+class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
+    """
+    Admin customisation for Transaction
+    """
+
+
 @admin.register(TransactionTemplate)
 class TransactionTemplateAdmin(admin.ModelAdmin):
     """
diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py
index fbd12038fc122122a01afd1a5c3db1507717aa07..5625e5b5ef8b87233bf55d08026f1e174fda1c65 100644
--- a/apps/note/api/serializers.py
+++ b/apps/note/api/serializers.py
@@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
         Note: NoteSerializer,
         NoteUser: NoteUserSerializer,
         NoteClub: NoteClubSerializer,
-        NoteSpecial: NoteSpecialSerializer
+        NoteSpecial: NoteSpecialSerializer,
     }
 
     class Meta:
diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py
index d1dcd7887eb62c36f7235e90d0b12454c80ce58e..83f8f914c56282e1bdb9e74022500f6fabb29cbb 100644
--- a/apps/note/models/transactions.py
+++ b/apps/note/models/transactions.py
@@ -46,12 +46,14 @@ class TransactionTemplate(models.Model):
         unique=True,
         error_messages={'unique': _("A template with this name already exist")},
     )
+
     destination = models.ForeignKey(
         NoteClub,
         on_delete=models.PROTECT,
         related_name='+',  # no reverse
         verbose_name=_('destination'),
     )
+
     amount = models.PositiveIntegerField(
         verbose_name=_('amount'),
         help_text=_('in centimes'),
@@ -62,9 +64,12 @@ class TransactionTemplate(models.Model):
         verbose_name=_('type'),
         max_length=31,
     )
+
     display = models.BooleanField(
         default=True,
+        verbose_name=_("display"),
     )
+
     description = models.CharField(
         verbose_name=_('description'),
         max_length=255,
@@ -140,6 +145,7 @@ class Transaction(PolymorphicModel):
         max_length=255,
         default=None,
         null=True,
+        blank=True,
     )
 
     class Meta:
diff --git a/apps/note/tables.py b/apps/note/tables.py
index 0d83e3cc574c49bfcb6076b4ede4b542953b6cd6..a38beb9a6c5934ab4670c5e85bb153803e9cd8a5 100644
--- a/apps/note/tables.py
+++ b/apps/note/tables.py
@@ -118,7 +118,8 @@ class AliasTable(tables.Table):
 
     delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
                                        extra_context={"delete_trans": _('delete')},
-                                       attrs={'td': {'class': 'col-sm-1'}})
+                                       attrs={'td': {'class': 'col-sm-1'}},
+                                       verbose_name=_("Delete"),)
 
 
 class ButtonTable(tables.Table):
@@ -134,17 +135,20 @@ class ButtonTable(tables.Table):
         }
 
         model = TransactionTemplate
+        exclude = ('id',)
 
     edit = tables.LinkColumn('note:template_update',
                              args=[A('pk')],
                              attrs={'td': {'class': 'col-sm-1'},
                                     'a': {'class': 'btn btn-sm btn-primary'}},
                              text=_('edit'),
-                             accessor='pk')
+                             accessor='pk',
+                             verbose_name=_("Edit"),)
 
     delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
                                        extra_context={"delete_trans": _('delete')},
-                                       attrs={'td': {'class': 'col-sm-1'}})
+                                       attrs={'td': {'class': 'col-sm-1'}},
+                                       verbose_name=_("Delete"),)
 
     def render_amount(self, value):
         return pretty_money(value)
diff --git a/apps/note/views.py b/apps/note/views.py
index 252792815f3ae58eacb42ded15b70376fe70c273..ac9b3e40623ce1ec676f340ddf7caa3a81639e43 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -9,6 +9,7 @@ from django_tables2 import SingleTableView
 from django.urls import reverse_lazy
 from note_kfet.inputs import AmountInput
 from permission.backends import PermissionBackend
+from permission.views import ProtectQuerysetMixin
 
 from .forms import TransactionTemplateForm
 from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
@@ -16,7 +17,7 @@ from .models.transactions import SpecialTransaction
 from .tables import HistoryTable, ButtonTable
 
 
-class TransactionCreateView(LoginRequiredMixin, SingleTableView):
+class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     """
     View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`.
     e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial`
@@ -26,12 +27,9 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
     model = Transaction
     # Transaction history table
     table_class = HistoryTable
-    table_pagination = {"per_page": 50}
 
-    def get_queryset(self):
-        return Transaction.objects.filter(PermissionBackend.filter_queryset(
-            self.request.user, Transaction, "view")
-        ).order_by("-id").all()[:50]
+    def get_queryset(self, **kwargs):
+        return super().get_queryset(**kwargs).order_by("-id").all()[:50]
 
     def get_context_data(self, **kwargs):
         """
@@ -42,12 +40,14 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
         context['amount_widget'] = AmountInput(attrs={"id": "amount"})
         context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
         context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
-        context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
+        context['special_types'] = NoteSpecial.objects\
+            .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
+            .order_by("special_type").all()
 
         return context
 
 
-class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
+class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     """
     Create TransactionTemplate
     """
@@ -56,7 +56,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
     success_url = reverse_lazy('note:template_list')
 
 
-class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
+class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     """
     List TransactionsTemplates
     """
@@ -64,7 +64,7 @@ class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
     table_class = ButtonTable
 
 
-class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
+class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     """
     """
     model = TransactionTemplate
@@ -72,21 +72,19 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
     success_url = reverse_lazy('note:template_list')
 
 
-class ConsoView(LoginRequiredMixin, SingleTableView):
+class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     """
     The Magic View that make people pay their beer and burgers.
     (Most of the magic happens in the dark world of Javascript see consos.js)
     """
+    model = Transaction
     template_name = "note/conso_form.html"
 
     # Transaction history table
     table_class = HistoryTable
-    table_pagination = {"per_page": 50}
 
-    def get_queryset(self):
-        return Transaction.objects.filter(
-            PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
-        ).order_by("-id").all()[:50]
+    def get_queryset(self, **kwargs):
+        return super().get_queryset(**kwargs).order_by("-id").all()[:50]
 
     def get_context_data(self, **kwargs):
         """
diff --git a/apps/permission/api/serializers.py b/apps/permission/api/serializers.py
index 0a52f4fe079d5ad60653e3f166b74596d0fc5e0c..e30ed7dcb6d3e66d7071d99ca997e701feafe94d 100644
--- a/apps/permission/api/serializers.py
+++ b/apps/permission/api/serializers.py
@@ -3,7 +3,7 @@
 
 from rest_framework import serializers
 
-from ..models import Permission
+from ..models import Permission, RolePermissions
 
 
 class PermissionSerializer(serializers.ModelSerializer):
@@ -15,3 +15,14 @@ class PermissionSerializer(serializers.ModelSerializer):
     class Meta:
         model = Permission
         fields = '__all__'
+
+
+class RolePermissionsSerializer(serializers.ModelSerializer):
+    """
+    REST API Serializer for RolePermissions types.
+    The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API.
+    """
+
+    class Meta:
+        model = RolePermissions
+        fields = '__all__'
diff --git a/apps/permission/api/urls.py b/apps/permission/api/urls.py
index d50344ea17773dd63be15e214515299536145d99..b5d534663cf0fabd21af89802e23920c43487411 100644
--- a/apps/permission/api/urls.py
+++ b/apps/permission/api/urls.py
@@ -1,11 +1,12 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from .views import PermissionViewSet
+from .views import PermissionViewSet, RolePermissionsViewSet
 
 
 def register_permission_urls(router, path):
     """
     Configure router for permission REST API.
     """
-    router.register(path, PermissionViewSet)
+    router.register(path + "/permission", PermissionViewSet)
+    router.register(path + "/roles", RolePermissionsViewSet)
diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py
index 965e82c928aa676555f4f8f47356284cbac6b0ff..6a068225668e51d90978765d9eb62e68f75eb22d 100644
--- a/apps/permission/api/views.py
+++ b/apps/permission/api/views.py
@@ -4,17 +4,29 @@
 from django_filters.rest_framework import DjangoFilterBackend
 from api.viewsets import ReadOnlyProtectedModelViewSet
 
-from .serializers import PermissionSerializer
-from ..models import Permission
+from .serializers import PermissionSerializer, RolePermissionsSerializer
+from ..models import Permission, RolePermissions
 
 
 class PermissionViewSet(ReadOnlyProtectedModelViewSet):
     """
     REST API View set.
-    The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
-    then render it on /api/logs/
+    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()
     serializer_class = PermissionSerializer
     filter_backends = [DjangoFilterBackend]
     filterset_fields = ['model', 'type', ]
+
+
+class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet):
+    """
+    REST API View set.
+    The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
+    then render it on /api/permission/roles/
+    """
+    queryset = RolePermissions.objects.all()
+    serializer_class = RolePermissionsSerializer
+    filter_backends = [DjangoFilterBackend]
+    filterset_fields = ['role', ]
diff --git a/apps/permission/backends.py b/apps/permission/backends.py
index e61b07191227d755f52ebe6953a0897029f980fe..4fb7b5775b1a0c4bd2431e175f7c60264ac3bfbe 100644
--- a/apps/permission/backends.py
+++ b/apps/permission/backends.py
@@ -1,6 +1,8 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+import datetime
+
 from django.contrib.auth.backends import ModelBackend
 from django.contrib.auth.models import User, AnonymousUser
 from django.contrib.contenttypes.models import ContentType
@@ -9,6 +11,7 @@ from note.models import Note, NoteUser, NoteClub, NoteSpecial
 from note_kfet.middlewares import get_current_session
 from member.models import Membership, Club
 
+from .decorators import memoize
 from .models import Permission
 
 
@@ -20,6 +23,28 @@ class PermissionBackend(ModelBackend):
     supports_anonymous_user = False
     supports_inactive_user = False
 
+    @staticmethod
+    @memoize
+    def get_raw_permissions(user, t):
+        """
+        Query permissions of a certain type for a user, then memoize it.
+        :param user: The owner of the permissions
+        :param t: The type of the permissions: view, change, add or delete
+        :return: The queryset of the permissions of the user (memoized) grouped by clubs
+        """
+        if isinstance(user, AnonymousUser):
+            # Unauthenticated users have no permissions
+            return Permission.objects.none()
+
+        return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
+            .filter(
+                rolepermissions__role__membership__user=user,
+                rolepermissions__role__membership__date_start__lte=datetime.date.today(),
+                rolepermissions__role__membership__date_end__gte=datetime.date.today(),
+                type=t,
+                mask__rank__lte=get_current_session().get("permission_mask", 0),
+        ).distinct('club', 'pk',)
+
     @staticmethod
     def permissions(user, model, type):
         """
@@ -29,16 +54,16 @@ class PermissionBackend(ModelBackend):
         :param type: The type of the permissions: view, change, add or delete
         :return: A generator of the requested permissions
         """
-        for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
-                .filter(
-            rolepermissions__role__membership__user=user,
-            model__app_label=model.app_label,  # For polymorphic models, we don't filter on model type
-            type=type,
-        ).all():
-            if not isinstance(model, permission.model.__class__):
+        clubs = {}
+
+        for permission in PermissionBackend.get_raw_permissions(user, type):
+            if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
                 continue
 
-            club = Club.objects.get(pk=permission.club)
+            if permission.club not in clubs:
+                clubs[permission.club] = club = Club.objects.get(pk=permission.club)
+            else:
+                club = clubs[permission.club]
             permission = permission.about(
                 user=user,
                 club=club,
@@ -52,10 +77,10 @@ class PermissionBackend(ModelBackend):
                 F=F,
                 Q=Q
             )
-            if permission.mask.rank <= get_current_session().get("permission_mask", 0):
-                yield permission
+            yield permission
 
     @staticmethod
+    @memoize
     def filter_queryset(user, model, t, field=None):
         """
         Filter a queryset by considering the permissions of a given user.
@@ -89,10 +114,23 @@ class PermissionBackend(ModelBackend):
             query = query | perm.query
         return query
 
-    def has_perm(self, user_obj, perm, obj=None):
+    @staticmethod
+    @memoize
+    def check_perm(user_obj, perm, obj=None):
+        """
+        Check is the given user has the permission over a given object.
+        The result is then memoized.
+        Exception: for add permissions, since the object is not hashable since it doesn't have any
+        primary key, the result is not memoized. Moreover, the right could change
+        (e.g. for a transaction, the balance of the user could change)
+        """
         if user_obj is None or isinstance(user_obj, AnonymousUser):
             return False
 
+        sess = get_current_session()
+        if sess is not None and sess.session_key is None:
+            return Permission.objects.none()
+
         if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
             return True
 
@@ -104,10 +142,13 @@ class PermissionBackend(ModelBackend):
         perm_field = perm[2] if len(perm) == 3 else None
         ct = ContentType.objects.get_for_model(obj)
         if any(permission.applies(obj, perm_type, perm_field)
-               for permission in self.permissions(user_obj, ct, perm_type)):
+               for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
             return True
         return False
 
+    def has_perm(self, user_obj, perm, obj=None):
+        return PermissionBackend.check_perm(user_obj, perm, obj)
+
     def has_module_perms(self, user_obj, app_label):
         return False
 
diff --git a/apps/permission/decorators.py b/apps/permission/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..f144935a7b1a3152c275d958e489dcec9f10c7e1
--- /dev/null
+++ b/apps/permission/decorators.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from functools import lru_cache
+from time import time
+
+from django.contrib.sessions.models import Session
+from note_kfet.middlewares import get_current_session
+
+
+def memoize(f):
+    """
+    Memoize results and store in sessions
+
+    This decorator is useful for permissions: they are loaded once needed, then stored for next calls.
+    The storage is contained with sessions since it depends on the selected mask.
+    """
+    sess_funs = {}
+    last_collect = time()
+
+    def collect():
+        """
+        Clear cache of results when sessions are invalid, to flush useless data.
+        This function is called every minute.
+        """
+        nonlocal sess_funs
+
+        new_sess_funs = {}
+        for sess_key in sess_funs:
+            if Session.objects.filter(session_key=sess_key).exists():
+                new_sess_funs[sess_key] = sess_funs[sess_key]
+        sess_funs = new_sess_funs
+
+    def func(*args, **kwargs):
+        nonlocal last_collect
+
+        if time() - last_collect > 60:
+            # Clear cache
+            collect()
+            last_collect = time()
+
+        # If there is no session, then we don't memoize anything.
+        sess = get_current_session()
+        if sess is None or sess.session_key is None:
+            return f(*args, **kwargs)
+
+        sess_key = sess.session_key
+        if sess_key not in sess_funs:
+            # lru_cache makes the job of memoization
+            # We store only the 512 latest data per session. It has to be enough.
+            sess_funs[sess_key] = lru_cache(512)(f)
+        try:
+            return sess_funs[sess_key](*args, **kwargs)
+        except TypeError:  # For add permissions, objects are not hashable (not yet created). Don't memoize this case.
+            return f(*args, **kwargs)
+
+    func.func_name = f.__name__
+
+    return func
diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json
index 31b59069ef832598460ae07716b30216ba4a781b..4cf3ecfa738ee500f3d6f42f53f9be9ce5ca0792 100644
--- a/apps/permission/fixtures/initial.json
+++ b/apps/permission/fixtures/initial.json
@@ -386,7 +386,7 @@
         "note",
         "transaction"
       ],
-      "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
+      "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
       "type": "add",
       "mask": 2,
       "field": "",
@@ -783,6 +783,66 @@
       "description": "Validate invitation transactions"
     }
   },
+  {
+    "model": "permission.permission",
+    "pk": 47,
+    "fields": {
+      "model": [
+        "member",
+        "club"
+      ],
+      "query": "{\"pk\": [\"club\", \"pk\"]}",
+      "type": "change",
+      "mask": 1,
+      "field": "",
+      "description": "Update club"
+    }
+  },
+  {
+    "model": "permission.permission",
+    "pk": 48,
+    "fields": {
+      "model": [
+        "member",
+        "membership"
+      ],
+      "query": "{\"user\": [\"user\"]}",
+      "type": "view",
+      "mask": 1,
+      "field": "",
+      "description": "View our memberships"
+    }
+  },
+  {
+    "model": "permission.permission",
+    "pk": 49,
+    "fields": {
+      "model": [
+        "member",
+        "membership"
+      ],
+      "query": "{\"club\": [\"club\"]}",
+      "type": "view",
+      "mask": 1,
+      "field": "",
+      "description": "View club's memberships"
+    }
+  },
+  {
+    "model": "permission.permission",
+    "pk": 50,
+    "fields": {
+      "model": [
+        "member",
+        "membership"
+      ],
+      "query": "{\"club\": [\"club\"]}",
+      "type": "add",
+      "mask": 2,
+      "field": "",
+      "description": "Add a membership to a club"
+    }
+  },
   {
     "model": "permission.rolepermissions",
     "pk": 1,
@@ -795,7 +855,8 @@
         8,
         9,
         10,
-        11
+        11,
+        48
       ]
     }
   },
@@ -880,5 +941,75 @@
         46
       ]
     }
+  },
+  {
+    "model": "permission.rolepermissions",
+    "pk": 6,
+    "fields": {
+      "role": 7,
+      "permissions": [
+        22,
+        47
+      ]
+    }
+  },
+  {
+    "model": "permission.rolepermissions",
+    "pk": 7,
+    "fields": {
+      "role": 5,
+      "permissions": [
+        1,
+        2,
+        3,
+        4,
+        5,
+        6,
+        7,
+        8,
+        9,
+        10,
+        11,
+        12,
+        13,
+        14,
+        15,
+        16,
+        17,
+        18,
+        19,
+        20,
+        21,
+        22,
+        23,
+        24,
+        25,
+        26,
+        27,
+        28,
+        29,
+        30,
+        31,
+        32,
+        33,
+        34,
+        35,
+        36,
+        37,
+        38,
+        39,
+        40,
+        41,
+        42,
+        43,
+        44,
+        45,
+        46,
+        47,
+        48,
+        49,
+        50
+      ]
+    }
   }
 ]
diff --git a/apps/permission/models.py b/apps/permission/models.py
index 205f5b418c7baf09254087c939a504ee919c7f0f..8aaf416c0d69d394bf476bc3a5537a6fb82c3a63 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -38,20 +38,33 @@ class InstancedPermission:
             if permission_type == self.type:
                 self.update_query()
 
-                # Don't increase indexes
-                obj.pk = 0
+                # Don't increase indexes, if the primary key is an AutoField
+                if not hasattr(obj, "pk") or not obj.pk:
+                    obj.pk = 0
+                    oldpk = None
+                else:
+                    oldpk = obj.pk
+                # Ensure previous models are deleted
+                self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete()
                 # Force insertion, no data verification, no trigger
+                obj._force_save = True
                 Model.save(obj, force_insert=True)
-                ret = obj in self.model.model_class().objects.filter(self.query).all()
+                # We don't want log anything
+                obj._no_log = True
+                ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists()
                 # Delete testing object
+                obj._force_delete = True
                 Model.delete(obj)
+
+                # If the primary key was specified, we restore it
+                obj.pk = oldpk
                 return ret
 
         if permission_type == self.type:
             if self.field and field_name != self.field:
                 return False
             self.update_query()
-            return obj in self.model.model_class().objects.filter(self.query).all()
+            return self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists()
         else:
             return False
 
diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py
index 7097085f04670a33fbb65431cfdc046e7e00d832..4032156767a0ad0cd68dbd9a3720482c809c6cdc 100644
--- a/apps/permission/permissions.py
+++ b/apps/permission/permissions.py
@@ -44,7 +44,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
 
         perms = self.get_required_object_permissions(request.method, model_cls)
         # if not user.has_perms(perms, obj):
-        if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms):
+        if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms):
             # If the user does not have permissions we need to determine if
             # they have read permissions to see 403, or not, and simply see
             # a 404 response.
diff --git a/apps/permission/signals.py b/apps/permission/signals.py
index 1e30f56f5793296e929b195231bb4a4eb4660d26..bf54b72f27f3c6e89fb525babe743989ac2cf7b6 100644
--- a/apps/permission/signals.py
+++ b/apps/permission/signals.py
@@ -2,8 +2,6 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from django.core.exceptions import PermissionDenied
-from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
-from logs import signals as logs_signals
 from note_kfet.middlewares import get_current_authenticated_user
 from permission.backends import PermissionBackend
 
@@ -29,6 +27,9 @@ def pre_save_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_force_save"):
+        return
+
     user = get_current_authenticated_user()
     if user is None:
         # Action performed on shell is always granted
@@ -43,7 +44,7 @@ def pre_save_object(sender, instance, **kwargs):
         # We check if the user can change the model
 
         # If the user has all right on a model, then OK
-        if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance):
+        if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance):
             return
 
         # In the other case, we check if he/she has the right to change one field
@@ -55,35 +56,17 @@ def pre_save_object(sender, instance, **kwargs):
             # If the field wasn't modified, no need to check the permissions
             if old_value == new_value:
                 continue
-            if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
+            if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
                 raise PermissionDenied
     else:
-        # We check if the user can add the model
-
-        # While checking permissions, the object will be inserted in the DB, then removed.
-        # We disable temporary the connectors
-        pre_save.disconnect(pre_save_object)
-        pre_delete.disconnect(pre_delete_object)
-        # We disable also logs connectors
-        pre_save.disconnect(logs_signals.pre_save_object)
-        post_save.disconnect(logs_signals.save_object)
-        post_delete.disconnect(logs_signals.delete_object)
-
         # We check if the user has right to add the object
-        has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance)
-
-        # Then we reconnect all
-        pre_save.connect(pre_save_object)
-        pre_delete.connect(pre_delete_object)
-        pre_save.connect(logs_signals.pre_save_object)
-        post_save.connect(logs_signals.save_object)
-        post_delete.connect(logs_signals.delete_object)
+        has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
 
         if not has_perm:
             raise PermissionDenied
 
 
-def pre_delete_object(sender, instance, **kwargs):
+def pre_delete_object(instance, **kwargs):
     """
     Before a model get deleted, we check the permissions
     """
@@ -91,6 +74,9 @@ def pre_delete_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_force_delete"):
+        return
+
     user = get_current_authenticated_user()
     if user is None:
         # Action performed on shell is always granted
@@ -101,5 +87,5 @@ def pre_delete_object(sender, instance, **kwargs):
     model_name = model_name_full[1]
 
     # We check if the user has rights to delete the object
-    if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance):
+    if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
         raise PermissionDenied
diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py
index aa2feeca1f6a110493e1626be689df06cc30cafd..a89c7f490d4701f9653c952d62eed5059466f25c 100644
--- a/apps/permission/templatetags/perms.py
+++ b/apps/permission/templatetags/perms.py
@@ -4,6 +4,7 @@
 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
 
@@ -19,13 +20,8 @@ def not_empty_model_list(model_name):
         return False
     elif user.is_superuser and session.get("permission_mask", 0) >= 42:
         return True
-    if session.get("not_empty_model_list_" + model_name, None):
-        return session.get("not_empty_model_list_" + model_name, None) == 1
-    spl = model_name.split(".")
-    ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
-    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all()
-    session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2
-    return session.get("not_empty_model_list_" + model_name) == 1
+    qs = model_list(model_name)
+    return qs.exists()
 
 
 @stringfilter
@@ -39,20 +35,54 @@ def not_empty_model_change_list(model_name):
         return False
     elif user.is_superuser and session.get("permission_mask", 0) >= 42:
         return True
-    if session.get("not_empty_model_change_list_" + model_name, None):
-        return session.get("not_empty_model_change_list_" + model_name, None) == 1
+    qs = model_list(model_name, "change")
+    return qs.exists()
+
+
+@stringfilter
+def model_list(model_name, t="view"):
+    """
+    Return the queryset of all visible instances of the given model.
+    """
+    user = get_current_authenticated_user()
+    if user is None:
+        return False
     spl = model_name.split(".")
     ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
-    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change"))
-    session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2
-    return session.get("not_empty_model_change_list_" + model_name) == 1
+    qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all()
+    return qs
 
 
 def has_perm(perm, obj):
-    return PermissionBackend().has_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:
+        return False
+    elif user.is_superuser and session.get("permission_mask", 0) >= 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('has_perm', has_perm)
diff --git a/apps/permission/views.py b/apps/permission/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbd9872f92d6d356ae36b87147eccd3a1bb56ac4
--- /dev/null
+++ b/apps/permission/views.py
@@ -0,0 +1,11 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from permission.backends import PermissionBackend
+
+
+class ProtectQuerysetMixin:
+    def get_queryset(self, **kwargs):
+        qs = super().get_queryset(**kwargs)
+
+        return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py
index 7fe7de4c3da65bc82e4b854353f4b756f7e33faa..ad479e143ed33ba07275261ecf86a0c2c09fc8b9 100644
--- a/apps/treasury/forms.py
+++ b/apps/treasury/forms.py
@@ -8,6 +8,7 @@ from crispy_forms.layout import Submit
 from django import forms
 from django.utils.translation import gettext_lazy as _
 from note_kfet.inputs import DatePickerInput, AmountInput
+from permission.backends import PermissionBackend
 
 from .models import Invoice, Product, Remittance, SpecialTransactionProxy
 
@@ -131,7 +132,8 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
         # Add submit button
         self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
 
-        self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
+        self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\
+            .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
 
     def clean_last_name(self):
         """
diff --git a/apps/treasury/views.py b/apps/treasury/views.py
index c374ced102a3e342d405e16f7585e568f30bba5c..f564ccb2b892650310a1a2806e98d78c2cb62a36 100644
--- a/apps/treasury/views.py
+++ b/apps/treasury/views.py
@@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView
 from django_tables2 import SingleTableView
 from note.models import SpecialTransaction, NoteSpecial
 from note_kfet.settings.base import BASE_DIR
+from permission.backends import PermissionBackend
+from permission.views import ProtectQuerysetMixin
 
 from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
 from .models import Invoice, Product, Remittance, SpecialTransactionProxy
 from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
 
 
-class InvoiceCreateView(LoginRequiredMixin, CreateView):
+class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     """
     Create Invoice
     """
@@ -67,7 +69,7 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
         return reverse_lazy('treasury:invoice_list')
 
 
-class InvoiceListView(LoginRequiredMixin, SingleTableView):
+class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
     """
     List existing Invoices
     """
@@ -75,7 +77,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
     table_class = InvoiceTable
 
 
-class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
+class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     """
     Create Invoice
     """
@@ -130,7 +132,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
 
     def get(self, request, **kwargs):
         pk = kwargs["pk"]
-        invoice = Invoice.objects.get(pk=pk)
+        invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk)
         products = Product.objects.filter(invoice=invoice).all()
 
         # Informations of the BDE. Should be updated when the school will move.
@@ -188,7 +190,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
         return response
 
 
-class RemittanceCreateView(LoginRequiredMixin, CreateView):
+class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
     """
     Create Remittance
     """
@@ -201,7 +203,9 @@ class RemittanceCreateView(LoginRequiredMixin, CreateView):
     def get_context_data(self, **kwargs):
         ctx = super().get_context_data(**kwargs)
 
-        ctx["table"] = RemittanceTable(data=Remittance.objects.all())
+        ctx["table"] = RemittanceTable(data=Remittance.objects
+                                       .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
+                                       .all())
         ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
 
         return ctx
@@ -216,22 +220,28 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
     def get_context_data(self, **kwargs):
         ctx = super().get_context_data(**kwargs)
 
-        ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all())
-        ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all())
+        ctx["opened_remittances"] = RemittanceTable(
+            data=Remittance.objects.filter(closed=False).filter(
+                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
+        ctx["closed_remittances"] = RemittanceTable(
+            data=Remittance.objects.filter(closed=True).filter(
+                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all())
 
         ctx["special_transactions_no_remittance"] = SpecialTransactionTable(
             data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
-                                                   specialtransactionproxy__remittance=None).all(),
+                                                   specialtransactionproxy__remittance=None).filter(
+                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
             exclude=('remittance_remove', ))
         ctx["special_transactions_with_remittance"] = SpecialTransactionTable(
             data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
-                                                   specialtransactionproxy__remittance__closed=False).all(),
+                                                   specialtransactionproxy__remittance__closed=False).filter(
+                PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
             exclude=('remittance_add', ))
 
         return ctx
 
 
-class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
+class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     """
     Update Remittance
     """
@@ -244,8 +254,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
     def get_context_data(self, **kwargs):
         ctx = super().get_context_data(**kwargs)
 
-        ctx["table"] = RemittanceTable(data=Remittance.objects.all())
-        data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all()
+        ctx["table"] = RemittanceTable(data=Remittance.objects.filter(
+            PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
+        data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
+            PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()
         ctx["special_transactions"] = SpecialTransactionTable(
             data=data,
             exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
@@ -253,7 +265,7 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
         return ctx
 
 
-class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
+class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
     """
     Attach a special transaction to a remittance
     """
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index e2c8075ee9e4db5db143be7e99997a5a37cc0398..16c73f358acba2b9afdb2ad6ef07d99bc6f03aca 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-30 17:31+0200\n"
+"POT-Creation-Date: 2020-04-01 18:39+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -44,9 +44,9 @@ msgid "You can't invite more than 3 people to this activity."
 msgstr ""
 
 #: apps/activity/models.py:23 apps/activity/models.py:48
-#: apps/member/models.py:64 apps/member/models.py:122
+#: apps/member/models.py:66 apps/member/models.py:169
 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
-#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231
+#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:232
 #: templates/member/club_info.html:13 templates/member/profile_info.html:14
 msgid "name"
 msgstr ""
@@ -68,7 +68,7 @@ msgid "activity types"
 msgstr ""
 
 #: apps/activity/models.py:53 apps/note/models/transactions.py:69
-#: apps/permission/models.py:90 templates/activity/activity_detail.html:16
+#: apps/permission/models.py:103 templates/activity/activity_detail.html:16
 msgid "description"
 msgstr ""
 
@@ -78,7 +78,7 @@ msgstr ""
 msgid "type"
 msgstr ""
 
-#: apps/activity/models.py:66 apps/logs/models.py:21
+#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:190
 #: apps/note/models/notes.py:117
 msgid "user"
 msgstr ""
@@ -169,11 +169,11 @@ msgstr ""
 msgid "Type"
 msgstr ""
 
-#: apps/activity/tables.py:77 apps/treasury/forms.py:120
+#: apps/activity/tables.py:77 apps/treasury/forms.py:121
 msgid "Last name"
 msgstr ""
 
-#: apps/activity/tables.py:79 apps/treasury/forms.py:122
+#: apps/activity/tables.py:79 apps/treasury/forms.py:123
 #: templates/note/transaction_form.html:92
 msgid "First name"
 msgstr ""
@@ -186,11 +186,11 @@ msgstr ""
 msgid "Balance"
 msgstr ""
 
-#: apps/activity/views.py:44 templates/base.html:94
+#: apps/activity/views.py:45 templates/base.html:94
 msgid "Activities"
 msgstr ""
 
-#: apps/activity/views.py:149
+#: apps/activity/views.py:153
 msgid "Entry for activity \"{}\""
 msgstr ""
 
@@ -251,121 +251,165 @@ msgstr ""
 msgid "member"
 msgstr ""
 
-#: apps/member/models.py:26
+#: apps/member/models.py:28
 msgid "phone number"
 msgstr ""
 
-#: apps/member/models.py:32 templates/member/profile_info.html:27
+#: apps/member/models.py:34 templates/member/profile_info.html:27
 msgid "section"
 msgstr ""
 
-#: apps/member/models.py:33
+#: apps/member/models.py:35
 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
 msgstr ""
 
-#: apps/member/models.py:39 templates/member/profile_info.html:30
+#: apps/member/models.py:41 templates/member/profile_info.html:30
 msgid "address"
 msgstr ""
 
-#: apps/member/models.py:45
+#: apps/member/models.py:47
 msgid "paid"
 msgstr ""
 
-#: apps/member/models.py:50 apps/member/models.py:51
+#: apps/member/models.py:52 apps/member/models.py:53
 msgid "user profile"
 msgstr ""
 
-#: apps/member/models.py:69 templates/member/club_info.html:36
+#: apps/member/models.py:71 templates/member/club_info.html:46
 msgid "email"
 msgstr ""
 
-#: apps/member/models.py:76
+#: apps/member/models.py:78
 msgid "parent club"
 msgstr ""
 
-#: apps/member/models.py:81 templates/member/club_info.html:30
-msgid "membership fee"
+#: apps/member/models.py:87
+msgid "require memberships"
+msgstr ""
+
+#: apps/member/models.py:88
+msgid "Uncheck if this club don't require memberships."
+msgstr ""
+
+#: apps/member/models.py:93 templates/member/club_info.html:35
+msgid "membership fee (paid students)"
 msgstr ""
 
-#: apps/member/models.py:85 templates/member/club_info.html:27
+#: apps/member/models.py:98 templates/member/club_info.html:38
+msgid "membership fee (unpaid students)"
+msgstr ""
+
+#: apps/member/models.py:104 templates/member/club_info.html:28
 msgid "membership duration"
 msgstr ""
 
-#: apps/member/models.py:86
-msgid "The longest time a membership can last (NULL = infinite)."
+#: apps/member/models.py:105
+msgid "The longest time (in days) a membership can last (NULL = infinite)."
 msgstr ""
 
-#: apps/member/models.py:91 templates/member/club_info.html:21
+#: apps/member/models.py:112 templates/member/club_info.html:22
 msgid "membership start"
 msgstr ""
 
-#: apps/member/models.py:92
+#: apps/member/models.py:113
 msgid "How long after January 1st the members can renew their membership."
 msgstr ""
 
-#: apps/member/models.py:97 templates/member/club_info.html:24
+#: apps/member/models.py:120 templates/member/club_info.html:25
 msgid "membership end"
 msgstr ""
 
-#: apps/member/models.py:98
+#: apps/member/models.py:121
 msgid ""
 "How long the membership can last after January 1st of the next year after "
 "members can renew their membership."
 msgstr ""
 
-#: apps/member/models.py:104 apps/note/models/notes.py:139
+#: apps/member/models.py:154 apps/member/models.py:196
+#: apps/note/models/notes.py:139
 msgid "club"
 msgstr ""
 
-#: apps/member/models.py:105
+#: apps/member/models.py:155
 msgid "clubs"
 msgstr ""
 
-#: apps/member/models.py:128 apps/permission/models.py:275
+#: apps/member/models.py:175 apps/permission/models.py:288
 msgid "role"
 msgstr ""
 
-#: apps/member/models.py:129
+#: apps/member/models.py:176 apps/member/models.py:201
 msgid "roles"
 msgstr ""
 
-#: apps/member/models.py:153
+#: apps/member/models.py:205
 msgid "membership starts on"
 msgstr ""
 
-#: apps/member/models.py:156
+#: apps/member/models.py:209
 msgid "membership ends on"
 msgstr ""
 
-#: apps/member/models.py:160
+#: apps/member/models.py:214
 msgid "fee"
 msgstr ""
 
-#: apps/member/models.py:172
+#: apps/member/models.py:226 apps/member/views.py:383
 msgid "User is not a member of the parent club"
 msgstr ""
 
-#: apps/member/models.py:176
+#: apps/member/models.py:236 apps/member/views.py:392
+msgid "User is already a member of the club"
+msgstr ""
+
+#: apps/member/models.py:271
+#, python-brace-format
+msgid "Membership of {user} for the club {club}"
+msgstr ""
+
+#: apps/member/models.py:274
 msgid "membership"
 msgstr ""
 
-#: apps/member/models.py:177
+#: apps/member/models.py:275
 msgid "memberships"
 msgstr ""
 
-#: apps/member/views.py:76 templates/member/profile_info.html:45
+#: apps/member/tables.py:73
+msgid "Renew"
+msgstr ""
+
+#: apps/member/views.py:80 templates/member/profile_info.html:45
 msgid "Update Profile"
 msgstr ""
 
-#: apps/member/views.py:89
+#: apps/member/views.py:93
 msgid "An alias with a similar name already exists."
 msgstr ""
 
+#: apps/member/views.py:379
+msgid ""
+"This user don't have enough money to join this club, and can't have a "
+"negative balance."
+msgstr ""
+
+#: apps/member/views.py:396 apps/member/views.py:428
+msgid "The membership must start after {:%m-%d-%Y}."
+msgstr ""
+
+#: apps/member/views.py:401 apps/member/views.py:433
+msgid "The membership must begin before {:%m-%d-%Y}."
+msgstr ""
+
+#: apps/member/views.py:455
+msgid "This membership is already renewed"
+msgstr ""
+
 #: apps/note/admin.py:120 apps/note/models/transactions.py:94
 msgid "source"
 msgstr ""
 
-#: apps/note/admin.py:128 apps/note/admin.py:156
+#: apps/note/admin.py:128 apps/note/admin.py:163
 #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107
 msgid "destination"
 msgstr ""
@@ -462,7 +506,7 @@ msgstr ""
 msgid "alias"
 msgstr ""
 
-#: apps/note/models/notes.py:211 templates/member/club_info.html:33
+#: apps/note/models/notes.py:211 templates/member/club_info.html:43
 #: templates/member/profile_info.html:36
 msgid "aliases"
 msgstr ""
@@ -524,45 +568,45 @@ msgstr ""
 msgid "invalidity reason"
 msgstr ""
 
-#: apps/note/models/transactions.py:146
+#: apps/note/models/transactions.py:147
 msgid "transaction"
 msgstr ""
 
-#: apps/note/models/transactions.py:147
+#: apps/note/models/transactions.py:148
 msgid "transactions"
 msgstr ""
 
-#: apps/note/models/transactions.py:201 templates/base.html:84
+#: apps/note/models/transactions.py:202 templates/base.html:84
 #: templates/note/transaction_form.html:19
 #: templates/note/transaction_form.html:140
 msgid "Transfer"
 msgstr ""
 
-#: apps/note/models/transactions.py:221
+#: apps/note/models/transactions.py:222
 msgid "Template"
 msgstr ""
 
-#: apps/note/models/transactions.py:236
+#: apps/note/models/transactions.py:237
 msgid "first_name"
 msgstr ""
 
-#: apps/note/models/transactions.py:241
+#: apps/note/models/transactions.py:242
 msgid "bank"
 msgstr ""
 
-#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24
+#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24
 msgid "Credit"
 msgstr ""
 
-#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28
+#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28
 msgid "Debit"
 msgstr ""
 
-#: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268
+#: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269
 msgid "membership transaction"
 msgstr ""
 
-#: apps/note/models/transactions.py:264
+#: apps/note/models/transactions.py:265
 msgid "membership transactions"
 msgstr ""
 
@@ -578,29 +622,29 @@ msgstr ""
 msgid "No reason specified"
 msgstr ""
 
-#: apps/note/views.py:41
+#: apps/note/views.py:39
 msgid "Transfer money"
 msgstr ""
 
-#: apps/note/views.py:102 templates/base.html:79
+#: apps/note/views.py:100 templates/base.html:79
 msgid "Consumptions"
 msgstr ""
 
-#: apps/permission/models.py:69 apps/permission/models.py:262
+#: apps/permission/models.py:82 apps/permission/models.py:275
 #, python-brace-format
 msgid "Can {type} {model}.{field} in {query}"
 msgstr ""
 
-#: apps/permission/models.py:71 apps/permission/models.py:264
+#: apps/permission/models.py:84 apps/permission/models.py:277
 #, python-brace-format
 msgid "Can {type} {model} in {query}"
 msgstr ""
 
-#: apps/permission/models.py:84
+#: apps/permission/models.py:97
 msgid "rank"
 msgstr ""
 
-#: apps/permission/models.py:147
+#: apps/permission/models.py:160
 msgid "Specifying field applies only to view and change permission types."
 msgstr ""
 
@@ -608,31 +652,32 @@ msgstr ""
 msgid "Treasury"
 msgstr ""
 
-#: apps/treasury/forms.py:84 apps/treasury/forms.py:132
+#: apps/treasury/forms.py:85 apps/treasury/forms.py:133
 #: templates/activity/activity_form.html:9
 #: templates/activity/activity_invite.html:8
 #: templates/django_filters/rest_framework/form.html:5
-#: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46
+#: templates/member/add_members.html:14 templates/member/club_form.html:9
+#: templates/treasury/invoice_form.html:46
 msgid "Submit"
 msgstr ""
 
-#: apps/treasury/forms.py:86
+#: apps/treasury/forms.py:87
 msgid "Close"
 msgstr ""
 
-#: apps/treasury/forms.py:95
+#: apps/treasury/forms.py:96
 msgid "Remittance is already closed."
 msgstr ""
 
-#: apps/treasury/forms.py:100
+#: apps/treasury/forms.py:101
 msgid "You can't change the type of the remittance."
 msgstr ""
 
-#: apps/treasury/forms.py:124 templates/note/transaction_form.html:98
+#: apps/treasury/forms.py:125 templates/note/transaction_form.html:98
 msgid "Bank"
 msgstr ""
 
-#: apps/treasury/forms.py:126 apps/treasury/tables.py:47
+#: apps/treasury/forms.py:127 apps/treasury/tables.py:47
 #: templates/note/transaction_form.html:128
 #: templates/treasury/remittance_form.html:18
 msgid "Amount"
@@ -879,19 +924,23 @@ msgstr ""
 msgid "Club Parent"
 msgstr ""
 
-#: templates/member/club_info.html:41
-msgid "Add member"
+#: templates/member/club_info.html:29
+msgid "days"
 msgstr ""
 
-#: templates/member/club_info.html:42 templates/note/conso_form.html:121
-msgid "Edit"
+#: templates/member/club_info.html:32
+msgid "membership fee"
 msgstr ""
 
-#: templates/member/club_info.html:43
-msgid "Add roles"
+#: templates/member/club_info.html:52
+msgid "Add member"
+msgstr ""
+
+#: templates/member/club_info.html:55 templates/note/conso_form.html:121
+msgid "Edit"
 msgstr ""
 
-#: templates/member/club_info.html:46 templates/member/profile_info.html:48
+#: templates/member/club_info.html:59 templates/member/profile_info.html:48
 msgid "View Profile"
 msgstr ""
 
@@ -900,7 +949,7 @@ msgid "search clubs"
 msgstr ""
 
 #: templates/member/club_list.html:12
-msgid "Créer un club"
+msgid "Create club"
 msgstr ""
 
 #: templates/member/club_list.html:19
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 08c1b174ec53b4721043b2124f48c2eaf4305922..9754fa9c4e2aa3dc91b5cb21af50d286e2036c62 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -3,7 +3,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-30 17:31+0200\n"
+"POT-Creation-Date: 2020-04-01 18:39+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -20,7 +20,8 @@ msgstr "activité"
 
 #: apps/activity/forms.py:45 apps/activity/models.py:217
 msgid "You can't invite someone once the activity is started."
-msgstr "Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
+msgstr ""
+"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
 
 #: apps/activity/forms.py:48 apps/activity/models.py:220
 msgid "This activity is not validated yet."
@@ -39,9 +40,9 @@ msgid "You can't invite more than 3 people to this activity."
 msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
 
 #: apps/activity/models.py:23 apps/activity/models.py:48
-#: apps/member/models.py:64 apps/member/models.py:122
+#: apps/member/models.py:66 apps/member/models.py:169
 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
-#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231
+#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:232
 #: templates/member/club_info.html:13 templates/member/profile_info.html:14
 msgid "name"
 msgstr "nom"
@@ -63,7 +64,7 @@ msgid "activity types"
 msgstr "types d'activité"
 
 #: apps/activity/models.py:53 apps/note/models/transactions.py:69
-#: apps/permission/models.py:90 templates/activity/activity_detail.html:16
+#: apps/permission/models.py:103 templates/activity/activity_detail.html:16
 msgid "description"
 msgstr "description"
 
@@ -73,7 +74,7 @@ msgstr "description"
 msgid "type"
 msgstr "type"
 
-#: apps/activity/models.py:66 apps/logs/models.py:21
+#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:190
 #: apps/note/models/notes.py:117
 msgid "user"
 msgstr "utilisateur"
@@ -164,11 +165,11 @@ msgstr "supprimer"
 msgid "Type"
 msgstr "Type"
 
-#: apps/activity/tables.py:77 apps/treasury/forms.py:120
+#: apps/activity/tables.py:77 apps/treasury/forms.py:121
 msgid "Last name"
 msgstr "Nom de famille"
 
-#: apps/activity/tables.py:79 apps/treasury/forms.py:122
+#: apps/activity/tables.py:79 apps/treasury/forms.py:123
 #: templates/note/transaction_form.html:92
 msgid "First name"
 msgstr "Prénom"
@@ -181,11 +182,11 @@ msgstr "Note"
 msgid "Balance"
 msgstr "Solde du compte"
 
-#: apps/activity/views.py:44 templates/base.html:94
+#: apps/activity/views.py:45 templates/base.html:94
 msgid "Activities"
 msgstr "Activités"
 
-#: apps/activity/views.py:149
+#: apps/activity/views.py:153
 msgid "Entry for activity \"{}\""
 msgstr "Entrées pour l'activité « {} »"
 
@@ -246,65 +247,77 @@ msgstr "Les logs ne peuvent pas être détruits."
 msgid "member"
 msgstr "adhérent"
 
-#: apps/member/models.py:26
+#: apps/member/models.py:28
 msgid "phone number"
 msgstr "numéro de téléphone"
 
-#: apps/member/models.py:32 templates/member/profile_info.html:27
+#: apps/member/models.py:34 templates/member/profile_info.html:27
 msgid "section"
 msgstr "section"
 
-#: apps/member/models.py:33
+#: apps/member/models.py:35
 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
 msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
 
-#: apps/member/models.py:39 templates/member/profile_info.html:30
+#: apps/member/models.py:41 templates/member/profile_info.html:30
 msgid "address"
 msgstr "adresse"
 
-#: apps/member/models.py:45
+#: apps/member/models.py:47
 msgid "paid"
 msgstr "payé"
 
-#: apps/member/models.py:50 apps/member/models.py:51
+#: apps/member/models.py:52 apps/member/models.py:53
 msgid "user profile"
 msgstr "profil utilisateur"
 
-#: apps/member/models.py:69 templates/member/club_info.html:36
+#: apps/member/models.py:71 templates/member/club_info.html:46
 msgid "email"
 msgstr "courriel"
 
-#: apps/member/models.py:76
+#: apps/member/models.py:78
 msgid "parent club"
 msgstr "club parent"
 
-#: apps/member/models.py:81 templates/member/club_info.html:30
-msgid "membership fee"
-msgstr "cotisation pour adhérer"
+#: apps/member/models.py:87
+msgid "require memberships"
+msgstr "nécessite des adhésions"
+
+#: apps/member/models.py:88
+msgid "Uncheck if this club don't require memberships."
+msgstr "Décochez si ce club n'utilise pas d'adhésions."
 
-#: apps/member/models.py:85 templates/member/club_info.html:27
+#: apps/member/models.py:93 templates/member/club_info.html:35
+msgid "membership fee (paid students)"
+msgstr "cotisation pour adhérer (normalien élève)"
+
+#: apps/member/models.py:98 templates/member/club_info.html:38
+msgid "membership fee (unpaid students)"
+msgstr "cotisation pour adhérer (normalien étudiant)"
+
+#: apps/member/models.py:104 templates/member/club_info.html:28
 msgid "membership duration"
 msgstr "durée de l'adhésion"
 
-#: apps/member/models.py:86
-msgid "The longest time a membership can last (NULL = infinite)."
-msgstr "La durée maximale d'une adhésion (NULL = infinie)."
+#: apps/member/models.py:105
+msgid "The longest time (in days) a membership can last (NULL = infinite)."
+msgstr "La durée maximale (en jours) d'une adhésion (NULL = infinie)."
 
-#: apps/member/models.py:91 templates/member/club_info.html:21
+#: apps/member/models.py:112 templates/member/club_info.html:22
 msgid "membership start"
 msgstr "début de l'adhésion"
 
-#: apps/member/models.py:92
+#: apps/member/models.py:113
 msgid "How long after January 1st the members can renew their membership."
 msgstr ""
 "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
 "adhésion."
 
-#: apps/member/models.py:97 templates/member/club_info.html:24
+#: apps/member/models.py:120 templates/member/club_info.html:25
 msgid "membership end"
 msgstr "fin de l'adhésion"
 
-#: apps/member/models.py:98
+#: apps/member/models.py:121
 msgid ""
 "How long the membership can last after January 1st of the next year after "
 "members can renew their membership."
@@ -312,59 +325,91 @@ msgstr ""
 "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année "
 "suivante avant que les adhérents peuvent renouveler leur adhésion."
 
-#: apps/member/models.py:104 apps/note/models/notes.py:139
+#: apps/member/models.py:154 apps/member/models.py:196
+#: apps/note/models/notes.py:139
 msgid "club"
 msgstr "club"
 
-#: apps/member/models.py:105
+#: apps/member/models.py:155
 msgid "clubs"
 msgstr "clubs"
 
-#: apps/member/models.py:128 apps/permission/models.py:275
+#: apps/member/models.py:175 apps/permission/models.py:288
 msgid "role"
 msgstr "rôle"
 
-#: apps/member/models.py:129
+#: apps/member/models.py:176 apps/member/models.py:201
 msgid "roles"
 msgstr "rôles"
 
-#: apps/member/models.py:153
+#: apps/member/models.py:205
 msgid "membership starts on"
 msgstr "l'adhésion commence le"
 
-#: apps/member/models.py:156
+#: apps/member/models.py:209
 msgid "membership ends on"
-msgstr "l'adhésion finie le"
+msgstr "l'adhésion finit le"
 
-#: apps/member/models.py:160
+#: apps/member/models.py:214
 msgid "fee"
 msgstr "cotisation"
 
-#: apps/member/models.py:172
+#: apps/member/models.py:226 apps/member/views.py:383
 msgid "User is not a member of the parent club"
 msgstr "L'utilisateur n'est pas membre du club parent"
 
-#: apps/member/models.py:176
+#: apps/member/models.py:236 apps/member/views.py:392
+msgid "User is already a member of the club"
+msgstr "L'utilisateur est déjà membre du club"
+
+#: apps/member/models.py:271
+#, python-brace-format
+msgid "Membership of {user} for the club {club}"
+msgstr "Adhésion de {user} pour le club {club}"
+
+#: apps/member/models.py:274
 msgid "membership"
 msgstr "adhésion"
 
-#: apps/member/models.py:177
+#: apps/member/models.py:275
 msgid "memberships"
 msgstr "adhésions"
 
-#: apps/member/views.py:76 templates/member/profile_info.html:45
+#: apps/member/tables.py:73
+msgid "Renew"
+msgstr ""
+
+#: apps/member/views.py:80 templates/member/profile_info.html:45
 msgid "Update Profile"
 msgstr "Modifier le profil"
 
-#: apps/member/views.py:89
+#: apps/member/views.py:93
 msgid "An alias with a similar name already exists."
 msgstr "Un alias avec un nom similaire existe déjà."
 
+#: apps/member/views.py:379
+msgid ""
+"This user don't have enough money to join this club, and can't have a "
+"negative balance."
+msgstr ""
+
+#: apps/member/views.py:396 apps/member/views.py:428
+msgid "The membership must start after {:%m-%d-%Y}."
+msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}."
+
+#: apps/member/views.py:401 apps/member/views.py:433
+msgid "The membership must begin before {:%m-%d-%Y}."
+msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}."
+
+#: apps/member/views.py:455
+msgid "This membership is already renewed"
+msgstr "Cette adhésion est déjà renouvelée"
+
 #: apps/note/admin.py:120 apps/note/models/transactions.py:94
 msgid "source"
 msgstr "source"
 
-#: apps/note/admin.py:128 apps/note/admin.py:156
+#: apps/note/admin.py:128 apps/note/admin.py:163
 #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107
 msgid "destination"
 msgstr "destination"
@@ -462,7 +507,7 @@ msgstr "Alias invalide"
 msgid "alias"
 msgstr "alias"
 
-#: apps/note/models/notes.py:211 templates/member/club_info.html:33
+#: apps/note/models/notes.py:211 templates/member/club_info.html:43
 #: templates/member/profile_info.html:36
 msgid "aliases"
 msgstr "alias"
@@ -524,45 +569,45 @@ msgstr "raison"
 msgid "invalidity reason"
 msgstr "Motif d'invalidité"
 
-#: apps/note/models/transactions.py:146
+#: apps/note/models/transactions.py:147
 msgid "transaction"
 msgstr "transaction"
 
-#: apps/note/models/transactions.py:147
+#: apps/note/models/transactions.py:148
 msgid "transactions"
 msgstr "transactions"
 
-#: apps/note/models/transactions.py:201 templates/base.html:84
+#: apps/note/models/transactions.py:202 templates/base.html:84
 #: templates/note/transaction_form.html:19
 #: templates/note/transaction_form.html:140
 msgid "Transfer"
 msgstr "Virement"
 
-#: apps/note/models/transactions.py:221
+#: apps/note/models/transactions.py:222
 msgid "Template"
 msgstr "Bouton"
 
-#: apps/note/models/transactions.py:236
+#: apps/note/models/transactions.py:237
 msgid "first_name"
 msgstr "prénom"
 
-#: apps/note/models/transactions.py:241
+#: apps/note/models/transactions.py:242
 msgid "bank"
 msgstr "banque"
 
-#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24
+#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24
 msgid "Credit"
 msgstr "Crédit"
 
-#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28
+#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28
 msgid "Debit"
 msgstr "Débit"
 
-#: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268
+#: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269
 msgid "membership transaction"
 msgstr "transaction d'adhésion"
 
-#: apps/note/models/transactions.py:264
+#: apps/note/models/transactions.py:265
 msgid "membership transactions"
 msgstr "transactions d'adhésion"
 
@@ -578,29 +623,29 @@ msgstr "Cliquez pour valider"
 msgid "No reason specified"
 msgstr "Pas de motif spécifié"
 
-#: apps/note/views.py:41
+#: apps/note/views.py:39
 msgid "Transfer money"
 msgstr "Transférer de l'argent"
 
-#: apps/note/views.py:102 templates/base.html:79
+#: apps/note/views.py:100 templates/base.html:79
 msgid "Consumptions"
 msgstr "Consommations"
 
-#: apps/permission/models.py:69 apps/permission/models.py:262
+#: apps/permission/models.py:82 apps/permission/models.py:275
 #, python-brace-format
 msgid "Can {type} {model}.{field} in {query}"
 msgstr ""
 
-#: apps/permission/models.py:71 apps/permission/models.py:264
+#: apps/permission/models.py:84 apps/permission/models.py:277
 #, python-brace-format
 msgid "Can {type} {model} in {query}"
 msgstr ""
 
-#: apps/permission/models.py:84
+#: apps/permission/models.py:97
 msgid "rank"
 msgstr "Rang"
 
-#: apps/permission/models.py:147
+#: apps/permission/models.py:160
 msgid "Specifying field applies only to view and change permission types."
 msgstr ""
 
@@ -608,31 +653,32 @@ msgstr ""
 msgid "Treasury"
 msgstr "Trésorerie"
 
-#: apps/treasury/forms.py:84 apps/treasury/forms.py:132
+#: apps/treasury/forms.py:85 apps/treasury/forms.py:133
 #: templates/activity/activity_form.html:9
 #: templates/activity/activity_invite.html:8
 #: templates/django_filters/rest_framework/form.html:5
-#: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46
+#: templates/member/add_members.html:14 templates/member/club_form.html:9
+#: templates/treasury/invoice_form.html:46
 msgid "Submit"
 msgstr "Envoyer"
 
-#: apps/treasury/forms.py:86
+#: apps/treasury/forms.py:87
 msgid "Close"
 msgstr "Fermer"
 
-#: apps/treasury/forms.py:95
+#: apps/treasury/forms.py:96
 msgid "Remittance is already closed."
 msgstr "La remise est déjà fermée."
 
-#: apps/treasury/forms.py:100
+#: apps/treasury/forms.py:101
 msgid "You can't change the type of the remittance."
 msgstr "Vous ne pouvez pas changer le type de la remise."
 
-#: apps/treasury/forms.py:124 templates/note/transaction_form.html:98
+#: apps/treasury/forms.py:125 templates/note/transaction_form.html:98
 msgid "Bank"
 msgstr "Banque"
 
-#: apps/treasury/forms.py:126 apps/treasury/tables.py:47
+#: apps/treasury/forms.py:127 apps/treasury/tables.py:47
 #: templates/note/transaction_form.html:128
 #: templates/treasury/remittance_form.html:18
 msgid "Amount"
@@ -881,19 +927,23 @@ msgstr "Ajouter un alias"
 msgid "Club Parent"
 msgstr "Club parent"
 
-#: templates/member/club_info.html:41
+#: templates/member/club_info.html:29
+msgid "days"
+msgstr "jours"
+
+#: templates/member/club_info.html:32
+msgid "membership fee"
+msgstr "cotisation pour adhérer"
+
+#: templates/member/club_info.html:52
 msgid "Add member"
 msgstr "Ajouter un membre"
 
-#: templates/member/club_info.html:42 templates/note/conso_form.html:121
+#: templates/member/club_info.html:55 templates/note/conso_form.html:121
 msgid "Edit"
 msgstr "Éditer"
 
-#: templates/member/club_info.html:43
-msgid "Add roles"
-msgstr "Ajouter des rôles"
-
-#: templates/member/club_info.html:46 templates/member/profile_info.html:48
+#: templates/member/club_info.html:59 templates/member/profile_info.html:48
 msgid "View Profile"
 msgstr "Voir le profil"
 
@@ -902,8 +952,8 @@ msgid "search clubs"
 msgstr "Chercher un club"
 
 #: templates/member/club_list.html:12
-msgid "Créer un club"
-msgstr ""
+msgid "Create club"
+msgstr "Créer un club"
 
 #: templates/member/club_list.html:19
 msgid "club listing "
diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py
index a31700076833cc51586b44c332f70801881e3525..ecd758e058cfa995f54974bec65e12503a80660b 100644
--- a/note_kfet/inputs.py
+++ b/note_kfet/inputs.py
@@ -299,4 +299,4 @@ class YearPickerInput(BasePickerInput):
     def _link_to(self, linked_picker):
         """Customize the options when linked with other date-time input"""
         yformat = self.config['options']['format'].replace('-01-01', '-12-31')
-        self.config['options']['format'] = yformat
\ No newline at end of file
+        self.config['options']['format'] = yformat
diff --git a/static/js/base.js b/static/js/base.js
index 22d1366a88a672ea753fc91ea607236a32e0ce8a..7febd3d663968a699119a2cf850ba48d1b145d7d 100644
--- a/static/js/base.js
+++ b/static/js/base.js
@@ -70,7 +70,7 @@ function refreshBalance() {
  * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
  */
 function getMatchedNotes(pattern, fun) {
-    $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
+    $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club|activity&ordering=normalized_name", fun);
 }
 
 /**
diff --git a/templates/activity/activity_detail.html b/templates/activity/activity_detail.html
index 0ed3c7198643d468b624427ab24d5c1dc49e1907..841820650e3ee5b1fce310fb83dbdf5abb8b4e83 100644
--- a/templates/activity/activity_detail.html
+++ b/templates/activity/activity_detail.html
@@ -25,7 +25,7 @@
                 <dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
                 <dd class="col-xl-6">{{ activity.date_end }}</dd>
 
-                {% if "view_"|has_perm:activity.creater %}
+                {% if ".view_"|has_perm:activity.creater %}
                     <dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt>
                     <dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
                 {% endif %}
@@ -53,17 +53,17 @@
         </div>
 
         <div class="card-footer text-center">
-            {% if activity.open and "change__open"|has_perm:activity %}
+            {% if activity.open and ".change__open"|has_perm:activity %}
                 <a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
             {% endif %}
 
-            {% if activity.valid and "change__open"|has_perm:activity %}
+            {% if activity.valid and ".change__open"|has_perm:activity %}
                 <a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
             {% endif %}
-            {% if not activity.open and "change__valid"|has_perm:activity %}
+            {% if not activity.open and ".change__valid"|has_perm:activity %}
                 <a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a>
             {% endif %}
-            {% if "view_"|has_perm:activity %}
+            {% if ".view_"|has_perm:activity %}
                 <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a>
             {% endif %}
             {% if activity.activity_type.can_invite and not activity_started %}
diff --git a/templates/base.html b/templates/base.html
index 26903c2f1a00defa1401ec5d9c7afd5d8eff1502..c44e24676fc4bcdc44ed89bc2ff252602153956e 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -84,6 +84,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
                         <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
                     </li>
                 {% endif %}
+                {% if "auth.user"|model_list|length >= 2 %}
+                    <li class="nav-item active">
+                        <a class="nav-link" href="{% url 'member:user_list' %}"><i class="fa fa-user"></i> {% trans 'Users' %}</a>
+                    </li>
+                {% endif %}
                 {% if "member.club"|not_empty_model_list %}
                     <li class="nav-item active">
                         <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
diff --git a/templates/member/add_members.html b/templates/member/add_members.html
index 8b57e7d4ae11f34b4d38e8c1564410ab1cf7f011..c44440bf40df596a4c7e3087bc262cd1711f30eb 100644
--- a/templates/member/add_members.html
+++ b/templates/member/add_members.html
@@ -1,29 +1,21 @@
 {% extends "member/noteowner_detail.html" %}
 {% load crispy_forms_tags %}
 {% load static %}
+{% load i18n %}
 
 {% block profile_info %}
 {% include "member/club_info.html" %}
 {% endblock %}
-{% block profile_content %}
 
+{% block profile_content %}
 <form method="post" action="">
     {% csrf_token %}
-    {% crispy formset helper %}
-    <div class="form-actions">
-        <input type="submit" name="submit" value="Add Members" class="btn btn-primary" id="submit-save">
-    </div>
+    {{ form|crispy }}
+    <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
 </form>
 {% endblock %}
 
 {% block extrajavascript %}
-<script src="{% static 'js/dynamic-formset.js' %}"></script>
 <script>
-    $('.formset-row').formset({
-        addText: 'add another',          // Text for the add link
-        deleteText: 'remove',            // Text for the delete link
-        addCssClass: 'btn btn-primary',          // CSS class applied to the add link
-        deleteCssClass: 'btn btn-danger h-50 my-auto',
-    });
 </script>
 {% endblock %}
diff --git a/templates/member/club_detail.html b/templates/member/club_detail.html
index 979c08971448746e22dccff52ddc2313d1d48315..3ad299010a5d375e6fd895ee407400f248539147 100644
--- a/templates/member/club_detail.html
+++ b/templates/member/club_detail.html
@@ -7,3 +7,12 @@
 {% block profile_content %}
 {% include "member/club_tables.html" %}
 {% endblock %}
+
+{% block extrajavascript %}
+    <script>
+    function refreshHistory() {
+        $("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list");
+        $("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos");
+    }
+    </script>
+{% endblock %}
diff --git a/templates/member/club_form.html b/templates/member/club_form.html
index 99c254e3a2d550f39208a5f070fb7f7a8a328735..9810ccab72c628258fe2c5044a62b020cc3481bb 100644
--- a/templates/member/club_form.html
+++ b/templates/member/club_form.html
@@ -9,3 +9,25 @@
 <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
 </form>
 {% endblock %}
+
+{% block extrajavascript %}
+    <script>
+        require_memberships_obj = $("#id_require_memberships");
+
+        if (!require_memberships_obj.is(":checked")) {
+            $("#div_id_membership_fee_paid").toggle();
+            $("#div_id_membership_fee_unpaid").toggle();
+            $("#div_id_membership_duration").toggle();
+            $("#div_id_membership_start").toggle();
+            $("#div_id_membership_end").toggle();
+        }
+
+        require_memberships_obj.change(function () {
+            $("#div_id_membership_fee_paid").toggle();
+            $("#div_id_membership_fee_unpaid").toggle();
+            $("#div_id_membership_duration").toggle();
+            $("#div_id_membership_start").toggle();
+            $("#div_id_membership_end").toggle();
+        });
+    </script>
+{% endblock %}
diff --git a/templates/member/club_info.html b/templates/member/club_info.html
index 1c8e86611b3876b918ce1d0aff9fdeddc26106de..a781bea85d7d1a08ac216c2e0c95d41cf2b00646 100644
--- a/templates/member/club_info.html
+++ b/templates/member/club_info.html
@@ -1,4 +1,4 @@
-{% load i18n static pretty_money %}
+{% load i18n static pretty_money perms %}
 <div class="card bg-light shadow">
     <div class="card-header text-center">
         <h4> Club {{ club.name }} </h4>
@@ -18,31 +18,44 @@
                 <dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
             {% endif %}
 
-            <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
-            <dd class="col-xl-6">{{ club.membership_start }}</dd>
+            {% if club.require_memberships %}
+                <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
+                <dd class="col-xl-6">{{ club.membership_start }}</dd>
 
-            <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
-            <dd class="col-xl-6">{{ club.membership_end }}</dd>
+                <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
+                <dd class="col-xl-6">{{ club.membership_end }}</dd>
 
-            <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
-            <dd class="col-xl-6">{{ club.membership_duration }}</dd>
+                <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
+                <dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd>
 
-            <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
-            <dd class="col-xl-6">{{ club.membership_fee|pretty_money }}</dd>
+                {% if club.membership_fee_paid == club.membership_fee_unpaid %}
+                    <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
+                    <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
+                {% else %}
+                    <dt class="col-xl-6">{% trans 'membership fee (paid students)'|capfirst %}</dt>
+                    <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
+
+                    <dt class="col-xl-6">{% trans 'membership fee (unpaid students)'|capfirst %}</dt>
+                    <dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}</dd>
+                {% endif %}
+            {% endif %}
             
             <dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
             <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd>
 
             <dt class="col-xl-3">{% trans 'email'|capfirst %}</dt>
-            <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email}}</a></dd>
+            <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd>
         </dl>
     </div>
     <div class="card-footer text-center">
-        <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
-        <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
-        <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a>
+        {% if can_add_members %}
+            <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
+        {% endif %}
+        {% if ".change_"|has_perm:club %}
+            <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
+        {% endif %}
         {% url 'member:club_detail' club.pk as club_detail_url %}
         {%if request.get_full_path != club_detail_url %}
-        <a class="btn btn-primary btn-sm my-1" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a>
+        <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
         {% endif %}    </div>
 </div>
diff --git a/templates/member/club_list.html b/templates/member/club_list.html
index 7f0b02a10f035c4c3ad3aef2089415f163028594..2653ace8388704aa45e3ba8cd235de9ab4e4ab63 100644
--- a/templates/member/club_list.html
+++ b/templates/member/club_list.html
@@ -9,7 +9,7 @@
         </h4>
         <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/>
         <hr>
-        <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Créer un club" %}</a>
+        <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
     </div>
 </div>
 <div class="row justify-content-center">   
diff --git a/templates/member/noteowner_detail.html b/templates/member/noteowner_detail.html
index ad329aee5e48b3fb66a9bd0c80cdf59403f609a4..fc781549eb6bb6abedf9c5221a3227c165d0aec3 100644
--- a/templates/member/noteowner_detail.html
+++ b/templates/member/noteowner_detail.html
@@ -19,7 +19,7 @@
 
 {% block extrajavascript %}
     <script>
-    function refreshhistory() {
+    function refreshHistory() {
         $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
         $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
     }
diff --git a/templates/member/user_list.html b/templates/member/user_list.html
index 821ea6190097b7ce3d2016aac3df427b8f8dcd3c..d0eaaedb288c85d568eff2a4a7af449404c1fe50 100644
--- a/templates/member/user_list.html
+++ b/templates/member/user_list.html
@@ -2,28 +2,44 @@
 {% load render_table from django_tables2 %}
 {% load crispy_forms_tags%}
 {% block content %}
+    <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
 
-<a class="btn btn-primary" href="{% url 'member:signup' %}">New User</a>
+    <hr>
 
-<div class="row">
-{% crispy filter.form filter.form.helper %}
-</div>
-<div class="row">
-    <div id="replaceable-content" class="col-6">
-        {% render_table  table %}
+    <div id="user_table">
+        {% render_table table %}
     </div>
-</div>
 
 {% endblock %}
 
 {% block extrajavascript %}
 <script type="text/javascript">
+    $(document).ready(function() {
+        let old_pattern = null;
+        let searchbar_obj = $("#searchbar");
 
-$(document).ready(function($) {
-    $(".table-row").click(function() {
-        window.document.location = $(this).data("href");
-    });
-});
+        function reloadTable() {
+            let pattern = searchbar_obj.val();
+
+            if (pattern === old_pattern || pattern === "")
+                return;
+
+            $("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
+
+            $(".table-row").click(function() {
+                window.document.location = $(this).data("href");
+            });
+        }
 
+        searchbar_obj.keyup(reloadTable);
+
+        function init() {
+            $(".table-row").click(function() {
+                window.document.location = $(this).data("href");
+            });
+        }
+
+        init();
+    });
 </script>
 {% endblock %}