mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-26 18:37:12 +00:00
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 import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||||
from permission.models import PermissionMask
|
from permission.models import PermissionMask
|
||||||
|
|
||||||
@ -23,10 +24,14 @@ class SignUpForm(UserCreationForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['username'].widget.attrs.pop("autofocus", None)
|
self.fields['username'].widget.attrs.pop("autofocus", None)
|
||||||
self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"})
|
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:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ['first_name', 'last_name', 'username', 'email']
|
fields = ('first_name', 'last_name', 'username', 'email', )
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(forms.ModelForm):
|
class ProfileForm(forms.ModelForm):
|
||||||
@ -37,7 +42,7 @@ class ProfileForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Profile
|
model = Profile
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
exclude = ['user']
|
exclude = ('user', 'email_confirmed', )
|
||||||
|
|
||||||
|
|
||||||
class ClubForm(forms.ModelForm):
|
class ClubForm(forms.ModelForm):
|
||||||
|
@ -45,6 +45,12 @@ class Profile(models.Model):
|
|||||||
)
|
)
|
||||||
paid = models.BooleanField(
|
paid = models.BooleanField(
|
||||||
verbose_name=_("paid"),
|
verbose_name=_("paid"),
|
||||||
|
help_text=_("Tells if the user receive a salary."),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_confirmed = models.BooleanField(
|
||||||
|
verbose_name=_("email confirmed"),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
30
apps/member/tokens.py
Normal file
30
apps/member/tokens.py
Normal file
@ -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'
|
app_name = 'member'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('signup/', views.UserCreateView.as_view(), name="signup"),
|
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/', views.ClubListView.as_view(), name="club_list"),
|
||||||
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
|
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.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import HiddenInput
|
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.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.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 import CreateView, DetailView, UpdateView, TemplateView
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
from django.views.generic.edit import FormMixin
|
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 .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
|
||||||
from .models import Club, Membership
|
from .models import Club, Membership
|
||||||
from .tables import ClubTable, UserTable, MembershipTable
|
from .tables import ClubTable, UserTable, MembershipTable
|
||||||
|
from .tokens import account_activation_token
|
||||||
|
|
||||||
|
|
||||||
class CustomLoginView(LoginView):
|
class CustomLoginView(LoginView):
|
||||||
@ -57,15 +64,90 @@ class UserCreateView(CreateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
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)
|
profile_form = ProfileForm(self.request.POST)
|
||||||
if form.is_valid() and profile_form.is_valid():
|
if not profile_form.is_valid():
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
user = form.save(commit=False)
|
user = form.save(commit=False)
|
||||||
|
user.is_active = False
|
||||||
user.profile = profile_form.save(commit=False)
|
user.profile = profile_form.save(commit=False)
|
||||||
user.save()
|
user.save()
|
||||||
user.profile.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)
|
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):
|
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = User
|
model = User
|
||||||
fields = ['first_name', 'last_name', 'username', 'email']
|
fields = ['first_name', 'last_name', 'username', 'email']
|
||||||
@ -75,14 +157,20 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**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['profile_form'] = self.profile_form(instance=context['user_object'].profile)
|
||||||
context['title'] = _("Update Profile")
|
context['title'] = _("Update Profile")
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def form_valid(self, form):
|
||||||
form = super().get_form(form_class)
|
|
||||||
if 'username' not in form.data:
|
|
||||||
return form
|
|
||||||
new_username = form.data['username']
|
new_username = form.data['username']
|
||||||
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
|
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
|
||||||
note = NoteUser.objects.filter(
|
note = NoteUser.objects.filter(
|
||||||
@ -90,9 +178,8 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
if note.exists() and note.get().user != self.object:
|
if note.exists() and note.get().user != self.object:
|
||||||
form.add_error('username',
|
form.add_error('username',
|
||||||
_("An alias with a similar name already exists."))
|
_("An alias with a similar name already exists."))
|
||||||
return form
|
return super().form_invalid(form)
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
profile_form = ProfileForm(
|
profile_form = ProfileForm(
|
||||||
data=self.request.POST,
|
data=self.request.POST,
|
||||||
instance=self.object.profile,
|
instance=self.object.profile,
|
||||||
|
16
templates/registration/account_activation_complete.html
Normal file
16
templates/registration/account_activation_complete.html
Normal file
@ -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 %}
|
11
templates/registration/account_activation_email.html
Normal file
11
templates/registration/account_activation_email.html
Normal file
@ -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
Block a user