Chicken Scratches

Making a Facebook app (with Django) – part 3: Python & FBML

Welcome 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.

Track comments by RSS

3 Responses to “Making a Facebook app (with Django) – part 3: Python & FBML”

  1. Avlok Kohli says:

    Hey,

    Awesome work you’ve done here!

    Cheers,

    Avlok

  2. Avlok Kohli says:

    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

Leave a Reply