Creating Gmail style real time notifications using SwampDragon in Django.

A lot of websites display notifications in one form or another these days. Facebook shows the new message notification, Gmail have the new mail notifications (with the support of browser notifications).

I was asked today how hard it would be to implement a real-time notification system. The answer, as it turns out, is: not very hard at all.

It was in fact so easy that I decided to write up this blog post on the subject to show how to implement a simple notification system in a Django app.

This has been tested with Django 1.6.8 and 1.7.1, and python 2.7.4 and python 3.4, with the following browsers:

  • Chrome 39.0.2171.71 (64-bit)
  • Safari 8.0 (10600.1.25.1)
  • Firefox developer edition 36.0a2 (2014-12-07)

Note: you need redis-server running. If you don't have Redis installed see redis.io, or use your default package manager to install it.

You can download the full source code here.

Setup

This could easily be implemented in an existing application but since I'm doing this from scratch I'll go through the steps of setting everything up.

First step is to install SwampDragon.

    pip install swampdragon

The next thing is to create a new project.

    dragon-admin startproject notifications
    cd notifications
    django-admin.py startapp demo

That's our project files and directories setup. Now we add paths to settings.py for the static files and html templates. Open notifications/settings.py in a text editor and add the following lines

    STATIC_ROOT = os.path.join(BASE_DIR, 'static_root')

    MEDIA_URL = '/media/'
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

    # Additional locations of static files
    STATICFILES_DIRS = [
        os.path.join(BASE_DIR, 'static'),
    ]

    TEMPLATE_DIRS = [
        os.path.join(BASE_DIR, 'templates')
    ]

Make sure you add demo to INSTALLED_APPS as well.

This tells Django where to find our static resources and templates.

That's all we need to do for our setup step.

Models

We store the notification as a model. This way we can load existing notification when the user loads the page the first time.

Open demo/models.py in a text editor and add the following code

    from django.db import models
    from swampdragon.models import SelfPublishModel
    from .serializers import NotificationSerializer


    class Notification(SelfPublishModel, models.Model):
        serializer_class = NotificationSerializer
        message = models.TextField()

We have yet to create the NotificationSerializer so we will do that in the next step.

We add the SelfPublishModel mixin, so we don't have to worry about actually publishing the model, as that will happen as soon as it's created (note: for this post I have omitted handling updates to notifications).

The only field on this model is a message field, but you could easily add a title to the notification.

The notifications we are creating here are global, so everyone on the site will receive them. You could tailor the notifications to be on a per-user basis by adding a foreign key to the User model, and using swampdragon-auth to subscribe each user to their own notification channels (based on their username or some other unique identifier).

I will add some notes about this at the end of this post, but for now I will keep it simple.

Serializers

Add a new file: demo/serializers.py and open it in a text editor and add the following code

    from swampdragon.serializers.model_serializer import ModelSerializer


    class NotificationSerializer(ModelSerializer):
        class Meta:
            model = 'demo.Notification'
            publish_fields = ['message']

We only want to publish the message field, so we specify this in the publish_fields property.

Routers

Without routers SwampDragon won't know how to deal with the incoming data.

Create a new file: demo/routers.py and add the following code

    from swampdragon import route_handler
    from swampdragon.route_handler import ModelPubRouter
    from .models import Notification
    from .serializers import NotificationSerializer


    class NotificationRouter(ModelPubRouter):
        valid_verbs = ['subscribe']
        route_name = 'notifications'
        model = Notification
        serializer_class = NotificationSerializer


    route_handler.register(NotificationRouter)

Admin

Since we don't have anything generating notifications, we'll add the Notification model to django admin so we can create them from there.

Open demo/admin.py in a text editor and replace the content with the following:

    from django.contrib import admin
    from demo.models import Notification


    admin.site.register(Notification)

Views

We want to load existing notifications, and be able to show these notifications even if the SwampDragon server is not responding.

Open demo/views.py in a text editor and add the following code

    from django.views.generic import ListView
    from .models import Notification


    class Notifications(ListView):
        model = Notification
        template_name = 'home.html'

        def get_queryset(self):
            return self.model.objects.order_by('-pk')[:5]

We use a standard Django CBV (class based view) to load the five last notifications. We set the model to Notification and specify the template to be home.html.

Open notifications/urls.py in a text editor and change it to the following code

    from django.conf.urls import patterns, include, url
    from django.contrib import admin
    from demo.views import Notifications

    admin.autodiscover()

    urlpatterns = patterns('',
        url(r'^$', Notifications.as_view(), name='home'),
        url(r'^admin/', include(admin.site.urls)),
    )

That's all the python code we have to write for this. Now we need to add the template and a bit of JavaScript.

HTML

Create two new directories:

    mkdir templates
    mkdir static

Create a new html template: templates/home.html and add the following

    <!DOCTYPE html>
    <html>
    <head lang="en">
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>

    <h1>Notifications demo</h1>

    <!-- This is our list of notifications -->
    <ul id="notifications">
        {% for notification in object_list %}
        <li>{{ notification.message }}</li>
        {% endfor %}
    </ul>


    <!-- SwampDragon -->
    <script type="text/javascript" src="{{ STATIC_URL }}swampdragon/js/vendor/sockjs-0.3.4.min.js"></script>
    <script type="text/javascript" src="{{ STATIC_URL }}swampdragon/js/swampdragon.js"></script>
    <script type="text/javascript" src="{{ STATIC_URL }}swampdragon/js/datamapper.js"></script>
    <script type="text/javascript" src="{{ STATIC_URL }}swampdragon/js/swampdragon-vanilla.js"></script>
    <script type="text/javascript" src="http://localhost:9999/settings.js"></script>

    <!-- notifications -->
    <script type="text/javascript" src="{{ STATIC_URL }}notifications.js"></script>
    </body>
    </html>

If you want to run this on anything but the local dev server, see this post on adding a context processor for the DRAGON_URL.

Javascript

The last piece of the puzzle is the JavaScript.

Create a new JavaScript file: static/notifications.js and add the following:

    // Ask the browser for permission to show notifications
    // Taken from https://developer.mozilla.org/en-US/docs/Web/API/Notification/Using_Web_Notifications
    window.addEventListener('load', function () {
        Notification.requestPermission(function (status) {
            // This allows to use Notification.permission with Chrome/Safari
            if (Notification.permission !== status) {
                Notification.permission = status;
            }
        });
    });


    // Create an instance of vanilla dragon
    var dragon = new VanillaDragon({onopen: onOpen, onchannelmessage: onChannelMessage});

    // This is the list of notifications
    var notificationsList = document.getElementById("notifications");


    // New channel message received
    function onChannelMessage(channels, message) {
        // Add the notification
        addNotification((message.data));
    }


    // SwampDragon connection open
    function onOpen() {
        // Once the connection is open, subscribe to notifications
        dragon.subscribe('notifications', 'notifications');
    }


    // Add new notifications
    function addNotification(notification) {
        // If we have permission to show browser notifications
        // we can show the notifiaction
        if (window.Notification && Notification.permission === "granted") {
            new Notification(notification.message);
        }

        // Add the new notification
        var li = document.createElement("li");
        notificationsList.insertBefore(li, notificationsList.firstChild);
        li.innerHTML = notification.message;

        // Remove excess notifications
        while (notificationsList.getElementsByTagName("li").length > 5) {
            notificationsList.getElementsByTagName("li")[5].remove();
        }
    }

Finally

Create a database

If you are using Django 1.7+

    python manage.py makemigrations
    python manage.py migrate

Otherwise

    python manage.py syncdb

Create a super user. This is done by either answering "yes" to the question when you run syncdb or migrate. You can do it manually by running python manage.py createsuperuser.

Open a new terminal and type:

    python manage.py runserver 

and another terminal and type

    python server.py 

To test this out, open a web browser to http://localhost:8000. The browser will ask if localhost can have permission to show notifications. If you answer no, you will only see the notifications on the page.

Open another browser window to http://localhost:8000/admin/. Log in and add a new notification instance and you should be able to see the new notification.

Additional Notes

As mentioned above, you can have user specific notifications if you use something like swampdragon-auth.

Update the model to the following:

    class Notification(SelfPublishModel, models.Model):
        serializer_class = NotificationSerializer
        message = models.TextField()
        user = models.ForeignKey(User)

You only have to make a small change to the router by adding get_subscription_contexts and setting the @login_required decorator on the subscribe verb.

    @login_required
    def subscribe(self, **kwargs):
        super().subscribe(**kwargs)

    def get_subscription_contexts(self, **kwargs):
        return {'user_id': self.connection.user.pk}

Now notifications are user specific (and users who are not signed in can simply not subscribe).

The example code for this post is available at https://github.com/wildfish/swampdragon-django-notifications-demo

comments powered by Disqus

Pingbacks

Pingbacks are open.