Two-Faced Django Part 7: PyFacebook and FBJS Ajax
- You can find details about seeing a live version of this project, both web and Facebook interfaces, here. The live version now has the nifty ajax voting we will implement in this segment as well.
Now we have most of the fb app put together, we just need to add a little ajax functionality to the fb app and we're ready to go. If you haven't followed along, you can download [a snapshot of the code we've developed][polling6].
In general this is where we'd begin to spin up the urls -> views -> tempates development engine, but we actually already implemented the urls and views for this in the web app. Its a little bit unseemly to make our apps dependent on each other, and a cleaner way would be to have a third app that would hold all universal or shared components (I like to call that third app api).
Refreshing our memory
Lets take a quick gander at the urls and views that we are reusing. First lets look at the url we are reusing from the web app.
urlpatterns = patterns('',
(r'^vote/$', 'polling.web.views.vote')
)
And remember what the polling.web.views.vote view looks like:
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')
So, basically we want to send a GET request to "www.ourserver.com/vote/", and have it contain the arguments pk and vote. The argument vote should contain either the value up or the value down.
But there is a slight problem. FaceBook Javascript (described more below) doesn't support sending GET requests. So we have to send POST requests instead, and our vote() function doesn't know how to deal with POST requests. So, we need to make a few minor changes to polling/web/views.py.
emacs polling/web/views.py
And change vote() to look like this:
def vote(request):
results = {'success':False}
if request.method == u'GET':
data = request.GET
elif request.method == u'POST':
data = request.POST
if data.has_key(u'pk') and data.has_key(u'vote'):
pk = int(data[u'pk'])
vote = data[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')
This code is a bit awkward, and you may prefer to rewrite it as:
def vote(request):
results = {'success':False}
if request.REQUEST.has_key(u'pk') and request.REQUEST.has_key(u'vote'):
pk = int(request.REQUEST[u'pk'])
vote = request.REQUEST[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 REQUEST attribute of request first looks in the POST attribute, and then looks in the GET attribute. As such, it is exactly what we want in this situation. However, using REQUEST is a bit discouraged (it is less explicit). At some point choosing between the two becomes a philosophical question that each individual must answer for themselves.
Either way, lets save views.py and proceed.
FaceBook JavaScript
First, you may want to take a glance at the Facebook Javascript documentation, and specifically the documentation about the FBJS Ajax object.
FBJS is essentially javascript being run in a security sandbox. For the most part things will work the same as standard javascript, but some things simply won't work. Another difference is that FBJS exposes some functionality via object instances that exist within its namespace. We are going to be using two of those exposed objects.
The first is the Dialog object. This a Facebook stylized way to require simple interactions from users. We're going to use this in the same way we used alert() in the JQuery example (to notify the user of success).
The FBJS to create our dialog is going to look like this:
var successful = "True";
var d = new Dialog(Dialog.DIALOG_POP);
d.showMessage("Ajax", "The operation was " + successful);
Other than the Dialog object, we're also going to use one other FBJS object, the Ajax object. The developers' wiki describes it as "a very powerful AJAX object," so lets see what all that power is good for.
It turns out that the Ajax object is very similar to the simplified ajax functionality you get from JQuery, Prototype, or MochiKit.
Sending a simple POST request looks like this:
var a = new Ajax();
// Can also be Ajax.RAW or Ajax.FBML
a.responseType = Ajax.JSON;
// If you want to require login
a.requireLogin = true;
var p = { "one":10, "name":"Jack" };
a.post('http://www.abc.com/d/', p);
About as simple as we could hope for. Now lets put FBJS into action.
Editing the detail.fbml template
We're going to be editing the polling/templates/fb/detail.fbml template. So lets open it up quickly:
emacs polling/templates/fb/detail.fbml
Starting out, this is what it looks like (before we add any Ajax functionality):
{% load facebook %}
<fb:dashboard>
<fb:create-button href="{% fburl '/create/' %}">Create a new poll</fb:create-button>
</fb:dashboard>
<fb:tabs>
<fb:tab-item href="{% fburl '/' %}" title='All Polls'/>
</fb:tabs>
<div class="poll">
<p> Score for "{ { poll.question }}" is { { poll.score }}!</p>
<p>Do you <span id="up">agree</span> or <span id="down">disagree</span>?</p>
</div>
Now lets first make a javascript function that votes and object either up or down depending on the parameter passed to it:
function vote(pk, voteUp) {
if (voteUp == true)
typ = "up";
else
typ = "down";
var ajax = new Ajax();
ajax.responseType = Ajax.JSON;
ajax.requireLogin = true;
var params = { "pk":pk, "vote":typ };
ajax.post('http://krit.willarson.com/vote/', params);
}
Okay, and now we just need to modify our template to make use of the vote() function we just wrote. In our detail.fbml template we're going to rewrite the html div with id poll. After our modifications its going to look like:
<div class="poll">
<p> Score for "{ { poll.question }}" is { { poll.score }}!</p>
<p>
Do you
<a href="#" onclick="vote({ { poll.pk }}, true)">agree</a>
or
<a href="#" onclick="vote({ { poll.pk }}, false)">disagree</a>?
</p>
</div>
Notice that we can still use our handy "customize javascript using Django templates" trick.
Okay. What we have works, but we have one little problem: its a pain to check that it works (you have to reload the page you are on manually). So, as a slight improvement, lets use the Dialog object to notify us if the vote was successful or not.
Lets modify the vote() function a bit. Now its going to look like:
function vote(pk, voteUp) {
if (voteUp == true)
typ = "up";
else
typ = "down";
var ajax = new Ajax();
ajax.ondone = function(data) {
var d = new Dialog(Dialog.DIALOG_POP);
if (data.success == true)
msg = "Your vote was successful!";
else
msg = "Your vote failed. :(";
d.showMessage("Voting Result", msg);
}
ajax.responseType = Ajax.JSON;
var params = { "pk":pk, "vote":typ };
ajax.post('http://krit.willarson.com/vote/', params);
}
We are assigning an anonymous function to the ondone field of ajax. Any methods assigned to that field is called with one parameter: the content returned by Ajax request. That content is in the format specified when you created your Ajax object (either raw, json, or fbml).
And, with these changes, our Ajax functionality for the fb app is done as well.
Whats next?
After these seven tutorial segments you've learned about a lot of pieces: the Django testing framework, PyFacebook, FBJS, and others. This is a good time to take a step back and to start modifying the pieces we've been playing with, and seeing if you can impose your own will onto the code by taking it to newer (and more interesting) places. If anything goes wrong, you can always download the [code snapshot we just finished][polling7].
At this point this series is finished, but a second series that picks up here and continues is in the works. That series will be a bit different, because it will look at improving the quality of what we have built, as opposed to simply adding rough functionality.