nk20/apps/member/views.py

561 lines
21 KiB
Python
Raw Normal View History

2020-02-18 20:30:26 +00:00
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
2019-08-10 17:01:15 +00:00
# SPDX-License-Identifier: GPL-3.0-or-later
2020-02-18 20:30:26 +00:00
2020-03-07 21:28:59 +00:00
import io
2020-04-01 16:47:56 +00:00
from datetime import datetime, timedelta
2020-03-07 21:28:59 +00:00
from PIL import Image
from django.conf import settings
2019-08-10 17:01:15 +00:00
from django.contrib.auth.mixins import LoginRequiredMixin
2020-03-07 21:28:59 +00:00
from django.contrib.auth.models import User
2020-03-19 15:12:52 +00:00
from django.contrib.auth.views import LoginView
2020-04-01 16:47:56 +00:00
from django.core.exceptions import ValidationError
2020-03-07 21:28:59 +00:00
from django.db.models import Q
2020-04-01 01:42:19 +00:00
from django.forms import HiddenInput
from django.shortcuts import redirect, resolve_url
2020-03-07 21:28:59 +00:00
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.http import urlsafe_base64_decode
2019-08-10 17:01:15 +00:00
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_protect
2020-03-27 13:19:55 +00:00
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
2020-04-01 16:47:56 +00:00
from django.views.generic.base import View
2020-02-28 12:37:31 +00:00
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
2020-02-17 18:25:33 +00:00
from rest_framework.authtoken.models import Token
from note.forms import ImageForm
2020-02-18 20:14:29 +00:00
from note.models import Alias, NoteUser
from note.models.transactions import Transaction
2020-03-31 12:57:44 +00:00
from note.tables import HistoryTable, AliasTable
2020-03-20 13:43:35 +00:00
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
2020-03-20 17:13:34 +00:00
2020-03-31 21:54:14 +00:00
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
2020-03-07 21:28:59 +00:00
from .models import Club, Membership
2020-04-01 01:42:19 +00:00
from .tables import ClubTable, UserTable, MembershipTable
from .tokens import account_activation_token
2019-09-23 10:50:14 +00:00
2020-02-18 11:31:15 +00:00
2020-03-19 15:12:52 +00:00
class CustomLoginView(LoginView):
form_class = CustomAuthenticationForm
def form_valid(self, form):
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form)
2019-08-11 22:30:29 +00:00
class UserCreateView(CreateView):
2019-08-11 14:22:52 +00:00
"""
Une vue pour inscrire un utilisateur et lui créer un profil
2019-08-11 14:22:52 +00:00
"""
form_class = SignUpForm
success_url = reverse_lazy('member:login')
2020-02-18 11:31:15 +00:00
template_name = 'member/signup.html'
second_form = ProfileForm
2019-08-11 14:22:52 +00:00
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-02-03 18:24:23 +00:00
context["profile_form"] = self.second_form()
2019-08-11 14:22:52 +00:00
return context
2019-08-11 15:39:05 +00:00
def form_valid(self, form):
"""
If the form is valid, then the user is created with is_active set to False
so that the user cannot log in until the email has been validated.
"""
profile_form = ProfileForm(self.request.POST)
if not profile_form.is_valid():
return self.form_invalid(form)
user = form.save(commit=False)
user.is_active = False
user.profile = profile_form.save(commit=False)
user.save()
user.profile.save()
user.profile.send_email_validation_link()
2019-08-11 15:39:05 +00:00
return super().form_valid(form)
2019-08-11 21:25:27 +00:00
2020-02-18 11:31:15 +00:00
class UserActivateView(TemplateView):
title = _("Account Activation")
template_name = 'registration/account_activation_complete.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""
The dispatch method looks at the request to determine whether it is a GET, POST, etc,
and relays the request to a matching method if one is defined, or raises HttpResponseNotAllowed
if not. We chose to check the token in the dispatch method to mimic PasswordReset from
django.contrib.auth
"""
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
user = self.get_user(kwargs['uidb64'])
token = kwargs['token']
if user is not None and account_activation_token.check_token(user, token):
self.validlink = True
user.is_active = True
user.profile.email_confirmed = True
user.save()
return super().dispatch(*args, **kwargs)
else:
# Display the "Account Activation unsuccessful" page.
return self.render_to_response(self.get_context_data())
def get_user(self, uidb64):
print(uidb64)
try:
# urlsafe_base64_decode() decodes to bytestring
uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
user = None
return user
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['login_url'] = resolve_url(settings.LOGIN_URL)
if self.validlink:
context['validlink'] = True
else:
context.update({
'title': _('Account Activation unsuccessful'),
'validlink': False,
})
return context
class UserActivationEmailSentView(TemplateView):
template_name = 'registration/account_activation_email_sent.html'
title = _('Account activation email sent')
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
2020-02-03 18:25:05 +00:00
model = User
2020-02-18 11:31:15 +00:00
fields = ['first_name', 'last_name', 'username', 'email']
2020-02-03 18:25:05 +00:00
template_name = 'member/profile_update.html'
2020-02-27 19:56:06 +00:00
context_object_name = 'user_object'
2020-03-03 13:25:16 +00:00
profile_form = ProfileForm
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
2020-02-03 18:25:05 +00:00
context = super().get_context_data(**kwargs)
form = context['form']
form.fields['username'].widget.attrs.pop("autofocus", None)
form.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
form.fields['first_name'].required = True
form.fields['last_name'].required = True
form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.")
2020-03-03 13:25:16 +00:00
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
2020-02-21 10:17:14 +00:00
context['title'] = _("Update Profile")
2020-02-03 18:25:05 +00:00
return context
def form_valid(self, form):
new_username = form.data['username']
2020-02-17 10:36:46 +00:00
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
2020-02-18 11:31:15 +00:00
note = NoteUser.objects.filter(
alias__normalized_name=Alias.normalize(new_username))
2020-02-27 19:56:06 +00:00
if note.exists() and note.get().user != self.object:
2020-02-18 11:31:15 +00:00
form.add_error('username',
_("An alias with a similar name already exists."))
return super().form_invalid(form)
2020-02-18 11:31:15 +00:00
profile_form = ProfileForm(
data=self.request.POST,
2020-02-27 19:56:06 +00:00
instance=self.object.profile,
2020-02-18 11:31:15 +00:00
)
2020-02-03 18:25:05 +00:00
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
if not alias.exists():
2020-02-18 11:31:15 +00:00
similar = Alias.objects.filter(
normalized_name=Alias.normalize(new_username))
if similar.exists():
similar.delete()
2020-02-17 10:36:46 +00:00
olduser = User.objects.get(pk=form.instance.pk)
2020-02-17 10:36:46 +00:00
user = form.save(commit=False)
2020-02-18 11:31:15 +00:00
profile = profile_form.save(commit=False)
2020-02-03 18:25:05 +00:00
profile.user = user
profile.save()
2020-02-17 10:36:46 +00:00
user.save()
if olduser.email != user.email:
user.profile.email_confirmed = False
user.profile.send_email_validation_link()
2020-02-03 18:25:05 +00:00
return super().form_valid(form)
def get_success_url(self, **kwargs):
2020-02-18 11:31:15 +00:00
if kwargs:
return reverse_lazy('member:user_detail',
kwargs={'pk': kwargs['id']})
2020-02-03 18:25:05 +00:00
else:
2020-03-07 21:28:59 +00:00
return reverse_lazy('member:user_detail', args=(self.object.id,))
2020-02-18 11:31:15 +00:00
2020-02-03 18:25:05 +00:00
class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
2020-02-21 10:53:37 +00:00
Affiche les informations sur un utilisateur, sa note, ses clubs...
"""
2020-02-25 21:55:27 +00:00
model = User
context_object_name = "user_object"
template_name = "member/profile_detail.html"
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-02-25 21:55:27 +00:00
user = context['user_object']
history_list = \
2020-03-30 23:03:30 +00:00
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)
2020-04-01 16:47:56 +00:00
club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
2020-04-01 01:42:19 +00:00
context['club_list'] = MembershipTable(data=club_list)
return context
2019-08-11 22:30:29 +00:00
2020-02-18 11:31:15 +00:00
class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Affiche la liste des utilisateurs, avec une fonction de recherche statique
"""
2019-09-23 10:50:14 +00:00
model = User
table_class = UserTable
template_name = 'member/user_list.html'
2020-02-18 11:31:15 +00:00
def get_queryset(self, **kwargs):
qs = super().get_queryset()
2020-04-01 18:14:16 +00:00
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()
2020-04-01 18:56:24 +00:00
return qs[:20]
2019-09-23 10:50:14 +00:00
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
2019-09-23 10:50:14 +00:00
context = super().get_context_data(**kwargs)
2020-04-01 18:56:24 +00:00
context["title"] = _("Search user")
2019-09-23 10:50:14 +00:00
return context
2020-03-27 13:19:55 +00:00
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
2020-02-28 12:37:31 +00:00
model = User
2020-03-03 10:05:02 +00:00
template_name = 'member/profile_alias.html'
2020-02-28 12:37:31 +00:00
context_object_name = 'user_object'
2020-03-27 13:19:55 +00:00
2020-03-07 21:28:59 +00:00
def get_context_data(self, **kwargs):
2020-02-28 12:37:31 +00:00
context = super().get_context_data(**kwargs)
2020-03-25 17:00:40 +00:00
note = context['object'].note
2020-02-28 15:12:35 +00:00
context["aliases"] = AliasTable(note.alias_set.all())
2020-02-28 12:37:31 +00:00
return context
2020-03-07 21:28:59 +00:00
class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
2020-03-04 15:34:12 +00:00
form_class = ImageForm
2020-03-07 21:28:59 +00:00
2020-03-30 23:03:30 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-03-07 21:28:59 +00:00
context['form'] = self.form_class(self.request.POST, self.request.FILES)
2020-03-04 15:34:12 +00:00
return context
2020-03-07 21:28:59 +00:00
2020-03-04 15:34:12 +00:00
def get_success_url(self):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id})
2020-03-07 21:28:59 +00:00
def post(self, request, *args, **kwargs):
form = self.get_form()
2020-03-04 15:34:12 +00:00
self.object = self.get_object()
if form.is_valid():
return self.form_valid(form)
else:
print('is_invalid')
print(form)
return self.form_invalid(form)
2020-03-07 21:28:59 +00:00
def form_valid(self, form):
image_field = form.cleaned_data['image']
2020-03-05 22:32:01 +00:00
x = form.cleaned_data['x']
y = form.cleaned_data['y']
w = form.cleaned_data['width']
h = form.cleaned_data['height']
# image crop and resize
image_file = io.BytesIO(image_field.read())
2020-03-07 21:28:59 +00:00
# ext = image_field.name.split('.')[-1].lower()
# TODO: support GIF format
image = Image.open(image_file)
2020-03-07 21:28:59 +00:00
image = image.crop((x, y, x + w, y + h))
image_clean = image.resize((settings.PIC_WIDTH,
2020-03-07 21:28:59 +00:00
settings.PIC_RATIO * settings.PIC_WIDTH),
Image.ANTIALIAS)
image_file = io.BytesIO()
2020-03-07 21:28:59 +00:00
image_clean.save(image_file, "PNG")
image_field.file = image_file
# renaming
2020-03-07 16:58:41 +00:00
filename = "{}_pic.png".format(self.object.note.pk)
image_field.name = filename
self.object.note.display_image = image_field
2020-03-04 15:34:12 +00:00
self.object.note.save()
return super().form_valid(form)
2020-03-25 15:58:15 +00:00
class ProfilePictureUpdateView(PictureUpdateView):
model = User
template_name = 'member/profile_picture_update.html'
context_object_name = 'user_object'
2020-03-07 21:28:59 +00:00
2020-03-25 15:58:15 +00:00
2020-02-17 20:32:08 +00:00
class ManageAuthTokens(LoginRequiredMixin, TemplateView):
2020-02-17 18:25:33 +00:00
"""
2020-02-17 20:32:08 +00:00
Affiche le jeton d'authentification, et permet de le regénérer
2020-02-17 18:25:33 +00:00
"""
2020-02-17 20:32:08 +00:00
model = Token
template_name = "member/manage_auth_tokens.html"
2020-02-17 18:25:33 +00:00
2020-02-17 22:30:55 +00:00
def get(self, request, *args, **kwargs):
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
2020-02-17 18:25:33 +00:00
Token.objects.get(user=self.request.user).delete()
2020-02-18 11:31:15 +00:00
return redirect(reverse_lazy('member:auth_token') + "?show",
permanent=True)
2020-02-17 18:25:33 +00:00
2020-02-17 22:30:55 +00:00
return super().get(request, *args, **kwargs)
2020-02-17 20:32:08 +00:00
2020-02-17 22:30:55 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
2020-02-17 18:25:33 +00:00
return context
2020-02-18 11:31:15 +00:00
2020-02-18 20:14:29 +00:00
# ******************************* #
# CLUB #
# ******************************* #
2019-08-11 22:30:29 +00:00
2020-02-18 11:31:15 +00:00
class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
2019-08-11 21:25:27 +00:00
"""
Create Club
"""
model = Club
form_class = ClubForm
success_url = reverse_lazy('member:club_list')
2020-03-25 15:58:15 +00:00
2020-02-18 11:31:15 +00:00
def form_valid(self, form):
2019-08-11 21:25:27 +00:00
return super().form_valid(form)
2020-03-26 23:40:35 +00:00
2020-02-18 11:31:15 +00:00
class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
2019-08-11 21:25:27 +00:00
"""
List existing Clubs
2019-08-11 21:25:27 +00:00
"""
model = Club
2019-08-15 19:49:32 +00:00
table_class = ClubTable
2019-08-11 22:30:29 +00:00
2020-02-18 11:31:15 +00:00
class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
2019-08-11 21:25:27 +00:00
model = Club
2020-02-18 11:31:15 +00:00
context_object_name = "club"
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-03-31 21:54:14 +00:00
club = context["club"]
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
2020-03-31 21:54:14 +00:00
club.update_membership_dates()
2020-03-30 23:03:30 +00:00
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')
2019-08-15 21:11:52 +00:00
context['history_list'] = HistoryTable(club_transactions)
2020-03-31 21:54:14 +00:00
club_member = Membership.objects.filter(
club=club,
2020-04-01 16:47:56 +00:00
date_end__gte=datetime.today(),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
2020-04-01 01:42:19 +00:00
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
2020-03-27 13:19:55 +00:00
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
2020-03-25 17:00:40 +00:00
model = Club
template_name = 'member/club_alias.html'
context_object_name = 'club'
2020-03-27 13:19:55 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(note.alias_set.all())
return context
2020-03-25 17:00:40 +00:00
2020-03-25 15:58:15 +00:00
class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Club
context_object_name = "club"
form_class = ClubForm
template_name = "member/club_form.html"
2020-03-30 23:03:30 +00:00
def get_success_url(self):
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
class ClubPictureUpdateView(PictureUpdateView):
model = Club
template_name = 'member/club_picture_update.html'
context_object_name = 'club'
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})
2020-02-18 11:31:15 +00:00
class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
2020-02-18 11:31:15 +00:00
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)
2020-03-25 16:42:54 +00:00
context['club'] = club
2020-02-21 17:28:21 +00:00
return context
2020-03-31 21:54:14 +00:00
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
2020-03-31 21:54:14 +00:00
form.instance.club = club
2020-04-01 01:42:19 +00:00
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(
2020-04-01 16:47:56 +00:00
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."))
2020-04-01 01:42:19 +00:00
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,
2020-04-01 01:42:19 +00:00
).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:
2020-04-01 01:42:19 +00:00
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}.")
2020-04-01 01:42:19 +00:00
.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):
context = super().get_context_data(**kwargs)
club = self.object.club
context['club'] = club
form = context['form']
form.fields['user'].disabled = True
form.fields['date_start'].widget = HiddenInput()
return context
def form_valid(self, form):
if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start:
2020-04-01 01:42:19 +00:00
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}.")
2020-04-01 01:42:19 +00:00
.format(form.instance.club.membership_start))
return super().form_invalid(form)
2020-03-31 21:54:14 +00:00
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
2020-04-01 16:47:56 +00:00
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}))