Django База [2023]: Регистрация с подтверждением email адреса #24
В этой статье мы модернизируем существующий функционал наших представлений для регистрации в Django, дополнив возможность подтверждения email адреса для защиты от лишнего спама.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Перед началом работы с кодом, рекомендую изучить эти статьи:
Создадим миксин для запрета регистрации авторизованных юзеров
Создадим миксин, который предотвратит посещение страницы регистрации авторизованных пользователей. В одном из уроков мы создавали папку services в папке modules, а в ней уже файл mixins.py.
Дополним этот файл следующим кодом:
from django.contrib import messages
from django.shortcuts import redirect
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import PermissionDenied
class UserIsNotAuthenticated(UserPassesTestMixin):
def test_func(self):
if self.request.user.is_authenticated:
messages.info(self.request, 'Вы уже авторизованы. Вы не можете посетить эту страницу.')
raise PermissionDenied
return True
def handle_no_permission(self):
return redirect('home')
Отлично. Мы его будем подмешивать в наше представление для регистрации.
Работа с представлением регистрации пользователя
Для создания функционала отправки письма нам необходимо дополнить наше представление UserRegisterView в файле views.py нашего приложения system.
Изменяем наше представление с этого:
from django.views.generic import CreateView
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from .forms import UserRegisterForm
class UserRegisterView(SuccessMessageMixin, CreateView):
"""
Представление регистрации на сайте с формой регистрации
"""
form_class = UserRegisterForm
success_url = reverse_lazy('home')
template_name = 'system/user_register.html'
success_message = 'Вы успешно зарегистрировались. Можете войти на сайт!'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Регистрация на сайте'
return context
На следующий код:
from django.views.generic import CreateView
from django.urls import reverse_lazy
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.shortcuts import redirect
from .forms import UserRegisterForm
from ..services.mixins import UserIsNotAuthenticated
class UserRegisterView(UserIsNotAuthenticated, CreateView):
"""
Представление регистрации на сайте с формой регистрации
"""
form_class = UserRegisterForm
success_url = reverse_lazy('home')
template_name = 'system/registration/user_register.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Регистрация на сайте'
return context
def form_valid(self, form):
user = form.save(commit=False)
user.is_active = False
user.save()
# Функционал для отправки письма и генерации токена
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
activation_url = reverse_lazy('confirm_email', kwargs={'uidb64': uid, 'token': token})
current_site = Site.objects.get_current().domain
send_mail(
'Подтвердите свой электронный адрес',
f'Пожалуйста, перейдите по следующей ссылке, чтобы подтвердить свой адрес электронной почты: http://{current_site}{activation_url}',
'service.notehunter@gmail.com',
[user.email],
fail_silently=False,
)
return redirect('email_confirmation_sent')
Создаем представление для подтверждения email адреса
Теперь нам нужно создать представление для обработки токена, который подтвердит нам пользователя, а также три представления для сообщений об успехе, информации, и ошибки.
from django.views.generic import CreateView, View, TemplateView
from django.urls import reverse_lazy
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.shortcuts import redirect
from django.contrib.auth import login
from django.contrib.auth import get_user_model
from .forms import UserRegisterForm
from ..services.mixins import UserIsNotAuthenticated
User = get_user_model()
class UserConfirmEmailView(View):
def get(self, request, uidb64, token):
try:
uid = urlsafe_base64_decode(uidb64)
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None
if user is not None and default_token_generator.check_token(user, token):
user.is_active = True
user.save()
login(request, user)
return redirect('email_confirmed')
else:
return redirect('email_confirmation_failed')
class EmailConfirmationSentView(TemplateView):
template_name = 'system/registration/email_confirmation_sent.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Письмо активации отправлено'
return context
class EmailConfirmedView(TemplateView):
template_name = 'system/registration/email_confirmed.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Ваш электронный адрес активирован'
return context
class EmailConfirmationFailedView(TemplateView):
template_name = 'system/registration/email_confirmation_failed.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Ваш электронный адрес не активирован'
return context
Полный файл views.py
Предоставляю полный файл views.py на основе всех предыдущих уроков:
from django.views.generic import DetailView, UpdateView, CreateView, View, TemplateView
from django.db import transaction
from django.urls import reverse_lazy
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.views import LoginView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView
from django.contrib.sites.models import Site
from django.contrib.auth import get_user_model
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes
from django.core.mail import send_mail
from django.shortcuts import redirect
from django.contrib.auth import login
from .models import Profile
from .forms import UserUpdateForm, ProfileUpdateForm, UserRegisterForm, UserLoginForm, UserPasswordChangeForm, UserForgotPasswordForm, UserSetNewPasswordForm
from ..services.mixins import UserIsNotAuthenticated
User = get_user_model()
class ProfileDetailView(DetailView):
"""
Представление для просмотра профиля
"""
model = Profile
context_object_name = 'profile'
template_name = 'system/profile_detail.html'
queryset = model.objects.all().select_related('user')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Страница пользователя: {self.object.user.username}'
return context
class ProfileUpdateView(UpdateView):
"""
Представление для редактирования профиля
"""
model = Profile
form_class = ProfileUpdateForm
template_name = 'system/profile_edit.html'
def get_object(self, queryset=None):
return self.request.user.profile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Редактирование профиля пользователя: {self.request.user.username}'
if self.request.POST:
context['user_form'] = UserUpdateForm(self.request.POST, instance=self.request.user)
else:
context['user_form'] = UserUpdateForm(instance=self.request.user)
return context
def form_valid(self, form):
context = self.get_context_data()
user_form = context['user_form']
with transaction.atomic():
if all([form.is_valid(), user_form.is_valid()]):
user_form.save()
form.save()
else:
context.update({'user_form': user_form})
return self.render_to_response(context)
return super(ProfileUpdateView, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('profile_detail', kwargs={'slug': self.object.slug})
class UserLoginView(SuccessMessageMixin, LoginView):
"""
Авторизация на сайте
"""
form_class = UserLoginForm
template_name = 'system/registration/user_login.html'
next_page = 'home'
success_message = 'Добро пожаловать на сайт!'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Авторизация на сайте'
return context
class UserPasswordChangeView(SuccessMessageMixin, PasswordChangeView):
"""
Изменение пароля пользователя
"""
form_class = UserPasswordChangeForm
template_name = 'system/registration/user_password_change.html'
success_message = 'Ваш пароль был успешно изменён!'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Изменение пароля на сайте'
return context
def get_success_url(self):
return reverse_lazy('profile_detail', kwargs={'slug': self.request.user.profile.slug})
class UserForgotPasswordView(SuccessMessageMixin, PasswordResetView):
"""
Представление по сбросу пароля по почте
"""
form_class = UserForgotPasswordForm
template_name = 'system/registration/user_password_reset.html'
success_url = reverse_lazy('home')
success_message = 'Письмо с инструкцией по восстановлению пароля отправлена на ваш email'
subject_template_name = 'system/email/password_subject_reset_mail.txt'
email_template_name = 'system/email/password_reset_mail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Запрос на восстановление пароля'
return context
class UserPasswordResetConfirmView(SuccessMessageMixin, PasswordResetConfirmView):
"""
Представление установки нового пароля
"""
form_class = UserSetNewPasswordForm
template_name = 'system/registration/user_password_set_new.html'
success_url = reverse_lazy('home')
success_message = 'Пароль успешно изменен. Можете авторизоваться на сайте.'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Установить новый пароль'
return context
class UserRegisterView(UserIsNotAuthenticated, CreateView):
"""
Представление регистрации на сайте с формой регистрации
"""
form_class = UserRegisterForm
success_url = reverse_lazy('home')
template_name = 'system/registration/user_register.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Регистрация на сайте'
return context
def form_valid(self, form):
user = form.save(commit=False)
user.is_active = False
user.save()
# Функционал для отправки письма и генерации токена
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
activation_url = reverse_lazy('confirm_email', kwargs={'uidb64': uid, 'token': token})
current_site = Site.objects.get_current().domain
send_mail(
'Подтвердите свой электронный адрес',
f'Пожалуйста, перейдите по следующей ссылке, чтобы подтвердить свой адрес электронной почты: http://{current_site}{activation_url}',
'service.notehunter@gmail.com',
[user.email],
fail_silently=False,
)
return redirect('email_confirmation_sent')
class UserConfirmEmailView(View):
def get(self, request, uidb64, token):
try:
uid = urlsafe_base64_decode(uidb64)
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None
if user is not None and default_token_generator.check_token(user, token):
user.is_active = True
user.save()
login(request, user)
return redirect('email_confirmed')
else:
return redirect('email_confirmation_failed')
class EmailConfirmationSentView(TemplateView):
template_name = 'system/registration/email_confirmation_sent.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Письмо активации отправлено'
return context
class EmailConfirmedView(TemplateView):
template_name = 'system/registration/email_confirmed.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Ваш электронный адрес активирован'
return context
class EmailConfirmationFailedView(TemplateView):
template_name = 'system/registration/email_confirmation_failed.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Ваш электронный адрес не активирован'
return context
Настроим обработку представлений
Наши представления необходимо обработать в urls.py, для этого в файле urls.py нашего приложения system добавим следующие строки:
from django.urls import path
from .views import ProfileUpdateView, ProfileDetailView, UserRegisterView, UserLoginView, UserPasswordChangeView, \
UserForgotPasswordView, UserPasswordResetConfirmView, UserConfirmEmailView, EmailConfirmationSentView, EmailConfirmedView, EmailConfirmationFailedView
urlpatterns = [
path('user/edit/', ProfileUpdateView.as_view(), name='profile_edit'),
path('user/<str:slug>/', ProfileDetailView.as_view(), name='profile_detail'),
path('login/', UserLoginView.as_view(), name='login'),
path('password-change/', UserPasswordChangeView.as_view(), name='password_change'),
path('password-reset/', UserForgotPasswordView.as_view(), name='password_reset'),
path('set-new-password/<uidb64>/<token>/', UserPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('register/', UserRegisterView.as_view(), name='register'),
path('email-confirmation-sent/', EmailConfirmationSentView.as_view(), name='email_confirmation_sent'),
path('confirm-email/<str:uidb64>/<str:token>/', UserConfirmEmailView.as_view(), name='confirm_email'),
path('email-confirmed/', EmailConfirmedView.as_view(), name='email_confirmed'),
path('confirm-email-failed/', EmailConfirmationFailedView.as_view(), name='email_confirmation_failed'),
]
Отлично. Теперь перейдем к шаблонам.
Создаем шаблоны для представлений
Я немного изменю расположение наших шаблонов для представлений аутентификации, создав в папке system ещё одну папку registration.
Теперь заполним шаблоны для email, а именно: email_confirmed.html, email_confirmation_sent.html, email_confirmation_failed.html
Добавляем html разметку для email_confirmed.html
{% extends "main.html" %}
{% block content %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">Электронная почта подтверждена</h4>
<p>Ваш адрес электронной почты успешно подтвержден. Спасибо за регистрацию!</p>
</div>
{% endblock %}
Добавляем html разметку для email_confirmation_sent.html
{% extends "main.html" %}
{% block content %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">Письмо подтверждения отправлено!</h4>
<p>
На ваш адрес электронной почты было отправлено письмо с подтверждением. Пожалуйста, проверьте свою электронную почту и нажмите на ссылку подтверждения, чтобы завершить регистрацию.
Если письмо не пришло, проверьте папку спам.
</p>
</div>
{% endblock %}
Добавляем html разметку для email_confirmation_failed.html
{% extends "main.html" %}
{% block content %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Ошибка подтверждения по электронной почте</h4>
<p>Ссылка для подтверждения по электронной почте недействительна или срок ее действия истек. Пожалуйста, зарегистрируйтесь снова.</p>
</div>
{% endblock %}
Отлично. Мы все доработали.
Проверка работы подтверждения email адреса для регистрации
Отлично, все работает так, как нужно, без установки лишних пакетов, со стандартным функционалом Django.