Build GitHub like notifications with Django messages and AngularJS



Written by: masci Last updated: 22 Oct 2013

Foreword

GitHub has a very nice notification system, very similar to a plain old email inbox. You receive a notification which remains unread until you actually read it; then it’s archived and removed from your inbox, which it happens could remain empty:

github inbox

For those who don’t know, Django ships a library for displaying “one-time” messages to the users, it’s called Message Framework and you can find it in the contrib package. Messages are produced during users’ activity and delivered subsequently; in the meantime, they are stored in cookies or sessions.

The ephemeral nature of Django’s contrib.messages makes them not suitable for storing notifications in GitHub style: notifications have to be persisted until user actually reads it, messages instead are marked as read the moment they are, let’s say, observed. Nevertheless Django message framework is flexible enough to let you provide your own storage policy, and third-party applications like Django Stored Messages use this feature to store messages in a persistent way (specifically, an sql database).

Disclaimer: The frontend code will make use of AngularJs even if I am a total newbie and I don’t really know how to angular. If you are a newbie too, please go read this effective blog post by Todd Motto and then come back here. If you’re not an Angular newbie, please take into account my code could offend you.

The application

Fire up a virtualenv and install Django:

mkvirtualenv notification_example
pip install django

Start an empty project:

django-admin.py startproject notification_example

…and an app:

cd notification_example
python manage.py startapp notification

Now for some dependencies - install Django Stored Messages and Django Rest Framework:

pip install django-stored-messages djangorestframework

Configure all the things! In notification_example/settings.py be sure to have these:

PROJECT_ROOT = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(PROJECT_ROOT, 'db.sqlite'),
    }
}

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'notification',
    'stored_messages',
    'rest_framework',
)

MESSAGE_STORAGE = 'stored_messages.storage.PersistentStorage'

Now let’s go for some views. We will provide a view to serve the homepage, plus a view to show messages for the current logged in user.

The homepage view

Django Stored Messages can persist messages only when they are sent to a valid user, and such user has to login for viewing the messages, so we provide a login form directly inside the homepage. To produce some notifications, visiting the index will trigger a message as well. The code:

from django.views.generic import FormView
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.messages import add_message
from django.contrib.auth import login

import stored_messages

class IndexView(FormView):
    template_name = 'notification/homepage.html'
    form_class = AuthenticationForm
    success_url = '/'

    def get_context_data(self, **kwargs):
        add_message(self.request, stored_messages.STORED_INFO, 'You visited the homepage')
        return super(IndexView, self).get_context_data(**kwargs)

    def form_valid(self, form):
        login(self.request, form.get_user())
        return super(IndexView, self).form_valid(form)

We’re going to use class based views, of course. Notice Django Stored Messages let us make use of the builtin messages api, thus passing in a message of type stored_messages.STORED_INFO will cause that message to be stored on the database. The homepage template will be extended from a basic Boostrap3 template, we’re going onto details later.

The message view

This is a simple TemplateView, the only trick here is getting from the urlstring whether user wants to see all the notifications or only the unread ones:

from django.views.generic import TemplateView

class MessagesView(TemplateView):
    template_name = 'notification/messages.html'

    def get(self, request, *args, **kwargs):
        if 'unread' in request.GET:  # quick and dirty
            kwargs['unread'] = True
        return super(MessagesView, self).get(request, *args, **kwargs)

The view code is rather simple because all the magic is left to Django Stored Messages template tags and its REST api. The Html template for the message view will try to mimic GitHub’s notification page, here is the code and this is the result:

messages screenshot

As you can see from this chunk:

<div class="col-md-9">
{% if not unread %}
    {% stored_messages_archive 100 %}
{% else %}
    ...
{% endif %}
</div>
 

If user requested the archive (i.e. to show messages that were already read), the template tag stored_messages_archive provided by Django Stored Messages will show a list of 100 messages rendering the template at stored_messages/stored_messages_list.html. Here is the template ovverrided to add Bootstrap3 classes:

{% if messages %}
    <ul class="list-group">
        {% for message in messages %}
            <li class="list-group-item">
                {{ message.message }}
            </li>
        {% empty %}
            <li class="list-group-item">No messages here!</li>
        {% endfor %}
    </ul>
{% endif %}

We will get into the details for the else branch in the messages template later.

Plug the urls

Nothing special here but notice the inclusion of the REST api urls coming from stored_messages package:

urlpatterns = patterns('',
    url(r'^logout/$', 'django.contrib.auth.views.logout',  {'next_page': '/'}, name='logout'),
    url(r'^$', IndexView.as_view(), name='home'),
    url(r'^messages/$', MessagesView.as_view(), name='messages'),
    url(r'^api/', include('stored_messages.urls')),
)

Final touches

To add some noise to the notification stream, we will add messages for the user when she logs in and out:

def _user_logged_in(*args, **kwargs):
    add_message(kwargs['request'], stored_messages.STORED_INFO, 'You were logged in!')
user_logged_in.connect(_user_logged_in)


def _user_logged_out(*args, **kwargs):
    add_message(kwargs['request'], stored_messages.STORED_INFO, 'You were logged out!')
user_logged_out.connect(_user_logged_out)

The Django app is complete now, time for some Javascript code!

The REST

Even if Django Stored Messages has a template tag to show unread messages, for this demo we will use the REST api, which let us retrieve unread messages and mark them as read. To interact with the api we use Angular, for the sake of simplicity we use a single controller:

var messageApp = angular.module('messageApp', []);

messageApp.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) {
    // Messages array
    $scope.messages = {};

    // ...
}]);

Notice the injection of the $http object we will use to make http requests. The messages array will be filled with data coming from the api, then it will be available through the $scope object. For the angular application to work properly, the html code in our templates needs to be aware of the angular stuff - we do this in the base.html so that every page could use angular facilities:

<!DOCTYPE html>
<html ng-app="messageApp">
    <head>
    ...

and:

    ...
    </head>

    <body ng-controller="MainCtrl">
        <!-- navbar -->
        <div class="navbar navbar-inverse navbar-fixed-top">
        ...

Inside the controller, this code will be added to retrieve all the unread messages for the logged-in user:

    // ...

    // retrieve Messages from the restAPI
    $http({
        method: 'GET',
        url: '//127.0.0.1:8000/api/inbox/'
    })
    .success(function (data, status, headers, config) {
        $scope.messages = data;
    })
    .error(function (data, status, headers, config) {
        // something went wrong :(
    });

    // ...

If everything goes fine, $scope.messages will contain our messages and we can use them inside the DOM. To do this, we need some angularities inside the html, for example in the message.html template:

{% verbatim %}
<ul class="list-group" ng-if="messages.length">
  <li class="list-group-item" ng-repeat="message in messages">
    {{ message.message.date | date:'MMM d, y h:mm:ss a' }} - {{ message.message.message }}
      <a ng-click="markRead($index)" style="cursor:pointer">Mark as read</a>
  </li>
</ul>
{% endverbatim %}

The ng-if attribute determines if we have some messages to show. If we do have, the ng-repeat attribute will take care of iterating the messages and show them in the DOM through angular’s template tags.

Warning: we’re mixing Django and Angular templates there and since they share the same template syntax (this could be changed in angular but it’s not generally advisable) we need to wrap angular code inside Django’s verbatim tags.

In the html code above notice this:

<a ng-click="markRead($index)" style="cursor:pointer">Mark as read</a>

For every unread message, we provide a link and we tell angular that when user clicks it (ng-click attribute) the function markRead() has to be called with the parameter $index. We define that function inside the angular controller and attach it to the $scope:

    // ...

    // mark messages read
    $scope.markRead = function (index) {
        var id = $scope.messages[index].id;
        $http({
            method: 'POST',
            url: '//127.0.0.1:8000/api/inbox/'+id+'/read/',
            xsrfHeaderName: 'X-CSRFToken',
            xsrfCookieName: 'csrftoken'
        })
        .success(function (data, status, headers, config) {
            $scope.messages.splice(index, 1);
        })
        .error(function (data, status, headers, config) {
            // something went wrong :(
        })
    };
    // ...

The parameter passed to $http contains all the logic needed to retrieve the csrf token from user’s cookie and pass it to Django inside the X-CSRFToken header. For my experience, I’ve never seen an easier way to do this (thank you so much, Angular!). After retrieving the database id for the index-th message, we call the /api/inbox/{lookup}/read/ endpoint which marks that message as read. In case the request goes well (and this is where magics happen), we remove the element from the $scope.messages - angular will remove that element from the DOM afterwards. No code. No explicit DOM manipulation. Just fun.

Since Django Stored Messages api exposes an endpoint to mark all messages read, we provide a button to do exactly this. The code for the button is very similar to the one to mark messages read:

<button type="button" class="btn btn-success" ng-click="markAllRead()">Mark all read</button>

This time the function name is markAllRead and we call it without parameters; the function is defined inside the controller:

    // ...

    // mark all read
    $scope.markAllRead = function () {
        $http({
            method: 'POST',
            url: '//127.0.0.1:8000/api/mark_all_read/',
            xsrfHeaderName: 'X-CSRFToken',
            xsrfCookieName: 'csrftoken'
        })
        .success(function(data, status, headers, config) {
            $scope.messages.splice(0, $scope.messages.length);
        })
        .error(function(data, status, headers, config){
            // something went wrong :(
        })
    };
    // ...

The csrf boilerplate is the same (for the record, this could be easily avoided using some advanced angular features) and the logic is very similar: in case the request succeeded, the array of messages is cleared and the DOM reflects the changes automagically.

References


← Previous Archive Next →
blog comments powered by Disqus