0

I have used the default Django admin panel as my backend. I have a Blogpost model. What I am trying to do is whenever an admin user saves a blogpost object on Django admin, I need to send an email to the newsletter subscribers notifying them that there is a new blog on the website.

I have to send mass emails so I am using django-celery. Also, I am using django signals to trigger the send email function.

But Right now, I am sending without using celery but it is too slow.

class Subscribers(models.Model):
    email = models.EmailField(unique=True)
    date_subscribed = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.email

    class Meta:
        verbose_name_plural = "Newsletter Subscribers"

    # binding signal:
    @receiver(post_save,sender=BlogPost)
    def send_mails(sender,instance,created,**kwargs):
        subscribers = Subscribers.objects.all()

        if created:
            blog = BlogPost.objects.latest('date_created')
            for abc in subscribers:
                emailad = abc.email
                send_mail('New Blog Post ', f" Checkout our new blog with title {blog.title} ",
                          EMAIL_HOST_USER, [emailad],
                          fail_silently=False)
        else:
            return

Using celery documentation i have written following files.

My celery.py

from __future__ import absolute_import
import os
from celery import Celery
from django.conf import settings


os.environ.setdefault('DJANGO_SETTINGS_MODULE','travel_crm.settings')

app = Celery('travel_crm')
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

@app.task(bind=True)
def debug_task(self):
    print('Request: {0!r}'.format(self.request))

Mu init file:

from __future__ import absolute_import, unicode_literals
from .celery import app as celery_app
__all__ = ('celery_app',)

Tasks file from docs:

 def my_first_task(duration):
        subject= 'Celery'
        message= 'My task done successfully'
        receiver= 'receiver_mail@gmail.com'
        is_task_completed= False
        error=''
        try:
            sleep(duration)
            is_task_completed= True
        except Exception as err:
            error= str(err)
            logger.error(error)
        if is_task_completed:
            send_mail_to(subject,message,receivers)
        else:
            send_mail_to(subject,error,receivers)
        return('first_task_done')

This task doesn't work because I am using Django signal to trigger the send email function, How to employ this into tasks.py

Reactoo
  • 916
  • 2
  • 12
  • 40

1 Answers1

1

I think I understand your question ... I was recently faced with similar challenge which included the complexity of a multi-tenant [schema] database [proved to be an issue with Redis]. I also tried django-celery, but it is dependent on a much older version of Celery. In addition, I wanted to send mass mail initiated by model signal post_save ... using EmailMultiAlternatives with 'bcc' and 'reply-to' features.

So now I am using the latest [as of this post] Django, the latest Celery with Redis ... running on macOS localhost with Poetry virtual env & package manager. The following worked for me:

  1. Celery: I spent several hours net searching for tutorials and advice ... among others, this one added value for me Celery w Django. Good practice to dig deeper into Celery anyway if you have not done it already.

  2. Redis: This will depend on your OS and if you are developing local or remote. The Redis website will guide you to set up. I also tried RabbitMQ but found it [personally] more complex to set up.

  3. The code fractions: There are 4 fractions ... myapp/signals.py, myapp/tasks.py, myproj/celery.py, myproj/settings.py Disclaimer: I'm a hobby programmer ... more experienced engineers may well improve on my code ... I've done some minor testing and all seems to work.

# myapp/signals.py
@receiver(post_save, sender=MyModel)
def post_save_handler(sender, instance, **kwargs):
    if instance.some_field == True:
        recipient_list = list(get_user_model().objects.filter('some filters'))
        from_email = SomeModel.objects.first().site_email
        to_email = SomeModel.first().admin_email
        # call async task Celery
        task_send_mail.delay(instance.some_field, instance.someother_field, from_email, to_email, recipient_list)

# myapp/tasks.py
@shared_task(name='task_sendmail')
def task_send_mail(instance.some_field, instance.someother_field, from_email, to_email, recipient_list):
    
    template_name = 'emails/sometemplate.html'
    html_message = render_to_string(template_name, {'body': instance.some_field,}) # These variables are added to the email template
    plain_message = strip_tags(html_message)
    subject = f'Do Not Reply : {instance.someother_field}'
    connection = get_connection()
    connection.open()  
    message = EmailMultiAlternatives(
        subject, 
        plain_message, 
        from_email,
        [to_email], 
        bcc=recipient_list, 
        reply_to=[to_email],
    )
    
    message.attach_alternative(html_message, "text/html")
    
    try:
        message.send()
        connection.close() 
    except SMTPException as e:
        print('There was an error sending email: ', e) 
        connection.close() 

# myproj/celery.py
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproj.settings')

app = Celery('myproj')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
    print(f'Request: {self.request!r}')

# myproj/settings.py
...
##Celery Configuration Options
CELERY_BROKER_URL = 'redis://localhost:6379//'
CELERY_TIMEZONE = "Africa/SomeCity"
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60
William
  • 43
  • 6