nk20/apps/member/views.py

984 lines
39 KiB
Python
Raw Permalink Normal View History

Update 131 files - /apps/activity/api/serializers.py - /apps/activity/api/urls.py - /apps/activity/api/views.py - /apps/activity/tests/test_activities.py - /apps/activity/__init__.py - /apps/activity/admin.py - /apps/activity/apps.py - /apps/activity/forms.py - /apps/activity/tables.py - /apps/activity/urls.py - /apps/activity/views.py - /apps/api/__init__.py - /apps/api/apps.py - /apps/api/serializers.py - /apps/api/tests.py - /apps/api/urls.py - /apps/api/views.py - /apps/api/viewsets.py - /apps/logs/signals.py - /apps/logs/apps.py - /apps/logs/__init__.py - /apps/logs/api/serializers.py - /apps/logs/api/urls.py - /apps/logs/api/views.py - /apps/member/api/serializers.py - /apps/member/api/urls.py - /apps/member/api/views.py - /apps/member/templatetags/memberinfo.py - /apps/member/__init__.py - /apps/member/admin.py - /apps/member/apps.py - /apps/member/auth.py - /apps/member/forms.py - /apps/member/hashers.py - /apps/member/signals.py - /apps/member/tables.py - /apps/member/urls.py - /apps/member/views.py - /apps/note/api/serializers.py - /apps/note/api/urls.py - /apps/note/api/views.py - /apps/note/models/__init__.py - /apps/note/static/note/js/consos.js - /apps/note/templates/note/mails/negative_balance.txt - /apps/note/templatetags/getenv.py - /apps/note/templatetags/pretty_money.py - /apps/note/tests/test_transactions.py - /apps/note/__init__.py - /apps/note/admin.py - /apps/note/apps.py - /apps/note/forms.py - /apps/note/signals.py - /apps/note/tables.py - /apps/note/urls.py - /apps/note/views.py - /apps/permission/api/serializers.py - /apps/permission/api/urls.py - /apps/permission/api/views.py - /apps/permission/templatetags/perms.py - /apps/permission/tests/test_oauth2.py - /apps/permission/tests/test_permission_denied.py - /apps/permission/tests/test_permission_queries.py - /apps/permission/tests/test_rights_page.py - /apps/permission/__init__.py - /apps/permission/admin.py - /apps/permission/backends.py - /apps/permission/apps.py - /apps/permission/decorators.py - /apps/permission/permissions.py - /apps/permission/scopes.py - /apps/permission/signals.py - /apps/permission/tables.py - /apps/permission/urls.py - /apps/permission/views.py - /apps/registration/tests/test_registration.py - /apps/registration/__init__.py - /apps/registration/apps.py - /apps/registration/forms.py - /apps/registration/tables.py - /apps/registration/tokens.py - /apps/registration/urls.py - /apps/registration/views.py - /apps/treasury/api/serializers.py - /apps/treasury/api/urls.py - /apps/treasury/api/views.py - /apps/treasury/templatetags/escape_tex.py - /apps/treasury/tests/test_treasury.py - /apps/treasury/__init__.py - /apps/treasury/admin.py - /apps/treasury/apps.py - /apps/treasury/forms.py - /apps/treasury/signals.py - /apps/treasury/tables.py - /apps/treasury/urls.py - /apps/treasury/views.py - /apps/wei/api/serializers.py - /apps/wei/api/urls.py - /apps/wei/api/views.py - /apps/wei/forms/surveys/__init__.py - /apps/wei/forms/surveys/base.py - /apps/wei/forms/surveys/wei2021.py - /apps/wei/forms/surveys/wei2022.py - /apps/wei/forms/surveys/wei2023.py - /apps/wei/forms/__init__.py - /apps/wei/forms/registration.py - /apps/wei/management/commands/export_wei_registrations.py - /apps/wei/management/commands/import_scores.py - /apps/wei/management/commands/wei_algorithm.py - /apps/wei/templates/wei/weilist_sample.tex - /apps/wei/tests/test_wei_algorithm_2021.py - /apps/wei/tests/test_wei_algorithm_2022.py - /apps/wei/tests/test_wei_algorithm_2023.py - /apps/wei/tests/test_wei_registration.py - /apps/wei/__init__.py - /apps/wei/admin.py - /apps/wei/apps.py - /apps/wei/tables.py - /apps/wei/urls.py - /apps/wei/views.py - /note_kfet/settings/__init__.py - /note_kfet/settings/base.py - /note_kfet/settings/development.py - /note_kfet/settings/secrets_example.py - /note_kfet/static/js/base.js - /note_kfet/admin.py - /note_kfet/inputs.py - /note_kfet/middlewares.py - /note_kfet/urls.py - /note_kfet/views.py - /note_kfet/wsgi.py - /entrypoint.sh
2024-02-07 01:26:49 +00:00
# Copyright (C) 2018-2024 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-08-14 17:04:44 +00:00
from datetime import timedelta, date
2020-08-18 16:19:39 +00:00
2020-03-07 21:28:59 +00:00
from django.conf import settings
from django.contrib.auth import logout
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-08-31 18:15:48 +00:00
from django.db import transaction
2020-08-06 11:07:22 +00:00
from django.db.models import Q, F
2020-04-05 03:17:28 +00:00
from django.shortcuts import redirect
2020-03-07 21:28:59 +00:00
from django.urls import reverse_lazy
from django.utils import timezone
2019-08-10 17:01:15 +00:00
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView
2020-02-28 12:37:31 +00:00
from django.views.generic.edit import FormMixin
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
2020-02-17 18:25:33 +00:00
from rest_framework.authtoken.models import Token
2024-07-18 11:51:56 +00:00
from api.viewsets import is_regex
from note.models import Alias, NoteClub, NoteUser, Trust
2020-04-05 16:37:04 +00:00
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
from note_kfet.middlewares import _set_current_request
2020-03-20 13:43:35 +00:00
from permission.backends import PermissionBackend
2020-07-25 17:40:30 +00:00
from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
2020-03-20 17:13:34 +00:00
2024-05-30 18:21:56 +00:00
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
2020-08-19 21:00:49 +00:00
CustomAuthenticationForm, MembershipRolesForm
2020-07-25 17:40:30 +00:00
from .models import Club, Membership
2020-07-31 15:01:52 +00:00
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
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):
2020-04-06 06:58:39 +00:00
"""
Login view, where the user can select its permission mask.
"""
2020-03-19 15:12:52 +00:00
form_class = CustomAuthenticationForm
2020-09-11 20:52:16 +00:00
@transaction.atomic
2020-03-19 15:12:52 +00:00
def form_valid(self, form):
logout(self.request)
self.request.user = form.get_user()
_set_current_request(self.request)
2020-03-19 15:12:52 +00:00
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form)
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
2020-04-06 06:58:39 +00:00
"""
Update the user information.
2020-08-18 16:19:39 +00:00
On this view both `:models:member.User` and `:models:member.Profile` are updated through forms
2020-04-06 06:58:39 +00:00
"""
2020-02-03 18:25:05 +00:00
model = User
form_class = UserForm
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-07-30 15:30:21 +00:00
extra_context = {"title": _("Update Profile")}
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.")
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
2020-10-25 20:49:16 +00:00
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency:
del context['profile_form'].fields["last_report"]
2020-08-05 12:14:51 +00:00
2020-02-03 18:25:05 +00:00
return context
2020-09-11 20:52:16 +00:00
@transaction.atomic
def form_valid(self, form):
2020-08-18 16:19:39 +00:00
"""
Check if ProfileForm is correct
then check if username is not already taken by someone else or by the user,
then check if email has changed, and if so ask for new validation.
"""
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-08-05 12:14:51 +00:00
profile_form.full_clean()
if not profile_form.is_valid():
return super().form_invalid(form)
new_username = form.data['username']
2020-08-18 16:19:39 +00:00
# Check if the new username is not already taken as an alias of someone else.
note = NoteUser.objects.filter(
alias__normalized_name=Alias.normalize(new_username))
if note.exists() and note.get().user != self.object:
2020-09-02 20:54:01 +00:00
form.add_error('username', _("An alias with a similar name already exists."))
2020-08-18 16:19:39 +00:00
return super().form_invalid(form)
# Check if the username is one of user's aliases.
2020-08-05 12:14:51 +00:00
alias = Alias.objects.filter(name=new_username)
if not alias.exists():
similar = Alias.objects.filter(
normalized_name=Alias.normalize(new_username))
if similar.exists():
similar.delete()
olduser = User.objects.get(pk=form.instance.pk)
user = form.save(commit=False)
if olduser.email != user.email:
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
user.profile.email_confirmed = False
user.profile.send_email_validation_link()
2020-08-18 16:19:39 +00:00
profile = profile_form.save(commit=False)
profile.user = user
profile.save()
user.save()
2020-02-03 18:25:05 +00:00
return super().form_valid(form)
def get_success_url(self, **kwargs):
url = 'member:user_detail' if self.object.profile.registration_valid else 'registration:future_user_detail'
return reverse_lazy(url, 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-08-18 16:19:39 +00:00
Display all information about a user.
"""
2020-02-25 21:55:27 +00:00
model = User
context_object_name = "user_object"
template_name = "member/profile_detail.html"
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Profile detail")}
2020-02-18 11:31:15 +00:00
def get_queryset(self, **kwargs):
"""
We can't display information of a not registered user.
"""
return super().get_queryset(**kwargs).filter(profile__registration_valid=True)
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
2020-08-18 16:19:39 +00:00
"""
Add history of transaction and list of membership of user.
"""
context = super().get_context_data(**kwargs)
2020-02-25 21:55:27 +00:00
user = context['user_object']
2020-08-31 18:15:48 +00:00
context["note"] = user.note
history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
2020-07-25 15:25:57 +00:00
.order_by("-created_at")\
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
2020-04-06 17:51:39 +00:00
history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
.filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
2020-10-07 08:03:43 +00:00
.order_by("club__name", "-date_start")
# Display only the most recent membership
2020-10-07 08:03:43 +00:00
club_list = club_list.distinct("club__name")\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
2024-08-25 13:13:02 +00:00
membership_table = MembershipTable(data=club_list, prefix='membership-')
2020-04-06 17:51:39 +00:00
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
context['club_list'] = membership_table
2020-08-31 18:15:48 +00:00
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteUser.objects.get(pk=user.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = False
2020-08-31 18:15:48 +00:00
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
2020-08-31 18:15:48 +00:00
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
2020-08-31 18:15:48 +00:00
old_note._force_save = True
old_note._no_signal = True
2020-08-31 18:15:48 +00:00
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
2020-08-31 18:15:48 +00:00
return context
2019-08-11 22:30:29 +00:00
2020-02-18 11:31:15 +00:00
class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
2020-04-06 06:58:39 +00:00
Display user list, with a search bar
"""
2019-09-23 10:50:14 +00:00
model = User
table_class = UserTable
template_name = 'member/user_list.html'
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Search user")}
2020-02-18 11:31:15 +00:00
def get_queryset(self, **kwargs):
2020-04-06 06:58:39 +00:00
"""
Filter the user list with the given pattern.
"""
2020-09-02 20:54:01 +00:00
qs = super().get_queryset().annotate(alias=F("note__alias__name"))\
2020-08-06 11:07:22 +00:00
.annotate(normalized_alias=F("note__alias__normalized_name"))\
2020-09-02 20:54:01 +00:00
.filter(profile__registration_valid=True)
2020-04-01 18:14:16 +00:00
2020-09-02 20:54:01 +00:00
# Sqlite doesn't support order by in subqueries
qs = qs.order_by("username").distinct("username")\
if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql' else qs.distinct()
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
2020-04-01 18:14:16 +00:00
2024-07-18 11:51:56 +00:00
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
2020-04-01 18:14:16 +00:00
qs = qs.filter(
2024-07-18 11:51:56 +00:00
Q(**{f"username{suffix}": prefix + pattern})
).union(
qs.filter(
2024-07-18 11:51:56 +00:00
(Q(**{f"alias{suffix}": prefix + pattern})
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
| Q(**{f"last_name{suffix}": prefix + pattern})
| Q(**{f"first_name{suffix}": prefix + pattern})
| Q(email__istartswith=pattern))
2024-07-18 11:51:56 +00:00
& ~Q(**{f"username{suffix}": prefix + pattern})
), all=True)
2020-04-01 18:14:16 +00:00
else:
qs = qs.none()
2020-08-31 18:15:48 +00:00
return qs
2019-09-23 10:50:14 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
.filter(profile__registration_valid=False)
context["can_manage_registrations"] = pre_registered_users.exists()
return context
2020-03-27 13:19:55 +00:00
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
"""
View and manage user trust relationships
"""
model = User
template_name = 'member/profile_trust.html'
context_object_name = 'user_object'
2021-10-04 18:45:05 +00:00
extra_context = {"title": _("Note friendships")}
tables = [
lambda data: TrustTable(data, prefix="trust-"),
lambda data: TrustedTable(data, prefix="trusted-"),
]
def get_tables_data(self):
note = self.object.note
return [
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tables = context["tables"]
for name, table in zip(["trusting", "trusted_by"], tables):
context[name] = table
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note,
trusted=context["object"].note
))
2021-10-04 18:45:05 +00:00
context["widget"] = {
"name": "trusted",
"resetable": True,
2021-10-04 18:45:05 +00:00
"attrs": {
"class": "autocomplete form-control",
"id": "trusted",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
2021-10-04 18:45:05 +00:00
}
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
2020-04-06 06:58:39 +00:00
"""
View and manage user aliases.
"""
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-07-30 15:30:21 +00:00
extra_context = {"title": _("Note aliases")}
2020-03-27 13:19:55 +00:00
table_class = AliasTable
context_table_name = "aliases"
def get_table_data(self):
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
.order_by('normalized_name')
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)
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
normalized_name="",
))
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-04-06 06:58:39 +00:00
"""
Update profile picture of the user note.
"""
2020-03-04 15:34:12 +00:00
form_class = ImageForm
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Update note picture")}
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):
2020-09-06 10:04:54 +00:00
"""Redirect to profile page after upload"""
2020-03-04 15:34:12 +00:00
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()
2020-09-02 20:54:01 +00:00
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
2020-03-04 15:34:12 +00:00
2020-09-11 20:52:16 +00:00
@transaction.atomic
2020-03-07 21:28:59 +00:00
def form_valid(self, form):
2020-09-06 10:04:54 +00:00
"""Save image to note"""
2020-09-06 16:54:21 +00:00
image = form.cleaned_data['image']
if image is None:
image = "pic/default.png"
2020-09-06 16:54:21 +00:00
else:
# Rename as a PNG or GIF
extension = image.name.split(".")[-1]
if extension == "gif":
image.name = "{}_pic.gif".format(self.object.note.pk)
else:
image.name = "{}_pic.png".format(self.object.note.pk)
2020-09-06 16:54:21 +00:00
# Save
self.object.note.display_image = image
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/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-07-30 15:30:21 +00:00
extra_context = {"title": _("Manage auth token")}
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-09-02 20:54:01 +00:00
return redirect(reverse_lazy('member:auth_token') + "?show")
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, ProtectedCreateView):
2019-08-11 21:25:27 +00:00
"""
Create Club
"""
model = Club
form_class = ClubForm
success_url = reverse_lazy('member:club_list')
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Create new club")}
2020-03-25 15:58:15 +00:00
def get_sample_object(self):
return Club(
name="",
email="",
)
2020-09-02 20:54:01 +00:00
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
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
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Search club")}
2019-08-11 22:30:29 +00:00
2020-06-21 20:27:32 +00:00
def get_queryset(self, **kwargs):
"""
Filter the user list with the given pattern.
"""
2020-07-25 16:18:53 +00:00
qs = super().get_queryset().distinct()
2020-06-21 20:27:32 +00:00
if "search" in self.request.GET:
pattern = self.request.GET["search"]
2024-07-18 11:51:56 +00:00
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
2020-06-21 20:27:32 +00:00
qs = qs.filter(
2024-07-18 11:51:56 +00:00
Q(**{f"name{suffix}": prefix + pattern})
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
2020-06-21 20:27:32 +00:00
)
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
name="",
email="club@example.com",
))
return context
2020-02-18 11:31:15 +00:00
class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
2020-04-06 06:58:39 +00:00
"""
Display details of a club
"""
2019-08-11 21:25:27 +00:00
model = Club
2020-02-18 11:31:15 +00:00
context_object_name = "club"
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Club detail")}
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
2020-08-18 16:19:39 +00:00
"""
Add list of managers (peoples with Permission/Roles in this club), history of transactions and members list
"""
context = super().get_context_data(**kwargs)
2020-03-31 21:54:14 +00:00
club = self.object
context["note"] = club.note
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
2020-03-31 21:54:14 +00:00
club.update_membership_dates()
2020-08-18 16:19:39 +00:00
# managers list
2020-10-01 07:17:02 +00:00
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
date_start__lte=date.today(), date_end__gte=date.today())\
2020-07-31 15:01:52 +00:00
.order_by('user__last_name').all()
2024-08-25 13:13:02 +00:00
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
2020-08-18 16:19:39 +00:00
# transaction history
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, Transaction, "view"))\
2020-07-25 15:25:57 +00:00
.order_by('-created_at')
2020-04-06 17:51:39 +00:00
history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
context['history_list'] = history_table
2020-08-18 16:19:39 +00:00
# member list
2020-03-31 21:54:14 +00:00
club_member = Membership.objects.filter(
club=club,
date_end__gte=date.today() - timedelta(days=15),
).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
2020-10-07 08:03:43 +00:00
.order_by("user__username", "-date_start")
# Display only the most recent membership
2020-10-07 08:03:43 +00:00
club_member = club_member.distinct("user__username")\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member
2020-04-01 01:42:19 +00:00
2024-08-25 13:13:02 +00:00
membership_table = MembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
2020-04-06 17:51:39 +00:00
context['member_list'] = membership_table
2020-04-01 01:42:19 +00:00
2020-04-06 06:58:39 +00:00
# Check if the user has the right to create a membership, to display the button.
2020-04-01 01:42:19 +00:00
empty_membership = Membership(
club=club,
user=User.objects.first(),
2020-08-15 17:10:23 +00:00
date_start=date.today(),
date_end=date.today(),
2020-04-01 01:42:19 +00:00
fee=0,
)
context["can_add_members"] = PermissionBackend()\
.has_perm(self.request.user, "member.add_membership", empty_membership)
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteClub.objects.get(pk=club.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = False
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note._force_save = True
old_note._no_signal = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
return context
2020-03-27 13:19:55 +00:00
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
2020-04-06 06:58:39 +00:00
"""
Manage aliases of a club.
"""
2020-03-25 17:00:40 +00:00
model = Club
template_name = 'member/club_alias.html'
context_object_name = 'club'
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Note aliases")}
2020-03-27 13:19:55 +00:00
table_class = AliasTable
context_table_name = "aliases"
def get_table_data(self):
return self.object.note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
normalized_name="",
))
return context
2020-03-25 17:00:40 +00:00
2020-03-25 15:58:15 +00:00
class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
2020-04-06 06:58:39 +00:00
"""
Update the information of a club.
"""
model = Club
context_object_name = "club"
form_class = ClubForm
template_name = "member/club_form.html"
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Update club")}
2020-03-30 23:03:30 +00:00
def get_queryset(self, **kwargs):
qs = super().get_queryset(**kwargs)
# Don't update a WEI club through this view
if "wei" in settings.INSTALLED_APPS:
qs = qs.filter(weiclub=None)
return qs
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):
2020-04-06 06:58:39 +00:00
"""
Update the profile picture of a club.
"""
model = Club
template_name = 'member/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, ProtectedCreateView):
2020-04-06 06:58:39 +00:00
"""
Add a membership to a club.
"""
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Add new member to the club")}
2020-02-18 11:31:15 +00:00
def get_sample_object(self):
if "club_pk" in self.kwargs:
club = Club.objects.get(pk=self.kwargs["club_pk"])
else:
club = Membership.objects.get(pk=self.kwargs["pk"]).club
return Membership(
user=self.request.user,
club=club,
fee=0,
date_start=timezone.now(),
date_end=timezone.now() + timedelta(days=1),
)
2020-02-18 11:31:15 +00:00
def get_context_data(self, **kwargs):
2020-08-18 16:19:39 +00:00
"""
Membership can be created, or renewed
In case of creation the url is /club/<club_pk>/add_member
For a renewal it will be `club/renew_membership/<pk>`
"""
context = super().get_context_data(**kwargs)
2020-04-05 19:56:56 +00:00
form = context['form']
2020-08-18 16:19:39 +00:00
if "club_pk" in self.kwargs: # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None)
2020-04-05 19:56:56 +00:00
form.fields['credit_amount'].initial = club.membership_fee_paid
2020-08-18 16:19:39 +00:00
# Ensure that the user is member of the parent club and all its the family tree.
c = club
clubs_renewal = []
additional_fee_renewal = 0
while c.parent_club is not None:
c = c.parent_club
clubs_renewal.append(c)
2020-08-06 10:50:24 +00:00
additional_fee_renewal += c.membership_fee_paid
context["clubs_renewal"] = clubs_renewal
context["additional_fee_renewal"] = additional_fee_renewal
2020-04-06 06:58:39 +00:00
# If the concerned club is the BDE, then we add the option that Société générale pays the membership.
if club.name != "BDE":
del form.fields['soge']
else:
fee = 0
bde = Club.objects.get(name="BDE")
fee += bde.membership_fee_paid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid
context["total_fee"] = "{:.02f}".format(fee / 100, )
2020-08-18 16:19:39 +00:00
else: # This is a renewal. Fields can be pre-completed.
context["renewal"] = True
2020-04-05 19:56:56 +00:00
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
user = old_membership.user
c = club
clubs_renewal = []
additional_fee_renewal = 0
while c.parent_club is not None:
c = c.parent_club
2020-08-18 16:19:39 +00:00
# check if a valid membership exists for the parent club
2020-08-06 12:11:55 +00:00
if c.membership_start and not Membership.objects.filter(
club=c,
user=user,
date_start__gte=c.membership_start,
).exists():
clubs_renewal.append(c)
additional_fee_renewal += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
context["clubs_renewal"] = clubs_renewal
context["additional_fee_renewal"] = additional_fee_renewal
2020-04-05 19:56:56 +00:00
form.fields['user'].initial = user
form.fields['user'].disabled = True
form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
form.fields['credit_amount'].initial = (club.membership_fee_paid if user.profile.paid
2020-08-06 10:50:24 +00:00
else club.membership_fee_unpaid) + additional_fee_renewal
2020-04-05 19:56:56 +00:00
form.fields['last_name'].initial = user.last_name
form.fields['first_name'].initial = user.first_name
2020-08-18 16:19:39 +00:00
# If this is a renewal of a BDE membership, Société générale can pays, if it has not been already done.
if (club.name != "BDE" and club.name != "Kfet") or user.profile.soge:
del form.fields['soge']
else:
fee = 0
bde = Club.objects.get(name="BDE")
if not Membership.objects.filter(
club=bde,
user=user,
date_start__gte=bde.membership_start,
).exists():
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
if not Membership.objects.filter(
club=kfet,
user=user,
date_start__gte=bde.membership_start,
).exists():
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
context["total_fee"] = "{:.02f}".format(fee / 100, )
2020-03-25 16:42:54 +00:00
context['club'] = club
2020-02-21 17:28:21 +00:00
return context
def perform_verifications(self, form, user, club, fee):
"""
Make some additional verifications to check that the membership can be created.
:return: True if the form is clean, False if there is an error.
"""
error = False
# Retrieve form data
credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"]
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
if not credit_type:
credit_amount = 0
if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter(
club__name="Kfet",
user=user,
date_start__lte=date.today(),
date_end__gte=date.today(),
).exists():
# Users without a valid Kfet membership can't have a negative balance.
# 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."))
error = True
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'))
error = True
# Must join the parent club before joining this club, except for the Kfet club where it can be at the same time.
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
user=form.instance.user,
club=club.parent_club,
date_start__gte=club.parent_club.membership_start,
).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
error = True
if club.membership_start and form.instance.date_start < club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
error = True
if club.membership_end and form.instance.date_start > club.membership_end:
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
.format(form.instance.club.membership_end))
error = True
if credit_amount and not SpecialTransaction.validate_payment_form(form):
# Check that special information for payment are filled
error = True
return not error
2020-09-11 20:52:16 +00:00
@transaction.atomic
2020-03-31 21:54:14 +00:00
def form_valid(self, form):
2020-04-06 06:58:39 +00:00
"""
Create membership, check that all is good, make transactions
"""
# Get the club that is concerned by the membership
2020-08-30 09:59:10 +00:00
if "club_pk" in self.kwargs: # get from url of new membership
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
2020-04-05 19:56:56 +00:00
.get(pk=self.kwargs["club_pk"])
user = form.instance.user
old_membership = None
2020-08-30 09:59:10 +00:00
else: # get from url for renewal
2020-04-05 19:56:56 +00:00
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
user = old_membership.user
# Update club membership date
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates()
2020-03-31 21:54:14 +00:00
form.instance.club = club
2020-04-01 01:42:19 +00:00
2020-04-06 06:58:39 +00:00
# Get form data
2020-04-05 16:37:04 +00:00
credit_type = form.cleaned_data["credit_type"]
2020-08-30 09:59:10 +00:00
# but with this way users can customize their section as they want.
2020-04-05 16:37:04 +00:00
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
# If Société générale pays, then we store that information but the payment must be controlled by treasurers
# later. The membership transaction will be invalidated.
if soge:
credit_type = None
form.instance._soge = True
2020-04-05 16:37:04 +00:00
if credit_type is None:
credit_amount = 0
fee = 0
c = club
2020-08-18 16:19:39 +00:00
# collect the fees required to be paid
2020-08-06 12:11:55 +00:00
while c is not None and c.membership_start:
if not Membership.objects.filter(
club=c,
user=user,
date_start__gte=c.membership_start,
).exists():
fee += c.membership_fee_paid if user.profile.paid else c.membership_fee_unpaid
c = c.parent_club
# Make some verifications about the form, and if there is an error, then assume that the form is invalid
if not self.perform_verifications(form, user, club, fee):
return self.form_invalid(form)
2020-04-01 01:42:19 +00:00
2020-04-06 06:58:39 +00:00
# Now, all is fine, the membership can be created.
if club.name == "BDE" or club.name == "Kfet":
2020-08-18 16:19:39 +00:00
# When we renew the BDE membership, we update the profile section
# that should happens at least once a year.
user.profile.section = user.profile.section_generated
user.profile._force_save = True
user.profile.save()
2020-04-06 06:58:39 +00:00
# Credit note before the membership is created.
2020-04-05 16:37:04 +00:00
if credit_amount > 0:
transaction = SpecialTransaction(
2020-04-05 16:37:04 +00:00
source=credit_type,
destination=user.note,
quantity=1,
amount=credit_amount,
reason="Crédit " + credit_type.special_type + " (Adhésion " + club.name + ")",
last_name=last_name,
first_name=first_name,
bank=bank,
valid=True,
)
transaction._force_save = True
transaction.save()
2020-04-05 16:37:04 +00:00
2020-09-02 20:54:01 +00:00
# Parent club memberships are automatically renewed / created.
# For example, a Kfet membership creates a BDE membership if it does not exist.
form.instance._force_renew_parent = True
2020-04-14 02:46:52 +00:00
ret = super().form_valid(form)
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
2020-09-02 20:54:01 +00:00
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
# Set the same roles as before
if old_membership:
member_role = member_role.union(old_membership.roles.all())
form.instance.roles.set(member_role)
form.instance._force_save = True
form.instance.save()
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
# Kfet membership.
2020-09-02 20:54:01 +00:00
if soge and club.name == "BDE":
kfet = Club.objects.get(name="Kfet")
2020-09-02 20:54:01 +00:00
fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
2020-09-02 20:54:01 +00:00
# Get current membership, to get the end date
old_membership = Membership.objects.filter(
club=kfet,
user=user,
).order_by("-date_start")
2020-09-02 20:54:01 +00:00
if not old_membership.filter(date_start__gte=kfet.membership_start).exists():
# If the membership is not already renewed
membership = Membership(
2020-09-02 20:54:01 +00:00
club=kfet,
user=user,
fee=fee,
2020-09-02 20:54:01 +00:00
date_start=max(old_membership.first().date_end + timedelta(days=1), kfet.membership_start)
if old_membership.exists() else form.instance.date_start,
)
membership._force_save = True
membership._soge = True
membership.save()
membership.refresh_from_db()
if old_membership.exists():
membership.roles.set(old_membership.get().roles.all())
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
membership.save()
2020-04-14 02:46:52 +00:00
return ret
2020-04-01 01:42:19 +00:00
def get_success_url(self):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
2020-04-01 01:42:19 +00:00
class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
2020-04-06 06:58:39 +00:00
"""
Manage the roles of a user in a club
"""
2020-04-01 01:42:19 +00:00
model = Membership
form_class = MembershipRolesForm
2020-04-01 01:42:19 +00:00
template_name = 'member/add_members.html'
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Manage roles of an user in the club")}
2020-04-01 01:42:19 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = self.object.club
context['club'] = club
return context
2020-04-05 19:56:56 +00:00
def get_form(self, form_class=None):
form = super().get_form(form_class)
club = self.object.club
form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
2020-04-05 19:56:56 +00:00
return form
2020-03-31 21:54:14 +00:00
def get_success_url(self):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = Membership
table_class = MembershipTable
template_name = "member/club_members.html"
extra_context = {"title": _("Members of the club")}
def get_queryset(self, **kwargs):
qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
if 'search' in self.request.GET:
pattern = self.request.GET['search']
2024-07-18 11:51:56 +00:00
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
qs = qs.filter(
2024-07-18 11:51:56 +00:00
Q(**{f"user__first_name{suffix}": prefix + pattern})
| Q(**{f"user__last_name{suffix}": prefix + pattern})
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
)
2020-07-31 15:01:52 +00:00
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
if only_active:
qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())
2020-07-31 15:01:52 +00:00
if "roles" in self.request.GET:
2020-09-02 20:54:01 +00:00
roles_str = self.request.GET["roles"].replace(' ', '').split(',') if self.request.GET["roles"] else ['0']
2020-07-31 15:01:52 +00:00
roles_int = map(int, roles_str)
qs = qs.filter(roles__in=roles_int)
qs = qs.order_by('-date_start', 'user__username')
return qs.distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-07-31 15:01:52 +00:00
club = Club.objects.filter(
PermissionBackend.filter_queryset(self.request, Club, "view")
).get(pk=self.kwargs["pk"])
2020-07-31 15:01:52 +00:00
context["club"] = club
applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
context["applicable_roles"] = applicable_roles
context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
2020-08-01 14:07:47 +00:00
return context