Real-time Django notifications with node.js, socket.io and Redis

How to push notifications to the user of a Django website in real-time regardless of the browser.

Motivation

It is quite common for modern web sites to include real-time notifications and alerts. Implementing this feature on sites based on Django is difficult due to the limitations on the architecture and underlying technology.

There are several alternatives, like our very own Telegraphy that uses a Twisted server for message delivery to and from the clients. Although this more than enough for our purposes, we wanted to show you how to integrate popular and mature solutions into an existing Django-based site, so we chose Node.js, Socket.io and Redis for message delivery and Django with django-notifications for the notification generation within the site itself.

The solution is based on the one proposed here, but it is extended for the use case of notifications which can be directed to a specific user that might be logged from several locations.

Another extension to Max Burnstein’s solution, is that we’re going to use Nginx as reverse proxy for our site. The access to the site itself is straight forward, but the WebSockets require special consideration.

The application

We’re going to build a Django application on an Ubuntu Server 12.04 LTS, using the following packages, python and node.js modules:

  • curl, python-software-properties, g++ and make
  • python
  • python-virtualenv
  • redis-server
  • nginx (from the official Nginx ppa)
  • nodejs (from Chris Leas’s ppa)
  • npm, socket.io and cookie
  • django
  • django-notifications-hq
  • django-user-sessions

The environment

Let us start by installing the necessary Ubuntu packages:

$ sudo apt-get install curl python-software-properties g++ make
$ sudo apt-get install python python-virtualenv
$ sudo apt-get install redis-server
$ sudo add-apt-repository ppa:chris-lea/node.js
$ sudo add-apt-repository ppa:nginx/stable
$ sudo apt-get update
$ sudo apt-get install nodejs nginx

Next, we are going to install the node.js modules. We chose to set them up as global modules to make things easier, but we could have just installed them locally and then made sure that they are available to our application:

$ curl https://www.npmjs.org/install.sh | sudo sh
$ sudo npm install -g socket.io
$ sudo npm install -g cookie

Now we have everything in place but the python-specific dependencies. In order to avoid messing with the system’s python modules, we’re going to create a new virtualenv, and install the dependencies on it:

$ virtualenv venv
$ . venb/bin/activate
$ pip install django
$ pip install django-notifications-hq
$ pip install django-user-sessions
$ pip install redis

At this point we have everything we need to start building our Django application. We’ll follow the first steps of the Django tutorial so you can have a complete picture of the inner workings of the application.

Initial steps

Unless otherwise indicated, every command listed from now on is supposed to be run from within the virtual environment we created on the previous section. Use:

$ . venb/bin/activate

to activate the virtual environment. We’ll use the django-admin.py script to perform the initial set-up of the application:

$ django-admin.py startproject realtime_notifications

This will create a new directory called realtime_notifications and create an empty Django project called realtime_notifications. Change into the realtime_notifications directory and create an empty application called rn with the following command:

$ python manage.py startapp rn

The contents of the realtime_notifications directory should look like this:

realtime_notifications/
├── manage.py
├── realtime_notifications
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── settings.py
│   ├── settings.pyc
│   ├── urls.py
│   └── wsgi.py
└── rn
    ├── admin.py
    ├── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py

Read the Django documentation for an explanation of the purpose of each file.

We’re going to use the default SQLite database, but the instructions that follow are valid for any of the supported back-ends. Run the following commands to set-up the database:

$ python manage.py syncdb
Creating tables ...
Creating table django_admin_log
Creating table auth_permission
Creating table auth_group_permissions
Creating table auth_group
Creating table auth_user_groups
Creating table auth_user_user_permissions
Creating table auth_user
Creating table django_content_type
Creating table django_session

You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'abarto'): admin
Email address: admin@localhost
Password:
Password (again):
Superuser created successfully.
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

Next, we are going to set-up the django-notifications-hq and django-user-sessions modules. Open the settings.py file and change the INSTALLED_APPS ad MIDDLEWARE_CLASSES definitions so they look like the following:

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
# 'django.contrib.sessions',
    'user_sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'notifications',
    'rn',
)

MIDDLEWARE_CLASSES = (
# 'django.contrib.sessions.middleware.SessionMiddleware',
    'user_sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

and add the following to the file:

SESSION_ENGINE = 'user_sessions.backends.db'

What we did is add notifications to the site’s apps, replaced the default session implementation with the one provided by django-user-sessions and set the database back-end for the session engine. Add the django-notifications-hq and django-user-sessions to the urls.py file:

import notifications

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'realtime_notifications.views.home', name='home'),
    # url(r'^blog/', include('blog.urls')),

    url(r'^admin/', include(admin.site.urls)),
    url('^inbox/notifications/', include(notifications.urls)),
    url(r'', include('user_sessions.urls', 'user_sessions'))
)

Run the syncdb command one more time to set-up the necessary tables for the django-notifications-hq and django-user-sessions modules:

$ python manage.py syncdb
Creating tables ...
Creating table user_sessions_session
Creating table notifications_notification
Installing custom SQL ...
Installing indexes ...
Installed 0 object(s) from 0 fixture(s)

Non real-time notifications

At this point we can start working on the application. Our site needs only very basic features to demonstrate the traditional (i.e. non real-time) notifications, as described in the following user stories:

  • As a User I want to to log into and out of the site
  • As a User I want to see the unread notifications sent to me
  • As a User I want to send notifications to other users
  • As a User I want to mark all received notifications as read

We’re going to make use of the existing Django login and logout views. So we only need to cover the rest of the use cases.

This is how our rn/views.py module looks like:

from notifications.models import Notification
from notifications import notify
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required


@login_required
def home(request):
    notifications = request.user.notifications.unread().order_by('-timestamp')

    return render(request, 'index.html', {'notifications': notifications})


@login_required
def send_notification(request):
    recipient_username = request.POST.get('recipient_username', None)

    if recipient_username:
        recipients = User.objects.filter(username=recipient_username)
    else:
        recipients = User.objects.all()

    for recipient in recipients:
        notify.send(
            request.user,
            recipient=recipient,
            verb=request.POST.get('verb', '')
        )

    return HttpResponseRedirect(reverse('home'))


@login_required
def mark_as_read(request):
    request.user.notifications.unread().mark_all_as_read()

    return HttpResponseRedirect(reverse('home'))

We’ve defined a view for the home page that displays the unread notifications for the user, a view to send notifications to one or all the users of the site and a view to mark all unread notifications as read.

These are the contents of the urls.py module:

import notifications

from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url('^inbox/notifications/', include(notifications.urls)),
    url(r'', include('user_sessions.urls', 'user_sessions')),

    url(r'^$', 'rn.views.home', name='home'),
    url(r'^send_notification/$', 'rn.views.send_notification', name='send_notification'),
    url(r'^mark_as_read/$', 'rn.views.mark_as_read', name='mark_as_read'),
    url(r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'admin/login.html'}, name='login'),
    url(r'^accounts/logout/$', 'django.contrib.auth.views.logout', {'next_page': '/'}, name='logout')
)

All we need now is a way to present the views. We created a template in rn/templates/index.html with the following content:

<!DOCTYPE html>

{% load static %}
{% load notifications_tags %}

<html>
<head>
    <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
    <link href="{% static 'css/bootstrap-theme.min.css' %}" rel="stylesheet">
    <script src="{% static 'js/jquery-2.1.0.min.js' %}"></script>
    <script src="{% static 'js/bootstrap.min.js' %}"></script>

    <script>
        $(document).ready(function() {
            $("#send-notification-button").popover({
                container: "body",
                html: true,
                placement: "right",
                content: function() {
                    return $("#send-notification-popover").html();
                },
                trigger: "click"
            });
        });
    </script>

    <title>Real-time notifications in Django {% if user.is_authenticated %}({{ user.username }}){% endif %}</title>
</head>
<body>
    <div class="container-fluid">
        <div class="navbar navbar-default" role="navigation">
            <div class="container-fluid">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">Real-time Django notifications</a>
                </div>
                <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav navbar-right">
                        {% if user.is_authenticated %}
                        <li><a href="{% url 'logout' %}">Logout</a></li>
                        {% else %}
                        <li><a href="{% url 'login' %}">Login</a></li>
                        {% endif %}
                    </ul>
                </div>
            </div>
        </div>
    </div>
    <div class="container-fluid">
        <div class="panel panel-default">
            {% notifications_unread as unread_count %}
            <div class="panel-heading">Unread notifications <span class="badge">{{ unread_count }}</span></div>
            <div class="panel-body">
                <div class="row">
                    <div class="col-md-2">
                        <button id="send-notification-button" type="button" class="btn btn-default btn-block">Send Notification</button>
                        <div id="send-notification-popover" class="hide">
                            <form id="send-notification-form" role="form" action="{% url 'send_notification' %}" method="post">
                                {% csrf_token %}
                                <div class="form-group">
                                    <label for="recipient_username">Username</label>
                                    <input type="text" class="form-control" id="recipient_username" name="recipient_username" placeholder="Recipient username">
                                </div>
                                <div class="form-group">
                                    <label for="verb">Verb</label>
                                    <input type="text" class="form-control" id="verb" name="verb" placeholder="Verb">
                                </div>
                                <button type="submit" class="btn btn-default">Submit</button>
                            </form>
                        </div>
                    </div>
                    <div class="col-md-2"><a id="clear-button" class="btn btn-default btn-block" href="{% url 'mark_as_read' %}">Mark as read</a></div>
                </div>
                <div class="row">
                    <div class="col-md-12">
                    </div>
                </div>
            </div>
            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>Timestamp</th>
                        <th>Recipient</th>
                        <th>Actor</th>
                        <th>Verb</th>
                        <th>Action Object</th>
                        <th>Target</th>
                        <th>Description</th>
                    </tr>
                </thead>
                <tbody>
                    {% for notification in notifications %}
                    <tr>
                        <td>{{ notification.timestamp|date:"c" }}</td>
                        <td>{{ notification.recipient|default:"" }}</td>
                        <td>{{ notification.actor|default:"" }}</td>
                        <td>{{ notification.verb|default:"" }}</td>
                        <td>{{ notification.action_object|default:"" }}</td>
                        <td>{{ notification.target|default:"" }}</td>
                        <td>{{ notification.description|default:"" }}</td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

Notice that we reference files from Bootstrap and jQuery. You can download them from Bootstrap’s and jQuery’s home pages and put them in the rn/static/ directory. We could have avoided these dependencies, but we wanted to make the example re-usable for real websites.

We now have covered all of the use cases, but there’s a problem: If a notification is generated, the user won’t see it until it reloads the page. We need a way to push those notifications to the user.

Real-time notifications

There have been a lot of solutions to the problem of pushing content to the client, but none of them had wide-spread adoption due to the limitations of the HTTP protocol. WebSockets provide a solution to the problem as it allows bi-directional full-duplex communication between the server and the client. Sadly, the WebSockets specification hasn’t been standarized yet and support for it has only recently been included on all the popular web browsers.

In order to have real-time communication with the client on every possible web browser, we need a more general solution. socket.io provides such solution. socket.io is a JavaScript library with components for both the client and the server. It has support for WebSockets, but if they aren’t available it can fallback to other means of real-time communication. The server part of the library runs on top of node.js, which provides a high-performance event-driven framework to manage the message exchange with the clients.

All we need now is a way to connect the socket.io server running on node.js with our Django site. This can be easily done using Redis. Redis is a basically a key-value store, but it also provides a way to subscribe and publish to keys, so it basically becomes a message bus. With this architecture, the socket.io server will subscribe a user specific key, onto which Django is going to write the notifications. Once the message is received, the server will send it to the connected client.

node.js server

There’s not much to our node.js/socket.io server:

var http = require('http');
var server = http.createServer().listen(8002);
var io = require('socket.io').listen(server);
var cookie_reader = require('cookie');
var querystring = require('querystring');
var redis = require('socket.io/node_modules/redis');

//Configure socket.io to store cookie set by Django
io.configure(function(){
    io.set('authorization', function(data, accept){
        if(data.headers.cookie){
            data.cookie = cookie_reader.parse(data.headers.cookie);
            return accept(null, true);
        }
        return accept('error', false);
    });
    io.set('log level', 1);
});

io.sockets.on('connection', function (socket) {
    // Create redis client
    client = redis.createClient();

    // Subscribe to the Redis events channel
    client.subscribe('notifications.' + socket.handshake.cookie['sessionid']);

    // Grab message from Redis and send to client
    client.on('message', function(channel, message){
        console.log('on message', message);
        socket.send(message);
    });

    // Unsubscribe after a disconnect event
    socket.on('disconnect', function () {
        client.unsubscribe('notifications.' + socket.handshake.cookie['sessionid']);
    });
});

Once the client connects, the handler function is invoked, which connects to the redis server, subscribes to key unique to that session and configures the event handler for when a message is written to the key. When a message is received, it is sent (in its JSON form) down the client’s channel. This file can be stored anywhere as long as node.js has access to the socket.io and cookie dependencies (that’s why we used the global installation). We put the file in nodejs/notifications.js under the root of the site directory. You can run the server typing:

$ node nodejs/notifications.js
info  - socket.io started

Real-time client

Before we make the change that’s responsible for writing the Django notifications onto the Redis key, we’ll change the client code to connect to the socket.io server and avoid reloading the page whenever we create a notification o mark them as read.

First we need to create views to allow AJAX calls to send_notification and mark_as_read and a new view that points to the real-time version of the home template. These are the changes to rn/views.py:

import json
import redis

from notifications import notify
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

from django.db.models.signals import post_save
from django.dispatch import receiver
from notifications.models import Notification

...

@login_required
def home_realtime(request):
    notifications = request.user.notifications.unread().order_by('-timestamp')

    return render(request, 'index_realtime.html', {'notifications': notifications})


@login_required
def ajax_send_notification(request):
    recipient_username = request.POST.get('recipient_username', None)

    if recipient_username:
        recipients = User.objects.filter(username=recipient_username)
    else:
        recipients = User.objects.all()

    for recipient in recipients:
        notify.send(
            request.user,
            recipient=recipient,
            verb=request.POST.get('verb', '')
        )

    return HttpResponse(json.dumps({"success": True}), content_type="application/json")


@login_required
def ajax_mark_as_read(request):
    request.user.notifications.unread().mark_all_as_read()

    redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)

    for session in request.user.session_set.all():
        redis_client.publish(
            'notifications.%s' % session.session_key,
            json.dumps({"mark_as_read": True, "unread_count": 0})
        )

    return HttpResponse(json.dumps({"success": True}), content_type="application/json")

@receiver(post_save, sender=Notification)
def on_notification_post_save(sender, **kwargs):
    redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)

    notification = kwargs['instance']
    recipient = notification.recipient

    for session in recipient.session_set.all():
        redis_client.publish(
            'notifications.%s' % session.session_key,
            json.dumps(
                dict(
                    timestamp=notification.timestamp.isoformat(),
                    recipient=notification.recipient.username,
                    actor=notification.actor.username,
                    verb=notification.verb,
                    action_object=notification.action_object,
                    target=notification.target,
                    description=notification.description
                )
            )
        )

We’ve created a new home_realtime view that points to the new template for realtime notifications, and two views similar to the send_notification and mark_as_read but inteded to be used with AJAX calls. The ajax_mark_as_read view also writes to the user’s redis notification key to signal that all the notifications were marked as read, and that the UI should be updated accordingly.

The last method defined is a handler for the ‘post_save’ signal on the Notification model. With this handler, whenever a Notification is saved, it is written (as a JSON object) into the recipient session keys on Redis. Usually signal handlers are defined on a ‘signals.py’ module, but we wanted to keep the example as simple as possible.

Now that we have the views, we need a template (which will be stored in rn/templates/index_realtime.html) to exposed them:

<!DOCTYPE html>

{% load static %}
{% load notifications_tags %}

<html>
<head>
    <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
    <link href="{% static 'css/bootstrap-theme.min.css' %}" rel="stylesheet">
    <script src="{% static 'js/jquery-2.1.0.min.js' %}"></script>
    <script src="{% static 'js/bootstrap.min.js' %}"></script>
    <script src="http://localhost:8002/socket.io/socket.io.js"></script>

    <script>
        function getCookie(name) {
            var cookieValue = null;

            if (document.cookie && document.cookie != '') {
                var cookies = document.cookie.split(';');
                for (var i = 0; i < cookies.length; i++) {
                    var cookie = jQuery.trim(cookies[i]);
                    if (cookie.substring(0, name.length + 1) == (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }

            return cookieValue;
        }

        $(document).ready(function() {
            $("#mark-as-read-button").on("click", function(event) {
                event.preventDefault();

                $.ajax({
                    url: "{% url 'ajax_mark_as_read' %}",
                    type: "POST",
                    success: function(data) {
                    },
                    beforeSend: function(xhr){
                        var csrftoken = getCookie("csrftoken");
                        xhr.setRequestHeader('X-CSRFToken', csrftoken);
                    }
                });
            });

            $("#send-notification-button").popover({
                container: "body",
                html: true,
                placement: "right",
                content: function() {
                    return $("div.send-notification-popover").html();
                },
                trigger: "click"
            });

            $("body").on("click", "button.send-notification-form-submit", function(event) {
                event.preventDefault();

                var $send_notification_form = $(event.target).parent();

                $.ajax({
                    url: "{% url 'ajax_send_notification' %}",
                    type: "POST",
                    data: $send_notification_form.serialize(),
                    success: function(data) {
                        $("#send-notification-button").popover("hide");
                    },
                    beforeSend: function(xhr){
                        var csrftoken = getCookie("csrftoken");
                        xhr.setRequestHeader('X-CSRFToken', csrftoken);
                    }
                });
            });

            var socket = io.connect("localhost", {port: 8002});

            socket.on('message', function(message) {
                var message_json = jQuery.parseJSON(message);

                $("#unread_count").text(message_json.unread_count);

                if ("mark_as_read" in message_json) {
                    $("#notifications-table tbody").empty();
                } else {
                    var $tr = $("<tr>");
                    $tr.append($("<td>").text(message_json.timestamp));
                    $tr.append($("<td>").text(message_json.recipient));
                    $tr.append($("<td>").text(message_json.actor));
                    $tr.append($("<td>").text(message_json.verb));
                    $tr.append($("<td>").text(message_json.action_object));
                    $tr.append($("<td>").text(message_json.target));
                    $tr.append($("<td>").text(message_json.description));

                    $("#notifications-table tbody").prepend($tr);
                }
            });
        });
    </script>

    <title>Real-time notifications in Django {% if user.is_authenticated %}({{ user.username }}){% endif %}</title>
</head>
<body>
    <div class="container-fluid">
        <div class="navbar navbar-default" role="navigation">
            <div class="container-fluid">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">Real-time Django notifications</a>
                </div>
                <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav navbar-right">
                        {% if user.is_authenticated %}
                        <li><a href="{% url 'logout' %}">Logout</a></li>
                        {% else %}
                        <li><a href="{% url 'login' %}">Login</a></li>
                        {% endif %}
                    </ul>
                </div>
            </div>
        </div>
    </div>
    <div class="container-fluid">
        <div class="panel panel-default">
            {% notifications_unread as unread_count %}
            <div class="panel-heading">Unread notifications <span id="unread_count" class="badge">{{ unread_count }}</span></div>
            <div class="panel-body">
                <div class="row">
                    <div class="col-md-2">
                        <a id="send-notification-button" type="button" class="btn btn-default btn-block">Send Notification</a>
                        <div class="hide send-notification-popover">
                            <form id="send-notification-form" role="form">
                                {% csrf_token %}
                                <div class="form-group">
                                    <label for="recipient_username">Username</label>
                                    <input type="text" class="form-control" id="recipient_username" name="recipient_username" placeholder="Recipient username">
                                </div>
                                <div class="form-group">
                                    <label for="verb">Verb</label>
                                    <input type="text" class="form-control" id="verb" name="verb" placeholder="Verb">
                                </div>
                                <button class="btn btn-default send-notification-form-submit">Submit</button>
                            </form>
                        </div>
                    </div>
                    <div class="col-md-2"><a id="mark-as-read-button" class="btn btn-default btn-block">Mark as read</a></div>
                </div>
                <div class="row">
                    <div class="col-md-12">
                    </div>
                </div>
            </div>
            <table id="notifications-table" class="table table-bordered">
                <thead>
                    <tr>
                        <th>Timestamp</th>
                        <th>Recipient</th>
                        <th>Actor</th>
                        <th>Verb</th>
                        <th>Action Object</th>
                        <th>Target</th>
                        <th>Description</th>
                    </tr>
                </thead>
                <tbody>
                    {% for notification in notifications %}
                    <tr>
                        <td>{{ notification.timestamp|date:"c" }}</td>
                        <td>{{ notification.recipient|default:"" }}</td>
                        <td>{{ notification.actor|default:"" }}</td>
                        <td>{{ notification.verb|default:"" }}</td>
                        <td>{{ notification.action_object|default:"" }}</td>
                        <td>{{ notification.target|default:"" }}</td>
                        <td>{{ notification.description|default:"" }}</td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

The template is very similar to the non real-time notifications version. The most important change is in the fourth script element. Within this script we use the typical $(document).ready(function() {} jQuery statement to have our event handler run once the document is ready. Let us go statement by statement analyzing what each does:

$("#mark-as-read-button").on("click", function(event) {
    event.preventDefault();

    $.ajax({
        url: "{% url 'ajax_mark_as_read' %}",
        type: "POST",
        success: function(data) {
        },
        beforeSend: function(xhr){
            var csrftoken = getCookie("csrftoken");
            xhr.setRequestHeader('X-CSRFToken', csrftoken);
        }
    });
});

Whenever the element with id mark-as-read-button we should make an AJAX call to the ajax_mark_as_read URL. We do nothing if the call succeeds. We haven’t done any error handling since it’s not the purpose of this article to teach the reader how to properly do AJAX calls.:

$("#send-notification-button").popover({
    container: "body",
    html: true,
    placement: "right",
    content: function() {
        return $("div.send-notification-popover").html();
    },
    trigger: "click"
});

We hook our Bootrap popover to the click event of the element with id send-notification-button:

$("body").on("click", "button.send-notification-form-submit", function(event) {
    event.preventDefault();

    var $send_notification_form = $(event.target).parent();

    $.ajax({
        url: "{% url 'ajax_send_notification' %}",
        type: "POST",
        data: $send_notification_form.serialize(),
        success: function(data) {
            $("#send-notification-button").popover("hide");
        },
        beforeSend: function(xhr){
            var csrftoken = getCookie("csrftoken");
            xhr.setRequestHeader('X-CSRFToken', csrftoken);
        }
    });
});

Whenever a button element with id send-notification-form-submit under the body element (this is defined in the popover content), we make an AJAX call to the ajax_send_notification url, using the existing form as data. When the call succeeds, we hide the popover.:

var socket = io.connect("localhost", {port: 8002});

socket.on('message', function(message) {
    var message_json = jQuery.parseJSON(message);

    $("#unread_count").text(message_json.unread_count);

    if ("mark_as_read" in message_json) {
        $("#notifications-table tbody").empty();
    } else {
        var $tr = $("<tr>");
        $tr.append($("<td>").text(message_json.timestamp));
        $tr.append($("<td>").text(message_json.recipient));
        $tr.append($("<td>").text(message_json.actor));
        $tr.append($("<td>").text(message_json.verb));
        $tr.append($("<td>").text(message_json.action_object));
        $tr.append($("<td>").text(message_json.target));
        $tr.append($("<td>").text(message_json.description));

        $("#notifications-table tbody").prepend($tr);
    }
});

This is the most important part of the template. We create a socket.io channel to localhost on port 8002, which was the one we used for our node.js service and hook an event handler to the message event for the channel. On this event handler we do three things:

  • We parse the payload of the message as JSON.
  • If mark_as_read is present in the message, we clear the notifications table
  • If mark_as_read is not present in the message, we create a new row at the top notifications table using the message information for each column.

So, whenever something is written to the session’s events key on Redis, that is picked up by the node.js service, which takes the message and sends it to the client through the channel, and when that is received, the UI is updated accordingly without reloading the page.

The last thing we need to do, is add the urls for the views into the urls.py module:

url(r'^realtime/$', 'rn.views.home_realtime', name='home_realtime'),
url(r'^ajax_send_notification/$', 'rn.views.ajax_send_notification', name='ajax_send_notification'),
url(r'^ajax_mark_as_read/$', 'rn.views.ajax_mark_as_read', name='ajax_mark_as_read'),

Run the notifications.js node.js module, and our Django site, and open the URL http://localhost:8000/realtime/. If everything went right, you’ll see that everything is updated without reloading the page. In order to properly test the functionality, we suggest you open a different browser and log in with the same user. Whenever a notification is sent to that user, you’ll see the page update itself. If you click on the “Mark as read” button, you’ll see that both tables are cleared.

Nginx support

It’s not uncommon for Django sites to be behind an Nginx reverse proxy. Starting from version 1.4, Nginx has support for WebSockets, but it needs to be configured to do so. This is a possible configuration for our site:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen   80;
    #listen   [::]:80 default_server ipv6only=on; ## listen for ipv6

    # Make site accessible from http://localhost/
    server_name 'localhost';

    location / {
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host 'localhost';
        proxy_pass http://127.0.0.1:8000;
    }

    location /socket.io {
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host ''localhost'';
        proxy_pass http://127.0.0.1:8002;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

We also need to modify the rn/templates/index_realtime.html template:

...
<script src="http://localhost/socket.io/socket.io.js"></script>
...
var socket = io.connect("localhost");
...

You should now be able to access the site at http://localhost/realtime.

Conclusion

We hope we were able show you how easy it is to have real-time communications with the client, if you leverage the existing functionality of node.js, socket.io, Redis and Django with the django-notifications-hq module. Something we haven’t mentioned yet is that this solution works on pretty much every web browser, as socket.io abstracts the problem of constructing the actual communication channel between the client and the node.js service.

Feel free to send us comments, suggestions of improvements to the solution.


Previous / Next posts


Comments