1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-12-26 17:32:22 +00:00
nk20/apps/registration/views.py

450 lines
17 KiB
Python
Raw 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
2020-04-05 03:17:28 +00:00
# SPDX-License-Identifier: GPL-3.0-or-later
2024-07-15 12:27:55 +00:00
from django import forms
2020-04-05 03:17:28 +00:00
from django.conf import settings
2020-04-05 04:40:03 +00:00
from django.contrib.auth.mixins import LoginRequiredMixin
2020-04-05 03:17:28 +00:00
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
2020-09-11 20:52:16 +00:00
from django.db import transaction
2020-04-16 22:48:54 +00:00
from django.db.models import Q
from django.shortcuts import resolve_url, redirect
2020-04-05 03:17:28 +00:00
from django.urls import reverse_lazy
from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _
from django.views import View
2020-04-16 21:31:36 +00:00
from django.views.generic import CreateView, TemplateView, DetailView
2020-04-06 08:45:32 +00:00
from django.views.generic.edit import FormMixin
2020-04-05 04:40:03 +00:00
from django_tables2 import SingleTableView
2024-07-18 11:51:56 +00:00
from api.viewsets import is_regex
2020-04-05 03:17:28 +00:00
from member.forms import ProfileForm
2020-07-25 17:40:30 +00:00
from member.models import Membership, Club
2020-09-03 18:03:40 +00:00
from note.models import SpecialTransaction, Alias
from note.templatetags.pretty_money import pretty_money
from permission.backends import PermissionBackend
2020-07-25 17:40:30 +00:00
from permission.models import Role
2020-04-05 04:40:03 +00:00
from permission.views import ProtectQuerysetMixin
from treasury.models import SogeCredit
2020-04-05 03:17:28 +00:00
2023-07-09 08:36:36 +00:00
# from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
from .forms import SignUpForm, ValidationForm
2020-04-05 04:40:03 +00:00
from .tables import FutureUserTable
2020-04-05 07:48:23 +00:00
from .tokens import email_validation_token
2020-04-05 03:17:28 +00:00
class UserCreateView(CreateView):
"""
2020-08-19 11:03:36 +00:00
A view to create a User and add a Profile
2020-04-05 03:17:28 +00:00
"""
form_class = SignUpForm
2020-04-05 04:40:03 +00:00
template_name = 'registration/signup.html'
2020-04-05 03:17:28 +00:00
second_form = ProfileForm
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Register new user")}
2020-04-05 03:17:28 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2020-08-05 14:27:44 +00:00
context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
# context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None)
2020-04-23 16:28:16 +00:00
del context["profile_form"].fields["section"]
2020-08-05 21:19:17 +00:00
del context["profile_form"].fields["report_frequency"]
del context["profile_form"].fields["last_report"]
2020-04-05 03:17:28 +00:00
return context
2020-09-11 20:52:16 +00:00
@transaction.atomic
2020-04-05 03:17:28 +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.
2020-04-06 06:58:39 +00:00
The user must also wait that someone validate her/his account.
2020-04-05 03:17:28 +00:00
"""
profile_form = ProfileForm(data=self.request.POST)
2020-04-05 03:17:28 +00:00
if not profile_form.is_valid():
return self.form_invalid(form)
2020-04-06 06:58:39 +00:00
# Save the user and the profile
2020-04-05 03:17:28 +00:00
user = form.save(commit=False)
user.is_active = False
profile_form.instance.user = user
profile = profile_form.save(commit=False)
user.profile = profile
user._force_save = True
2020-04-05 03:17:28 +00:00
user.save()
user.refresh_from_db()
profile.user = user
profile._force_save = True
profile.save()
2020-04-05 03:17:28 +00:00
user.profile.send_email_validation_link()
# soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
# if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
# # If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
# soge_credit = SogeCredit(user=user)
# soge_credit._force_save = True
# soge_credit.save()
2020-04-05 03:17:28 +00:00
return super().form_valid(form)
2020-04-16 22:48:54 +00:00
def get_success_url(self):
# Direct access to validation menu if we have the right to validate it
if PermissionBackend.check_perm(self.request, 'auth.view_user', self.object):
return reverse_lazy('registration:future_user_detail', args=(self.object.pk,))
return reverse_lazy('registration:email_validation_sent')
2020-04-16 22:48:54 +00:00
2020-04-05 03:17:28 +00:00
class UserValidateView(TemplateView):
2020-04-06 06:58:39 +00:00
"""
A view to validate the email address.
"""
2020-04-05 13:42:09 +00:00
title = _("Email validation")
2020-04-05 07:48:23 +00:00
template_name = 'registration/email_validation_complete.html'
2020-08-01 08:48:17 +00:00
extra_context = {"title": _("Validate email")}
2020-04-05 03:17:28 +00:00
2020-04-06 06:58:39 +00:00
def get(self, *args, **kwargs):
2020-04-05 03:17:28 +00:00
"""
2020-04-06 06:58:39 +00:00
With a given token and user id (in params), validate the email address.
2020-04-05 03:17:28 +00:00
"""
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
user = self.get_user(kwargs['uidb64'])
token = kwargs['token']
2020-04-06 06:58:39 +00:00
# Validate the token
2020-04-05 07:48:23 +00:00
if user is not None and email_validation_token.check_token(user, token):
2020-04-06 06:58:39 +00:00
# The user must wait that someone validates the account before the user can be active and login.
2020-08-01 08:48:17 +00:00
self.validlink = True
2020-04-06 08:45:32 +00:00
user.is_active = user.profile.registration_valid or user.is_superuser
2020-04-05 03:17:28 +00:00
user.profile.email_confirmed = True
user._force_save = True
2020-04-05 03:17:28 +00:00
user.save()
user.profile._force_save = True
user.profile.save()
2020-09-03 18:03:40 +00:00
return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
2020-04-05 03:17:28 +00:00
def get_user(self, uidb64):
2020-04-06 06:58:39 +00:00
"""
Get user from the base64-encoded string.
"""
2020-04-05 03:17:28 +00:00
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)
2020-04-16 21:31:36 +00:00
context['user_object'] = self.get_user(self.kwargs["uidb64"])
2020-04-05 03:17:28 +00:00
context['login_url'] = resolve_url(settings.LOGIN_URL)
if self.validlink:
context['validlink'] = True
else:
context.update({
2020-04-05 13:42:09 +00:00
'title': _('Email validation unsuccessful'),
2020-04-05 03:17:28 +00:00
'validlink': False,
})
return context
class UserValidationEmailSentView(TemplateView):
2020-04-06 06:58:39 +00:00
"""
Display the information that the validation link has been sent.
"""
2020-04-05 07:48:23 +00:00
template_name = 'registration/email_validation_email_sent.html'
2020-07-30 15:30:21 +00:00
extra_context = {"title": _('Email validation email sent')}
2020-04-05 03:17:28 +00:00
2020-04-05 04:40:03 +00:00
2020-04-05 07:48:23 +00:00
class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
2020-04-06 06:58:39 +00:00
"""
Rensend the email validation link.
"""
2020-04-05 07:48:23 +00:00
model = User
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Resend email validation link")}
2020-04-05 07:48:23 +00:00
def get(self, request, *args, **kwargs):
user = self.get_object()
user.profile.send_email_validation_link()
url = 'member:user_detail' if user.profile.registration_valid else 'registration:future_user_detail'
return redirect(url, user.id)
2020-04-05 04:40:03 +00:00
class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
2020-04-06 06:58:39 +00:00
Display pre-registered users, with a search bar
2020-04-05 04:40:03 +00:00
"""
model = User
table_class = FutureUserTable
template_name = 'registration/future_user_list.html'
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Pre-registered users list")}
2020-04-05 04:40:03 +00:00
def get_queryset(self, **kwargs):
2020-04-06 06:58:39 +00:00
"""
Filter the table with the given parameter.
:param kwargs:
:return:
"""
2020-07-25 16:18:53 +00:00
qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
2020-09-03 18:03:40 +00:00
if "search" in self.request.GET and self.request.GET["search"]:
2020-04-06 06:58:39 +00:00
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_username = "__iregex" if valid_regex else "__icontains"
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
2020-04-06 06:58:39 +00:00
qs = qs.filter(
2024-07-18 11:51:56 +00:00
Q(**{f"first_name{suffix}": pattern})
| Q(**{f"last_name{suffix}": pattern})
| Q(**{f"profile__section{suffix}": pattern})
| Q(**{f"username{suffix_username}": prefix + pattern})
2020-04-06 06:58:39 +00:00
)
return qs
2020-04-05 04:40:03 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Unregistered users")
return context
2020-04-06 08:45:32 +00:00
class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
"""
2020-04-06 06:58:39 +00:00
Display information about a pre-registered user, in order to complete the registration.
"""
model = User
form_class = ValidationForm
context_object_name = "user_object"
template_name = "registration/future_profile_detail.html"
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Registration detail")}
2020-04-06 08:45:32 +00:00
def post(self, request, *args, **kwargs):
form = self.get_form()
self.object = self.get_object()
2020-09-03 18:03:40 +00:00
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
2020-04-06 08:45:32 +00:00
def get_queryset(self, **kwargs):
"""
We only display information of a not registered user.
"""
return super().get_queryset().filter(profile__registration_valid=False)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
user = self.get_object()
fee = 0
bde = Club.objects.get(name="BDE")
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
2024-07-15 12:27:55 +00:00
for club in Club.objects.filter(add_registration_form=True):
fee += club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
return ctx
def get_form(self, form_class=None):
form = super().get_form(form_class)
2024-07-15 12:27:55 +00:00
# add clubs that are in registration form
for club in Club.objects.filter(add_registration_form=True).order_by("name"):
form_join_club = forms.BooleanField(
label=_("Join %(club)s Club") % {'club': club.name},
required=False,
initial=False,
)
form.fields.update({f"join_{club.id}": form_join_club})
user = self.get_object()
form.fields["last_name"].initial = user.last_name
form.fields["first_name"].initial = user.first_name
return form
2020-09-11 20:52:16 +00:00
@transaction.atomic
def form_valid(self, form):
"""
Finally validate the registration, with creating the membership.
"""
2020-04-06 08:45:32 +00:00
user = self.get_object()
2020-09-03 18:03:40 +00:00
if Alias.objects.filter(normalized_name=Alias.normalize(user.username)).exists():
# Don't try to hack an existing account.
2020-09-03 18:03:40 +00:00
form.add_error(None, _("An alias with a similar name already exists."))
return self.form_invalid(form)
2020-04-06 06:58:39 +00:00
# Get form data
# soge = form.cleaned_data["soge"]
credit_type = form.cleaned_data["credit_type"]
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"]
join_bde = form.cleaned_data["join_bde"]
join_kfet = form.cleaned_data["join_kfet"]
2024-07-15 12:27:55 +00:00
clubs_registration = Club.objects.filter(add_registration_form=True).order_by("name")
join_clubs = [(club, form.cleaned_data[f"join_{club.id}"]) for club in clubs_registration]
# if soge:
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
# join_bde = True
# join_kfet = True
2024-08-29 18:03:43 +00:00
if not (join_bde or any(b for _, b in join_clubs)):
# This software belongs to the BDE.
2024-08-29 18:03:43 +00:00
form.add_error('join_bde', _("You must join a club."))
2020-04-06 08:45:32 +00:00
return super().form_invalid(form)
2024-08-29 18:03:43 +00:00
if join_kfet and not join_bde:
2024-08-29 19:06:34 +00:00
form.add_error('join_bde', _("You must also join the parent club BDE."))
return super().form_invalid(form)
2020-04-06 08:45:32 +00:00
# Calculate required registration fee
fee = 0
bde = Club.objects.get(name="BDE")
bde_fee = bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
# This is mandatory.
fee += bde_fee if join_bde else 0
kfet = Club.objects.get(name="Kfet")
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
# Add extra fee for the full membership
fee += kfet_fee if join_kfet else 0
2024-07-15 12:27:55 +00:00
clubs_fee = dict()
for club, join_club in join_clubs:
club_fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
# Add extra fee for the club membership
clubs_fee[club] = club_fee
fee += club_fee if join_club else 0
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
# # and credit the note later.
# credit_type = None if soge else credit_type
# If the user does not select any payment method, then no credit will be performed.
credit_amount = 0 if credit_type is None else credit_amount
2020-04-06 08:45:32 +00:00
# if fee > credit_amount and not soge:
2023-07-09 08:36:36 +00:00
if fee > credit_amount:
2020-04-06 06:58:39 +00:00
# Check if the user credits enough money
form.add_error('credit_type',
_("The entered amount is not enough for the memberships, should be at least {}")
.format(pretty_money(fee)))
return self.form_invalid(form)
# Check that payment information are filled, like last name and first name
if credit_type is not None and credit_amount > 0 and not SpecialTransaction.validate_payment_form(form):
return self.form_invalid(form)
2020-04-06 06:58:39 +00:00
# Save the user and finally validate the registration
# Saving the user creates the associated note
ret = super().form_valid(form)
2020-04-06 08:45:32 +00:00
user.is_active = user.profile.email_confirmed or user.is_superuser
user.profile.registration_valid = True
user.save()
user.profile.save()
user.refresh_from_db()
# if not soge and SogeCredit.objects.filter(user=user).exists():
# # If the user declared that a bank account was opened but in the validation form the SoGé case was
# # unchecked, delete the associated credit
# soge_credit = SogeCredit.objects.get(user=user)
# soge_credit._force_delete = True
# soge_credit.delete()
if credit_type is not None and credit_amount > 0:
2020-04-06 06:58:39 +00:00
# Credit the note
SpecialTransaction.objects.create(
source=credit_type,
destination=user.note,
quantity=1,
amount=credit_amount,
reason="Crédit " + credit_type.special_type + " (Inscription)",
2023-07-09 08:36:36 +00:00
# reason="Crédit " + ("Société générale" if soge else credit_type.special_type) + " (Inscription)",
last_name=last_name,
first_name=first_name,
bank=bank,
valid=True,
)
if join_bde:
2020-04-06 06:58:39 +00:00
# Create membership for the user to the BDE starting today
membership = Membership(
club=bde,
user=user,
fee=bde_fee,
)
# if soge:
# membership._soge = True
membership.save()
membership.refresh_from_db()
membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE"))
2020-04-05 14:05:49 +00:00
membership.save()
if join_kfet:
2020-04-06 06:58:39 +00:00
# Create membership for the user to the Kfet starting today
membership = Membership(
club=kfet,
user=user,
fee=kfet_fee,
)
# if soge:
# membership._soge = True
membership.save()
membership.refresh_from_db()
membership.roles.add(Role.objects.get(name="Adhérent⋅e Kfet"))
2020-04-05 14:05:49 +00:00
membership.save()
2024-07-15 12:27:55 +00:00
for club, join_club in join_clubs:
if join_club:
# Create membership for the user to the BDA starting today
membership = Membership(
club=club,
user=user,
fee=clubs_fee[club],
)
membership.save()
membership.refresh_from_db()
membership.roles.add(Role.objects.get(name="Membre de club"))
membership.save()
# if soge:
# soge_credit = SogeCredit.objects.get(user=user)
# # Update the credit transaction amount
# soge_credit.save()
return ret
def get_success_url(self):
return reverse_lazy('member:user_detail', args=(self.get_object().pk, ))
class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
"""
2020-04-06 06:58:39 +00:00
Delete a pre-registered user.
"""
2020-07-30 15:30:21 +00:00
extra_context = {"title": _("Invalidate pre-registration")}
2020-04-06 06:58:39 +00:00
def get(self, request, *args, **kwargs):
"""
Delete the pre-registered user which id is given in the URL.
"""
user = User.objects.filter(profile__registration_valid=False)\
.filter(PermissionBackend.filter_queryset(request, User, "change", "is_valid"))\
.get(pk=self.kwargs["pk"])
# Delete associated soge credits before
SogeCredit.objects.filter(user=user).delete()
user.delete()
return redirect('registration:future_user_list')