Irrational Exuberance!

A Django Anti-Pattern, Rolling Your Own REST

June 13, 2008. Filed under djangorestanti-pattern

Last week I got an email from Charlie O'Keefe, and one of the topics it hit upon was the lack of simple support for RESTful apis in Django. I promised a quick blog entry on the topic, but it hasn't really been coming together. The problem is one of a library. Not that there aren't libraries, there are a handful, but none of them have quite hit the sweet spot yet.

I think that the community hasn't been too bothered by this lack because its usually trivial to write the RESTful view you need by hand using just a couple of views. This is why many AJAX heavy applications will end up with a code ghetto that implements the necessary subsection of a RESTful api for that application:

@login_required
def update(request):
    dict = request.POST.copy()
    id = dict.pop('pk')[0]
    model = dict.pop('model')[0]
    object = get_class(model).objects.get(pk=id)
    obj_dict = object.__dict__
    for key in dict.keys():
        if obj_dict.has_key(key):
            val = dict[key]
            if key in BOOLEAN_FIELDS:
                if val == u"true":
                    val = True
                elif val == u"false":
                    val = False
            elif key in SLUG_FIELDS:
                val = slugify(val)
            elif key in DATETIME_FIELDS:
                t = time.mktime(time.strptime(val, "%Y-%m-%d %H:%M:%S"))
                val = datetime.datetime.fromtimestamp(t)
            obj_dict[key] = val
        elif key in MANY_TO_MANY_FIELDS:
            vals = dict.getlist(key)
            manager = getattr(object, key)
            manager.clear()
            if not (len(vals) == 1 and vals[0] == -1):
                manager.add(*vals)
    object.save()
    return HttpResponse("success")

In this particular half-hearted rendition of a RESTful api, I am using predefined field tuples--instead of field lookup--to figure out how to treat certain fields. I'm also using an extremely limited permissions model--are you logged in?--since the application has no logged in component for non-administrators.

Here is a bit smarter version I wrote for a different project:

def generic_update(request, model):
    'Abstracted update operation for the API.'
    model = locate_class_by_string(model)
    status, obj_or_msg, params = retrieve_by_id(request, model)
    if status is True:
        try:
            for key in params.keys():
                val = params[key]
                f = get_field(model, key)
                if f.__class__ == fields.related.ForeignKey:
                    val = f.rel.to.objects.get(pk=val)
                setattr(obj_or_msg, key, val)
            obj_or_msg.save()
            data = simplejson.dumps({'successful':True})
            return HttpResponse(data, mimetype='application/json')
        except:
            msg = "Improper parameters for updating a %s." % model
            return improper_api_request(msg)
    else:
        return improper_api_request(obj_or_msg)

And we also have implementations for all the other RESTful things we'll be doing, like creating new instances:

def generic_create(request, model):
    'Abstracted create operation for the API.'
    def convert_keywords(dict):
        'Hacky way to convert unicode dict keys to strings.'
        new_dict = {}
        for key in dict.keys():
            new_dict[smart_str(key)]=dict[key]
        return new_dict
    model = locate_class_by_string(model)
    params = process_json_params(request)
    if params is not None:
        try:
            # Keywords passed as parameters must be strings,
            # so this is a hack to facilitate that, but there
            # must be a cleaner solution.
            params = convert_keywords(params)
            for key in params.keys():
                val = params[key]
                f = get_field(model, key)
                if f.__class__ == fields.related.ForeignKey:
                    params[key] = f.rel.to.objects.get(pk=val)
            new_model = model(**params)
            new_model.save()
            data = simplejson.dumps({'successful':True, 'pk':new_model.pk})
            return HttpResponse(data, mimetype='application/json')
        except:
            msg = "Improper parameters for creating an instance."
            return improper_api_request(msg)
    else:
        msg = "Improper parameters for creating an instance."
        return improper_api_request(msg)

And each of these implementations have a bevy of brilliant and not-so-brilliant support functions scattered around. Some of my personal examples:

Like a function to magic a lowercase string into proper model camelcasing.

def locate_class_by_string(string, module):
    '''
    Return a Class object from a module based on string.
    Expected input: 'case', 'event, 'email_address'
    '''
    def cap(str):
        return str.capitalize()
    def combine(a, b):
        return "%s_%s" % (a, b)
    capitalized_string = reduce(combine ,map(cap, string.split('_')))
    return getattr(module, capitalized_string)

Or a function to retrieve the instance a request is referring to...

def retrieve_by_id(request, model):
    'Attempt to retrieve a model instance by id.'
    params = process_json_params(request)
    if params is not None and params.has_key('id'):
        id = params['id']
        try:
            object = model.objects.get(id=id)
            return True, object, params
        except ObjectDoesNotExist:
            return None, "ID refers to a non-existant instance.", params
    else:
        return None, "Did not specify valid ID.", params

I mean, damn, how many times am I going to keep rewriting mediocre pieces of a RESTful API before I just sit down and write a complete one? At the moment I am looking at a few of the existing Django REST projects. I'm pretty sure there are enough people sufficiently interested in solving this problem that we can find a pretty compelling solution. As I type I am brainstorming about what I think an optimal REST for Django will involve, and will try to post the results of my thoughts in the next day or two.

I'd like to hear what other people need from a RESTful API before they would start adopting it for their projects.