aboutsummaryrefslogtreecommitdiffstats
path: root/docs/tutorial
diff options
context:
space:
mode:
authorTom Christie2012-08-29 20:57:37 +0100
committerTom Christie2012-08-29 20:57:37 +0100
commit578017e01d1da4746ae0045268043cfd74d41b42 (patch)
treed729eb93484247fa44b509a4b693811e6978a190 /docs/tutorial
parent21f59162db37c656b4f025cdd8e13cdb9933a4fc (diff)
downloaddjango-rest-framework-578017e01d1da4746ae0045268043cfd74d41b42.tar.bz2
New docs
Diffstat (limited to 'docs/tutorial')
-rw-r--r--docs/tutorial/1-serialization.md236
-rw-r--r--docs/tutorial/2-requests-and-responses.md137
-rw-r--r--docs/tutorial/3-class-based-views.md137
-rw-r--r--docs/tutorial/4-authentication-permissions-and-throttling.md3
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md9
-rw-r--r--docs/tutorial/6-resource-orientated-projects.md49
6 files changed, 571 insertions, 0 deletions
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
new file mode 100644
index 00000000..55a9f679
--- /dev/null
+++ b/docs/tutorial/1-serialization.md
@@ -0,0 +1,236 @@
+# Tutorial 1: Serialization
+
+## Introduction
+
+This tutorial will walk you through the building blocks that make up REST framework. It'll take a little while to get through, but it'll give you a comprehensive understanding of how everything fits together.
+
+## Getting started
+
+To get started, let's create a new project to work with.
+
+ django-admin.py startproject tutorial
+ cd tutorial
+
+Once that's done we can create an app that we'll use to create a simple Web API.
+
+ python manage.py startapp blog
+
+The simplest way to get up and running will probably be to use an `sqlite3` database for the tutorial. Edit the `tutorial/settings.py` file, and set the default database `"ENGINE"` to `"sqlite3"`, and `"NAME"` to `"tmp.db"`.
+
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'tmp.db',
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '',
+ 'PORT': '',
+ }
+ }
+
+We'll also need to add our new `blog` app and the `djangorestframework` app to `INSTALLED_APPS`.
+
+ INSTALLED_APPS = (
+ ...
+ 'djangorestframework',
+ 'blog'
+ )
+
+We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our blog views.
+
+ urlpatterns = patterns('',
+ url(r'^', include('blog.urls')),
+ )
+
+Okay, we're ready to roll.
+
+## Creating a model to work with
+
+For the purposes of this tutorial we're going to start by creating a simple `Comment` model that is used to store comments against a blog post. Go ahead and edit the `blog` app's `models.py` file.
+
+ from django.db import models
+
+ class Comment(models.Model):
+ email = models.EmailField()
+ content = models.CharField(max_length=200)
+ created = models.DateTimeField(auto_now_add=True)
+
+Don't forget to sync the database for the first time.
+
+ python manage.py syncdb
+
+## Creating a Serializer class
+
+We're going to create a simple Web API that we can use to edit these comment objects with. The first thing we need is a way of serializing and deserializing the objects into representations such as `json`. We do this by declaring serializers, that work very similarly to Django's forms. Create a file in the project named `serializers.py` and add the following.
+
+ from blog import models
+ from djangorestframework import serializers
+
+
+ class CommentSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+ content = serializers.CharField(max_length=200)
+ created = serializers.DateTimeField()
+
+ def restore_object(self, attrs, instance=None):
+ """
+ Create or update a new comment instance.
+ """
+ if instance:
+ instance.email = attrs['email']
+ instance.content = attrs['content']
+ instance.created = attrs['created']
+ return instance
+ return models.Comment(**attrs)
+
+The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
+
+We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
+
+## Working with Serializers
+
+Before we go any further we'll familiarise ourselves with using our new Serializer class. Let's drop into the Django shell.
+
+ python manage.py shell
+
+Okay, once we've got a few imports out of the way, we'd better create a few comments to work with.
+
+ from blog.models import Comment
+ from blog.serializers import CommentSerializer
+ from djangorestframework.renderers import JSONRenderer
+ from djangorestframework.parsers import JSONParser
+
+ c1 = Comment(email='leila@example.com', content='nothing to say')
+ c2 = Comment(email='tom@example.com', content='foo bar')
+ c3 = Comment(email='anna@example.com', content='LOLZ!')
+ c1.save()
+ c2.save()
+ c3.save()
+
+We've now got a few comment instances to play with. Let's take a look at serializing one of those instances.
+
+ serializer = CommentSerializer(instance=c1)
+ serializer.data
+ # {'email': u'leila@example.com', 'content': u'nothing to say', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}
+
+At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.
+
+ stream = JSONRenderer().render(serializer.data)
+ stream
+ # '{"email": "leila@example.com", "content": "nothing to say", "created": "2012-08-22T16:20:09.822"}'
+
+Deserialization is similar. First we parse a stream into python native datatypes...
+
+ data = JSONParser().parse(stream)
+
+...then we restore those native datatypes into to a fully populated object instance.
+
+ serializer = CommentSerializer(data)
+ serializer.is_valid()
+ # True
+ serializer.object
+ # <Comment object at 0x10633b2d0>
+
+Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.
+
+## Writing regular Django views using our Serializers
+
+Let's see how we can write some API views using our new Serializer class.
+We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`.
+
+Edit the `blog/views.py` file, and add the following.
+
+ from blog.models import Comment
+ from blog.serializers import CommentSerializer
+ from djangorestframework.renderers import JSONRenderer
+ from djangorestframework.parsers import JSONParser
+ from django.http import HttpResponse
+
+
+ class JSONResponse(HttpResponse):
+ """
+ An HttpResponse that renders it's content into JSON.
+ """
+
+ def __init__(self, data, **kwargs):
+ content = JSONRenderer().render(data)
+ kwargs['content_type'] = 'application/json'
+ super(JSONResponse, self).__init__(content, **kwargs)
+
+
+The root of our API is going to be a view that supports listing all the existing comments, or creating a new comment.
+
+ def comment_root(request):
+ """
+ List all comments, or create a new comment.
+ """
+ if request.method == 'GET':
+ comments = Comment.objects.all()
+ serializer = CommentSerializer(instance=comments)
+ return JSONResponse(serializer.data)
+
+ elif request.method == 'POST':
+ data = JSONParser().parse(request)
+ serializer = CommentSerializer(data)
+ if serializer.is_valid():
+ comment = serializer.object
+ comment.save()
+ return JSONResponse(serializer.data, status=201)
+ else:
+ return JSONResponse(serializer.error_data, status=400)
+
+We'll also need a view which corrosponds to an individual comment, and can be used to retrieve, update or delete the comment.
+
+ def comment_instance(request, pk):
+ """
+ Retrieve, update or delete a comment instance.
+ """
+ try:
+ comment = Comment.objects.get(pk=pk)
+ except Comment.DoesNotExist:
+ return HttpResponse(status=404)
+
+ if request.method == 'GET':
+ serializer = CommentSerializer(instance=comment)
+ return JSONResponse(serializer.data)
+
+ elif request.method == 'PUT':
+ data = JSONParser().parse(request)
+ serializer = CommentSerializer(data, instance=comment)
+ if serializer.is_valid():
+ comment = serializer.object
+ comment.save()
+ return JSONResponse(serializer.data)
+ else:
+ return JSONResponse(serializer.error_data, status=400)
+
+ elif request.method == 'DELETE':
+ comment.delete()
+ return HttpResponse(status=204)
+
+Finally we need to wire these views up, in the `tutorial/urls.py` file.
+
+ from django.conf.urls import patterns, url
+
+ urlpatterns = patterns('blog.views',
+ url(r'^$', 'comment_root'),
+ url(r'^(?P<pk>[0-9]+)$', 'comment_instance')
+ )
+
+It's worth noting that there's a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
+
+## Testing our first attempt at a Web API
+
+**TODO: Describe using runserver and making example requests from console**
+
+**TODO: Describe opening in a web browser and viewing json output**
+
+## Where are we now
+
+We're doing okay so far, we've got a serialization API that feels pretty similar to Django's Forms API, and some regular Django views.
+
+Our API views don't do anything particularly special at the moment, beyond serve `json` responses, and there's some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
+
+We'll see how we can start to improve things in [part 2 of the tutorial][1].
+
+[1]: 2-requests-and-responses.md \ No newline at end of file
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
new file mode 100644
index 00000000..2bb6c20e
--- /dev/null
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -0,0 +1,137 @@
+# Tutorial 2: Request and Response objects
+
+From this point we're going to really start covering the core of REST framework.
+Let's introduce a couple of essential building blocks.
+
+## Request objects
+
+REST framework intoduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
+
+ request.POST # Only handles form data. Only works for 'POST' method.
+ request.DATA # Handles arbitrary data. Works any HTTP request with content.
+
+## Response objects
+
+REST framework also introduces a `Response` object, which is a type of `TemplateResponse` that takes unrendered content and uses content negotiation to determine the correct content type to return to the client.
+
+ return Response(data) # Renders to content type as requested by the client.
+
+## Status codes
+
+Using numeric HTTP status codes in your views doesn't always make for obvious reading, and it's easy to not notice if you get an error code wrong. REST framework provides more explicit identifiers for each status code, such as `HTTP_400_BAD_REQUEST` in the `status` module. It's a good idea to use these throughout rather than using numeric identifiers.
+
+## Wrapping API views
+
+REST framework provides two wrappers you can use to write API views.
+
+1. The `@api_view` decorator for working with function based views.
+2. The `APIView` class for working with class based views.
+
+These wrappers provide a few bits of functionality such as making sure you recieve `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed.
+
+The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input.
+
+
+## Pulling it all together
+
+Okay, let's go ahead and start using these new components to write a few views.
+
+ from djangorestframework.decorators import api_view
+ from djangorestframework.status import *
+
+ @api_view(allow=['GET', 'POST'])
+ def comment_root(request):
+ """
+ List all comments, or create a new comment.
+ """
+ if request.method == 'GET':
+ comments = Comment.objects.all()
+ serializer = CommentSerializer(instance=comments)
+ return Response(serializer.data)
+
+ elif request.method == 'POST':
+ serializer = CommentSerializer(request.DATA)
+ if serializer.is_valid():
+ comment = serializer.object
+ comment.save()
+ return Response(serializer.data, status=HTTP_201_CREATED)
+ else:
+ return Response(serializer.error_data, status=HTTP_400_BAD_REQUEST)
+
+
+Our instance view is an improvement over the previous example. It's slightly more concise, and the code now feels very similar to if we were working with the Forms API.
+
+ @api_view(allow=['GET', 'PUT', 'DELETE'])
+ def comment_instance(request, pk):
+ """
+ Retrieve, update or delete a comment instance.
+ """
+ try:
+ comment = Comment.objects.get(pk=pk)
+ except Comment.DoesNotExist:
+ return Response(status=HTTP_404_NOT_FOUND)
+
+ if request.method == 'GET':
+ serializer = CommentSerializer(instance=comment)
+ return Response(serializer.data)
+
+ elif request.method == 'PUT':
+ serializer = CommentSerializer(request.DATA, instance=comment)
+ if serializer.is_valid():
+ comment = serializer.object
+ comment.save()
+ return Response(serializer.data)
+ else:
+ return Response(serializer.error_data, status=HTTP_400_BAD_REQUEST)
+
+ elif request.method == 'DELETE':
+ comment.delete()
+ return Response(status=HTTP_204_NO_CONTENT)
+
+This should all feel very familiar - it looks a lot like working with forms in regular Django views.
+
+Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
+
+## Adding optional format suffixes to our URLs
+
+To take advantage of that, let's add support for format suffixes to our API endpoints, so that we can use URLs that explicitly refer to a given format. That means our API will be able to handle URLs such as [http://example.com/api/items/4.json][1].
+
+Start by adding a `format` keyword argument to both of the views, like so.
+
+ def comment_root(request, format=None):
+
+and
+
+ def comment_instance(request, pk, format=None):
+
+Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs.
+
+ from djangorestframework.urlpatterns import format_suffix_patterns
+
+ urlpatterns = patterns('blogpost.views',
+ url(r'^$', 'comment_root'),
+ url(r'^(?P<pk>[0-9]+)$', 'comment_instance')
+ )
+
+ urlpatterns = format_suffix_patterns(urlpatterns)
+
+We don't necessarily need to add these extra url patterns in, but it gives us a simple, clean way of refering to a specific format.
+
+## How's it looking?
+
+Go ahead and test the API from the command line, as we did in [tutorial part 1][2]. Everything is working pretty similarly, although we've got some nicer error handling if we send invalid requests.
+
+**TODO: Describe using accept headers, content-type headers, and format suffixed URLs**
+
+Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/][3].
+
+**TODO: Describe browseable API awesomeness**
+
+## What's next?
+
+In [tutorial part 3][4], we'll start using class based views, and see how generic views reduce the amount of code we need to write.
+
+[1]: http://example.com/api/items/4.json
+[2]: 1-serialization.md
+[3]: http://127.0.0.1:8000/
+[4]: 3-class-based-views.md \ No newline at end of file
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
new file mode 100644
index 00000000..e56c7847
--- /dev/null
+++ b/docs/tutorial/3-class-based-views.md
@@ -0,0 +1,137 @@
+# Tutorial 3: Using Class Based Views
+
+We can also write our API views using class based views, rather than function based views. As we'll see this is a powerful pattern that allows us to reuse common functionality, and helps us keep our code [DRY][1].
+
+## Rewriting our API using class based views
+
+We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
+
+ from blog.models import Comment
+ from blog.serializers import ComentSerializer
+ from django.http import Http404
+ from djangorestframework.views import APIView
+ from djangorestframework.response import Response
+ from djangorestframework.status import *
+
+ class CommentRoot(views.APIView):
+ """
+ List all comments, or create a new comment.
+ """
+ def get(self, request, format=None):
+ comments = Comment.objects.all()
+ serializer = ComentSerializer(instance=comments)
+ return Response(serializer.data)
+
+ def post(self, request, format=None)
+ serializer = ComentSerializer(request.DATA)
+ if serializer.is_valid():
+ comment = serializer.object
+ comment.save()
+ return Response(serializer.serialized, status=HTTP_201_CREATED)
+ else:
+ return Response(serializer.serialized_errors, status=HTTP_400_BAD_REQUEST)
+
+So far, so good. It looks pretty similar to the previous case, but we've got better seperation between the different HTTP methods. We'll also need to update the instance view.
+
+ class CommentInstance(views.APIView):
+ """
+ Retrieve, update or delete a comment instance.
+ """
+
+ def get_object(self, pk):
+ try:
+ return Poll.objects.get(pk=pk)
+ except Poll.DoesNotExist:
+ raise Http404
+
+ def get(self, request, pk, format=None):
+ comment = self.get_object(pk)
+ serializer = CommentSerializer(instance=comment)
+ return Response(serializer.data)
+
+ def put(self, request, pk, format=None):
+ comment = self.get_object(pk)
+ serializer = CommentSerializer(request.DATA, instance=comment)
+ if serializer.is_valid():
+ comment = serializer.deserialized
+ comment.save()
+ return Response(serializer.data)
+ else:
+ return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
+
+ def delete(self, request, pk, format=None):
+ comment = self.get_object(pk)
+ comment.delete()
+ return Response(status=HTTP_204_NO_CONTENT)
+
+That's looking good. Again, it's still pretty similar to the function based view right now.
+
+Since we're now working with class based views, rather than function based views, we'll also need to update our urlconf slightly.
+
+ from blogpost import views
+ from djangorestframework.urlpatterns import format_suffix_patterns
+
+ urlpatterns = patterns('',
+ url(r'^$', views.CommentRoot.as_view()),
+ url(r'^(?P<id>[0-9]+)$', views.CommentInstance.as_view())
+ )
+
+ urlpatterns = format_suffix_patterns(urlpatterns)
+
+Okay, we're done. If you run the development server everything should be working just as before.
+
+## Using mixins
+
+One of the big wins of using class based views is that it allows us to easily compose reusable bits of behaviour.
+
+The create/retrieve/update/delete operations that we've been using so far is going to be pretty simliar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
+
+We can compose those mixin classes, to recreate our existing API behaviour with less code.
+
+ from blog.models import Comment
+ from blog.serializers import CommentSerializer
+ from djangorestframework import mixins, views
+
+ class CommentRoot(mixins.ListModelQuerysetMixin,
+ mixins.CreateModelInstanceMixin,
+ views.BaseRootAPIView):
+ model = Comment
+ serializer_class = CommentSerializer
+
+ get = list
+ post = create
+
+ class CommentInstance(mixins.RetrieveModelInstanceMixin,
+ mixins.UpdateModelInstanceMixin,
+ mixins.DestroyModelInstanceMixin,
+ views.BaseInstanceAPIView):
+ model = Comment
+ serializer_class = CommentSerializer
+
+ get = retrieve
+ put = update
+ delete = destroy
+
+## Reusing generic class based views
+
+That's a lot less code than before, but we can go one step further still. REST framework also provides a set of already mixed-in views.
+
+ from blog.models import Comment
+ from blog.serializers import CommentSerializer
+ from djangorestframework import views
+
+ class CommentRoot(views.RootAPIView):
+ model = Comment
+ serializer_class = CommentSerializer
+
+ class CommentInstance(views.InstanceAPIView):
+ model = Comment
+ serializer_class = CommentSerializer
+
+Wow, that's pretty concise. We've got a huge amount for free, and our code looks like
+good, clean, idomatic Django.
+
+Next we'll move onto [part 4 of the tutorial][2], where we'll take a look at how we can customize the behavior of our views to support a range of authentication, permissions, throttling and other aspects.
+
+[1]: http://en.wikipedia.org/wiki/Don't_repeat_yourself
+[2]: 4-authentication-permissions-and-throttling.md
diff --git a/docs/tutorial/4-authentication-permissions-and-throttling.md b/docs/tutorial/4-authentication-permissions-and-throttling.md
new file mode 100644
index 00000000..5c37ae13
--- /dev/null
+++ b/docs/tutorial/4-authentication-permissions-and-throttling.md
@@ -0,0 +1,3 @@
+[part 5][5]
+
+[5]: 5-relationships-and-hyperlinked-apis.md \ No newline at end of file
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
new file mode 100644
index 00000000..3d9598d7
--- /dev/null
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -0,0 +1,9 @@
+**TODO**
+
+* Create BlogPost model
+* Demonstrate nested relationships
+* Demonstrate and describe hyperlinked relationships
+
+[part 6][1]
+
+[1]: 6-resource-orientated-projects.md
diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md
new file mode 100644
index 00000000..ce51cce5
--- /dev/null
+++ b/docs/tutorial/6-resource-orientated-projects.md
@@ -0,0 +1,49 @@
+serializers.py
+
+ class BlogPostSerializer(URLModelSerializer):
+ class Meta:
+ model = BlogPost
+
+ class CommentSerializer(URLModelSerializer):
+ class Meta:
+ model = Comment
+
+resources.py
+
+ class BlogPostResource(ModelResource):
+ serializer_class = BlogPostSerializer
+ model = BlogPost
+ permissions = [AdminOrAnonReadonly()]
+ throttles = [AnonThrottle(rate='5/min')]
+
+ class CommentResource(ModelResource):
+ serializer_class = CommentSerializer
+ model = Comment
+ permissions = [AdminOrAnonReadonly()]
+ throttles = [AnonThrottle(rate='5/min')]
+
+Now that we're using Resources rather than Views, we don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls are handled automatically. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file.
+
+ from blog import resources
+ from djangorestframework.routers import DefaultRouter
+
+ router = DefaultRouter()
+ router.register(resources.BlogPostResource)
+ router.register(resources.CommentResource)
+ urlpatterns = router.urlpatterns
+
+## Trade-offs between views vs resources.
+
+Writing resource-orientated code can be a good thing. It helps ensure that URL conventions will be consistent across your APIs, and minimises the amount of code you need to write.
+
+The trade-off is that the behaviour is less explict. It can be more difficult to determine what code path is being followed, or where to override some behaviour.
+
+## Onwards and upwards.
+
+We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
+
+* Contribute on GitHub by reviewing issues, and submitting issues or pull requests.
+* Join the REST framework group, and help build the community.
+* Follow me on Twitter and say hi.
+
+Now go build something great. \ No newline at end of file