Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45

Django Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45


В этой статье мы рассмотрим асинхронную работу с отправкой писем подтверждения при регистрации пользователя и письма из формы обратной связи в Django 4.1. С помощью Celery и Redis мы ускорим отправку писем, тем самым улучшив пользовательский опыт.

Перед прочтением этой статьи, вам необходимо иметь или знать функционал предыдущих уроков:

  1. Настроить SMTP для отправки писем - из урока 22: Настройка почты для отправки писем через SMTP.
  2. Создать саму функцию для отправки письма подтверждения - из урока 24: Регистрация с подтверждением email адреса.
  3. Настроить форму обратной связи - из урока 28: Добавление формы обратной связи.
  4. Установить и настроить Redis и Celery - из урока 44: Установка Redis и Celery для асинхронных задач.

Переделаем функцию для отправки письма подтверждения пользователя

Нам необходимо убрать код для отправки письма активации из представления, чтобы сделать его асинхронным и работающим с Celery, а также удобочитаемым в отдельную функцию.

В одном из уроков мы уже создавали файл email.py для хранения функций для писем в модуле services. Делали мы его в уроке по форме обратной связи.

Давайте дополним код email.py следующей функцией send_activate_email_message():

modules/services/email.py
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 разметкой:

templates/system/email/activate_email_send.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 со следующим кодом:

modules/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 с этого:

modules/system/views.py
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')

На следующий:

modules/system/views.py
# Не забываем другие импорты...

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 с этого:

modules/system/views.py
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)

На следующий:

modules/system/views.py
# Не забываем другие импорты...

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

Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
При запуске Celery, можно увидеть созданные функциональные задачи
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
Регистрирую аккаунт
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
После нажатия моментально выходит окно, что сообщение отправлено
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
В терминале можно увидеть, что Celery принял задачу и выполнил её
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
Полученное асинхронное письмо подтверждения
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
При переходе успешное подтверждение

Теперь давайте отправим письмо из формы обратной связи:

Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
Заполняю данные для отправки письма из фидбека
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
Письмо моментально отправляется
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
В терминале наблюдаем выполнение функции Celery
Django База [2023]: Асинхронная отправка писем подтверждения и фидбека с помощью Celery и Redis 📨 #45
Ну и само доставленное письмо с помощью асинхронной функции Celery

На этом всё, я надеюсь, что у вас всё получилось и вы следовали урокам. Впереди будут ещё уроки и статьи, различных тем, а также докеризация проекта и деплой.