Content negotiation
+ +Content negotiation
HTTP has provisions for several mechanisms for "content negotiation" - the process of selecting the best representation for a given response when there are multiple representations available.
— RFC 2616, Fielding et al.
@@ -123,7 +124,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/exceptions.html b/api-guide/exceptions.html index 5a5e52d7e..61c98e1b5 100644 --- a/api-guide/exceptions.html +++ b/api-guide/exceptions.html @@ -176,7 +176,7 @@ Content-Length: 42 if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/format-suffixes.html b/api-guide/format-suffixes.html index 1d481f877..6920f24a1 100644 --- a/api-guide/format-suffixes.html +++ b/api-guide/format-suffixes.html @@ -125,7 +125,7 @@ used all the time. if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/generic-views.html b/api-guide/generic-views.html index 0181ede75..f3c3631a9 100644 --- a/api-guide/generic-views.html +++ b/api-guide/generic-views.html @@ -125,7 +125,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/parsers.html b/api-guide/parsers.html index 8210c3099..6c029e501 100644 --- a/api-guide/parsers.html +++ b/api-guide/parsers.html @@ -122,7 +122,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/permissions.html b/api-guide/permissions.html index fc4a5c004..7130511a9 100644 --- a/api-guide/permissions.html +++ b/api-guide/permissions.html @@ -191,7 +191,7 @@ def example_view(request, format=None): if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/renderers.html b/api-guide/renderers.html index 84d5dfca6..119cb82e0 100644 --- a/api-guide/renderers.html +++ b/api-guide/renderers.html @@ -122,7 +122,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/requests.html b/api-guide/requests.html index 2ae155b75..203dad4f2 100644 --- a/api-guide/requests.html +++ b/api-guide/requests.html @@ -163,7 +163,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/responses.html b/api-guide/responses.html index 84b2d3be4..31a4eef51 100644 --- a/api-guide/responses.html +++ b/api-guide/responses.html @@ -135,7 +135,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/reverse.html b/api-guide/reverse.html index 4ba0c5004..1a93dcac7 100644 --- a/api-guide/reverse.html +++ b/api-guide/reverse.html @@ -151,7 +151,7 @@ class MyView(APIView): if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/serializers.html b/api-guide/serializers.html index 6ffe87992..a2dbae281 100644 --- a/api-guide/serializers.html +++ b/api-guide/serializers.html @@ -317,7 +317,7 @@ TheModelSerializer
class lets you automatically create a Serialize if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/settings.html b/api-guide/settings.html index fd8725cff..871923b22 100644 --- a/api-guide/settings.html +++ b/api-guide/settings.html @@ -220,7 +220,7 @@ print api_settings.DEFAULT_AUTHENTICATION if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/status-codes.html b/api-guide/status-codes.html index c44a54119..ef372fce2 100644 --- a/api-guide/status-codes.html +++ b/api-guide/status-codes.html @@ -199,7 +199,7 @@ HTTP_511_NETWORD_AUTHENTICATION_REQUIRED if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/throttling.html b/api-guide/throttling.html index 45894524b..fd4fff608 100644 --- a/api-guide/throttling.html +++ b/api-guide/throttling.html @@ -239,7 +239,7 @@ class UploadView(APIView): if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/api-guide/views.html b/api-guide/views.html index 3dbc92400..620e0621f 100644 --- a/api-guide/views.html +++ b/api-guide/views.html @@ -154,7 +154,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/index.html b/index.html index ea52c98ab..801050ff9 100644 --- a/index.html +++ b/index.html @@ -239,7 +239,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/browsable-api.html b/topics/browsable-api.html index 59aa95e46..d69958b14 100644 --- a/topics/browsable-api.html +++ b/topics/browsable-api.html @@ -181,7 +181,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/changelog.html b/topics/changelog.html index 4ae8301ea..89a6b4564 100644 --- a/topics/changelog.html +++ b/topics/changelog.html @@ -243,7 +243,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/contributing.html b/topics/contributing.html index a85911f8d..7ebde9a22 100644 --- a/topics/contributing.html +++ b/topics/contributing.html @@ -124,7 +124,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/credits.html b/topics/credits.html index d45c7cd18..76da1d1e5 100644 --- a/topics/credits.html +++ b/topics/credits.html @@ -173,7 +173,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/csrf.html b/topics/csrf.html index f2e50a6a8..39d0299ce 100644 --- a/topics/csrf.html +++ b/topics/csrf.html @@ -128,7 +128,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/formoverloading.html b/topics/formoverloading.html index f83977350..375c93400 100644 --- a/topics/formoverloading.html +++ b/topics/formoverloading.html @@ -147,7 +147,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/topics/rest-hypermedia-hateoas.html b/topics/rest-hypermedia-hateoas.html new file mode 100644 index 000000000..1af060ad0 --- /dev/null +++ b/topics/rest-hypermedia-hateoas.html @@ -0,0 +1,151 @@ + + + +Django REST framework + + + + + + + + + + + + + + + + +++ + + + + + + + + + \ No newline at end of file diff --git a/tutorial/1-serialization.html b/tutorial/1-serialization.html index 78aec3d7d..01b71c2dd 100644 --- a/tutorial/1-serialization.html +++ b/tutorial/1-serialization.html @@ -270,7 +270,7 @@ class JSONResponse(HttpResponse): comment.save() return JSONResponse(serializer.data, status=201) else: - return JSONResponse(serializer.error_data, status=400) + return JSONResponse(serializer.errors, status=400)++++ +++ +++REST, Hypermedia & HATEOAS
+++You keep using that word "REST". I do not think it means what you think it means.
+— Mike Amundsen, REST fest 2012 keynote.
+First off, the disclaimer. The name "Django REST framework" was choosen with a view to making sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
+If you are serious about designing a Hypermedia APIs, you should look to resources outside of this documentation to help inform your design choices.
+The following fall into the "required reading" category.
++
+- Fielding's dissertation - Architectural Styles and +the Design of Network-based Software Architectures.
+- Fielding's "REST APIs must be hypertext-driven" blog post.
+- Leonard Richardson & Sam Ruby's RESTful Web Services.
+- Mike Amundsen's Building Hypermedia APIs with HTML5 and Node.
+- Steve Klabnik's Designing Hypermedia APIs.
+- The Richardson Maturity Model.
+For a more thorough background, check out Klabnik's Hypermedia API reading list.
+Building Hypermedia APIs with REST framework
+REST framework is an agnositic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style.
+What REST framework does provide.
+It is self evident that REST framework makes it possible to build Hypermedia APIs. The browseable API that it offers is built on HTML - the hypermedia language of the web.
+REST framework also includes serialization and parser/renderer components that make it easy to build appropriate media types, hyperlinked relations for building well-connected systems, and great support for content negotiation.
+What REST framework doesn't provide.
+What REST framework doesn't do is give you is machine readable hypermedia formats such as Collection+JSON by default, or the ability to auto-magically create HATEOAS style APIs. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
+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): @@ -294,7 +294,7 @@ class JSONResponse(HttpResponse): comment.save() return JSONResponse(serializer.data) else: - return JSONResponse(serializer.error_data, status=400) + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': comment.delete() @@ -334,7 +334,7 @@ urlpatterns = patterns('blog.views', if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/tutorial/2-requests-and-responses.html b/tutorial/2-requests-and-responses.html index b297582d7..40355be53 100644 --- a/tutorial/2-requests-and-responses.html +++ b/tutorial/2-requests-and-responses.html @@ -156,7 +156,7 @@ def comment_root(request): comment.save() return Response(serializer.data, status=status.HTTP_201_CREATED) else: - return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
@api_view(['GET', 'PUT', 'DELETE']) @@ -180,7 +180,7 @@ def comment_instance(request, pk): comment.save() return Response(serializer.data) else: - return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': comment.delete() @@ -236,7 +236,7 @@ urlpatterns = format_suffix_patterns(urlpatterns) if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/tutorial/3-class-based-views.html b/tutorial/3-class-based-views.html index 272e1d337..5621f93f9 100644 --- a/tutorial/3-class-based-views.html +++ b/tutorial/3-class-based-views.html @@ -130,8 +130,6 @@ class CommentRoot(APIView): comment.save() return Response(serializer.serialized, status=status.HTTP_201_CREATED) return Response(serializer.serialized_errors, status=status.HTTP_400_BAD_REQUEST) - -comment_root = CommentRoot.as_view()
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(APIView): @@ -157,17 +155,27 @@ comment_root = CommentRoot.as_view() comment = serializer.deserialized comment.save() return Response(serializer.data) - return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, pk, format=None): comment = self.get_object(pk) comment.delete() return Response(status=status.HTTP_204_NO_CONTENT) - -comment_instance = CommentInstance.as_view()
That's looking good. Again, it's still pretty similar to the function based view right now. -Okay, we're done. If you run the development server everything should be working just as before.
+That's looking good. Again, it's still pretty similar to the function based view right now.
+We'll also need to refactor our URLconf slightly now we're using class based views.
++from django.conf.urls import patterns, url +from djangorestframework.urlpatterns import format_suffix_patterns +from blogpost import views + +urlpatterns = patterns('', + url(r'^$', views.CommentRoot.as_view()), + url(r'^(?P<pk>[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 are 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.
@@ -188,8 +196,6 @@ class CommentRoot(mixins.ListModelMixin, def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) - -comment_root = CommentRoot.as_view()We'll take a moment to examine exactly what's happening here - We're building our view using
MultipleObjectBaseView
, and adding inListModelMixin
andCreateModelMixin
.The base class provides the core functionality, and the mixin classes provide the
@@ -208,8 +214,6 @@ comment_root = CommentRoot.as_view() def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) - -comment_instance = CommentInstance.as_view().list()
and.create()
actions. We're then explictly binding theget
andpost
methods to the appropriate actions. Simple enough stuff so far.Pretty similar. This time we're using the
SingleObjectBaseView
class to provide the core functionality, and adding in mixins to provide the.retrieve()
,.update()
and.destroy()
actions.Using generic class based views
@@ -222,13 +226,9 @@ class CommentRoot(generics.RootAPIView): model = Comment serializer_class = CommentSerializer -comment_root = CommentRoot.as_view() - class CommentInstance(generics.InstanceAPIView): model = Comment serializer_class = CommentSerializer - -comment_instance = CommentInstance.as_view()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, 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.
@@ -250,7 +250,7 @@ comment_instance = CommentInstance.as_view() if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/tutorial/4-authentication-permissions-and-throttling.html b/tutorial/4-authentication-permissions-and-throttling.html index afb082147..cb35bed40 100644 --- a/tutorial/4-authentication-permissions-and-throttling.html +++ b/tutorial/4-authentication-permissions-and-throttling.html @@ -118,7 +118,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/tutorial/5-relationships-and-hyperlinked-apis.html b/tutorial/5-relationships-and-hyperlinked-apis.html index 72b2a7c92..19bf18e12 100644 --- a/tutorial/5-relationships-and-hyperlinked-apis.html +++ b/tutorial/5-relationships-and-hyperlinked-apis.html @@ -124,7 +124,7 @@ if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); diff --git a/tutorial/6-resource-orientated-projects.html b/tutorial/6-resource-orientated-projects.html index aa33ca167..52af194ba 100644 --- a/tutorial/6-resource-orientated-projects.html +++ b/tutorial/6-resource-orientated-projects.html @@ -184,7 +184,7 @@ urlpatterns = router.urlpatterns if (location.hash) shiftWindow(); window.addEventListener("hashchange", shiftWindow); - $('.dropdown-menu').click(function(event) { + $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); });