Two-Faced Django Part 5: JQuery Ajax
We've been spending a lot of time together recently, and you're probably wondering if I'm going to quit writing this series before covering any Ajax at all. That is a very valid concern. But, be reassured, this segment of the tutorial is going to cover using JQuery to implement Ajax voting in our web app. If you haven't followed along with the previous articles, grab the snapshot of the current progress of our project.
Adding JQuery to the web app
The first thing we need to do is download JQuery. I recommend grabbing the uncompressed version, because it makes debugging things much more pleasant when you don't have to read minified Javascript...
var x = m45(k32, a32);
n41(x);
What does that mean? I don't have the faintest clue. But if you want to find out, you can get the gzipped and minified library version instead of the uncompressed library linked to above.
Once you have downloaded the library, you're going to want to move it into the polling/media/web/ folder. For sake of simplicity we are going to rename it to "jquery.js".
mv jquery-1.2.1.js jquery.js
Now we just need to modify our polling/templates/web/base.html file to import the JQuery library. First open up our file
emacs templates/web/base.html
Now we want to change the top of the template from
<html> <head>
<title>{% block title %}A Generic Title{% endblock %}</title>
</head>
to
<html> <head>
<script type="text/javascript" src="/media/web/jquery.js"></script>
<title>{% block title %}A Generic Title{% endblock %}</title>
</head>
One task crossed off the todo list. A handful more to go.
Adding URLs for the Ajax.
Go ahead and open up the urls.py for the web app. Its sleeping at polling/web/urls.py.
emacs web/urls.py
When designing your urls, you might decide that you'd want something that looks like this:
(r'^poll/(?P<id>\d+)/vote/up/$', 'polling.web.views.vote_up'),
(r'^poll/(?P<id>\d+)/vote/down/$', 'polling.web.views.down_down'),
So you'd send an Ajax (although, I really just mean asyncronous) request to /poll/4/vote/up/ to vote up poll 4, and you'd send an Ajax request to /poll/13/vote/down/ to vote down poll 13. Thats a perfectly reasonable way to structure your urls.
In this tutorial we're going to do it this way instead:
(r'^vote/$', 'polling.web.views.vote'),
We're not going to rely less on the url requested, and rely more on the GET parameters passed along with the request. Why is this better? Well, it does let us write one view instead of two views (although that one view will be more complex than the two views), but the real benefit here is a bit contrived: it gives us the opportunity to showcase rewriting javascript snippets with Django templates.
Your web/urls.py should look like this now:
from django.conf.urls.defaults import *
from polling.core.models import *
polls = Poll.objects.all()
urlpatterns = patterns('',
(r'^$', 'django.views.generic.list_detail.object_list',
dict(template_name='web/poll_list.html', queryset=polls)),
(r'^poll/(?P<object_id>\d+)/$', 'django.views.generic.list_detail.object_detail',
dict(template_name='web/poll_detail.html', queryset=polls)),
(r'^create/$', 'polling.web.views.create'),
(r'^vote/$', 'polling.web.views.vote')
)
Now we need to write that pesky vote view in the web/views.py file.
Writing the vote view
A little bit back I mentioned that we are going to be communicating with the vote view using data sent via GET requests. The first thing we need to pin down is what data will we be expecting, and how will we label it?
We need two pieces of data:
- the primary key of the poll to vote on, and
- whether it is an up vote or a down vote.
Because I possess a simple mind, we're going to translate those requirements into two keys: 'pk' and 'vote', and we're expecting 'pk' to be an integer representing the poll's primary key, and we're going to expect the value of 'vote' to be either 'up' or 'down'.
After that highly democratic design session, we're ready to start writing our view. First thing we need to open up the web/views.py file.
emacs web/views.py
The first thing we need to do is add an import for simplejson:
from django.utils import simplejson
and change the line importing HttpResponseRedirect from
from django.http import HttpResponseRedirect
to
from django.http import HttpResponse, HttpResponseRedirect
Now we need to write our vote view. Lets plan how it should work before we look at the code:
- Check if the request is of type GET.
- If not of type GET, fail.
- If is of type GET, check for existance of parameters 'pk' and 'vote'.
- If one or both parameters does not exist, fail.
- If both exist, grab the corresponding poll and vote up or down depending on the value of 'vote'.
Okay, now lets see how it looks in Python.
def vote(request):
results = {'success':False}
if request.method == u'GET':
GET = request.GET
if GET.has_key(u'pk') and GET.has_key(u'vote'):
pk = int(GET[u'pk'])
vote = GET[u'vote']
poll = Poll.objects.get(pk=pk)
if vote == u"up":
poll.up()
elif vote == u"down":
poll.down()
results = {'success':True}
json = simplejson.dumps(results)
return HttpResponse(json, mimetype='application/json')
The first thing you'll notice is that we're making a default value for result in the first line of our function. This isn't necessary, but it means we only have to set it in one place, and also means we only have one exit from the function (which makes it easier to follow the logic of the function, and easier to debug if things start going wrong).
The next thing you'll notice (because.. its the second line) is:
if request.method == u'GET':
That is the approved Django way for determining the type of the request you are dealing with. The other situation you'll see this used is when dealing with POSTs (we actually did that in the last segment of the tutorial when writing the view for creating new polls).
Notice that we use the *has_key()* method to check that the QuerySet (a Django class that is similar to, but slightly quickier than, a Python dictionary) for the two keys we care about 'pk' and 'vote'. The last two things I'll mention are that we're using unicode strings everywhere here, this is because Django uses unicode strings internally now, so everything you'll be comparing with will be unicode. Since you're apps are likely to run into non-ascii data at some point, it just makes sense to use unicode everywhere.
The last bit is about simplejson. Simplejson is a library for serializing basic Python structures into JSON, and thus makes it the easiest way to communicate with Javascript. The simplejson.dumps() function takes a basic Python structure, and returns a string representing it as JSON.
Okay. And thats it for our view. Now we just need to inject some JQuery javascript into one of our templates and we'll be finished.
Using JQuery for voting
Now we just need to write a few lines of javascript using JQuery to avoid most nastiness. Go ahead and open up the polling/templates/web/poll_detail.html.
emacs templates/web/poll_detail.html
Now we're going to go ahead and inline a few lines of javascript (I put it directly above the paragraph with the spans whose id's are 'upvote' and 'downvote').
<script>
function vote(kind) {
$.getJSON("/vote/", { pk:{ { object.pk }}, vote: kind }, function(json){
alert("Was successful?: " + json['success']);
});
}
function addClickHandlers() {
$("#upvote").click( function() { vote("up") });
$("#downvote").click( function() { vote("down") });
}
$(document).ready(addClickHandlers);
</script>
The vote() function is used to vote up or down, notice how we create a hashmap (similar to a Python dictionary) containing the values 'pk' and 'vote'. Those are the parameters passed along with our GET request to our Django server at the '/vote/' url. The most interesting part there is the { { object.pk }} segment. There we are using the Django templating language to modify our Javascript on the fly. A handy trick.
Other interesting bits in no particular order:
- $("#upvote") is how JQuery selects the element with id 'upvote'
- $(document).ready(callThisFunc); is a trick that JQuery uses to load Javascript once the dom elements in the webpage have been created (in English: to call Javascript when the html elements actually exist to be modified).
- $("someelement").click(callThisFunc); Instead of using the standard Javascript ele.onclick = someFunc, JQuery passes a function as an argument to the click function. This is a continuation of the JQuery concept that you should always have useful values returned by functions, and thus you can chain calls together.
Okay. So, altogether our templates/web/poll_detail.html should look like this:
{% extends "web/base.html" %}
{% block title %} Poll: { { object.question }} {% endblock %}
{% block body %}
<div id="poll">
<p>We have a question for you. { { object.question }}
<p> So far we have { { object.up_votes }} upvotes and { { object.down_votes }} downvotes, for an overall vote of { { object.score }}! </p>
<script>
function vote(kind) {
$.getJSON("/vote/", { pk:{ { object.pk }}, vote: kind }, function(json){
alert("Was successful?: " + json['success']);
});
}
function addClickHandlers() {
$("#upvote").click( function() { vote("up") });
$("#downvote").click( function() { vote("down") });
}
$(document).ready(addClickHandlers);
</script>
<p> Do you <span id="upvote">agree</span> or <span id="downvote">disagree</span>? </p>
</div>
{% endblock %}
And, now we just need to make sure it works, and then we're done.
Verifying our progress
First, lets fire up the development server:
python manage.py runserver
And then navigate to the list of polls (unless you haven't created any polls yet, then go here to create a poll first. Click on any of the polls, and you shold get transported to the detail page for that poll.
Now click on the text near the bottom of the screen labeled agree or disagree. It should then popup a javascript alert saying something like "Was successful?: True". If you look at our piece of Javascript above, you can notice that JQuery is deserializing the returned JSON array, and we are checking it for the 'success' key (which we are returning via our vote view in polling/web/views.py).
The score won't actively update in this screen, you'll need to return to the list of polls to see the updated scores. (If anyone runs into trouble figuring out how to dynamically update the page, I can write another segment covering that, but it'll require at least one actual request. :) A snapshot of our current code can be downloaded here.
And, we that we are done with the web app. Phew. We just need to start writing the PyFacebook app now, in section six of the tutorial.
By the way, writing tests for the create view in web/views.py is a really good idea. I'll leave that as an exercise for the reader, but hopefully I'll come back and walkthrough that as well.