Skip to content
Snippets Groups Projects
views.py 16.2 KiB
Newer Older
ynerant's avatar
ynerant committed
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

from django.conf import settings
ynerant's avatar
ynerant committed
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
ynerant's avatar
ynerant committed
from django.views.generic import CreateView, TemplateView, DetailView
ynerant's avatar
ynerant committed
from django.views.generic.edit import FormMixin
ynerant's avatar
ynerant committed
from django_tables2 import SingleTableView
from member.forms import ProfileForm
ynerant's avatar
ynerant committed
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
ynerant's avatar
ynerant committed
from permission.models import Role
ynerant's avatar
ynerant committed
from permission.views import ProtectQuerysetMixin
from treasury.models import SogeCredit
bleizi's avatar
bleizi committed
# from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
from .forms import SignUpForm, ValidationForm
ynerant's avatar
ynerant committed
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
ynerant's avatar
ynerant committed
    template_name = 'registration/signup.html'
    second_form = ProfileForm
ynerant's avatar
ynerant committed
    extra_context = {"title": _("Register new user")}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
ynerant's avatar
ynerant committed
        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)
ynerant's avatar
ynerant committed
        del context["profile_form"].fields["section"]
ynerant's avatar
ynerant committed
        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.
ynerant's avatar
ynerant committed
        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)

ynerant's avatar
ynerant committed
        # 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):
ynerant's avatar
ynerant committed
    """
    A view to validate the email address.
    """
ynerant's avatar
ynerant committed
    title = _("Email validation")
    template_name = 'registration/email_validation_complete.html'
    extra_context = {"title": _("Validate email")}
ynerant's avatar
ynerant committed
    def get(self, *args, **kwargs):
ynerant's avatar
ynerant committed
        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']

ynerant's avatar
ynerant committed
        # Validate the token
        if user is not None and email_validation_token.check_token(user, token):
ynerant's avatar
ynerant committed
            # The user must wait that someone validates the account before the user can be active and login.
            self.validlink = True
ynerant's avatar
ynerant committed
            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
        return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)

    def get_user(self, uidb64):
ynerant's avatar
ynerant committed
        """
        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)
ynerant's avatar
ynerant committed
        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({
ynerant's avatar
ynerant committed
                'title': _('Email validation unsuccessful'),
                'validlink': False,
            })
        return context


class UserValidationEmailSentView(TemplateView):
ynerant's avatar
ynerant committed
    """
    Display the information that the validation link has been sent.
    """
    template_name = 'registration/email_validation_email_sent.html'
ynerant's avatar
ynerant committed
    extra_context = {"title": _('Email validation email sent')}
ynerant's avatar
ynerant committed

class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
ynerant's avatar
ynerant committed
    """
    Rensend the email validation link.
    """
    model = User
ynerant's avatar
ynerant committed
    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)


ynerant's avatar
ynerant committed
class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
    """
ynerant's avatar
ynerant committed
    Display pre-registered users, with a search bar
ynerant's avatar
ynerant committed
    """
    model = User
    table_class = FutureUserTable
    template_name = 'registration/future_user_list.html'
ynerant's avatar
ynerant committed
    extra_context = {"title": _("Pre-registered users list")}
ynerant's avatar
ynerant committed

    def get_queryset(self, **kwargs):
ynerant's avatar
ynerant committed
        """
        Filter the table with the given parameter.
        :param kwargs:
        :return:
        """
ynerant's avatar
ynerant committed
        qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
        if "search" in self.request.GET and self.request.GET["search"]:
ynerant's avatar
ynerant committed
            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)
            )

ynerant's avatar
ynerant committed

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context["title"] = _("Unregistered users")

        return context

ynerant's avatar
ynerant committed
class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
ynerant's avatar
ynerant committed
    Display information about a pre-registered user, in order to complete the registration.
    form_class = ValidationForm
    context_object_name = "user_object"
    template_name = "registration/future_profile_detail.html"
ynerant's avatar
ynerant committed
    extra_context = {"title": _("Registration detail")}
ynerant's avatar
ynerant committed
    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
        if Club.objects.filter(name__iexact="BDA").exists():
            bda = Club.objects.get(name__iexact="BDA")
            fee += bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
        ctx["total_fee"] = "{:.02f}".format(fee / 100, )

#        ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
    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.
        """
ynerant's avatar
ynerant committed
        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)

        # Check if BDA exist to propose membership at regisration
        bda_exists = False
        if Club.objects.filter(name__iexact="BDA").exists():
            bda_exists = True

ynerant's avatar
ynerant committed
        # 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 bda_exists:
            join_bda = form.cleaned_data["join_bda"]
#        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."))
ynerant's avatar
ynerant committed
            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 bda_exists:
            bda = Club.objects.get(name__iexact="BDA")
            bda_fee = bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
            # Add extra fee for the bda membership
            fee += bda_fee if join_bda 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:
bleizi's avatar
bleizi committed
        if fee > credit_amount:
ynerant's avatar
ynerant committed
            # 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
ynerant's avatar
ynerant committed
        if credit_type is not None and credit_amount > 0 and not SpecialTransaction.validate_payment_form(form):
            return self.form_invalid(form)
ynerant's avatar
ynerant committed
        # Save the user and finally validate the registration
        # Saving the user creates the associated note
        ret = super().form_valid(form)
ynerant's avatar
ynerant committed
        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:
ynerant's avatar
ynerant committed
            # 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)",
bleizi's avatar
bleizi committed
                # 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,
            )

ynerant's avatar
ynerant committed
            # 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 BDE"))
            membership.save()
ynerant's avatar
ynerant committed
            # 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 Kfet"))
            membership.save()

        if bda_exists and join_bda:
            # Create membership for the user to the BDA starting today
            membership = Membership(
                club=bda,
                user=user,
                fee=bda_fee,
            )
            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):
    """
ynerant's avatar
ynerant committed
    Delete a pre-registered user.
ynerant's avatar
ynerant committed
    extra_context = {"title": _("Invalidate pre-registration")}
ynerant's avatar
ynerant committed
    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')