mirror of https://gitlab.crans.org/bde/nk20
Send an e-mail verification to a new registered user
This commit is contained in:
parent
72e5df0cf5
commit
26281af673
|
@ -4,6 +4,7 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||
from permission.models import PermissionMask
|
||||
|
||||
|
@ -23,10 +24,14 @@ class SignUpForm(UserCreationForm):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.fields['username'].widget.attrs.pop("autofocus", None)
|
||||
self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
|
||||
self.fields['first_name'].required = True
|
||||
self.fields['last_name'].required = True
|
||||
self.fields['email'].required = True
|
||||
self.fields['email'].help_text = _("This address must be valid.")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
fields = ('first_name', 'last_name', 'username', 'email', )
|
||||
|
||||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
|
@ -37,7 +42,7 @@ class ProfileForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
exclude = ['user']
|
||||
exclude = ('user', 'email_confirmed', )
|
||||
|
||||
|
||||
class ClubForm(forms.ModelForm):
|
||||
|
|
|
@ -45,6 +45,12 @@ class Profile(models.Model):
|
|||
)
|
||||
paid = models.BooleanField(
|
||||
verbose_name=_("paid"),
|
||||
help_text=_("Tells if the user receive a salary."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
email_confirmed = models.BooleanField(
|
||||
verbose_name=_("email confirmed"),
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copied from https://gitlab.crans.org/bombar/codeflix/-/blob/master/codeflix/codeflix/tokens.py
|
||||
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
|
||||
|
||||
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
|
||||
"""
|
||||
Create a unique token generator to confirm email addresses.
|
||||
"""
|
||||
def _make_hash_value(self, user, timestamp):
|
||||
"""
|
||||
Hash the user's primary key and some user state that's sure to change
|
||||
after an account validation to produce a token that invalidated when
|
||||
it's used:
|
||||
1. The user.profile.email_confirmed field will change upon an account
|
||||
validation.
|
||||
2. The last_login field will usually be updated very shortly after
|
||||
an account validation.
|
||||
Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually
|
||||
invalidates the token.
|
||||
"""
|
||||
# Truncate microseconds so that tokens are consistent even if the
|
||||
# database doesn't support microseconds.
|
||||
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
|
||||
return str(user.pk) + str(user.profile.email_confirmed) + str(login_timestamp) + str(timestamp)
|
||||
|
||||
|
||||
account_activation_token = AccountActivationTokenGenerator()
|
|
@ -8,6 +8,9 @@ from . import views
|
|||
app_name = 'member'
|
||||
urlpatterns = [
|
||||
path('signup/', views.UserCreateView.as_view(), name="signup"),
|
||||
path('accounts/activate/sent', views.UserActivationEmailSentView.as_view(), name='account_activation_sent'),
|
||||
path('accounts/activate/<uidb64>/<token>', views.UserActivateView.as_view(), name='account_activation'),
|
||||
|
||||
|
||||
path('club/', views.ClubListView.as_view(), name="club_list"),
|
||||
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
|
||||
|
|
|
@ -9,12 +9,18 @@ from django.conf import settings
|
|||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.forms import HiddenInput
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import redirect, resolve_url
|
||||
from django.template import loader
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
||||
from django.views.generic.base import View
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
@ -30,6 +36,7 @@ from permission.views import ProtectQuerysetMixin
|
|||
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
|
||||
from .models import Club, Membership
|
||||
from .tables import ClubTable, UserTable, MembershipTable
|
||||
from .tokens import account_activation_token
|
||||
|
||||
|
||||
class CustomLoginView(LoginView):
|
||||
|
@ -57,15 +64,90 @@ class UserCreateView(CreateView):
|
|||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
If the form is valid, then the user is created with is_active set to False
|
||||
so that the user cannot log in until the email has been validated.
|
||||
"""
|
||||
profile_form = ProfileForm(self.request.POST)
|
||||
if form.is_valid() and profile_form.is_valid():
|
||||
user = form.save(commit=False)
|
||||
user.profile = profile_form.save(commit=False)
|
||||
user.save()
|
||||
user.profile.save()
|
||||
if not profile_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
user = form.save(commit=False)
|
||||
user.is_active = False
|
||||
user.profile = profile_form.save(commit=False)
|
||||
user.save()
|
||||
user.profile.save()
|
||||
site = get_current_site(self.request)
|
||||
subject = "Activate your {} account".format(site.name)
|
||||
message = loader.render_to_string('registration/account_activation_email.html',
|
||||
{
|
||||
'user': user,
|
||||
'domain': site.domain,
|
||||
'site_name': "La Note Kfet",
|
||||
'protocol': 'https',
|
||||
'token': account_activation_token.make_token(user),
|
||||
'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode('UTF-8'),
|
||||
})
|
||||
user.email_user(subject, message)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UserActivateView(TemplateView):
|
||||
title = _("Account Activation")
|
||||
template_name = 'registration/account_activation_complete.html'
|
||||
|
||||
@method_decorator(csrf_protect)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""
|
||||
The dispatch method looks at the request to determine whether it is a GET, POST, etc,
|
||||
and relays the request to a matching method if one is defined, or raises HttpResponseNotAllowed
|
||||
if not. We chose to check the token in the dispatch method to mimic PasswordReset from
|
||||
django.contrib.auth
|
||||
"""
|
||||
assert 'uidb64' in kwargs and 'token' in kwargs
|
||||
|
||||
self.validlink = False
|
||||
user = self.get_user(kwargs['uidb64'])
|
||||
token = kwargs['token']
|
||||
|
||||
if user is not None and account_activation_token.check_token(user, token):
|
||||
self.validlink = True
|
||||
user.is_active = True
|
||||
user.profile.email_confirmed = True
|
||||
user.save()
|
||||
return super().dispatch(*args, **kwargs)
|
||||
else:
|
||||
# Display the "Account Activation unsuccessful" page.
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def get_user(self, uidb64):
|
||||
print(uidb64)
|
||||
try:
|
||||
# urlsafe_base64_decode() decodes to bytestring
|
||||
uid = urlsafe_base64_decode(uidb64).decode()
|
||||
user = User.objects.get(pk=uid)
|
||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
|
||||
user = None
|
||||
return user
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['login_url'] = resolve_url(settings.LOGIN_URL)
|
||||
if self.validlink:
|
||||
context['validlink'] = True
|
||||
else:
|
||||
context.update({
|
||||
'title': _('Account Activation unsuccessful'),
|
||||
'validlink': False,
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class UserActivationEmailSentView(TemplateView):
|
||||
template_name = 'registration/account_activation_email_sent.html'
|
||||
title = _('Account activation email sent')
|
||||
|
||||
|
||||
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
|
@ -75,14 +157,20 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
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.")
|
||||
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
|
||||
context['title'] = _("Update Profile")
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
if 'username' not in form.data:
|
||||
return form
|
||||
def form_valid(self, form):
|
||||
new_username = form.data['username']
|
||||
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
|
||||
note = NoteUser.objects.filter(
|
||||
|
@ -90,9 +178,8 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||
if note.exists() and note.get().user != self.object:
|
||||
form.add_error('username',
|
||||
_("An alias with a similar name already exists."))
|
||||
return form
|
||||
return super().form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
profile_form = ProfileForm(
|
||||
data=self.request.POST,
|
||||
instance=self.object.profile,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{% trans Activation %}</h2>
|
||||
|
||||
{% if validlink %}
|
||||
{% blocktrans trimmed %}
|
||||
Your account have successfully been activated. You can now <a href="{{ login_url }}">log in</a>.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
The link was invalid. The token may have expired. Please send us an email to activate your account.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
|||
Hi {{ user.username }},
|
||||
|
||||
Welcome to {{ site_name }}. Please click on the link below to confirm your registration.
|
||||
|
||||
{{ protocol }}://{{ domain }}{% url 'member:account_activation' uidb64=uid token=token %}
|
||||
|
||||
This link is only valid for a couple of days, after that you will need to contact us to validate your email.
|
||||
|
||||
Thanks,
|
||||
|
||||
{{ site_name }} team.
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Account Activation</h2>
|
||||
|
||||
An email has been sent. Please click on the link to activate your account.
|
||||
{% endblock %}
|
Loading…
Reference in New Issue