Aaron Bassett talks about how and why he built django-pusherable, a mixin library for Django Class-based views to add real-time notifications.
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 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 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.
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.
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.
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.
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.
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!