Custom Django Views for Happier Ajax
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.
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 anNSMutableDictionary
.On the other hand, words like usable start with a consonant sound despite beginning with a vowel.↩