Custom Django Views for Happier Ajax

September 24, 2008. Filed under jquerydjangojavascript

This post continues where Intro to Unintrusive JavaScript with Django left off. In the first segment we persisted against writing custom views to service the Ajax aspects of the app, and it lead to a lot of ugly code and awkward functionality.

In this second segment we're going to make that plunge and write two custom views to handle the Ajax, and open up a world of simple JavaScript with 90% less awful.

Updating notes/urls.py

First we're going to update the notes/urls.py file to include two new urls. Once updated, the file should look like this:

from django.conf.urls.defaults import *
from models import Note

notes = Note.objects.all()

urlpatterns = patterns(
    '',
    (r'^$',
     'django.views.generic.list_detail.object_list',
     dict(queryset=notes)),
    (r'^note/(?P<slug>[-\w]+)/$',
     'django.views.generic.list_detail.object_detail',
     dict(queryset=notes, slug_field='slug')),
    (r'^create/$','notes.views.create_note'),
    (r'^ajax_create/$','notes.views.ajax_create_note'),
    (r'^note/(?P<slug>[-\w]+)/update/$','notes.views.update_note'),
    (r'^note/(?P<slug>[-\w]+)/ajax_update/$','notes.views.ajax_update_note'),
)

All we did here was add the ^ajax_create/% and ^note/(?P<slug>[-\w]+)/ajax_update/$' urls.

Writing the ajax_create_note view

Next we have to actually write our two new views: ajax_create_note and ajax_update_note.

Because it's easy, we're going to go ahead and keep passing them the same data in the same format as we were before.

The big difference is that we'll be returning our responses serialized into JSON, which is the recommended diet for all JavaScript lifeforms.

First add these imports at the top of the notes/views.py file:

from django.utils import simplejson
from django.http import HttpResponse

And then add this function as well:

def ajax_create_note(request):
    success = False
    to_return = {'msg':u'No POST data sent.' }
    if request.method == "POST":
        post = request.POST.copy()
        if post.has_key('slug') and post.has_key('title'):
            slug = post['slug']
            if Note.objects.filter(slug=slug).count() > 0:
                to_return['msg'] = u"Slug '%s' already in use." % slug
            else:
                title = post['title']
                new_note = Note.objects.create(title=title,slug=slug)
                to_return['title'] = title
                to_return['slug'] = slug
                to_return['url'] = new_note.get_absolute_url()
                success = True
        else:
            to_return['msg'] = u"Requires both 'slug' and 'title'!"
    serialized = simplejson.dumps(to_return)
    if success == True:
        return HttpResponse(serialized, mimetype="application/json")
    else:
        return HttpResponseServerError(serialized, mimetype="application/json")

As mentioned, the only big difference between ajax_create_note and create_note is that we're serializing the output.

If we were sufficiently industrious, we could refactor the two methods pretty far, but for this tutorial we'll leave them are they are.

Now we need to update the JavaScript in the notes/note_list.html template to take advantage of this changes.

Updating the notes/note_list.html template

First we need to modify the create_note JavaScript method to send data to the new url.

Change this line from:

var args = { type:"POST", url:"/create/", data:data, complete:done };

to this:

var args = { type:"POST", url:"/ajax_create/", data:data, complete:done };

After that, the only change we need to make here is to strip the regex crap out of done and replace it with some blissful simplicity (and a security vulnerability, ahem).

The new done function looks like this:

var done = function(res, status) {
  var txt = res.responseText;
  var data = eval('('+txt+')');
  if (status == "success") {
    var newLi = $('<li><a href="'+data.url+'">'+data.title+'</a></li>');
    $("#notes").prepend(newLi);
    $("#title").val("");
    $("#slug").val("");
  }
  else display_error(data.msg, $(".new"));
}

The first thing we do is convert the incoming JSON into a JavaScript datastructure via the eval function. This is the simplest way to convert JSON into a usable1 JSON.

This isn't safe, because you are literally executing the recieved JSON, and if there were any malicious instructions contained within it, you'd execute those as well.

For the time being we're going to skim over that problem, but you can take a look at this article under the header 'JSON Via Parse' to get an idea of how to be more secure.

Now you can go ahead and run the development server and test out the front page.

It's going to work the same way as before, but isn't relying on the haphazard regular expressions to strip out necessary content.

Writing the ajax_update_note view

Next we're going to create the ajax_update_note view in our notes/views.py file. Once again there will be a lot of overlap between the Ajax and non-Ajax update views, and the big difference will simply be serializing the output.

Open up notes/views.py.

def ajax_update_note(request, slug):
    success = False
    to_return = { 'msg': u"No POST data recieved." }
    if request.method == "POST":
        post = request.POST.copy()
        note = Note.objects.get(slug=slug)
        to_return['msg'] = "Updated successfully."
        success = True
        if post.has_key('slug'):
            slug_str = post['slug']
            if note.slug != slug_str:
                if Note.objects.filter(slug=slug_str).count() > 0:
                    to_return['msg'] = u"Slug '%s' already taken." % slug_str
                    to_return['slug'] = note.slug
                    success = False
                else:
                    note.slug = slug_str
                    to_return['url'] = note.get_absolute_url()
        if post.has_key('title'):
            note.title = post['title']
        if post.has_key('text'):
            note.text = post['text']
        note.save()
    print success
    print to_return
    print request.method
    serialized = simplejson.dumps(to_return)
    if success == True:
        return HttpResponse(serialized, mimetype="application/json")
    else:
        return HttpResponseServerError(serialized, mimetype="application/json")

Read through and make sure you're comfortable with everything there, and then we're off into JavaScript land once more.

Updating the notes/note_detail.html template

Open up notes/note_detail.html.

In the perform_upate function, we'll once again change the args dictionary. This time it will end up looking like this:

var args = { type:"POST", url:"ajax_update/", data:data, complete:done };

We only changed the url we're posting to. Now, however, we're going to change things up a bit more. One of the biggest problems with our first implementation was that it would send unnecessary updates.

We couldn't improve upon it easily because we were lacking some crucial information, but no longer. The ajax_update_note view is returning us all the information we need to maintain a simple history of the data, and to only submit changes when changes have actually occured.

Further, for the field where errors are likely to occur (slug) we have enough information to rollback failed updates, and also redirect to the note's new url when the slug is successfully updated.

We'll begin with the history. Delete the initialTitleChange and initialSlugChange values, and replace them with this code:

var history = { title: $("#title").val(), slug: $("#slug").val() };

Next we'll need to update the title_to_span and slug_to_span functions to check against the value stored in the history before sending an update.

var title_to_span = function() {
  var title = $("#title");
  if (title.val() != history['title']) {
    perform_update("title", title.val());
    history['title'] = title.val()
  }
  var span = $('<span id="title"><em>'+title.val()+'</em></span>');
  span.hover(title_to_input,function() {});
  title.replaceWith(span);
}

The changes are at lines 3 through 5. Before sending an update we check that it differs from the current value. If we do send an update, then we update the current value stored in history.

We'll also do the same for slug_to_span.

var slug_to_span = function() {
  var slug = $("#slug");
  if (slug.val() != history['slug']) {
    perform_update("slug", slug.val());
    history['slug'] = slug.val();
  }
  var span = $('<span id="slug"><em>'+slug.val()+'</em></span>');
  span.hover(slug_to_input,function() {});
  slug.replaceWith(span);
}

We could do the same for the textfield as well, but we'll skip on that for the time being.

Finally we need to update the done function a bit.

var done = function(res, status) {
  var txt = res.responseText;
  var data = eval('('+txt+')');
  if (status == "success") {
    display_success("Updated successfully.", $(".text"));
    if (data.url) {
      window.location = data.url
    }
  }
  else {
    display_error(data.msg, $(".text"));
    if (data.slug) {
      history['slug'] = data.slug;
      $("#slug").text(data.slug);
    }
  }
}

We begin by evaluating the returned JSON into a JavaScript datastructure. Then we have the standard logic for displaying success and error messages, as well as some custom logic for handling updates to the slug field.

Specifically, if we successfully update the slug, then we use JavaScript to redirect to the new url where the note exists, and if we fail to update the slug, then we revert the value in the slug field (and history.slug) to its actual current value (instead of what we attempted to change it to).

With those changes, the Ajax on the note_detail.html template evolves from a burdensome mess of awfulness into something that provides a quicker and more pleasant experience than that provided by the original non-Ajax version.

Download

You can download the Git repository for part two here.

Moving Forward

By writing these two extra views we were able to really simplify the JavaScript, as well as improve the usability of the app. Although it's too bad we can't gain the same benefits using only one view, with great Ajax comes great reponsibility.

Or something like that.

In the next segment we're going to take a look at how authentication has to be reexamined for Ajax applications.


  1. Because I just had this discussion with the person sitting here with me, I'll launch into a brief mention of the correct usage of 'a' and 'an in the English language.

    You use 'an' infront of words that begin with a vowel sound, not necessarily with a vowel. This is why people who write about Cocoa and an NSString or an NSMutableDictionary.

    On the other hand, words like usable start with a consonant sound despite beginning with a vowel.