Hypermedia APIs in Django: Leveraging Class Based Views

It seems that I keep rewriting code that generates APIs from django. I think I’m getting closer to actually getting it right, though :)

I’m rather keen on Collection+JSON at the moment, and spent some time over easter writing an almost complete Collection+JSON client, using KnockoutJS. It loads up a root API url, and then allows navigation around the API using links. While working on this, it occurred to me that Collection+JSON really encodes the same information as a web page:

  • every <link> or <a href=...></a> element is either in links or queries.
  • form-based queries map nicely to queries elements that have a data attribute.
  • items encapsulates the actual data that should be presented.
  • template contains data that can be used to render a form for creating/updating an object.

Ideally, what feels best from my perspective is to have a pure HTML representation of the API, which can be rendered by browsers with JS disabled, and then all of the same urls could also be fetched as Collection+JSON. Then, you are sharing the code, right up to the point where the output is generated.

To handle this, I’ve come up with a protocol for developing django Class Based Views that can be represented as Collection+JSON or plain old HTML. Basically, your view needs to be able to provide links, queries, items. template comes from a form object (called form), and by default items is the queryset attribute.

leveraging views

I subscribe the idea that the less code that is written the better, and I believe that the API wrapper should (a) have minimal code itself, and (b) allow the end developer to write as little code as possible. Django is a great framework, we should leverage as much as is possible of that well written (and well tested) package.

The part of a hypermedia API that is sometimes ignored by web developers is handling the media type selection. I believe this is the domain of the “Accept” and “Content-Type” headers, not anything to do with the URL. Thus, I have a mixin that allows for selecting the output format based on the Accept header. It uses the inbuilt render_to_response method that a django View class has, and handles choosing how to render the response. As it should.

The other trick is how to get the links, queries, items and template into the context. For this, we can use get_context_data. We can call self.get_FOO(**kwargs) for FOO in each of those items. It is then up to the View class to handle those methods.

By default, a Model-based Resource is likely to have a form class, and a model class or a queryset. These can be used to get the items, and in the case of the form, the template. Even in the instance of the queryset (or model), we use the form class to turn the objects into something that can be rendered.

Finally, so it’s super-easy to use the same pattern as with django’s Views (generic.CreateView, for instance), I have a couple of classes: ListResource and DetailResource, which map directly onto CreateView and UpdateView. In the simplest case, you can just use:

urlpatterns = patterns('',
    url(r'^foo/$', ListResource.as_view(model=Foo)),
    url(r'^foo/(<?P<pk>\d+)/$', DetailResource.as_view(model=Foo))
)

There is also a Resource, which just combines the resource-level bits with generic.TemplateView. You can use ResourceMixin with any other class-based-view, but make sure it appears earlier than the django view class, to make sure we get the correct method resolution order.

There is still the matter of the links attribute. Knowing what to put into this can be a bit tricky. I’ve come to realise that this should contain a list of the valid states that can be accessed when you are in a given state. You will want to use django’s reverse function to populate the href attribute:

class Root(Resource):
    template_name = 'base.html'
    
    def get_links(self):
        return [
            {"rel": "root", "href": reverse('root'), "prompt": "Home"},
            {"rel": "user", "href": reverse('user'), "prompt": "You"},
            {"rel": "links", "href": reverse('tasks:list'), "prompt": "Task List"},
        ]

Note that you actually need to provide the view names (and namespaces, if appropriate) to reverse. Similarly, for any queries, you would want to use reverse, to make it easier to change the URL later. Also, django will complain if you have not installed something you reference, meaning your links and queries should never 404.

I’m still toying with the feature of having an automatic list of links that should be used for every view. Obviously, this should only contain a list of states that can be moved to from any state within the system.

For rendering HTML, you may need to change your templates: actually, you should change your templates. Instead of using:

<a href="{% url 'foo'  %}">Foo Link</a>

You would reference that items in your links array:

<a href="{{ links.foo.href }}">{{ links.foo.prompt }}</a>

I have used a little bit of magic here too: in order to be able to access links items according to their rel attribute, when rendering HTML, we use a sub-class of list that allows for __getattr__ to look through the items and find the first one that matches by rel type.

enter django-hypermedia

As you may surmise from the above text: I’ve already written a big chunk of this. It’s not complete (see below), but you can see where it is at now: django-hypermedia.

There is a demo/test project included, that has some functionality. It shows how you still need to do things “the django way”, and then you get the nice hypermedia stuff automatically.

what is still to come?

I’ve never really been happy with Collection+JSON’s error object, so I haven’t started handling that yet. I want to be able to reference where the error lies, similar to how django’s forms can display their own errors.

I want to flesh out the demo/test project. It has some nice bits already, but I want to have it so that it also uses my nice KnockoutJS client. Pretty helps. :)

Belair Hill Climb

Since I bought my Garmin Forerunner 405cx in November 2010, I’ve gradually gotten more and more into running. Having a computer that tracks when, where, and how fast I run appeals to me, and has probably been the biggest motivator to me running as much as I do now.

I was tracking my running using Garmin Connect, but recently, thanks to my good friend Travis, got hooked on Strava. The key feature for me is the segments, and how competitive they enable me to be. Mostly against myself (my hard running occurs on the same track each week, which no-one else using Strava seems to have discovered).

Technically, I’m in training for this year’s City to Bay, although that is a long way away, so I’m spending the time working on getting faster over that type of distance. Last year I finished in 1490th place, with a time of 00:54:43. My target time had been 00:54:00, so I was a little disappointed to miss that by such a short margin. I did speed up a little too early (2km out), which nearly killed me, and I needed to back off. Also, I was absolutely exhausted by the end.

In fact, if I look at my performance, at the 47 minute mark I sped up, increasing my pace from 4:30 to 4:00 min/km, and promptly had to slow to a walk. There may have been some dry retching going on there too.

So, I set a target time for this year’s race of 00:48:00, with the possibility of reducing that to 00:44:00 if I could get to my target time 12 weeks before the race. Now, it does occur to me that my target pace is the one that forced me to stop early last year, but I think I’m already in much better shape now than I was then.

To improve my speed, I determined that first I needed to reduce my heart-rate. In last year’s City to Bay, my heartrate was basically in the Threshold zone (Z4) for the entire race. It did get a little higher when I sped up, but otherwise was fairly constant, which means I probably did run that race as fast as I could have then.

So, my training plan of late has been to run a lot more with my HR in zone 3, and see if I can get my speed up. So far, it seems to be working. I’ve been doing lots of 30 and 60 minute easy runs, where my Forerunner will beep at me if I get my HR above Z3. I’m finding that lately I’ve been running at around the same pace, but find my HR sometimes dips below Z3, so perhaps I can speed up a little.

Tonight, I ran faster up the Belair Hill Climb than I had ever done so before. Not only that, but each Strava segment was faster, too. More importantly, my Strava “Suffer Score” was only 44, as compared to a 62 on my previous PB up the hill. When done, I was all primed to then run some more hill climbs (6x600m uphill), but it started to rain, and it was time for dinner.

Perhaps the only thing that’s missing from Strava for me was the ability to track my weight: Garmin Connect has it’s “Health” tab, which enables you to enter a weight manually, or accepts data from a supported scale. This information is useful to me, as I can see that my weight increased significantly over the leadup to NTL, where I was training much harder, but obviously bulking up a bit too. Lots more speed and strength work there: I do recall having pants that no longer fit my thighs. I’m now down to 78.1kg, after a high of 84.6, and I’d love to be able to import this data into Strava too.

Something that even Garmin Connect doesn’t do, and which I need to keep Garmin Training Center around for is the advanced workouts. Oh, you can enter them into Garmin Connect, but the interface is slow and clunky, and I was never able to get more than one to upload to my watch at a time. Not that useful when I was in a more free-form mode, and would pick workouts based on how I felt. Now, I have a pre-programmed schedule for the next 20-odd weeks, all stored in there. I think I’ll look at a web-app that improves on the process though, as GTC is a bit rubbish.

Oh, and I have my eye on the Garmin Forerunner 610. Not sure when I will get around to upgrading. The 405cx still works really well for my needs, but there are a few nice new features in the 610.