I had a problem. I wanted to create a newsletter for all the blogs on www.travelblogwave.com so that users could sign up for a weekly or monthly newsletter.

The first thing I did was implement the functionality for users to sign up for the newsletter, which I accomplished through some fairly straightforward actions of creating a model, form and implementing it into the blog post templates.

The model consists of an email, blog (email and blog make the unique id) and frequency, which is a choice field with the choices of weekly or monthly. After I had that functionality setup, it was time for the fun to begin. How would I go about sending scheduled emails to those users?

Creating the Email

I think the way that emails is kind of cool. There are probably some better ways to do it, but for now the way that I am generating them has worked really well. What I do is I create a base template which has most of the formatting so that when I extend that base email template, all of my emails are consistent.

Unfotunately TinyMCE is not letting me paste and show HTML code, so if you'd like to see, here is the base email template (in case you didn't know, emails need inline css, it's that I am just terrible at front-end) and here is the newsletter template that extends.

With this method, I can just pass in the posts an the blog that I want to show up in the email as context.

Now that the email format was ready, and I had the functionality implemented for people to join a weekly or monthly list for a blog, how would I send those emails on a schedule?

 

Scheduling with Django Cron

The answer is an app called Django Cron which helps integrate cron jobs into a django project, using python. Technically you could write some code and run that code with a separate cron job, but then that starts to become an extra piece to your project instead of being included. There are also some problems with cron jobs on shared hosting services such as Heroku because the server changes depending on availability. 

What Django Cron does is that it allows you to include logic for when to run the cron job logic, so that I can have a crons.py in my project. For the blog subscription functionality, crons.py fit nicely in my blog app.

import datetime

from django.conf import settings
from django.core.mail import send_mail
from django.template import Context
from django.template.loader import get_template
from django.utils import timezone

from django_cron import CronJobBase, Schedule

from .models import PersonalBlog, Post, BlogSubscription, BlogSubscriptionLog


class NewsletterJob(CronJobBase):
    RUN_EVERY_MINS = None
    blogs = PersonalBlog.objects.all()
    days_ago = 0
    frequency = ""

    def do(self):
        """
        We're going to go and get the emails that need to be sent.
        When we have the emails that need to be sent, we send the mass
        mail. 
        """

        for b in self.blogs:
            """
            if there are no subscribers to the blog, don't do anything. Also if there
            are no posts to be sent, don't do anything.
            """
            if self.get_subscribers(b) and self.get_posts(b):
                newsletter = get_template('blogemail/newsletter.html').render(Context({
                    'posts': self.get_posts(b),
                    'blog': b,
                    'logo_url': settings.WEB_ROOT_URL + '/static/img/logo_dark.png'
                }))
            
                # send an individual email to each of the subscribers
                for s in self.get_subscribers(b):
                    send_mail(
                        'New posts from %s' % b,
                        '', # use html_message
                        'noreply@travelblogwave.com', 
                        [s],
                        html_message=newsletter
                    )

                    # log that the email was sent
                    subscription = BlogSubscription.objects.get(blog=b, email=s)
                    BlogSubscriptionLog.objects.create(subscription=subscription)


    def get_posts(self, blog):
        # get all the posts that should be included in the email
        posts = Post.objects.filter(active=True, blog=blog,
            create_date__gte=timezone.now().today() - datetime.timedelta(days=self.days_ago))
        return posts

    def get_subscribers(self, blog):
        # get a list of the subscriber emails
        email_list = blog.get_subscriber_emails(self.frequency)
        return email_list


class SendWeeklyNewsletters(NewsletterJob):
    RUN_EVERY_MINS = 60 * 24 * 7 #1 week
    days_ago = 7
    code = 'send_weekly_newsletters'
    frequency = "W"
    schedule = Schedule(run_every_mins=RUN_EVERY_MINS)


class SendMonthlyNewsletters(NewsletterJob):
    RUN_EVERY_MINS = 60 * 24 * 31  #1 month... just about
    days_ago = 31
    code = 'send_monthly_newsletters'
    frequency = "M"
    schedule = Schedule(run_every_mins=RUN_EVERY_MINS)

I started this crons.py file off with 2 separate classes SendWeeklyNewsletters and SendMonthlyNewsletters with the do function as part of the class, as can be seen in the docs of Django Cron. I noticed that there was a ton of shared logic, so I managed to split that logic out into the NewsletterJob base class.

With this code I had functioning emails going out, but there was one problem. I still was having to run the ./manage.py runcrons command manually. They wouldn't run if the time amount (from RUN_EVERY_MINS) hadn't expired, so all that was left was to figure out how to schedule the command to run.

The Travel Blog Wave project is hosted on Heroku and luckily they have so many different functions you can add on to your site. They have an addon called Scheduler and it was easy to implement.

I installed the scheduler add-on and after some minor problems (had to do with importing today from the datetime library instead of django.utils.timezone, aka nieve datetimes with no timezone), things were up and running and newsletters were firing out every week or month.