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

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

Razilator

В этой статье мы рассмотрим асинхронную работу с отправкой писем подтверждения при регистрации пользователя и письма из формы обратной связи в 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

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

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

Заполняю данные для отправки письма из фидбека
Заполняю данные для отправки письма из фидбека
Письмо моментально отправляется
Письмо моментально отправляется
В терминале наблюдаем выполнение функции Celery
В терминале наблюдаем выполнение функции Celery
Ну и само доставленное письмо с помощью асинхронной функции Celery
Ну и само доставленное письмо с помощью асинхронной функции Celery

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

;