Dugan Chen's Homepage

Various Things

Django, Tastypie, Generic Foreign Keys and Backbone

I’m going to show you how to implement, in Django, a tagging database and a web service to query it. By the time we’re done, our web service will also be usable from an unmodified copy of Backbone.js.

Requirements

Our system will have pagesposts and tags. Tags can be attached to either pages or posts. This data also needs to be readable externally, through a RESTful web service API. Keeping in mind The Zen of Python principles that “flat is better than nested” and “simple is better than complex” we propose an API with three endpoints: for pages, posts and tags. The returned lists of pages and posts can be filtered by a tag id, while tags can be filtered by either a page id or a post id. All three endpoints will return JSON, and only the information we need.

We know we’re going to use Tastypie to implement the web services. Accordingly, we design the URLs:

Pages

Given a tag, fetch all pages with that tag.

Example url: /api/v1/pages?tag=1

Example result:

[{"id": 1, 'title': "Cat Page"}]

Posts

Given a tag, fetch all posts with that tag.

Example url: /api/v1/posts?tag=1

Example result:

[{"id": 1, "title": "Cat Post"}]

Tags

Tags on a Page

Given a page, fetch all tags on that page.

Example url: /api/v1/tags?page=1

Example result:

[{"id": 1, 'name': "cat"}]

Tags on a Post

Given a post, fetch all tags on that post.

Example url: /api/v1/tags?post=1

Example result:

[{"id": 1, 'name': "cat"}]

Having the requirements done allows us to write the data model.

Data Model

We implement the data model in the most obvious way: using Django’s contenttypes system and its generic foreign keys.

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Tag(models.Model):
    name = models.SlugField(unique=True)

    def __unicode__(self):
        return self.name

class TagLink(models.Model):
    tag = models.ForeignKey(Tag)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    link = generic.GenericForeignKey()

    class Meta:
        unique_together = ('tag', 'content_type',
                           'object_id')

class TaggableModel(models.Model):

    title = models.CharField(max_length=255)
    tag_links = generic.GenericRelation(TagLink)

    def __unicode__(self):
        return self.title

    class Meta:
        abstract = True

class Post(TaggableModel):
    pass

class Page(TaggableModel):
    pass

Note the GenericRelation fields. Do we need them? The answer is yes. We need them to use as query fields (i.e. in filter), as you’ll see when we make the web service.

Unit Tests

With the data model written, it’s now possible to write the unit tests. These verify that the example URLs given above are present and that they return expected values.

from django.test import TestCase
from django.test.client import Client
from json import loads
from urllib import urlencode
from models import Page, Post, Tag, TagLink

class WebServiceTest(TestCase):

    def setUp(self):

        self.__cat_page = Page(title='Cat Page')
        self.__cat_page.save()

        self.__cat_post = Post(title='Cat Post')
        self.__cat_post.save()

        self.__cat_tag = Tag(name='cat')
        self.__cat_tag.save()

        self.__page_tag = TagLink(link=self.__cat_page,
                                tag=self.__cat_tag)
        self.__page_tag.save()

        self.__post_tag = TagLink(link=self.__cat_post,
                                tag=self.__cat_tag)
        self.__post_tag.save()

    def test_get_pages(self):

        json = self.__get_json('pages', {'tag': 0})
        self.assertEquals([], json)

        json = self.__get_json('pages',
                               {'tag': self.__cat_tag.pk})
        self.assertEquals([{'id': self.__cat_page.pk,
                         'title': self.__cat_page.title}],
                          json)

    def test_get_posts(self):

        json = self.__get_json('posts', {'tag': 0})
        self.assertEquals([], json)

        json = self.__get_json('posts',
                              {'tag': self.__cat_tag.pk})
        self.assertEquals([{'id': self.__cat_post.pk,
                         'title': self.__cat_post.title}],
                         json)

    def test_get_tags_by_page(self):

        json = self.__get_json('tags', {'page': 0})
        self.assertEquals([], json)

        json = self.__get_json('tags',
                            {'page': self.__cat_page.pk})
        self.assertEquals([{'id': self.__cat_tag.pk,
                            'name': self.__cat_tag.name}],
                          json)

    def test_get_tags_by_post(self):

        json = self.__get_json('tags', {'post': 0})
        self.assertEquals([], json)

        json = self.__get_json('tags',
                             {'post': self.__cat_post.pk})
        self.assertEquals([{'id': self.__cat_tag.pk,
                          'name': self.__cat_tag.name}],
                          json)

    def __get_json(self, endpoint, data):
        params = {'format': 'json'}
        params = dict(params.items() + data.items())
        url_params = urlencode(params)
        template = '/api/v1/{0}/?{1}'
        url = template.format(endpoint, url_params)
        client = Client()
        response = client.get(url)
        return loads(response.content)

    def tearDown(self):
        self.__page_tag.delete()
        self.__post_tag.delete()
        self.__cat_page.delete()
        self.__cat_post.delete()
        self.__cat_tag.delete()

Now we can build the web service API that the unit tests are verifying.

Web Services

We have suite of unit tests to verify the web service API. We have a set of data models for the web services to read. Let’s write the web services.

The AbstractResource’s methods are reimplemented to transform the data into the format we expect. It throws away all but the actual objects being returned, and converts the “id” field into an integer (it is by default a string).

Each of the resources that implement it is responsible for knowing how to query the data model. Note that none of the queries hit the database more than once.

In this case, the model classes are in an application called “tagdata”. That’s where the “app_label == tagdata” value you see in the queries comes from.

from models import Tag, Page, Post
from tastypie import fields
from tastypie.resources import ModelResource

class AbstractResource(ModelResource):

    def alter_list_data_to_serialize(self, request, data):
        return data['objects']

    def dehydrate_id(self, bundle):
        return int(bundle.data['id'])

    class Meta:
        include_resource_uri = False
        limit = 0

class TaggedResource(AbstractResource):

    def build_filters(self, filters=None):
        orm_filters = super(TaggedResource, self).build_filters(filters)

        if 'tag' in filters:
            orm_filters['tag_links__tag__exact'] = filters['tag']
        return orm_filters

    class Meta(AbstractResource.Meta):
        filtering = {'tag': ['exact']}

class PagesResource(TaggedResource):

    class Meta(TaggedResource.Meta):
        queryset = Page.objects.all()

class PostsResource(TaggedResource):

    class Meta(TaggedResource.Meta):
        queryset = Post.objects.all()

class TagsResource(AbstractResource):

    def build_filters(self, filters=None):
        orm_filters = super(TagsResource, self).build_filters(filters)

        if 'page' in filters or 'post' in filters:
            orm_filters['taglink__content_type__app_label__exact'] = 'tagdata'
        if 'page' in filters:
            orm_filters['taglink__content_type__model__exact'] = 'page'
            orm_filters['taglink__object_id__exact'] = filters['page']
        elif 'post' in filters:
            orm_filters['taglink__content_type__model__exact'] = 'post'
            orm_filters['taglink__object_id__exact'] = filters['post']
        return orm_filters

    class Meta(AbstractResource.Meta):
        filtering = {'page': ['exact'],
                     'post': ['exact']}
        queryset = Tag.objects.all()

The unit tests now pass, which means we’re done.

Backbone

Note that as a side effect of using Tastypie, our web service can be consumed from Backbone.js. The following URLs are also available. They are in, and they return data in, the exact format that Backbone expects:

  • /api/v1/pages/
  • /api/v1/pages/1
  • /api/v1/posts/
  • /api/v1/posts/1
  • /api/v1/tags/
  • /api/v1/tags/1

No need for backbone-tastypie.