How to add real-time notifications to your Django app with django-pusherable

django-logo.png

Aaron Bassett talks about how and why he built django-pusherable, a mixin library for Django Class-based views to add real-time notifications.

Introduction

This article is part of

In this guest blog post Aaron Bassett talks about how and why he built django-pusherable, a mixin library for Django Class-based views that makes it easy to add real-time notifications to your Django apps.

Aaron Bassett is a freelance developer and technology strategist who is most comfortable with Python and CoffeeScript, but is a bit of a programming polyglot. He works with clients as Rawtech.io Limited and is currently developing systems to open Government data to the world. You can follow him at @aaronbassett

Content management systems are so completely ubiquitous that they have replaced Hello World as the introduction to new “full stack” frameworks. I struggle to think of a single web site or application I’ve built for a client in the last decade which has not had some form of CMS included.

As clients have become more comfortable at managing their own content we have had to build better tools to help them to do so. We’ve added better HTML editors, access control layers, publishing workflows, and so on. The greatest leap forward however has been realtime collaborative editing. The ability to see when someone else is making changes on the same piece of content or is accessing the same instance of an object helps us avoid over-writes and conflicts, and work more efficiently.

However, while adding realtime collaboration to your application is possible it may not be applicable for some types of workflow and applications. The most important element in many cases is the knowledge that another editor has opened some content you’re currently working on.

Help Scout Notifications

Help Scout uses this pattern to great effect within their help desk software. Notifying other agents when one of their colleagues has opened the same thread.

Sending our notifications

Sending events via Pusher is already very simple in Django (or in Flask, Bottle, web.py, etc) using the Pusher Python HTTP library

1pusher.trigger('a_channel', 'an_event', {'some': ‘data'})

But how would we go about adding this to our update view? Let’s use the example of a blog post. We have a model Article with a model form of ArticleUpdateForm, and our standard class based generic view may look something like

1class ArticleUpdate(UpdateView):
2    model = Article
3    form_class = ArticleUpdateForm

If we wanted to trigger a Pusher event whenever anyone accessed the ArticleUpdate view we could add it to our render_to_response method.

1def render_to_response(self, context, **response_kwargs):
2
3    channel = u"article_{pk}".format(pk=self.object.pk)
4    event_data = {'user': self.request.user.username}
5    pusher.trigger(
6        [channel, ], 
7        u"update", 
8        event_data
9    )
10
11    return super(ArticleUpdate, self).render_to_response(context, **response_kwargs)

Notice how we set the channel to be the model name plus the object’s primary key. This way we create a channel unique to this particular object. We don’t want the client to have to subscribe to updates for all model instances and filter for particular instances. It can instead subscribe to updates for a single instance.

This works great, now whenever someone accesses the update view for an object, anyone who is subscribed to that object’s channel will be notified instantly! But adding similar code to each different view on other models isn’t very DRY. So, let’s make it more generic by creating a mixin.

Creating our Mixin

Let’s define a PusherMixin:

1from django.conf import settings
2from pusher import Pusher
3
4class PusherMixin(object):
5
6    def render_to_response(self, context, **response_kwargs):
7
8        channel = u"{model}_{pk}".format(
9            model=self.object._meta.model_name,
10            pk=self.object.pk
11        )
12        event_data = {'user': self.request.user.username}
13
14        pusher = Pusher(app_id=settings.PUSHER_APP_ID,
15                        key=settings.PUSHER_KEY,
16                        secret=settings.PUSHER_SECRET)
17        pusher.trigger(
18            [channel, ],
19            self.pusher_event_name,
20            event_data
21        )
22
23        return super(PusherMixin, self).render_to_response(context, **response_kwargs)

The first tricky part of creating our mixin is ensuring our channel is unique and uses the correct model name. But, we can find this as part of the object’s meta object._meta.model_name. In the example above we also need to define a pusher_event_name to identify the event we’re going to trigger.

We now have the basis of our generic mixin that can be used with any UpdateView to enable automatic event notifications on the object’s Pusher channel. Our new ArticleUpdate view looks something like this

1class ArticleUpdate(PusherMixin, UpdateView):
2    model = Article
3    form_class = ArticleUpdateForm
4    pusher_event_name = u"update"

Sometimes it can be handy to get the data that has changed on the client. So a final improvement to this is to send the data for the current object along with the notification. Unfortunately Django models won’t serialise directly so we need to create a dict that can be.

1import json
2from django.core.serializers.json import DjangoJSONEncoder
3from django.forms.models import model_to_dict
4
5class PusherMixin(object):
6
7    def render_to_response(self, context, **response_kwargs):
8
9        # ...
10        event_data = self.__object_to_json_serializable(self.object)      
11
12        # trigger & return
13
14    def __object_to_json_serializable(self, object):
15        model_dict = model_to_dict(object)
16        json_data = json.dumps(model_dict, cls=DjangoJSONEncoder)
17        data = json.loads(json_data)
18        return data

There are probably more elegant ways of doing this, but it does the trick.

Subscribing to events

Once you have your view pushing out notifications you need to allow your user’s to subscribe to them. As we’re going to be adding this to our web application we will use Pusher’s Javascript API.

1<script src="//js.pusher.com/2.2/pusher.min.js"></script>
2<script>
3    var pusher = new Pusher('{{ settings.PUSHER_KEY }}');
4    var channel = pusher.subscribe('model_{{ object.pk }}');
5    channel.bind('update', function(data) {
6      alert(data.user + " has begun updating this object");
7    });
8</script>

Brilliant! Eight lines of HTML/JavaScript and we now have basic notifications. But, again it isn’t very DRY. We don’t want to copy and paste this piece of JavaScript on every page we need notifications, and then remember to change the model name and event type. So lets create a custom template tag to make things neater for us.

1@register.simple_tag
2def pusherable_subscribe(event, instance):
3
4    channel = u"{model}_{pk}".format(
5        model=instance._meta.model_name,
6        pk=instance.pk
7    )
8
9    return """
10    <script>
11    var pusher = new Pusher('{key}');
12    var channel = pusher.subscribe('{channel}');
13    channel.bind('{event}', function(data) {{
14      pusherable_notify('{event}', data);
15    }});
16    </script>
17    """.format(
18        key=settings.PUSHER_KEY,
19        channel=channel,
20        event=event
21    )

When we want to add notifications to a page we can simply use our template tag:

1{% pusherable_subscribe 'update' object %}

Our new tag takes two arguments. The type of event to subscribe to, in this case update, and a reference to the object we want to receive events for.

When a new event is triggered we call a JavaScript function called pusherable_notify which receives the event type as well as any data that is sent via Pusher. This function could show a modal, or place a banner along the top of the page, or even use the web Notifications API to send a desktop notification! We’ve left pusherable_notify as undefined as the specifics will be different on each site.

Introducing django-pusherable

To help you get started we’ve combined all of the above into an easy to use package. You can view it on Github or install it via pip.

1pip install django-pusherable

It comes with mixins for the most common object views; PusherDetailMixin, PusherUpdateMixin and PusherDeleteMixin. However you can extend the PusherMixin object to add any others you require. We’ve also included the template tag to make subscribing to events a breeze. View the Quickstart guide for more details.

Wait! What was that about our own mixins?

For example imagine you are adding Private Messaging to your application. You have already included the PusherDetailMixin to provide realtime read receipts to the sender, but you want to notify them when their friend begins to write a reply.

Replying animation

First create your custom mixin, and extend the PusherMixin

1from pusherable.mixins import PusherMixin
2
3class PusherReplyMixin(PusherMixin):
4    pusher_event_name = u”reply"

Now add this to your reply view

1class MessageReply(PusherReplyMixin, View):

The only requirements for your custom mixins is that they define a pusher_event_name and are able to access the model instance via self.object.

To subscribe to this new event, you can still use the template tag as normal, but don’t forget to use your custom event name!

1{% load pusherable_tags %}
2
3{% pusherable_subscribe 'reply' object %}

Or to implement a pusherable_notify. For example, in this case a basic implementation may be:

1function pusherable_notify(eventName, data) {
2    if(eventName === 'reply') {
3        alert(data.user + ' has started to ' + eventName);
4    }
5    // ... handle other event types
6}

django-pusherable is only at v0.1.0 so we’re keen to get your feedback and input. If you develop any great new mixins I’d love to see them, send me a tweet or even better open a pull request!

Further reading

  • This article is part of our Building Realtime Applications tutorials, which covers a wide range of frameworks, stacks, use cases, and is updated on a regular basis.
  • Making Disqus Realtime looks at the infrastructure required to roll your own real-time notification or messaging system.
  • Another excellent article on real-time infrastructure and Python/Django is Lessons Learned Architecting Realtime Applications
  • If you’re interested in rolling your own notification system by running a Node.js server in parallel then you can take a look at django-realtime by Anish Menon that uses iShout.js.
  • Not about notifications, but still an interesting resource, is django-realtime-playground by FZambia. It covers a whole bunch of possibilities for Django real-time integration including Node.js server + Socket.IO, Node.js server + Sock.js, Python Tornado + Socket.IO, Python Tornado + Sock.js and Python Cyclone + jQuery Eventsource lib.