Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
В этой статье мы рассмотрим асинхронную работу с отправкой писем подтверждения при регистрации пользователя и письма из формы обратной связи в Django 4.1. С помощью Celery и Redis мы ускорим отправку писем, тем самым улучшив пользовательский опыт.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Перед прочтением этой статьи, вам необходимо иметь или знать функционал предыдущих уроков:
- Настроить SMTP для отправки писем - из урока 22: Настройка почты для отправки писем через SMTP.
- Создать саму функцию для отправки письма подтверждения - из урока 24: Регистрация с подтверждением email адреса.
- Настроить форму обратной связи - из урока 28: Добавление формы обратной связи.
- Установить и настроить Redis и Celery - из урока 44: Установка Redis и Celery для асинхронных задач.
Переделаем функцию для отправки письма подтверждения пользователя
Нам необходимо убрать код для отправки письма активации из представления, чтобы сделать его асинхронным и работающим с Celery, а также удобочитаемым в отдельную функцию.
В одном из уроков мы уже создавали файл email.py для хранения функций для писем в модуле services. Делали мы его в уроке по форме обратной связи.
Давайте дополним код email.py следующей функцией send_activate_email_message()
:
from django.core.mail import EmailMessage
from django.conf import settings
from django.template.loader import render_to_string
from django.contrib.auth import get_user_model
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.utils.http import urlsafe_base64_encode
from django.urls import reverse_lazy
from django.utils.encoding import force_bytes
from django.shortcuts import get_object_or_404
User = get_user_model()
def send_contact_email_message(subject, email, content, ip, user_id):
"""
Функция отправки письма из формы обратной связи
"""
user = User.objects.get(id=user_id) if user_id else None
message = render_to_string('system/email/feedback_email_send.html', {
'email': email,
'content': content,
'ip': ip,
'user': user,
})
email = EmailMessage(subject, message, settings.EMAIL_SERVER, settings.EMAIL_ADMIN)
email.send(fail_silently=False)
def send_activate_email_message(user_id):
"""
Функция отправки письма с подтверждением для аккаунта
"""
user = get_object_or_404(User, id=user_id)
current_site = Site.objects.get_current().domain
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})
subject = f'Активируйте свой аккаунт, {user.username}!'
message = render_to_string('system/email/activate_email_send.html', {
'user': user,
'activation_url': f'http://{current_site}{activation_url}',
})
return user.email_user(subject, message)
О созданной функции: эта функция предназначена для отправки электронного письма с подтверждением для активации аккаунта.
Входной параметр user_id
используется для получения объекта User из базы данных с помощью функции get_object_or_404()
, которая возвращает объект или сообщение об ошибке 404, если объект не найден.
Затем текущий домен сайта получается с помощью Site.objects.get_current().domain
.
Токен генерируется с помощью default_token_generator.make_token()
, используя объект User
. Этот токен используется для создания URL-адреса активации, который передается в контексте шаблона.
Для кодирования идентификатора пользователя используется функция urlsafe_base64_encode()
, а затем этот идентификатор и токен используются в reverse_lazy()
для создания URL-адреса активации.
Затем создается тема и сообщение письма, используя шаблон system/email/activate_email_send.html
и контекст с объектом User
и URL-адресом активации.
Наконец, функция email_user()
объекта User
вызывается с темой и сообщением, чтобы отправить письмо.
Создадим шаблон activate_email_send.html в папке templates/system/email со следующей HTML разметкой:
{% autoescape off %}
Здравствуйте, {{ user.get_full_name }}!
Пожалуйста, перейдите по следующей ссылке, чтобы подтвердить свой адрес электронной почты на нашем сайте:
{{ activation_url }}
Команда сайта созданного на Django
{% endautoescape %}
Отлично. Теперь у нас есть две функции: send_contact_email_message()
и send_activate_email_message()
, которые мы можем сделать асинхронными с помощью Celery.
Создание задач для функций отправки писем Django в Celery
Нам необходимо создать файл tasks.py, я его буду хранить в модуле services, рядом с другим функционалом.
Создаем файл tasks.py со следующим кодом:
from celery import shared_task
from .email import send_activate_email_message, send_contact_email_message
@shared_task
def send_activate_email_message_task(user_id):
"""
1. Задача обрабатывается в представлении: UserRegisterView
2. Отправка письма подтверждения осуществляется через функцию: send_activate_email_message
"""
return send_activate_email_message(user_id)
@shared_task
def send_contact_email_message_task(subject, email, content, ip, user_id):
"""
1. Задача обрабатывается в представлении: FeedbackCreateView
2. Отправка письма из формы обратной связи осуществляется через функцию: send_contact_email_message
"""
return send_contact_email_message(subject, email, content, ip, user_id)
Этот код относится к использованию Celery для асинхронной отправки электронной почты.
shared_task
- это декоратор Celery, который превращает обычную функцию в задачу, которую можно запускать асинхронно.
send_activate_email_message_task()
и send_contact_email_message_task()
- это задачи, которые используются для отправки электронной почты.
send_activate_email_message_task()
принимает параметр user_id
, который используется для получения соответствующего пользователя. Затем он вызывает функцию send_activate_email_message()
, чтобы отправить электронное письмо с подтверждением аккаунта. Эта задача используется в представлении UserRegisterView, чтобы отправить письмо с подтверждением аккаунта после успешной регистрации пользователя.
send_contact_email_message_task()
принимает несколько параметров, таких как subject
, email
, content
, ip
и user_id
, которые используются для формирования сообщения обратной связи. Затем он вызывает функцию send_contact_email_message()
, чтобы отправить письмо. Эта задача используется в представлении FeedbackCreateView, чтобы отправить электронное письмо, когда пользователь заполняет форму обратной связи на веб-сайте.
Использование Celery позволяет отправлять электронную почту асинхронно, что ускоряет работу приложения и улучшает пользовательский опыт.
Изменим существующие представления для работы с Celery
Нам необходимо изменить два представления из modules/system/views.py, а именно: UserRegisterView и FeedbackCreateView.
Меняем код UserRegisterView с этого:
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')
На следующий:
# Не забываем другие импорты...
from ..services.tasks import send_activate_email_message_task
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()
send_activate_email_message_task.delay(user.id)
return redirect('email_confirmation_sent')
Теперь мы вызываем функцию отправки письма подтверждения асинхронно, вызывая нашу задачу, обязательно добавляйте метод delay()
к асинхронной функции для Celery.
И изменим FeedbackCreateView с этого:
class FeedbackCreateView(SuccessMessageMixin, CreateView):
model = Feedback
form_class = FeedbackCreateForm
success_message = 'Ваше письмо успешно отправлено администрации сайта'
template_name = 'system/feedback.html'
extra_context = {'title': 'Контактная форма'}
success_url = reverse_lazy('home')
def form_valid(self, form):
if form.is_valid():
feedback = form.save(commit=False)
feedback.ip_address = get_client_ip(self.request)
if self.request.user.is_authenticated:
feedback.user = self.request.user
send_contact_email_message(feedback.subject, feedback.email, feedback.content, feedback.ip_address, feedback.user_id)
return super().form_valid(form)
На следующий:
# Не забываем другие импорты...
from ..services.tasks import send_contact_email_message_task
class FeedbackCreateView(SuccessMessageMixin, CreateView):
model = Feedback
form_class = FeedbackCreateForm
success_message = 'Ваше письмо успешно отправлено администрации сайта'
template_name = 'system/feedback.html'
extra_context = {'title': 'Контактная форма'}
success_url = reverse_lazy('home')
def form_valid(self, form):
if form.is_valid():
feedback = form.save(commit=False)
feedback.ip_address = get_client_ip(self.request)
if self.request.user.is_authenticated:
feedback.user = self.request.user
send_contact_email_message_task.delay(feedback.subject, feedback.email, feedback.content, feedback.ip_address, feedback.user_id)
return super().form_valid(form)
Лишние импорты, если такие есть, можно удалить из views.py, они нам больше не нужны.
Зачем нужен метод delay() в Celery
Метод delay()
используется в связке с Celery для асинхронного выполнения задач.
В данном случае, после сохранения формы регистрации, создается новый пользователь и отправляется электронное письмо для подтверждения адреса электронной почты. Однако, отправка письма может занять некоторое время, что может замедлить работу страницы и ухудшить пользовательский опыт.
Для избежания этой проблемы, метод delay()
используется для асинхронного выполнения функции send_activate_email_message_task()
в фоновом режиме, не блокируя основной поток выполнения. Таким образом, пользователь может продолжать работу с сайтом, в то время как задача по отправке письма выполняется в фоновом режиме.
Проверяем асинхронную отправку писем в Django с Celery
Для этого я я запущу наше приложение Django: py manage.py runserver
, а также Celery: celery --app=backend worker --loglevel=info --pool=solo
Теперь давайте отправим письмо из формы обратной связи:
На этом всё, я надеюсь, что у вас всё получилось и вы следовали урокам. Впереди будут ещё уроки и статьи, различных тем, а также докеризация проекта и деплой.