Making a Facebook app (with Django) - part 3: Python & FBML
September 29th, 2008 by Eddie SullivanWelcome to the third part in my series of posts about creating a Facebook application. I am using Django as my web development framework, and this post will focus on some of the backend techniques I have worked out to make this work easier. This is not a tutorial, but a set of tools that I have developed. This is a long post, with a lot of source code; I hope you find at least some of it useful.
Keep in mind as you read this that the Facebook platform is still very new, and likely to change. In fact, if you're a FB user, you are probably aware they recently completed a major transition to a new profile design. This included many changes behind the scenes for developers, some of which are still playing out. I recommend keeping up with the Facebook Platform Developer Forum and the Facebook Developer Blog.
Also, I will assume you have already read the API Documentation and the documentation for PyFacebook, and that you know how to create a web app using Django. If not, you will want to start there.
PyFacebook is very useful and includes some documentation on getting up and running with Django. You do still need an understanding of how Django works and how URLs are mapped.
My goal
My goal with these code snippets and techniques is to make developing a Facebook app as close as possible to developing a regular web app. The application I am using to develop and test these features is The Limerick Book. Compare that page to The Limerick Book Facebook App. In fact, they are the same application, sharing the same code. I also have developed a multiplayer card game based on the traditional Italian game Scopa. This is a Facebook-only application, but I wanted to be able to test it outside of Facebook.
Ideally, we would be able to write code and templates that can work equally well both inside Facebook and outside. This is important even if you want users to only see your application within Facebook, because it makes testing infinitely easier. You want to be able to test on your local machine before publishing content, and you want to be able to test things out free from the limitations and frequent bugginess of the Facebook platform.
First steps
Here are some simple helper functions to make life as a Facebook Python developer easier.
I am making a conscious decision here not to package all these helper functions into a downloadable library. My point here to explain code, rather than just hand it out. Many of these things are specific to my needs, and may not fit exactly what you want, so some tweaking may be required. I have most of these functions in a file called fbUtil.py that I import from most of my Django view code (except for the template tags, which need to be in a specific place, per Django). Feel free to do the same, or to copy and paste the code for your own use, but I recommend reading through the code, as your needs may not be the same as mine.
inFb is a simple function that takes a Request object and returns a boolean telling whether the request is taking place within the context of a Facebook canvas page.
def inFb(request): return (request.facebook and (request.facebook.check_session(request) or request.facebook.in_canvas))
Once we have this, we can create some more useful functions.
facebookUrl = settings.FACEBOOK_URL def fbReverse(view, args=None, kwargs=None): ''' Much like django.core.urlresolvers.reverse, except works in Facebook. Returns an absolute URL to a Facebook canvas page. ''' ret = reverse(view, args=args, kwargs=kwargs) return facebookUrl + ret[1:] # Remove leading slash def makeReverse(request, view, args = None): ''' Returns the URL of a the specified view, either in or out of Facebook. ''' if inFb(request): return fbReverse(view, args) return reverse(view, args=args) def makeRedirect(request, view, args = None, extra = ''): ''' Returns a Response object for a HTTP redirect, either in or out of Facebook. ''' if inFb(request): return request.facebook.redirect(fbReverse(view, args) + extra) return HttpResponseRedirect(reverse(view, args=args) + extra)
These are designed to simplify URL calculations. See the comments in the code for explanations of what they do.
def makeResponse(request, template, context, common=False): context['pageName'] = template if (not common) and inFb(request): tmpl = 'fb/%s.fbml' % template else: tmpl = template + '.tmpl' return render_to_response(tmpl, context, context_instance=RequestContext(request))
This one is a little different, and may need updating depending on how your code is organized. I keep my templates in the directory 'mySite/myApp/templates' and give them names like 'myTemplate.tmpl'. I put my Facebook-specific templates in the subdirectory called 'fb' and give them names like 'myTemplate.fbml'. This function allows me to create two templates for a view: one for inside Facebook and one for outside. The function will detect which one to use and render it into a Response object. request is the Request object that is passed to the view function. template is a string holding the base-name of the template file, for example 'myTemplate'. context is a dictionary with the template context variables. And if common is True, the function will use the main non-Facebook version of the template no matter what (because ideally we would be able to share even these as much as possible).
One extra little bit that the function does is add an extra template variable called pageName with the base-name of the template. I find this useful for code re-use within templates, though it's not actually a Facebook-related feature.
Authentication
The next thing I wanted to do was to tie in Facebook's user information with Django's authentication mechanism. Depending on your application, you may or may not want to do this. If you want to remember information about user-contributed content, it is useful. The way I did it was to create a UserProfile model, tied in to the User model by a ForeignKey one-to-one relationship, and with a facebookId field (among other, app-specific fields). Then I created a Django authentication backend to allow authenticating by facebook ID. Here is my code for the authentication back end. If a user does not exist with the given facebook ID, a new one is created. (That, of course, may not be what you want, so you may have to modify that code.)
This code assumes that Python's logging facilities have been set up, for error reporting.
class FacebookBackend: ''' Authenticate against Facebook. ''' def authenticate(self, facebookId=None): if not facebookId: return None try: profile = UserProfile.objects.get(facebookId=facebookId) UpdateFbUserDetails(profile.user, facebookId) return profile.user except UserProfile.DoesNotExist: # No user. Create one. pass username = 'fb_%s' % facebookId try: user = User.objects.get(username=username) # This shouldn't really happen. Log an error. logging.error('Strange: user %s already exists.' % username) except User.DoesNotExist: user = User.objects.create_user('fb_%s' % facebookId, '') if not UpdateFbUserDetails(user, facebookId): return None user.save() profile, created = UserProfile.objects.get_or_create(user=user) profile.facebookId = facebookId profile.save() return user def get_user(self, user_id): try: return User.objects.get(pk=user_id) except User.DoesNotExist: return None def UpdateFbUserDetails(user, fbId): """ Fill in a user's first and last name, from Facebook. """ if (not user.first_name) or (not user.last_name) : try: fb = get_facebook_client() userDetails = fb.users.getInfo(fbId, ['last_name', 'first_name']) user.first_name = userDetails[0]['first_name'][:30] user.last_name = userDetails[0]['last_name'][:30] user.save() return True except Exception, ex: logging.error('Error updating user: %s' % ex) return False return True
Now here is a function decorator you can use on your view functions. It will perform Facebook authentication if possible. It can also be used to require a login - either via Facebook or through a login page. With a parameter of True, it is equivalent to Django's built-in login_required decorator or to PyFacebook's facebook.require_login decorator, depending on whether the view is accessed inside or outside of Facebook.
def facebookView(requireLogin=False): def decorator(func): def wrapper(request, *listArgs, **kwArgs): facebookLogin(request) fb = request.facebook if requireLogin and (not request.user.is_authenticated()): if inFb(request): return fb.redirect(fb.get_login_url(next=request.path)) else: return HttpResponseRedirect(settings.LOGIN_URL + '?next=%s' % request.path) else: return func(request, *listArgs, **kwArgs) wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ wrapper.__dict__ = func.__dict__ wrapper.__module__ = func.__module__ return wrapper return decorator def facebookLogin(request): ''' Attempt to login the user based on their Facebook credentials. Does nothing outside of Facebook. ''' facebook = get_facebook_client() if (not request.user.is_authenticated()) or UserProfile.Get(request.user).facebookId != facebook.uid: if request.facebook and request.facebook.check_session(request): user = authenticate(facebookId=facebook.uid) login(request, user)
And here is a very simple example of how to use it:
@facebookView(True) # Require login, in and out of Facebook def myView(request): # Put important view processing here. return makeResponse(request, 'myTemplate', {'templateVar':'important data'})
Templates
We are getting closer to the holy grail of being able to write one set of code that can run both in and out of Facebook. It would also be useful to be able to share templates, so I have worked out several mechanisms to facilitate this.
The Context Processor
Django has the useful concept of a "Context Processor," which allows pre-processing of a RequestContext object before the rendering of any template. I take advantage of this quite a bit. I already discussed the cacheBreaker variable in my post about Facebook and JavaScript. Here are a couple more variables I've found useful:
- fb - The facebook object, for accessing the Facebook API, or None outside of Facebook.
- profile - The UserProfile object, if the user is logged in, or None otherwise.
- baseTemplate - The template to extend - either "base.tmpl" outside of Facebook or "fb/base.fbml" inside of Facebook.
- loginRequired - A useful string for including in hyperlinks. Within Facebook, it contains 'loginrequired=true'. Outside of Facebook, it contains something like 'onclick="return checkLogin()"', which is some custom JavaScript to require the user to login before following the link. Of course, if the link already has an "onclick" event, this cannot be used.
I'll leave it to you to write your custom template processor, as they can be very site-specific, but the above should get you started. Here is Django's context processor documentation.
Template tags
To allow more sharing, I've defined a couple of useful template tags. These must be defined in a file in a "templatetags" directory under your application directory, as described in the Django custom tag and filter documentation.
The first is fbUrl. It is the equivalent of the built-in Django url tag, except that when used inside Facebook it produces an absolute link to the requested page within the context of the Facebook canvas. In fact, the code is copied directly from the Django implementation of url. fbUrl relies on the fb context variable, and the fbReverse function, as described above.
from django.template import Node class FBURLNode(Node): def __init__(self, view_name, args, kwargs): self.view_name = view_name self.args = args self.kwargs = kwargs def render(self, context): fb = template.Variable('fb').resolve(context) if fb: reverseFunc = fbReverse else: reverseFunc = django.core.urlresolvers.reverse from django.core.urlresolvers import reverse, NoReverseMatch args = [arg.resolve(context) for arg in self.args] kwargs = dict([(smart_str(k,'ascii'), v.resolve(context)) for k, v in self.kwargs.items()]) try: return reverseFunc(self.view_name, args=args, kwargs=kwargs) except NoReverseMatch: try: project_name = settings.SETTINGS_MODULE.split('.')[0] return reverseFunc(project_name + '.' + self.view_name, args=args, kwargs=kwargs) except NoReverseMatch: return '' def fbUrl(parser, token): """ Just like Django's url tag, except also works inside Facebook. """ bits = token.contents.split(' ', 2) if len(bits) < 2: raise TemplateSyntaxError("'%s' takes at least one argument" " (path to a view)" % bits[0]) args = [] kwargs = {} if len(bits) > 2: for arg in bits[2].split(','): if '=' in arg: k, v = arg.split('=', 1) k = k.strip() kwargs[k] = parser.compile_filter(v) else: args.append(parser.compile_filter(arg)) return FBURLNode(bits[1], args, kwargs) fbUrl = register.tag(fbUrl)
Next is fbName. This is to provide the same functionality as Facebook's fb:name FBML tag, except also useable outside of Facebook.
The basic use of it looks like {% fbName user %}, where "user" is a template variable containing the user whose name to display. Then you can add options like linked=false, or useyou=false, as described in the Facebook documentation.
Some differences from the FBML version are:
- shownetwork, ifcantsee, and subjectid are ignored outside of Facebook.
- linked behaves slightly differently. Outside of Facebook, or if its value is set to internal, the user's name will be linked to an app-specific profile page. The view for this page is specified in the variable userProfileView. The view is expected to take one parameter: the user ID.
Here is the code:
userProfileView = 'userProfile' class FbNameNode(Node): def __init__(self, user, args, kwArgs): self.user = template.Variable(user) self.args = args self.kwArgs = kwArgs def getBoolArg(self, name, default=False): val = self.kwArgs.get(name, default) if type(val) is not bool: return (val.lower() == 'true') return val def render(self, context): user = self.user.resolve(context) fb = template.Variable('fb').resolve(context) loggedInUser = template.Variable('user').resolve(context) request = template.Variable('request').resolve(context) if fb: fbUserId = UserFbId(user) if fbUserId: # In Facebook internalLink = False ret = '<fb:name uid="%s" ' % fbUserId for item, val in self.kwArgs.items(): if item == 'linked' and val == 'internal': internalLink = True ret += 'linked="false" ' else: if type(val) is bool: if val: val = 'true' else: val = 'false' ret += '%s="%s" ' % (item, val) ret += '/>' if internalLink: ret = '<a href="%s">%s</a>' % (fbReverse(userProfileView, [user.id]), ret) return mark_safe(ret) # Not in Facebook if self.getBoolArg('useyou', True) and user == loggedInUser: if self.getBoolArg('capitalize'): ret = 'You' else: ret = 'you' if self.getBoolArg('possessive'): ret += 'r' elif self.getBoolArg('reflexive'): ret += 'rself' # ES: How to handle subjectid? else: ret = UserDisplayName(user) if self.getBoolArg('firstnameonly'): ret = user.first_name or user.username if self.getBoolArg('lastnameonly'): ret = user.last_name or user.username if self.getBoolArg('possessive'): ret += "'s" if self.getBoolArg('linked', True) or self.kwArgs.get('linked', None) == 'internal': ret = '<a href="%s">%s</a>' % \ (makeReverse(request, userProfileView, args=[user.id]), ret) return mark_safe(ret) @register.tag def fbName(parser, token): ''' Returns the name for the given user, based on the parameters. Acts much like the fb:name FBML tag, except can work in or out of Facebook. ''' try: bits = token.split_contents()[1:] except ValueError: raise template.TemplateSyntaxError, "%r tag requires at least 1 argument: the user (%s)" %\ (token.contents.split()[0], token.split_contents()) args = [] kwArgs = {} for b in bits: if '=' in b: name,val = b.split('=', 1) kwArgs[name.strip()] = val.strip() else: args.append(b.strip()) if len(args) < 1: raise template.TemplateSyntaxError, "%r tag requires at least one argument: the user" %\ token.contents.split()[0] return FbNameNode(args[0], args[1:], kwArgs) def UserFbId(user): try: return UserProfile.objects.get(user=user).facebookId except UserProfile.DoesNotExist: return None
Moving on
I think that should be enough to work with for now. Next time, I'll discuss publishing stories to news feeds and all that social good stuff. Until then, please feel free to post any comments, questions, or improvements below.
October 7th, 2008 at 12:45 pm Hey, Awesome work you've done here! Cheers, Avlok
October 7th, 2008 at 12:58 pm Thanks, Avlok!
October 16th, 2008 at 1:13 am No Problem. I'm actually developing out a code-base that will serve as a foundation for facebook connect applications (based on the pinaxproject.com philosophy). I want to open-source my work so far and would love to get your feedback on how I should go about doing it. Feel free to email me at avlok.kohli@gmail.com