Отправка электронных писем по расписанию через django

У меня есть модель группы:

class Group(models.Model):
   leader = models.ForeignKey(User, on_delete=models.CASCADE)
   name = models.CharField(max_length=55)
   description = models.TextField()
   joined = models.ManyToManyField(User, blank=True)
   start_time = models.TimeField(null=True)
   end_time = models.TimeField(null=True)

и я хочу отправить электронное письмо всем пользователям, которые joined входят в определенную группу за 30 минут до start_time. Например: если группа имеет время start_time в 13:00, я хочу отправить электронное письмо всем joined пользователям в 12:30, сообщая им, что группа скоро соберется.

Я использовал функцию send_mail() для других типов уведомлений по электронной почте (например, когда пользователь присоединяется к группе или создает ее), но я не могу понять, как отправить письмо на основе start_time Group.

Я видел, что люди говорят, что Celery - это способ сделать это, но мне трудно понять, как реализовать это в моем проекте django. Будет ли это конфликтовать с другими настройками электронной почты, которые у меня уже есть?

# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = '587'
EMAIL_HOST_USER = NOTIFICATION_EMAIL
EMAIL_HOST_PASSWORD = NOTIFICATION_PASSWORD
EMAIL_USE_TLS = True

Есть ли конкретная функция, которую я должен написать, а затем вызвать ее в моей модели Group? Или я вызываю ее в представлении? Или она находится в собственном файле, который вызывается автоматически?

Я также хотел бы добавить возможность для лидера группы установить время до start_time, когда будет отправлено письмо. Например: 10, минут, 30 минут, 1 час до встречи.

Вы можете выполнить шаги здесь для настройки celery для периодических задач.

В соответствии с этим вы можете сделать что-то приблизительно похожее на это:

import datetime
from celery import Celery
from myapp.models import Group


app = Celery()


@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    # Setup and call send_reminders() every 60 seconds.

    sender.add_periodic_task(60.0, send_reminders, name='check reminders to be sent every minute')


@app.task
def send_reminders():
    # Celery task that gets all groups that needs reminders to be sent 30 minutes from now

    thirty_minutes_from_now = datetime.datetime.now() + datetime.timedelta(minutes=30)

    groups = Group.objects.filter(
        start_time__hour=thirty_minutes_from_now.hour, 
        start_time__minute=thirty_minutes_from_now.minute
    ).prefetch_related("joined")

    for group in groups:
        for member in group.joined.all():
            send_email_task.delay(member.email)


@app.task
def send_email_task(recipient):
    # Celery task to send emails

    send_mail(
        'group starting',
        'group starting',
        NOTIFICATION_EMAIL,
        [recipient],
        fail_silently=False
    )

Отказ от ответственности: Это не протестировано и не оптимизировано ;)

Почему периодические задачи не работают?

В Celery <= 5.2.7(stable-version) обнаружена ошибка.

Пусть периодические задания не работают.

Я исправил это в этом PR, вы можете отредактировать исходный код Celery как в этом PR, или попробовать Celery dev-версию.

Растворение 1

# your_app/task.py
@app.on_on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
    for group in Group.object.all():
        notification_time = group.start_time - timedelta(minutes=30)
        sender.add_periodic_task(clocked(notification_time),
                                 start_group_notification_task,
                                 kwargs={'recipients':group.recipients}
                                 name='send mail when group start time')
    # You need to connect the `Group.post_save` and `Group.post_delete` signal here,
    # to setup/revoke your periodic tasks when `Group` changed.

Вы можете использовать clocked в качестве вашего пользовательского класса планировщика, я процитировал его из django-celery-beat.

Солюшен 2

Возможно, вы можете использовать django-celery-beat, и создать m2m, связанные с вашими Group и PeriodicTask.

Выглядит проще.


Отказ от ответственности: Это тоже не протестировано и не оптимизировано.

Я смог понять, как запустить задачу на основе start_time, но все еще получаю некоторые ошибки, когда пытаюсь передать group в качестве аргумента для итерации через group.email_list.

Я добавил это в свой celery.py файл:

app.conf.beat_schedule = {
    'start_group_notification': {
        'task': 'start_group_notification_task',
        'schedule': crontab(),
    }
}

Задание запускается каждую минуту. Затем задача проверяет, есть ли у группы время начала в пределах 30 минут. В group/tasks.py

@shared_task(name='start_group_notification_task')
def start_group_notification_task():
    logger.info('sent email to whole group that group is starting')
    thirty_minutes_from_now = datetime.datetime.now() + datetime.timedelta(minutes=30)
    groups = Group.objects.filter(
        start_time__hour=thirty_minutes_from_now.hour, 
        start_time__minute=thirty_minutes_from_now.minute
    ).prefetch_related("joined")
    for group in groups: 
        send_mail (
                'group starting in 30 minutes',
                group.name,
                NOTIFICATION_EMAIL,
                ['me@gmail.com'],
                fail_silently=False
            )

Теперь это работает, но только для одного пользователя за раз. Идея заключается в том, чтобы задача выполнялась для каждого пользователя, который присоединился к группе. В ответе @Brian это было сделано, поэтому я реализовал это следующим образом:

@shared_task(name='start_group_notification_task')
def group_starting_in_30_task(group):
    logger.info('group starts in 30 email')
    for email in group.email_list:
        send_mail (
                'group starting in 30 minutes',
                group.name,
                NOTIFICATION_EMAIL,
                [email],
                fail_silently=False
            )

@shared_task(name='start_group_notification_task')
def start_group_notification_task():
    logger.info('sent email to whole group that group is starting')
    thirty_minutes_from_now = datetime.datetime.now() + datetime.timedelta(minutes=30)
    groups = Group.objects.filter(
        start_time__hour=thirty_minutes_from_now.hour, 
        start_time__minute=thirty_minutes_from_now.minute
    ).prefetch_related("joined")
    for group in groups: 
        group_starting_in_30_task(group)

Я думал, что это будет работать, но я продолжаю получать ошибки о позиционных аргументах. Я не вижу проблемы, потому что задача, вызываемая crontab(), не принимает никаких аргументов, она только вызывает другую задачу и передает ей group:

TypeError: start_group_notification_task() takes 0 positional arguments but 1 was given

Где давали одного, я думал, что он сам дает?

Я получаю эту ошибку в терминале celery (не в терминале celery-beat), но только когда группа находится в 30 минутах от запуска. Каждую вторую минуту задача выполняется, но None, потому что она фильтруется здесь:

    groups = Group.objects.filter(
        start_time__hour=thirty_minutes_from_now.hour, 
        start_time__minute=thirty_minutes_from_now.minute
    ).prefetch_related("joined")

Все части вроде бы работают, но перестают работать, когда я добавляю другую задачу и другой цикл for для отправки уведомления всем в группе email_list и я не понимаю, как это исправить на данный момент.

Вернуться на верх