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 pages, posts 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.