Two-Faced-Django Part 6: PyFacebook
- You can find details about seeing a live version of this project, both web and Facebook interfaces, here.
Now we are finally getting into Facebook App territory. Just to recap a bit, we are coming in from part five, where we implemented Ajax in our web app. If you didn't follow along with the series thus far, you can get the snapshot of our code. We are using PyFacebook to interface with the Facebook api, and if you need a wider overview of the Facebook system take a look at the Developer's Wiki.
I won't be walking through getting a FaceBook key for your app, but it is covered in detail in the wiki.
Planning our Approach
The Facebook application we are going to build is going to be functionally equivalent to the web app we just built. Just like when we built the web app, the first part we are going to handle everything except Ajax interactivity.
Our plan looks like this:
- decide on our URLs,
- write our views,
- write the templates
You may be getting the impression this is my approach to writing Django applications... and you'd be correct.
Designing the fb urls
The first thing we want to do is to create a urls.py file in the fb app directory.
urlpatterns = patterns('',
(r'^vote/$', 'polling.web.views.vote')
)
And then we want to start with the standard code for all urls.py files:
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')
and then we want to add an import for our Poll model:
emacs polling/web/views.py
For the web app we used mostly generic views to handle everything, but we're going to use custom views to handle things in the fb app. The reason is that we are doing some custom validation on all requests, and custom views make it very easy for us to do so. All of our custom views are going to live in the polling.fb.views module, so we can make things a bit simpler by sharing that piece of information with the fb/urls.py file.
Todo so we change this line:
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')
to
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')
This means that any views we specify in patterns can be in the format 'list' instead of 'polling.fb.views.list'. When you are using generic views it doesn't work out because it would try to find the generic views within the 'polling.fb.views' module (where they definitely do not exist), but when all of your views are coming from one place, its a handy way to avoid repeating yourself.
Okay, now lets actually write the our urls:
var successful = "True";
var d = new Dialog(Dialog.DIALOG_POP);
d.showMessage("Ajax", "The operation was " + successful);
Fairly simple, and it looks very similar to what we did for the web app.
A quick intermission
Back in the first segment of this tutorial we configured the polling/settings.py file to play nicely with PyFacebook. If you didn't do so, you need to edit these settings:
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);
You need to fill in the correct name of your app, your api key and your secret key. The last two, interal and callback_path, are need the values specified above. Again, if you don't have an api key and a secret key, take a look at the developer's wiki for help.
Putting together the fb Views
Now we begin writing our views. First open up the polling/fb/views.py file:
emacs polling/templates/fb/detail.fbml
At the top of this file we should already have these imports (we set this up in part one):
{% 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>
We want to add one more import to that list:
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);
}
and then we're ready to start writing views.
First lets write the scaffolding for our three views:
<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>
Pretty simple stuff. However, now we are going to add a little bit more to that scaffolding, specifically, we are going to require the users to login to view any of our pages.
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're going to add those three lines to every one of our views that communicate directly with Facebook. If your look through the PyFacebook documentation, you may wonder why we are doing these things in this manner (if you have forgotten, these functions are being supplied by the polling.fb.helpers module, which we included in our package in the first portion of this tutorial). It all boils down to using PyFacebook without using the PyFacebook middleware (because that would be unnecessary overhead placed on the web app functionality).
Okay, now we just need to make a few changes in polling/fb/helpers.py, and then we'll be ready to put together our views.
First, open up helpers.py:
qaodmasdkwaspemas12ajkqlsmdqpakldnzsdfls
First, add this line to the imports at the top of the file:
qaodmasdkwaspemas13ajkqlsmdqpakldnzsdfls
Then scroll down to the get_fb_user() function (its on line 75). At the moment most of it is commented out. Now we need to rework it a wee bit. Well, thats a lie, we really just need to remove the triple quotes around it so it looks like:
qaodmasdkwaspemas14ajkqlsmdqpakldnzsdfls
The reason that it was commented out is that this function is completely dependent on the specific details of our User model in polling.core.models. That means you may have to edit it to match up with what you are doing in future projects, and thus it has a lower-than-normal dose of "It Just Works" pixie dust.
Looking at the function, it does a few things. It grabs the user id associated with the facebook parameter passed to it, and uses that uid to either retrieve or create a User from your database.
If it happens to create the user (an entry in your database with that uid doesn't exist yet), then it populates the name of that user using Facebook Query Language (more conveniently referred to as FQL). This is the only example in this tutorial of querying Facebook for data, but its actually a quite verstitle one.
If you read the fql documentation you can see that using the same Python code
qaodmasdkwaspemas15ajkqlsmdqpakldnzsdfls
you can retrieve any available information from Facebook. So in a very real sense once you know how to make FQL queries with PyFacebook, then thats all you really need to know about accessing Facebook data.
Okay, getting back to the *views.py and writing our views, we're going to take advantage of the get_fb_user()* function to extend our scaffolding just a bit.
Here is what a prototypical view functions will look like for us:
qaodmasdkwaspemas16ajkqlsmdqpakldnzsdfls
So, lets look at what is happening:
- We add a facebook instance to the request (accomplishing the same thing that the PyFacebook middleware would be doing if we were using it).
- If the current Facebook user is not logged in to our app, we redirect them to the login page for our app.
- We grab the User model from our local database that has information about the Facebook user who is currently using our application.
- We "Do Things".
- We render an appropriate template for the view (and we pass along some appropriate context, in this case the user, but in our real views we will also be adding some other context, like a list of objects, or a specific object, etc).
This could be accomplished more cleanly using a decorator (that is how PyFacebook accomplishes it, albeit its decorator requires the middleware and thus isn't immediately convenient for us) instead of a handful of function calls. The "if redirect is not None" bit is especially hard on the eyes. I think the cleanest solution would be to make a second decorator "add_facebook_instance" that preceeds any other PyFacebook decorators, and would attach a facebook instance to the request, and thus you could use all of the PyFacebook decorators as is, and could avoid using the PyFacebook middleware as well: i.e. you could have your cake and eat it too. Unfortunately, I haven't actually written that decorator yet (although, I suspect writing it would be a ten or twenty minute affair, but... it just hasn't happened yet).
Writing the views, Take two
Okay, now lets actually write the views. First we're going to put together the list view:
qaodmasdkwaspemas17ajkqlsmdqpakldnzsdfls
Its very similar to our staffolding we put together above. Basically the only differences are
- we're adding a list of all polls to the rendering context,
- we're rendering a template named 'fb/list.fbml' instead of 'sometemplate.fbml'.
Next we put together the detail view:
qaodmasdkwaspemas18ajkqlsmdqpakldnzsdfls
Very similar to the list view above, except we're only adding the Poll represented by the parameter id to the tendering context, instead of a list of all the polls. Oh, and we're rendering a templated named 'detail.fbml' instead of 'list.fbml' (oh, by the way, 'fbml' stands for FaceBookMarkupLanguage).
Now we just need to put together the view for creating new polls, the create view. This is going to be a bit more complex (but still nothing too awful).
In the end its going to look like this:
qaodmasdkwaspemas19ajkqlsmdqpakldnzsdfls
So lets follow the logic of the create view:
- If the user is not logged into our FaceBook app, redirect them to the login page.
- If there is a value for the 'question' key in the POST data, then populate a PollForm using that the POST data.
- If the ensuing PollForm is valid, then create the corresponding poll and redirect the user.
- If the ensuing PollForm is not valid, then return the invalid form to the page, which will allow us to display errors.
- If there is not a value for key 'question' in the POST data, return a blank poll.
Its quite similar to the create view we made in polling/web/views in a previous tutorial. All the validation of the incoming data is handled by the PollForm, so it means that the logic is, fortunately, quite simple.
Writing the fbml templates
So, the only thing we have left to do is write our templates. Because we want things to look FaceBook-ish its going to mean that we're going to do things a little bit differently than in the web app, particularly where we have to write our own custom form using fbml elements instead of the 'free' form rendering we get from django.newforms.
Before we dive into writing the templates themselves, lets look at a problem we're going to encounter when writing templates for our FaceBook apps: we need to link to our pages, but we don't want to have to specify the entire "http://apps.facebook.com/myAppName/poll/1/" url each time. For one its a lot of repetition, and for another it means that our templates would be tied to the name of our application as well.
We're already specifying the name of our FaceBook app in our polling/settings.py file, so lets take advantage of that to craft a helpful tool to mitigate this potential awkward spot.
Our tool? A custom tag!
First we need to make a new folder in the polling/fb folder.
qaodmasdkwaspemas20ajkqlsmdqpakldnzsdfls
Then we need to make an init file just so that the folder can be recognized as a module:
qaodmasdkwaspemas21ajkqlsmdqpakldnzsdfls
And now we need to make another file, but this time its actually going to have some Python code inside of it:
qaodmasdkwaspemas22ajkqlsmdqpakldnzsdfls
And now paste this code into it:
qaodmasdkwaspemas23ajkqlsmdqpakldnzsdfls
Please note that this decorator syntax is dependent on Python 2.5! It will look a little different in Python 2.4, and more different in Python 2.3, but let me know if anyone needs help dealing with those differences, and I'll write and post 2.4 and 2.3 versions.
Then go ahead and save the file. I won't go into great detail about explaining this code (because I find the way Django template tags are written to be a bit awkward, and not something that can be cleanly explained in a several sentences), but essentially what it does is take a url and append it to the long "http://apps.facebook.com/yourAppName" url that is mentioned briefly above.
Writing the templates, Take two
Okay, now we're going to actually start writing our templates. Lets start with the fb/list.fbml template that we're going to use to display the list of all polls.
qaodmasdkwaspemas24ajkqlsmdqpakldnzsdfls
First take a look at what the template is going to look like, then we'll look at any difficult spots:
qaodmasdkwaspemas25ajkqlsmdqpakldnzsdfls
Its a fairly standard template, except for two things:
- {% load facebook %}: this is how we make make fburl, the custom templatetag we wrote, available for the current template.
- It uses a handful of FaceBookMarkupLanguage tags that are not standard html tags (much of fbml is identical to html, but most of the more interesting stuff is not). To better understand those your best bet is to look at the fbml documentation links that I keep scattering around whenever I mention fbml. For the most part the names of the elements are self-documenting.
Okay, next up we are going to write the template for viewing individual polls. Go ahead and create polling/templates/fb/detail.fbml.
qaodmasdkwaspemas26ajkqlsmdqpakldnzsdfls
And now this is what it is going to look like:
qaodmasdkwaspemas27ajkqlsmdqpakldnzsdfls
Just like when we wrote the web app, the two labeled spans ('up' and 'down') are going to be used for voting, but we're not going to implement voting until the next segment of the tutorial.
Nothing too complex here either. However, our last template is going to be a bit more complex.
The create template
Our last template is for creating new polls. This is made slightly more complex because we have a handful of fbml elements to use instead of standard html.
Open up polling/templates/fb/create.fbml...
qaodmasdkwaspemas28ajkqlsmdqpakldnzsdfls
And insert this text into it:
qaodmasdkwaspemas29ajkqlsmdqpakldnzsdfls
Like the previous two templates, you just need to know fbml for this to really make any sense. The one piece that won't make sense to much anything is the line
qaodmasdkwaspemas30ajkqlsmdqpakldnzsdfls
What is happening here is that the default way of displaying error messages in Django are all awful in one way or another. There are not particularly convenient ways of displaying error messages, especially when you don't want them to be entries in an unordered list. The *as_text() method here is returning something like " That question has already been asked!", and the slice filter is stripping off the "* ", so we're only seeing the "That question has already been asked!".
Its definitely a bit of a hack, but it gets the job done. Albeit awkwardly.
Moving Onward
And with that, we're done putting together our fb app. Except for the Ajax. Which we will implement when we continue in section seven. The code snapshot from the end of tutorial six is available here.