转载自:http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html
A ReSTful API is becoming a standard component of any modern web application. The Django Rest Framework is powerful framework for developing ReST endpoints for your Django based project. AngularJS is modern javascript framework for creating complex web applications within the browser. It focuses on strong separation of concerns (MVC) and dependency injection to encourage creating maintainable (and testable) modules that can be integrated to develop rich client side functionality.
In this blog post, I'll walk through creating a sample project that exposes a ReST API consumed by AngularJS on the client to showcase how to combine the frontend and backend frameworks to simplify creating complex applications. I'll make heavy use of code examples to demonstrate both the solution and the process and there's a companion Github project with all the code.
Let's Build a Sample Django Project
For a sample project, let's create a simple photo-sharing app (not unlike a rudimentary Instagram) and build a feed view for a given user to scan through all the photos shared on the site.
All the sample code for this project is available on a GitHub repository. To setup the sample project in your own environment, consult the installation instructions included in the repository. This includes installing AngularJS (and other javascript assets) via bower+grunt.
Finally, there's some sample data in fixtures available to demonstrate the API. We have a few users (['Bob', 'Sally', 'Joe', 'Rachel']
), two posts (['This is a great post', 'Another thing I wanted to share']
) and some sample photos as well. The included Makefile builds the sample data for you.
Couple notes about the sample code:
- I'll skip over the details on configuring, building and running the sample application. There's instructions in the repository that covers many of these details. Please report any issues on GitHub and I'll be sure to fix them.
- I've written the frontend code using Coffee-Script since I find it more legible and more efficient (and a bit more Pythonic). There's a supplied Grunt task that collects all the coffee script files and concats them into a single
script.js
file for inclusion.
Project Data Layer (the Models)
Our model layer is straightforward to what you might find in an introductory tutorial for Django. You have three models of note: User
, Post
, and Photo
. A user can author many posts (as well as have many followers) and a post can showcase many photos (such as a collection or gallery) along with a title and optional description/body.
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
followers = models.ManyToManyField('self', related_name='followees', symmetrical=False)
class Post(models.Model):
author = models.ForeignKey(User, related_name='posts')
title = models.CharField(max_length=255)
body = models.TextField(blank=True, null=True)
class Photo(models.Model):
post = models.ForeignKey(Post, related_name='photos')
image = models.ImageField(upload_to="%Y/%m/%d")
Django Rest Framework based API
The Django Rest Framework (DRF) provides a clean architecture to develop both simple, turn-key API endpoints as well as more complex ReST constructs. The key is a clean separation with Serializer
which describes the mapping between a model and the generalized wire representation (be it JSON, XML or whatever) and separate set of generic Class-Based-Views that can be extended to meet the needs of the specific API endpoint. You also define your own URL structure rather than rely on an auto-generated one. This is what separates DRF from other frameworks like Tastypie and Piston that automate much of the conversion from django models to ReST endpoints, but come at the cost of flexibility in adapting to different use cases (especially around permissions and nested resources).
Model Serializers
The Serializers
in DRF focus on the responsibility to convert django model instances into their representation in the API. This gives us the opportunity to convert any data types, or provide supplemental information in a given model. For example, for the user, we only included some of the fields, stripping private attributes such as password
and email
. For the photo, we converted the ImageField
to return the url of the image (rather than the media path location).
For the PostSerializer
, we elected to embed the author directly in the Post (rather the the common case to provide a hyperlink). This makes that information readily accessible to our client rather than requiring extra API requests at the cost of duplicating users on each post. The alternative hyperlink is listed with a comment for comparison. The power of serializers is that you can extend them to create a derivative version, that uses hyperlinks instead of nested records (say, for the case of listing posts by a specific users' feed).
To assign the author
to the PostSerializer
, we're gonna have that provided by the API View. So we'll make it optional (required=False
) in our serializer and add it to the validation exclusion.
from rest_framework import serializers
from .models import User, Post, Photo
class UserSerializer(serializers.ModelSerializer):
posts = serializers.HyperlinkedIdentityField('posts', view_name='userpost-list', lookup_field='username')
class Meta:
model = User
fields = ('id', 'username', 'first_name', 'last_name', 'posts', )
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(required=False)
photos = serializers.HyperlinkedIdentityField('photos', view_name='postphoto-list')
# author = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='username')
def get_validation_exclusions(self):
# Need to exclude `author` since we'll add that later based off the request
exclusions = super(PostSerializer, self).get_validation_exclusions()
return exclusions + ['author']
class Meta:
model = Post
class PhotoSerializer(serializers.ModelSerializer):
image = serializers.Field('image.url')
class Meta:
model = Photo
Okay, given our samples are fixtures are loaded, let's play with these serializers. You'll likely see DeprecationWarning
because we're using HyperlinkedIdentityField
without providing a request object to construct the URL. In the actual views, this is provided, so you can safely ignore.
>>> from example.api.models import User
>>> user = User.objects.get(username='bob')
>>> # Need to generate a fake request for our hyperlinked results
>>> from django.test.client import RequestFactory
>>> from example.api.serializers import *
>>> context = dict(request=RequestFactory().get('/'))
>>> serializer = UserSerializer(user, context=context)
>>> serializer.data
{'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': 'http://testserver/api/users/bob/posts'}
>>> post = user.posts.all()[0]
>>> PostSerializer(post, context=context).data
{'author': {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': 'http://testserver/api/users/bob/posts'}, 'photos': 'http://testserver/api/posts/2/photos', u'id': 2, 'title': u'Title #2', 'body': u'Another thing I wanted to share'}
>>> serializer = PostSerializer(user.posts.all(), many=True, context=context)
>>> serializer.data
[{'author': {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': 'http://testserver/api/users/bob/posts'}, 'photos': 'http://testserver/api/posts/2/photos', u'id': 2, 'title': u'Title #2', 'body': u'Another thing I wanted to share'}]
API URL Structure
For our API structure, we want to maintain a relatively flat structure to try to define canonical endpoints for given resources, but also provide some convenient nested listings for common filterings (such as posts for a given user and photos in a given post). Note that we use model primary keys as the identifier, but for users, we use their username since that's also unique identifying (we'll see this later in the views).
from django.conf.urls import patterns, url, include
from .api import UserList, UserDetail
from .api import PostList, PostDetail, UserPostList
from .api import PhotoList, PhotoDetail, PostPhotoList
user_urls = patterns('',
url(r'^/(?P<username>[0-9a-zA-Z_-]+)/posts$', UserPostList.as_view(), name='userpost-list'),
url(r'^/(?P<username>[0-9a-zA-Z_-]+)$', UserDetail.as_view(), name='user-detail'),
url(r'^$', UserList.as_view(), name='user-list')
)
post_urls = patterns('',
url(r'^/(?P<pk>d+)/photos$', PostPhotoList.as_view(), name='postphoto-list'),
url(r'^/(?P<pk>d+)$', PostDetail.as_view(), name='post-detail'),
url(r'^$', PostList.as_view(), name='post-list')
)
photo_urls = patterns('',
url(r'^/(?P<pk>d+)$', PhotoDetail.as_view(), name='photo-detail'),
url(r'^$', PhotoList.as_view(), name='photo-list')
)
urlpatterns = patterns('',
url(r'^users', include(user_urls)),
url(r'^posts', include(post_urls)),
url(r'^photos', include(photo_urls)),
)
API Views
Much of the power of Django Rest Framework is that the generic views make it easy to work with the common CRUD cases with little or no modifications. For the simplest views, you provide a model
and a serializer_class
and extend one of the built in generics (like ListAPIView
or RetrieveAPIView
).
For our use case, we have a couple customizations. First, for users, we wanted to use username
as the lookup field rather than pk
. So we set lookup_field
on the view (by default it's both the url_kwarg
and the field name on the model).
We also wanted to create nested versions of the views for a given user's posts or the photos within a post. You simply override get_queryset
on the view to customize the queryset to filter down the results based on the nested parameters (username
and pk
respectively).
from rest_framework import generics, permissions
from .serializers import UserSerializer, PostSerializer, PhotoSerializer
from .models import User, Post, Photo
class UserList(generics.ListCreateAPIView):
model = User
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [
permissions.AllowAny
]
class UserDetail(generics.RetrieveAPIView):
model = User
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_field = 'username'
class PostList(generics.ListCreateAPIView):
model = Post
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [
permissions.AllowAny
]
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
model = Post
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [
permissions.AllowAny
]
class UserPostList(generics.ListAPIView):
model = Post
queryset = Post.objects.all()
serializer_class = PostSerializer
def get_queryset(self):
queryset = super(UserPostList, self).get_queryset()
return queryset.filter(author__username=self.kwargs.get('username'))
class PhotoList(generics.ListCreateAPIView):
model = Photo
queryset = Photo.objects.all()
serializer_class = PhotoSerializer
permission_classes = [
permissions.AllowAny
]
class PhotoDetail(generics.RetrieveUpdateDestroyAPIView):
model = Photo
queryset = Photo.objects.all()
serializer_class = PhotoSerializer
permission_classes = [
permissions.AllowAny
]
class PostPhotoList(generics.