0

I'm struggling with the following. I'm trying to create a custom signal that will trigger when the current time will be equal to the value of my model's notify_on DateTimeField.

Something like this:

class Notification(models.Model):
    ...
    notify_on = models.DateTimeField()



def send_email(*args, **kwargs):
    # send email


signals.when_its_time.connect(send_email, sender=User)

After I've read through all docs and I found no information on how to implement such a signal.

Any ideas?

UPDATE: Less naive approach with ability to discard irrelevant tasks: https://stackoverflow.com/a/55337663/9631956

Nikita Tonkoskur
  • 1,440
  • 1
  • 16
  • 28
  • 3
    I believe you may need Celery delayed tasks as signals starts running after some events but not by time. – Sergey Pugach Mar 24 '19 at 13:32
  • @SergeyPugach I already use celery to send confirmation emails, now I'll need to figure out how to configure to it to check all my `Notification` instances. Thank you – Nikita Tonkoskur Mar 24 '19 at 13:53
  • 2
    When you call task with `.apply_async()` you can pass `eta` or `countdown` arg in order your task yo be run in certain time: http://docs.celeryproject.org/en/latest/userguide/calling.html – Sergey Pugach Mar 24 '19 at 14:13

2 Answers2

2

Ok, thanks to comments by @SergeyPugach I've done the following:

Added a post_save signal that calls a function that adds a task to the celery. apply_async let's you pass eta - estimated time of arrival which can accept DateTimeField directly, that's very convenient.

# models.py
from django.db.models import signals
from django.db import models
from .tasks import send_notification

class Notification(models.Model):
    ...
    notify_on = models.DateTimeField()


def notification_post_save(instance, *args, **kwargs):
    send_notification.apply_async((instance,), eta=instance.notify_on)


signals.post_save.connect(notification_post_save, sender=Notification)

And the actual task in the tasks.py

import logging
from user_api.celery import app
from django.core.mail import send_mail
from django.template.loader import render_to_string


@app.task
def send_notification(self, instance):
    try:
        mail_subject = 'Your notification.'
        message = render_to_string('notify.html', {
            'title': instance.title,
            'content': instance.content
        })
        send_mail(mail_subject, message, recipient_list=[instance.user.email], from_email=None)
    except instance.DoesNotExist:
        logging.warning("Notification does not exist anymore")

I will not get into details of setting up celery, there's plenty of information out there.

Now I will try to figure out how to update the task after it's notification instance was updated, but that's a completely different story.

Nikita Tonkoskur
  • 1,440
  • 1
  • 16
  • 28
1

In django's documentation there is two interesting signals that may help you on this task: pre_save and post_save. It depends on your needs, but let's say you want to check if your model's notify_on is equal to the current date after saving your model (actually after calling the save() or create() method). If it's your case you can do:

from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save


class Notification(models.Model):
    ...
    # Every notification is related to a user
    # It depends on your model, but i guess you're doing something similar
    user = models.ForeignKey(User, related_name='notify', on_delete=models.DO_NOTHING)
    notify_on = models.DateTimeField()
    ...
    def send_email(self, *args, **kwargs):
        """A model method to send email notification"""
        ...


@receiver(post_save, sender=User)
def create_notification(sender, instance, created, **kwargs):
    # check if the user instance is created
    if created:
        obj = Notification.objects.create(user=instance, date=datetime.now().date())
        if obj.notify_on == datetime.now().date():
            obj.send_email()

And you should know, that django signals won't work by they own only if there is an action that triggers them. What this mean is that Django signals won't loop over your model's instances and perform an operation, but django signals will trigger when your application is performing an action on the model connected to a signal.

Bonus: To perform a loop over your instances and process an action regulary you may need an asyncworker with a Queue database (mostly, Celery with Redis or RabbitMQ).

Chiheb Nexus
  • 9,104
  • 4
  • 30
  • 43