# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import transaction from django.db.models import Q from django.shortcuts import resolve_url, redirect 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 from django.views.generic import CreateView, TemplateView, DetailView from django.views.generic.edit import FormMixin from django_tables2 import SingleTableView from member.forms import ProfileForm from member.models import Membership, Club from note.models import SpecialTransaction, Alias from note.templatetags.pretty_money import pretty_money from permission.backends import PermissionBackend from permission.models import Role from permission.views import ProtectQuerysetMixin from treasury.models import SogeCredit from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm from .tables import FutureUserTable from .tokens import email_validation_token class UserCreateView(CreateView): """ A view to create a User and add a Profile """ form_class = SignUpForm template_name = 'registration/signup.html' second_form = ProfileForm extra_context = {"title": _("Register new user")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) 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) del context["profile_form"].fields["section"] del context["profile_form"].fields["report_frequency"] del context["profile_form"].fields["last_report"] return context @transaction.atomic 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. The user must also wait that someone validate her/his account. """ profile_form = ProfileForm(data=self.request.POST) if not profile_form.is_valid(): return self.form_invalid(form) # Save the user and the profile 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 user.save() user.refresh_from_db() profile.user = user profile._force_save = True profile.save() 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() return super().form_valid(form) 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') class UserValidateView(TemplateView): """ A view to validate the email address. """ title = _("Email validation") template_name = 'registration/email_validation_complete.html' extra_context = {"title": _("Validate email")} def get(self, *args, **kwargs): """ With a given token and user id (in params), validate the email address. """ assert 'uidb64' in kwargs and 'token' in kwargs self.validlink = False user = self.get_user(kwargs['uidb64']) token = kwargs['token'] # Validate the token if user is not None and email_validation_token.check_token(user, token): # The user must wait that someone validates the account before the user can be active and login. self.validlink = True user.is_active = user.profile.registration_valid or user.is_superuser user.profile.email_confirmed = True user._force_save = True user.save() user.profile._force_save = True user.profile.save() return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400) def get_user(self, uidb64): """ Get user from the base64-encoded string. """ 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['user_object'] = self.get_user(self.kwargs["uidb64"]) context['login_url'] = resolve_url(settings.LOGIN_URL) if self.validlink: context['validlink'] = True else: context.update({ 'title': _('Email validation unsuccessful'), 'validlink': False, }) return context class UserValidationEmailSentView(TemplateView): """ Display the information that the validation link has been sent. """ template_name = 'registration/email_validation_email_sent.html' extra_context = {"title": _('Email validation email sent')} class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView): """ Rensend the email validation link. """ model = User extra_context = {"title": _("Resend email validation link")} 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) class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Display pre-registered users, with a search bar """ model = User table_class = FutureUserTable template_name = 'registration/future_user_list.html' extra_context = {"title": _("Pre-registered users list")} def get_queryset(self, **kwargs): """ Filter the table with the given parameter. :param kwargs: :return: """ qs = super().get_queryset().distinct().filter(profile__registration_valid=False) if "search" in self.request.GET and self.request.GET["search"]: pattern = self.request.GET["search"] qs = qs.filter( Q(first_name__iregex=pattern) | Q(last_name__iregex=pattern) | Q(profile__section__iregex=pattern) | Q(username__iregex="^" + pattern) ) return qs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = _("Unregistered users") return context class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): """ 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" extra_context = {"title": _("Registration detail")} def post(self, request, *args, **kwargs): form = self.get_form() self.object = self.get_object() return self.form_valid(form) if form.is_valid() else self.form_invalid(form) 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 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) user = self.get_object() form.fields["last_name"].initial = user.last_name form.fields["first_name"].initial = user.first_name return form @transaction.atomic def form_valid(self, form): """ Finally validate the registration, with creating the membership. """ user = self.get_object() if Alias.objects.filter(normalized_name=Alias.normalize(user.username)).exists(): # Don't try to hack an existing account. form.add_error(None, _("An alias with a similar name already exists.")) return self.form_invalid(form) # 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"] if soge: # If Société Générale pays the inscription, the user automatically joins the two clubs. join_bde = True join_kfet = True if not join_bde: # This software belongs to the BDE. form.add_error('join_bde', _("You must join the BDE.")) return super().form_invalid(form) # 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 # 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 if fee > credit_amount and not soge: # 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) # Save the user and finally validate the registration # Saving the user creates the associated note ret = super().form_valid(form) 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: # Credit the note SpecialTransaction.objects.create( source=credit_type, destination=user.note, quantity=1, amount=credit_amount, 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: # 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")) membership.save() if join_kfet: # 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")) 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): """ Delete a pre-registered user. """ extra_context = {"title": _("Invalidate pre-registration")} 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')