- 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.
emacs fb/urls.py
And then we want to start with the standard code for all urls.py files:
from django.conf.urls.defaults import *
urlpatterns = patterns('',)
and then we want to add an import for our Poll model:
from polling.core.models import *
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:
urlpatterns = patterns('',)
to
urlpatterns = patterns('polling.fb.views',)
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:
urlpatterns = patterns('polling.fb.views',
(r'^$', 'list'),
(r'^poll/(?P<id>\d+)/$', 'detail'),
(r'^create/$', 'create'),
)
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:
FACEBOOK_API_KEY = '11111111111111111111111111111111111'
FACEBOOK_SECRET_KEY = '11111111111111111111111111111111111'
FACEBOOK_APP_NAME = "application_name"
FACEBOOK_INTERNAL = True
FACEBOOK_CALLBACK_PATH = "/facebook/"
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 fb/views.py
At the top of this file we should already have these imports (we set this up in part one):
from django.http import HttpResponse, HttpResponseRedirect
from django.views.generic.simple import direct_to_template
from django.shortcuts import get_object_or_404
from django.utils import simplejson
from polling.fb.helpers import *
from polling.core.models import *
We want to add one more import to that list:
from polling.core.forms import *
and then we're ready to start writing views.
First lets write the scaffolding for our three views:
def list(request):
pass
def detail(request, id):
pass
def create(request):
pass
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.
def list(request):
add_fb_instance(request)
redirect = require_fb_login(request)
if redirect is not None: return redirect
pass
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:
emacs fb/helpers.py
First, add this line to the imports at the top of the file:
from polling.core.models import User
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:
def get_fb_user(facebook):
user, created = User.objects.get_or_create(facebook_id=int(facebook.uid))
# if the object is newly created
if created is True:
# get first and last name using FQL
query = "SELECT uid, first_name, last_name FROM user WHERE uid=%s" % facebook.uid
# FQL results a list of dicts, retrieve the first (and only) one
results = facebook.fql.query(query)[0]
user.name = "%s %s" % (results[u'first_name'], results[u'last_name'])
user.save()
return user
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
facebook.fql.query("some query")
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:
def a_view(request):
add_fb_instance(request)
redirect = require_fb_login(request)
if redirect is not None: return redirect
user = get_fb_user(request.facebook)
# do things
return direct_to_template(request, "some_template.fbml",
extra_context={'user':user})
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:
def list(request):
add_fb_instance(request)
redirect = require_fb_login(request)
if redirect is not None: return redirect
user = get_fb_user(request.facebook)
polls = Poll.objects.all()
return direct_to_template(request, 'fb/list.fbml',
extra_context={'user':user,
'polls':polls,})
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:
def detail(request, id):
add_fb_instance(request)
redirect = require_fb_login(request)
if redirect is not None: return redirect
user = get_fb_user(request.facebook)
poll = Poll.objects.get(pk=id)
return direct_to_template(request, 'fb/detail.fbml',
extra_context={'user':user,
'poll':poll,})
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:
def create(request):
add_fb_instance(request)
redirect = require_fb_login(request)
if redirect is not None: return redirect
user = get_fb_user(request.facebook)
post = request.POST
if request.method=='POST' and post.has_key('question'):
form = PollForm(post)
if form.is_valid():
question = form.cleaned_data['question']
poll = Poll(question=question)
poll.save()
return HttpResponseRedirect('../')
else:
form = PollForm()
return direct_to_template(request, 'fb/create.fbml',
extra_context={'user':user,
'form':form,})
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.
mkdir polling/fb/templatetags
Then we need to make an init file just so that the folder can be recognized as a module:
touch polling/fb/templatetags/__init__.py
And now we need to make another file, but this time its actually going to have some Python code inside of it:
emacs polling/fb/templatetags/facebook.py
And now paste this code into it:
from django import template
from django.conf import settings
register = template.Library()
@register.tag(name='fburl')
def fburl(parser, token):
try:
tag_name, url = token.split_contents()
return FormatFacebookURL(url)
except ValueError:
raise template.TemplateSyntaxError, "Improper number of arguments."
class FormatFacebookURL(template.Node):
def __init__(self, url):
self.app_name = getattr(settings, "FACEBOOK_APP_NAME", "appname")
self.url = url.strip("\"\'")
def render(self, context):
return "http://apps.facebook.com/%s%s" % (self.app_name, self.url)
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.
emacs polling/templates/fb/list.fbml
First take a look at what the template is going to look like, then we'll look at any difficult spots:
{% 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' selected='true'/>
</fb:tabs>
<div class="polls">
<ol>
{% for poll in polls %}
<li> <a href="{% fburl '/poll/' %}{{ poll.pk }}/"> Results for "{{ poll.question }}". </a></li>
{% endfor %}
</ol>
</div>
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.
emacs polling/templates/fb/detail.fbml
And now this is what it is going to look like:
{% 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>
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...
emacs polling/templates/fb/create.fbml
And insert this text into it:
{% 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="create_poll">
<h2 text-align="center"> Creating a new poll </h2>
<form method="post" action="">
<table>
<fb:editor action="{% fburl '/create/' %}" labelwidth="100">
<fb:editor-text label="Question" name="question" value=""/>
{% if form.errors.question %}
<fb:editor-custom>
{{ form.errors.question.as_text|slice:"2:" }}
</fb:editor-custom>
{% endif %}
<fb:editor-buttonset>
<fb:editor-button value="Create"/>
<fb:editor-cancel string="{% fburl "/" %}" />
</fb:editor-buttonset>
</fb:editor>
</table>
</form>
</div>
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
{{ form.errors.question.as_text|slice:"2:" }}
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.
@amit, I agree, I think I mentioned this somewhere (or at least thought it to myself loudly), but you're certainly right that this is a great opportunity to introduce decorators. Unfortunately, discussing decorators isn't trivial (don't exist in 2.3, and have different syntax in 2.4 and 2.5), and the series is already somewhat large at over 10,000 words.
It may be the case that I'll be able to add another entry onto the end where I go back and refactor these pieces into decorators.
You had the perfect opportunity to introduce python decorators when you were repeating the same block of code in all the views. May be the decorator should go in helper.py?
Very nice, thanks for writing this tutorial on Django & Facebook!
I am using Django myself (and jQuery, a little bit) and I like it a lot, but I have no experience with the Facebook platform.
It's nice to see how you made a webapp work similarly on both a traditional website and on Facebook.
I hope to see your tutorial continued.
Hi there,
just a quick note to say that the custom templatetag works with fine Python2.4.
Just wondering how the template tag would work if you had to make a facebook request using it.....
Reply to this entry