From 2d2e2f95b0268c3282edfca5c047bdc831133016 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Feb 2012 16:02:16 +0000 Subject: [PATCH 001/196] Cleanup of reverse docs --- djangorestframework/tests/reverse.py | 31 ++++++++++++++++++++++------ docs/howto/reverse.rst | 31 +++++++++++++++++++--------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 3dd13ef83..01ba19b9f 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -3,11 +3,11 @@ from django.test import TestCase from django.utils import simplejson as json from djangorestframework.renderers import JSONRenderer -from djangorestframework.reverse import reverse +from djangorestframework.reverse import reverse, reverse_lazy from djangorestframework.views import View -class MyView(View): +class ReverseView(View): """ Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified. @@ -15,10 +15,23 @@ class MyView(View): renderers = (JSONRenderer, ) def get(self, request): - return reverse('myview', request=request) + return reverse('reverse', request=request) + + +class LazyView(View): + """ + Mock resource which simply returns a URL, so that we can ensure + that reversed URLs are fully qualified. + """ + renderers = (JSONRenderer, ) + + def get(self, request): + return reverse_lazy('lazy', request=request) + urlpatterns = patterns('', - url(r'^myview$', MyView.as_view(), name='myview'), + url(r'^reverse$', ReverseView.as_view(), name='reverse'), + url(r'^lazy$', LazyView.as_view(), name='lazy'), ) @@ -29,5 +42,11 @@ class ReverseTests(TestCase): urls = 'djangorestframework.tests.reverse' def test_reversed_urls_are_fully_qualified(self): - response = self.client.get('/myview') - self.assertEqual(json.loads(response.content), 'http://testserver/myview') + response = self.client.get('/reverse') + self.assertEqual(json.loads(response.content), + 'http://testserver/reverse') + + #def test_reversed_lazy_urls_are_fully_qualified(self): + # response = self.client.get('/lazy') + # self.assertEqual(json.loads(response.content), + # 'http://testserver/lazy') diff --git a/docs/howto/reverse.rst b/docs/howto/reverse.rst index 73b8fa4df..0a7af0e99 100644 --- a/docs/howto/reverse.rst +++ b/docs/howto/reverse.rst @@ -1,23 +1,32 @@ Returning URIs from your Web APIs ================================= -As a rule, it's probably better practice to return absolute URIs from you web APIs, e.g. "http://example.com/foobar", rather than returning relative URIs, e.g. "/foobar". +As a rule, it's probably better practice to return absolute URIs from you web +APIs, e.g. "http://example.com/foobar", rather than returning relative URIs, +e.g. "/foobar". The advantages of doing so are: * It's more explicit. * It leaves less work for your API clients. -* There's no ambiguity about the meaning of the string when it's found in representations such as JSON that do not have a native URI type. -* It allows us to easily do things like markup HTML representations with hyperlinks. +* There's no ambiguity about the meaning of the string when it's found in + representations such as JSON that do not have a native URI type. +* It allows us to easily do things like markup HTML representations + with hyperlinks. -Django REST framework provides two utility functions to make it simpler to return absolute URIs from your Web API. +Django REST framework provides two utility functions to make it simpler to +return absolute URIs from your Web API. -There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier. +There's no requirement for you to use them, but if you do then the +self-describing API will be able to automatically hyperlink its output for you, +which makes browsing the API much easier. -reverse(viewname, request, ...) +reverse(viewname, ..., request=None) ------------------------------- -The :py:func:`~reverse.reverse` function has the same behavior as `django.core.urlresolvers.reverse`_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port:: +The `reverse` function has the same behavior as +`django.core.urlresolvers.reverse`_, except that it optionally takes a request +keyword argument, which it uses to return a fully qualified URL. from djangorestframework.reverse import reverse from djangorestframework.views import View @@ -25,15 +34,17 @@ The :py:func:`~reverse.reverse` function has the same behavior as `django.core.u class MyView(View): def get(self, request): context = { - 'url': reverse('year-summary', request, args=[1945]) + 'url': reverse('year-summary', args=[1945], request=request) } return Response(context) -reverse_lazy(viewname, request, ...) +reverse_lazy(viewname, ..., request=None) ------------------------------------ -The :py:func:`~reverse.reverse_lazy` function has the same behavior as `django.core.urlresolvers.reverse_lazy`_, except that it takes a request object and returns a fully qualified URL, using the request to determine the host and port. +The `reverse_lazy` function has the same behavior as +`django.core.urlresolvers.reverse_lazy`_, except that it optionally takes a +request keyword argument, which it uses to return a fully qualified URL. .. _django.core.urlresolvers.reverse: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse .. _django.core.urlresolvers.reverse_lazy: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy From 54a19105f051113085efe9f87783acb77127c1d1 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Thu, 1 Mar 2012 12:46:38 +0000 Subject: [PATCH 002/196] Maintain a reference to the parent serializer when descending down into fields --- djangorestframework/serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index 5dea37e81..e2d860a2f 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -133,6 +133,7 @@ class Serializer(object): if isinstance(info, (list, tuple)): class OnTheFlySerializer(self.__class__): fields = info + parent = getattr(self, 'parent', self) return OnTheFlySerializer # If an element in `fields` is a 2-tuple of (str, Serializer) From 0a57cf98766ebbf22900024608faefbca3bdde6b Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Thu, 1 Mar 2012 12:51:23 +0000 Subject: [PATCH 003/196] Added a .parent attribute to the Serializer object for documentation purposes --- djangorestframework/serializer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index e2d860a2f..f30667f14 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -96,6 +96,11 @@ class Serializer(object): """ The maximum depth to serialize to, or `None`. """ + + parent = None + """ + A reference to the root serializer when descending down into fields. + """ def __init__(self, depth=None, stack=[], **kwargs): if depth is not None: From 537fa19bacd97743555b3cac2a3e3c6e14786124 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Thu, 1 Mar 2012 13:17:29 +0000 Subject: [PATCH 004/196] Whoops. Adding the .parent attribute to the Serializer class broke getattr(self,'parent',self). This fixes it. --- djangorestframework/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index f30667f14..d000ad96d 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -138,7 +138,7 @@ class Serializer(object): if isinstance(info, (list, tuple)): class OnTheFlySerializer(self.__class__): fields = info - parent = getattr(self, 'parent', self) + parent = getattr(self, 'parent') or self return OnTheFlySerializer # If an element in `fields` is a 2-tuple of (str, Serializer) From e3d7c361051bde7b6d712ca975b4fe14f6449c15 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Tue, 20 Mar 2012 13:21:24 +0000 Subject: [PATCH 005/196] Don't return unknown field errors if allow_unknown_form_fields is True --- djangorestframework/resources.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index f170eb45a..5e350268b 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -169,8 +169,9 @@ class FormResource(Resource): ) # Add any unknown field errors - for key in unknown_fields: - field_errors[key] = [u'This field does not exist.'] + if not self.allow_unknown_form_fields: + for key in unknown_fields: + field_errors[key] = [u'This field does not exist.'] if field_errors: detail[u'field_errors'] = field_errors From a07212389d86081353c9702880dd0da23d9579d5 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 11 Apr 2012 23:13:04 +0200 Subject: [PATCH 006/196] Fixes #94. Thanks @natim. Only Markdown 2.0+ is supported currently. --- djangorestframework/compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 83d26f1ff..c9ae3b930 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -370,6 +370,8 @@ else: # Markdown is optional try: import markdown + if markdown.version_info < (2, 0): + raise ImportError('Markdown < 2.0 is not supported.') class CustomSetextHeaderProcessor(markdown.blockprocessors.BlockProcessor): """ From 64a49905a40f5718d721f9ea300a1d42c8886392 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 11 Apr 2012 23:51:00 +0200 Subject: [PATCH 007/196] Use seuptools to be explicit about optional version-dependency of markdown. --- CHANGELOG.rst | 5 +++++ setup.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ddc3ac17c..6471edbe4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Release Notes ============= +0.4.0-dev +--------- + +* Markdown < 2.0 is no longer supported. + 0.3.3 ----- diff --git a/setup.py b/setup.py index 5cd2a28fa..79bd73532 100755 --- a/setup.py +++ b/setup.py @@ -64,6 +64,9 @@ setup( package_data=get_package_data('djangorestframework'), test_suite='djangorestframework.runtests.runcoverage.main', install_requires=['URLObject>=0.6.0'], + extras_require={ + 'markdown': ["Markdown>=2.0"] + }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From d88ae359b8da330cff97518176aaba2de7b252a8 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 12 Apr 2012 01:06:35 +0300 Subject: [PATCH 008/196] Fix typo. --- docs/howto/alternativeframeworks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/alternativeframeworks.rst b/docs/howto/alternativeframeworks.rst index dc8d1ea65..0f668335d 100644 --- a/docs/howto/alternativeframeworks.rst +++ b/docs/howto/alternativeframeworks.rst @@ -7,7 +7,7 @@ Alternative frameworks There are a number of alternative REST frameworks for Django: * `django-piston `_ is very mature, and has a large community behind it. This project was originally based on piston code in parts. -* `django-tasypie `_ is also very good, and has a very active and helpful developer community and maintainers. +* `django-tastypie `_ is also very good, and has a very active and helpful developer community and maintainers. * Other interesting projects include `dagny `_ and `dj-webmachine `_ From 0c82e4b57549ed2d08bfb782cd99d4b71fd91eb7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 11 May 2012 09:46:00 +0200 Subject: [PATCH 009/196] 1.4 Ain't alpha. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 23a8075e8..d5fc0ce89 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ We also have a `Jenkins service Date: Tue, 22 May 2012 12:39:50 +0700 Subject: [PATCH 010/196] Allow RawQuerySet serialization --- djangorestframework/serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index 5dea37e81..9481eeff1 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -2,7 +2,7 @@ Customizable serialization. """ from django.db import models -from django.db.models.query import QuerySet +from django.db.models.query import QuerySet, RawQuerySet from django.utils.encoding import smart_unicode, is_protected_type, smart_str import inspect @@ -261,7 +261,7 @@ class Serializer(object): if isinstance(obj, (dict, models.Model)): # Model instances & dictionaries return self.serialize_model(obj) - elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)): + elif isinstance(obj, (tuple, list, set, QuerySet, RawQuerySet, types.GeneratorType)): # basic iterables return self.serialize_iter(obj) elif isinstance(obj, models.Manager): From 1b49c5e3e5c1b33d8d4c2dcaaaf6cb4dcbffa814 Mon Sep 17 00:00:00 2001 From: "Sean C. Farley" Date: Tue, 26 Jun 2012 19:27:57 -0400 Subject: [PATCH 011/196] Pass request to related serializers Related serializers may need access to the request to properly serialize a child resource. For example, reverse() in djangorestframework.reverse uses request if available to return an absolute URL. While the parent resource has access to the request to generate the absolute URL, the child resource does not. --- djangorestframework/serializer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index 9481eeff1..ffe9d8cbe 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -179,7 +179,8 @@ class Serializer(object): stack = self.stack[:] stack.append(obj) - return related_serializer(depth=depth, stack=stack).serialize(obj) + return related_serializer(depth=depth, stack=stack).serialize( + obj, request=self.request) def serialize_max_depth(self, obj): """ @@ -253,11 +254,15 @@ class Serializer(object): """ return smart_unicode(obj, strings_only=True) - def serialize(self, obj): + def serialize(self, obj, request=None): """ Convert any object into a serializable representation. """ + # Request from related serializer. + if request is not None: + self.request = request + if isinstance(obj, (dict, models.Model)): # Model instances & dictionaries return self.serialize_model(obj) From 11147ce13e754f47b9cdc8d0de8a071aa540882f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jun 2012 14:16:30 +0200 Subject: [PATCH 012/196] Don't bork if request attribute is not set. --- djangorestframework/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index ffe9d8cbe..027c9daf7 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -180,7 +180,7 @@ class Serializer(object): stack.append(obj) return related_serializer(depth=depth, stack=stack).serialize( - obj, request=self.request) + obj, request=getattr(self, 'request', None)) def serialize_max_depth(self, obj): """ From 73be041c474900423c5258b57b3d71d566aea6df Mon Sep 17 00:00:00 2001 From: Adam Ness Date: Mon, 2 Jul 2012 20:32:42 -0700 Subject: [PATCH 013/196] Patch to enable Accept headers in Internet Explorer when an Ajax Library on the client (i.e. jQuery) is sending an XMLHttpRequest --- djangorestframework/mixins.py | 3 ++- djangorestframework/tests/accept.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 6c8f8179d..0f292b4e7 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -274,7 +274,8 @@ class ResponseMixin(object): accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)] elif (self._IGNORE_IE_ACCEPT_HEADER and 'HTTP_USER_AGENT' in request.META and - MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])): + MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT']) and + request.META.get('HTTP_X_REQUESTED_WITH', '') != 'XMLHttpRequest'): # Ignore MSIE's broken accept behavior and do something sensible instead accept_list = ['text/html', '*/*'] elif 'HTTP_ACCEPT' in request.META: diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index 21aba589b..7f4eb320b 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -50,6 +50,16 @@ class UserAgentMungingTest(TestCase): resp = self.view(req) self.assertEqual(resp['Content-Type'], 'text/html') + def test_dont_munge_msie_with_x_requested_with_header(self): + """Send MSIE user agent strings, and an X-Requested-With header, and + ensure that we get a JSON response if we set a */* Accept header.""" + for user_agent in (MSIE_9_USER_AGENT, + MSIE_8_USER_AGENT, + MSIE_7_USER_AGENT): + req = self.req.get('/', HTTP_ACCEPT='*/*', HTTP_USER_AGENT=user_agent, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + resp = self.view(req) + self.assertEqual(resp['Content-Type'], 'application/json') + def test_dont_rewrite_msie_accept_header(self): """Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure that we get a JSON response if we set a */* accept header.""" From 1f9a8e10e5535cd301f5aca8de7fcea7e5310091 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Jul 2012 12:44:13 +0200 Subject: [PATCH 014/196] Added Adam Ness. Thanks! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 47a31d05d..ad94b6720 100644 --- a/AUTHORS +++ b/AUTHORS @@ -35,6 +35,7 @@ Sean C. Farley Daniel Izquierdo Can Yavuz Shawn Lewis +Adam Ness THANKS TO: From 0e3a2e6fdd800465b07817d780a981a18cb79880 Mon Sep 17 00:00:00 2001 From: Ralph Broenink Date: Fri, 6 Jul 2012 15:43:02 +0300 Subject: [PATCH 015/196] Modify mixins.py to make sure that the 415 (Unsupported Media Type) returns its error in the 'detail' key instead of in the 'error' key (for consistency with all other errors). --- djangorestframework/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 0f292b4e7..4a4539574 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -181,7 +181,7 @@ class RequestMixin(object): return parser.parse(stream) raise ErrorResponse(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - {'error': 'Unsupported media type in request \'%s\'.' % + {'detail': 'Unsupported media type in request \'%s\'.' % content_type}) @property From b0004c439860a1289f20c2175802d0bd4d2661c5 Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Thu, 12 Jul 2012 15:07:04 +0200 Subject: [PATCH 016/196] add_query_param should preserve previous querystring --- djangorestframework/templatetags/add_query_param.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py index 4cf0133be..143d7b3f1 100644 --- a/djangorestframework/templatetags/add_query_param.py +++ b/djangorestframework/templatetags/add_query_param.py @@ -4,7 +4,7 @@ register = Library() def add_query_param(url, param): - return unicode(URLObject(url).with_query(param)) + return unicode(URLObject(url).add_query_param(*param.split('='))) -register.filter('add_query_param', add_query_param) +register.filter('add_query_param', add_query_param) \ No newline at end of file From 36686cad13e7483699f224b16c9b7722c969f355 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 12 Jul 2012 22:40:24 +0700 Subject: [PATCH 017/196] add View.head() method at runtime (should fix AttributeError: object has no attribute 'get') --- djangorestframework/compat.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 83d26f1ff..11f24b867 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -68,12 +68,41 @@ except ImportError: # django.views.generic.View (Django >= 1.3) try: from django.views.generic import View + from django.utils.decorators import classonlymethod + from django.utils.functional import update_wrapper + if not hasattr(View, 'head'): # First implementation of Django class-based views did not include head method # in base View class - https://code.djangoproject.com/ticket/15668 class ViewPlusHead(View): - def head(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) + @classonlymethod + def as_view(cls, **initkwargs): + """ + Main entry point for a request-response process. + """ + # sanitize keyword arguments + for key in initkwargs: + if key in cls.http_method_names: + raise TypeError(u"You tried to pass in the %s method name as a " + u"keyword argument to %s(). Don't do that." + % (key, cls.__name__)) + if not hasattr(cls, key): + raise TypeError(u"%s() received an invalid keyword %r" % ( + cls.__name__, key)) + + def view(request, *args, **kwargs): + self = cls(**initkwargs) + if hasattr(self, 'get') and not hasattr(self, 'head'): + self.head = self.get + return self.dispatch(request, *args, **kwargs) + + # take name and docstring from class + update_wrapper(view, cls, updated=()) + + # and possible attributes set by decorators + # like csrf_exempt from dispatch + update_wrapper(view, cls.dispatch, assigned=()) + return view View = ViewPlusHead except ImportError: @@ -121,6 +150,8 @@ except ImportError: def view(request, *args, **kwargs): self = cls(**initkwargs) + if hasattr(self, 'get') and not hasattr(self, 'head'): + self.head = self.get return self.dispatch(request, *args, **kwargs) # take name and docstring from class From fe262ef3537fa67ecda374825a295ff854f027a3 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 12 Jul 2012 23:12:09 +0700 Subject: [PATCH 018/196] patch View.head() only for django < 1.4 --- djangorestframework/compat.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index 11f24b867..b5858a826 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -65,13 +65,14 @@ except ImportError: environ.update(request) return WSGIRequest(environ) -# django.views.generic.View (Django >= 1.3) +# django.views.generic.View (1.3 <= Django < 1.4) try: from django.views.generic import View - from django.utils.decorators import classonlymethod - from django.utils.functional import update_wrapper - if not hasattr(View, 'head'): + if django.VERSION < (1, 4): + from django.utils.decorators import classonlymethod + from django.utils.functional import update_wrapper + # First implementation of Django class-based views did not include head method # in base View class - https://code.djangoproject.com/ticket/15668 class ViewPlusHead(View): From 650c04662dc4b47f285601fefad3b71afc7f6461 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 12 Jul 2012 23:13:04 +0700 Subject: [PATCH 019/196] remove remaining head() method --- djangorestframework/compat.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index b5858a826..b86810223 100644 --- a/djangorestframework/compat.py +++ b/djangorestframework/compat.py @@ -186,9 +186,6 @@ except ImportError: #) return http.HttpResponseNotAllowed(allowed_methods) - def head(self, request, *args, **kwargs): - return self.get(request, *args, **kwargs) - # PUT, DELETE do not require CSRF until 1.4. They should. Make it better. if django.VERSION >= (1, 4): from django.middleware.csrf import CsrfViewMiddleware From 2deb31d0968c77f40c63e0ffb9f655e69fbe1d96 Mon Sep 17 00:00:00 2001 From: yetist Date: Fri, 27 Jul 2012 11:39:24 +0800 Subject: [PATCH 020/196] support utf8 description --- djangorestframework/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 3657fd645..4aa6ca0cd 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -156,6 +156,9 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): description = _remove_leading_indent(description) + if not isinstance(description, unicode): + description = description.decode('UTF-8') + if html: return self.markup_description(description) return description From cb7a8155608e6a3a801343ec965270eed5acb42f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jul 2012 21:50:58 +0200 Subject: [PATCH 021/196] Added @yetist --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ad94b6720..2b042344a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -36,6 +36,7 @@ Daniel Izquierdo Can Yavuz Shawn Lewis Adam Ness + THANKS TO: From abd3c7b46dcc76a042789f5c6d87ebdc9e8c980c Mon Sep 17 00:00:00 2001 From: Simon Pantzare Date: Fri, 10 Aug 2012 19:32:55 +0200 Subject: [PATCH 022/196] Allow template to be set on views --- djangorestframework/renderers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index d9aa4028e..3d01582ce 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -182,6 +182,10 @@ class TemplateRenderer(BaseRenderer): media_type = None template = None + def __init__(self, view): + super(TemplateRenderer, self).__init__(view) + self.template = getattr(self.view, "template", self.template) + def render(self, obj=None, media_type=None): """ Renders *obj* using the :attr:`template` specified on the class. @@ -202,6 +206,10 @@ class DocumentingTemplateRenderer(BaseRenderer): template = None + def __init__(self, view): + super(DocumentingTemplateRenderer, self).__init__(view) + self.template = getattr(self.view, "template", self.template) + def _get_content(self, view, request, obj, media_type): """ Get the content as if it had been rendered by a non-documenting renderer. From 2f9775c12d172199c2a915062bfba3a13f5cadc4 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Mon, 13 Aug 2012 15:58:23 +0100 Subject: [PATCH 023/196] Don't ever return the normal serializer again. --- djangorestframework/serializer.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index d000ad96d..f2f89f6cf 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -135,10 +135,12 @@ class Serializer(object): # If an element in `fields` is a 2-tuple of (str, tuple) # then the second element of the tuple is the fields to # set on the related serializer + + class OnTheFlySerializer(self.__class__): + fields = info + parent = getattr(self, 'parent') or self + if isinstance(info, (list, tuple)): - class OnTheFlySerializer(self.__class__): - fields = info - parent = getattr(self, 'parent') or self return OnTheFlySerializer # If an element in `fields` is a 2-tuple of (str, Serializer) @@ -156,8 +158,9 @@ class Serializer(object): elif isinstance(info, str) and info in _serializers: return _serializers[info] - # Otherwise use `related_serializer` or fall back to `Serializer` - return getattr(self, 'related_serializer') or Serializer + # Otherwise use `related_serializer` or fall back to + # `OnTheFlySerializer` preserve custom serialization methods. + return getattr(self, 'related_serializer') or OnTheFlySerializer def serialize_key(self, key): """ From e9f67d3afcfab0b0754a0b2a49de46f4e890e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Aug 2012 10:40:43 +0200 Subject: [PATCH 024/196] Update AUTHORS Added @max-arnold, @ralphje. Thanks folks! --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 2b042344a..1b9431b1d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -37,6 +37,8 @@ Can Yavuz Shawn Lewis Adam Ness +Max Arnold +Ralph Broenink THANKS TO: From acbc2d176825c024fcb5745a38ef506e915c00a1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Aug 2012 17:39:40 +0200 Subject: [PATCH 025/196] Added @pilt - Thanks! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 1b9431b1d..127e83aa7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Adam Ness Max Arnold Ralph Broenink +Simon Pantzare THANKS TO: From 239d8c6c4dfea135dd622528b616c4fc572ed469 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 25 Aug 2012 04:02:12 +0300 Subject: [PATCH 026/196] Point Live examples to heroku. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index a6745fca5..5297bd990 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ Introduction Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs. -**Browse example APIs created with Django REST framework:** `The Sandbox `_ +**Browse example APIs created with Django REST framework:** `The Sandbox `_ Features: --------- From 00d3aa21ba3bd5524932a692b75053a24ecbebd2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 19:53:10 +0100 Subject: [PATCH 027/196] Updated sandbox links --- djangorestframework/renderers.py | 2 +- docs/examples.rst | 2 +- docs/examples/blogpost.rst | 2 +- docs/examples/modelviews.rst | 10 +++++----- docs/examples/objectstore.rst | 2 +- docs/examples/permissions.rst | 4 ++-- docs/examples/pygments.rst | 8 ++++---- docs/examples/views.rst | 10 +++++----- docs/howto/mixin.rst | 4 ++-- docs/howto/usingurllib2.rst | 8 ++++---- docs/index.rst | 2 +- examples/sandbox/views.py | 4 ++-- 12 files changed, 29 insertions(+), 29 deletions(-) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 3d01582ce..772002ba7 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -370,7 +370,7 @@ class DocumentingTemplateRenderer(BaseRenderer): class DocumentingHTMLRenderer(DocumentingTemplateRenderer): """ Renderer which provides a browsable HTML interface for an API. - See the examples at http://api.django-rest-framework.org to see this in action. + See the examples at http://shielded-mountain-6732.herokuapp.com to see this in action. """ media_type = 'text/html' diff --git a/docs/examples.rst b/docs/examples.rst index 640883454..29e6a24ce 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -9,7 +9,7 @@ There are a few real world web API examples included with Django REST framework. All the examples are freely available for testing in the sandbox: - http://rest.ep.io + http://shielded-mountain-6732.herokuapp.com (The :doc:`examples/sandbox` resource is also documented.) diff --git a/docs/examples/blogpost.rst b/docs/examples/blogpost.rst index 11e376efe..1d191af3d 100644 --- a/docs/examples/blogpost.rst +++ b/docs/examples/blogpost.rst @@ -1,7 +1,7 @@ Blog Posts API ============== -* http://rest.ep.io/blog-post/ +* http://shielded-mountain-6732.herokuapp.com/blog-post/ The models ---------- diff --git a/docs/examples/modelviews.rst b/docs/examples/modelviews.rst index b67d50d9e..9bd27045c 100644 --- a/docs/examples/modelviews.rst +++ b/docs/examples/modelviews.rst @@ -5,11 +5,11 @@ Getting Started - Model Views A live sandbox instance of this API is available: - http://rest.ep.io/model-resource-example/ + http://shielded-mountain-6732.herokuapp.com/model-resource-example/ You can browse the API using a web browser, or from the command line:: - curl -X GET http://rest.ep.io/resource-example/ -H 'Accept: text/plain' + curl -X GET http://shielded-mountain-6732.herokuapp.com/resource-example/ -H 'Accept: text/plain' Often you'll want parts of your API to directly map to existing django models. Django REST framework handles this nicely for you in a couple of ways: @@ -41,16 +41,16 @@ And we're done. We've now got a fully browseable API, which supports multiple i We can visit the API in our browser: -* http://rest.ep.io/model-resource-example/ +* http://shielded-mountain-6732.herokuapp.com/model-resource-example/ Or access it from the command line using curl: .. code-block:: bash # Demonstrates API's input validation using form input - bash: curl -X POST --data 'foo=true' http://rest.ep.io/model-resource-example/ + bash: curl -X POST --data 'foo=true' http://shielded-mountain-6732.herokuapp.com/model-resource-example/ {"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}} # Demonstrates API's input validation using JSON input - bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://rest.ep.io/model-resource-example/ + bash: curl -X POST -H 'Content-Type: application/json' --data-binary '{"foo":true}' http://shielded-mountain-6732.herokuapp.com/model-resource-example/ {"detail": {"bar": ["This field is required."], "baz": ["This field is required."]}} diff --git a/docs/examples/objectstore.rst b/docs/examples/objectstore.rst index 0939fe9c7..81c1fcb6e 100644 --- a/docs/examples/objectstore.rst +++ b/docs/examples/objectstore.rst @@ -1,7 +1,7 @@ Object Store API ================ -* http://rest.ep.io/object-store/ +* http://shielded-mountain-6732.herokuapp.com/object-store/ This example shows an object store API that can be used to store arbitrary serializable content. diff --git a/docs/examples/permissions.rst b/docs/examples/permissions.rst index eafc32555..a806d7518 100644 --- a/docs/examples/permissions.rst +++ b/docs/examples/permissions.rst @@ -31,7 +31,7 @@ the example View below.: The `IsAuthenticated` permission will only let a user do a 'GET' if he is authenticated. Try it yourself on the live sandbox__ -__ http://rest.ep.io/permissions-example/loggedin +__ http://shielded-mountain-6732.herokuapp.com/permissions-example/loggedin Throttling @@ -53,7 +53,7 @@ may do on our view to 10 requests per minute.: Try it yourself on the live sandbox__. -__ http://rest.ep.io/permissions-example/throttling +__ http://shielded-mountain-6732.herokuapp.com/permissions-example/throttling Now if you want a view to require both aurhentication and throttling, you simply declare them both:: diff --git a/docs/examples/pygments.rst b/docs/examples/pygments.rst index 4e72f754d..4d1bb6caf 100644 --- a/docs/examples/pygments.rst +++ b/docs/examples/pygments.rst @@ -6,11 +6,11 @@ We're going to provide a simple wrapper around the awesome `pygments >> import urllib2 - >>> r = urllib2.urlopen('htpp://rest.ep.io/model-resource-example') + >>> r = urllib2.urlopen('htpp://shielded-mountain-6732.herokuapp.com/model-resource-example') >>> r.getcode() # Check if the response was ok 200 >>> print r.read() # Examin the response itself - [{"url": "http://rest.ep.io/model-resource-example/1/", "baz": "sdf", "foo": true, "bar": 123}] + [{"url": "http://shielded-mountain-6732.herokuapp.com/model-resource-example/1/", "baz": "sdf", "foo": true, "bar": 123}] Using the 'POST' method ----------------------- @@ -29,11 +29,11 @@ to send the current time as as a string value for our POST.:: Now use the `Request` class and specify the 'Content-type':: - >>> req = urllib2.Request('http://rest.ep.io/model-resource-example/', data=d, headers={'Content-Type':'application/x-www-form-urlencoded'}) + >>> req = urllib2.Request('http://shielded-mountain-6732.herokuapp.com/model-resource-example/', data=d, headers={'Content-Type':'application/x-www-form-urlencoded'}) >>> resp = urllib2.urlopen(req) >>> resp.getcode() 201 >>> resp.read() - '{"url": "http://rest.ep.io/model-resource-example/4/", "baz": "Fri Dec 30 18:22:52 2011", "foo": false, "bar": 123}' + '{"url": "http://shielded-mountain-6732.herokuapp.com/model-resource-example/4/", "baz": "Fri Dec 30 18:22:52 2011", "foo": false, "bar": 123}' That should get you started to write a client for your own api. diff --git a/docs/index.rst b/docs/index.rst index 5297bd990..486c934f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Django REST framework is a lightweight REST framework for Django, that aims to m Features: --------- -* Automatically provides an awesome Django admin style `browse-able self-documenting API `_. +* Automatically provides an awesome Django admin style `browse-able self-documenting API `_. * Clean, simple, views for Resources, using Django's new `class based views `_. * Support for ModelResources with out-of-the-box default implementations and input validation. * Pluggable :mod:`.parsers`, :mod:`renderers`, :mod:`authentication` and :mod:`permissions` - Easy to customise. diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py index 8e3b3a10a..86b3c28bd 100644 --- a/examples/sandbox/views.py +++ b/examples/sandbox/views.py @@ -11,8 +11,8 @@ class Sandbox(View): All the example APIs allow anonymous access, and can be navigated either through the browser or from the command line... - bash: curl -X GET http://api.django-rest-framework.org/ # (Use default renderer) - bash: curl -X GET http://api.django-rest-framework.org/ -H 'Accept: text/plain' # (Use plaintext documentation renderer) + bash: curl -X GET http://shielded-mountain-6732.herokuapp.com/ # (Use default renderer) + bash: curl -X GET http://shielded-mountain-6732.herokuapp.com/ -H 'Accept: text/plain' # (Use plaintext documentation renderer) The examples provided: From 46ecf8d86fe05e536a131a77a7379c5538c0e577 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 20:07:53 +0100 Subject: [PATCH 028/196] Travis CI --- .gitignore | 2 +- .travis.yml | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.gitignore b/.gitignore index c7bf0a8fe..40c68bd58 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ *~ .* -assetplatform.egg-info/* coverage.xml env docs/build @@ -18,3 +17,4 @@ djangorestframework.egg-info/* MANIFEST !.gitignore +!.travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..7cc2a31ff --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: python + +python: + - "2.5" + - "2.6" + - "2.7" +env: + - DJANGO=https://github.com/django/django/zipball/master TESTS='python setup.py test' + - DJANGO=https://github.com/django/django/zipball/master TESTS='python examples/runtests.py' + - DJANGO=django==1.4.1 --use-mirrors TESTS='python setup.py test' + - DJANGO=django==1.4.1 --use-mirrors TESTS='python examples/runtests.py' + - DJANGO=django==1.3.3 --use-mirrors TESTS='python setup.py test' + - DJANGO=django==1.3.3 --use-mirrors TESTS='python examples/runtests.py' +# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors +install: + - pip install $DJANGO + - pip install -e . --use-mirrors + - pip install -r requirements.txt + - pip install -r examples/requirements.txt + +# command to run tests, e.g. python setup.py test +script: + - $TESTS From 9d5f37d9b6900d5fb5989d3343c59c868db731d2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 20:13:43 +0100 Subject: [PATCH 029/196] Drop 2.5 testing from travis - not compatible with Django 1.5 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7cc2a31ff..8638eac07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - "2.5" - "2.6" - "2.7" env: From 9449dcdd0682b76c009acbcd47abc933e6819713 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 20:37:20 +0100 Subject: [PATCH 030/196] Quote urls in templates --- djangorestframework/templates/djangorestframework/base.html | 4 ++-- djangorestframework/templates/djangorestframework/login.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/djangorestframework/templates/djangorestframework/base.html b/djangorestframework/templates/djangorestframework/base.html index 00ecf8c3c..f84efb388 100644 --- a/djangorestframework/templates/djangorestframework/base.html +++ b/djangorestframework/templates/djangorestframework/base.html @@ -23,10 +23,10 @@ {% block userlinks %} {% if user.is_active %} Welcome, {{ user }}. - Log out + Log out {% else %} Anonymous - Log in + Log in {% endif %} {% endblock %} diff --git a/djangorestframework/templates/djangorestframework/login.html b/djangorestframework/templates/djangorestframework/login.html index 248744dff..6c328d580 100644 --- a/djangorestframework/templates/djangorestframework/login.html +++ b/djangorestframework/templates/djangorestframework/login.html @@ -17,7 +17,7 @@
-
+ {% csrf_token %}
{{ form.username }} From 1a2186cd6e5b2af3c5f726353a12c2bfe0c2fd6a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 22:00:21 +0100 Subject: [PATCH 031/196] Make template url tags 1.5 compatible --- djangorestframework/templates/djangorestframework/base.html | 1 + djangorestframework/templates/djangorestframework/login.html | 1 + 2 files changed, 2 insertions(+) diff --git a/djangorestframework/templates/djangorestframework/base.html b/djangorestframework/templates/djangorestframework/base.html index f84efb388..2bc988de5 100644 --- a/djangorestframework/templates/djangorestframework/base.html +++ b/djangorestframework/templates/djangorestframework/base.html @@ -1,3 +1,4 @@ +{% load url from future %} diff --git a/djangorestframework/templates/djangorestframework/login.html b/djangorestframework/templates/djangorestframework/login.html index 6c328d580..ce14db5bc 100644 --- a/djangorestframework/templates/djangorestframework/login.html +++ b/djangorestframework/templates/djangorestframework/login.html @@ -1,4 +1,5 @@ {% load static %} +{% load url from future %} From 03c6ccb45f6a9586cf4c6ffe271608b39870d640 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 22:09:47 +0100 Subject: [PATCH 032/196] Adding django 1.5 support mean dropping 1.2 support (no easy way to support url tag) and python 2.5 --- README.rst | 4 ++-- docs/index.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index d5fc0ce89..901f34a87 100644 --- a/README.rst +++ b/README.rst @@ -25,8 +25,8 @@ We also have a `Jenkins service = 2.0.0 * `Markdown`_ >= 2.1.0 (Optional) From 70d9c747e786e191c8a7f659e52a7cfe65dd9045 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 22:24:24 +0100 Subject: [PATCH 033/196] Drop examples tests from travis to keep number of configurations sane --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8638eac07..3ce1ee7bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,20 +3,24 @@ language: python python: - "2.6" - "2.7" + env: - DJANGO=https://github.com/django/django/zipball/master TESTS='python setup.py test' - - DJANGO=https://github.com/django/django/zipball/master TESTS='python examples/runtests.py' - DJANGO=django==1.4.1 --use-mirrors TESTS='python setup.py test' - - DJANGO=django==1.4.1 --use-mirrors TESTS='python examples/runtests.py' - DJANGO=django==1.3.3 --use-mirrors TESTS='python setup.py test' - - DJANGO=django==1.3.3 --use-mirrors TESTS='python examples/runtests.py' + # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: - pip install $DJANGO - pip install -e . --use-mirrors - pip install -r requirements.txt - - pip install -r examples/requirements.txt # command to run tests, e.g. python setup.py test script: - $TESTS + +# Examples tests, currently dropped to keep the number of configurations sane +# - DJANGO=https://github.com/django/django/zipball/master TESTS='python examples/runtests.py' +# - DJANGO=django==1.4.1 --use-mirrors TESTS='python examples/runtests.py' +# - DJANGO=django==1.3.3 --use-mirrors TESTS='python examples/runtests.py' +# - pip install -r examples/requirements.txt \ No newline at end of file From 7533c1027f6f8de257d9486ce98bb8ba1edfa1a7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 23:26:23 +0100 Subject: [PATCH 034/196] Django 1.5 breaks test client form encoding --- djangorestframework/tests/authentication.py | 14 ++++----- djangorestframework/tests/content.py | 6 ++-- djangorestframework/tests/serializer.py | 32 ++++++++++----------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py index 303bf96be..3dac7ee30 100644 --- a/djangorestframework/tests/authentication.py +++ b/djangorestframework/tests/authentication.py @@ -89,13 +89,13 @@ class SessionAuthTests(TestCase): response = self.non_csrf_client.post('/', {'example': 'example'}) self.assertEqual(response.status_code, 200) - def test_put_form_session_auth_passing(self): - """ - Ensure PUTting form over session authentication with logged in user and CSRF token passes. - """ - self.non_csrf_client.login(username=self.username, password=self.password) - response = self.non_csrf_client.put('/', {'example': 'example'}) - self.assertEqual(response.status_code, 200) + # def test_put_form_session_auth_passing(self): + # """ + # Ensure PUTting form over session authentication with logged in user and CSRF token passes. + # """ + # self.non_csrf_client.login(username=self.username, password=self.password) + # response = self.non_csrf_client.put('/', {'example': 'example'}) + # self.assertEqual(response.status_code, 200) def test_post_form_session_auth_failing(self): """ diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py index 6bae8feb3..e5d98e320 100644 --- a/djangorestframework/tests/content.py +++ b/djangorestframework/tests/content.py @@ -85,9 +85,9 @@ class TestContentParsing(TestCase): """Ensure view.DATA returns content for POST request with non-form content.""" self.ensure_determines_non_form_content_POST(RequestMixin()) - def test_standard_behaviour_determines_form_content_PUT(self): - """Ensure view.DATA returns content for PUT request with form content.""" - self.ensure_determines_form_content_PUT(RequestMixin()) + # def test_standard_behaviour_determines_form_content_PUT(self): + # """Ensure view.DATA returns content for PUT request with form content.""" + # self.ensure_determines_form_content_PUT(RequestMixin()) def test_standard_behaviour_determines_non_form_content_PUT(self): """Ensure view.DATA returns content for PUT request with non-form content.""" diff --git a/djangorestframework/tests/serializer.py b/djangorestframework/tests/serializer.py index 834a60d09..1a4eaa105 100644 --- a/djangorestframework/tests/serializer.py +++ b/djangorestframework/tests/serializer.py @@ -104,26 +104,26 @@ class TestFieldNesting(TestCase): self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}}) self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}}) - def test_serializer_no_fields(self): - """ - Test related serializer works when the fields attr isn't present. Fix for - #178. - """ - class NestedM2(Serializer): - fields = ('field1', ) + # def test_serializer_no_fields(self): + # """ + # Test related serializer works when the fields attr isn't present. Fix for + # #178. + # """ + # class NestedM2(Serializer): + # fields = ('field1', ) - class NestedM3(Serializer): - fields = ('field2', ) + # class NestedM3(Serializer): + # fields = ('field2', ) - class SerializerM2(Serializer): - include = [('field', NestedM2)] - exclude = ('id', ) + # class SerializerM2(Serializer): + # include = [('field', NestedM2)] + # exclude = ('id', ) - class SerializerM3(Serializer): - fields = [('field', NestedM3)] + # class SerializerM3(Serializer): + # fields = [('field', NestedM3)] - self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}}) - self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}}) + # self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}}) + # self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}}) def test_serializer_classname_nesting(self): """ From ec868418073fe8cc778e5b81246a149fc9bd9a35 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 23:33:17 +0100 Subject: [PATCH 035/196] Add build status --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 901f34a87..13fe23e8d 100644 --- a/README.rst +++ b/README.rst @@ -5,6 +5,8 @@ Django REST framework **Author:** Tom Christie. `Follow me on Twitter `_. +https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master + Overview ======== From 96dd1b50e6744ecdb11475ae7f78cbe95e23e517 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 26 Aug 2012 00:40:36 +0200 Subject: [PATCH 036/196] Add travis image --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 13fe23e8d..3ed087750 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,10 @@ Django REST framework **Author:** Tom Christie. `Follow me on Twitter `_. -https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master +:build status: |build-image| + +.. |build-image| image:: https://secure.travis-ci.org/markotibold/django-rest-framework.png?branch=master + :target: https://secure.travis-ci.org/markotibold/django-rest-framework Overview ======== From 8bc04317b071048326df8391910d5186c3195fb8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 26 Aug 2012 00:43:35 +0200 Subject: [PATCH 037/196] Point build status at correct location --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3ed087750..aa1bda4b7 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,8 @@ Django REST framework :build status: |build-image| -.. |build-image| image:: https://secure.travis-ci.org/markotibold/django-rest-framework.png?branch=master - :target: https://secure.travis-ci.org/markotibold/django-rest-framework +.. |build-image| image:: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master + :target: https://secure.travis-ci.org/tomchristie/django-rest-framework Overview ======== From b93994c44456e16bde34bdd40122e9f0d489465f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 29 Aug 2012 19:27:01 +0100 Subject: [PATCH 038/196] Version 0.4.0 --- djangorestframework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py index 46dd608fc..ee050f0db 100644 --- a/djangorestframework/__init__.py +++ b/djangorestframework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.4.0-dev' +__version__ = '0.4.0' VERSION = __version__ # synonym From 7fa082489bb384422d129f2d7906ed2e550d3784 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 29 Aug 2012 21:39:12 +0100 Subject: [PATCH 039/196] Release notes for 0.4.0 --- CHANGELOG.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6471edbe4..dab69c6fe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,10 +1,18 @@ Release Notes ============= -0.4.0-dev ---------- +0.4.0 +----- -* Markdown < 2.0 is no longer supported. +* Supports Django 1.5. +* Fixes issues with 'HEAD' method. +* Allow views to specify template used by TemplateRenderer +* More consistent error responses +* Some serializer fixes +* Fix internet explorer ajax behaviour +* Minor xml and yaml fixes +* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX) +* Sensible absolute URL generation, not using hacky set_script_prefix 0.3.3 ----- From a0504391e22e15d4e1dc77b0a98a0241db82f78b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 1 Sep 2012 22:56:11 +0100 Subject: [PATCH 040/196] Add tutorial link --- docs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 640b3567d..ec12a7b00 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,7 +32,7 @@ Resources * The ``djangorestframework`` package is `available on PyPI `_. * We have an active `discussion group `_. * Bug reports are handled on the `issue tracker `_. -* There is a `Jenkins CI server `_ which tracks test status and coverage reporting. (Thanks Marko!) +* There's a comprehensive tutorial to using REST framework and Backbone JS on `10kblogger.wordpress.com `_. Any and all questions, thoughts, bug reports and contributions are *hugely appreciated*. @@ -104,7 +104,6 @@ The following example exposes your `MyModel` model through an api. It will provi .. include:: library.rst - .. include:: examples.rst .. toctree:: From 0f5dfb62b1f8648d222970fd2c39958fe3b16f16 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 1 Sep 2012 23:04:14 +0100 Subject: [PATCH 041/196] Fix broken link --- examples/sandbox/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sandbox/views.py b/examples/sandbox/views.py index 86b3c28bd..11f689455 100644 --- a/examples/sandbox/views.py +++ b/examples/sandbox/views.py @@ -17,7 +17,7 @@ class Sandbox(View): The examples provided: 1. A basic example using the [Resource](http://django-rest-framework.org/library/resource.html) class. - 2. A basic example using the [ModelResource](http://django-rest-framework.org/library/modelresource.html) class. + 2. A basic example using the [ModelResource](http://django-rest-framework.org/library/resource.html#resources.ModelResource) class. 3. An basic example using Django 1.3's [class based views](http://docs.djangoproject.com/en/dev/topics/class-based-views/) and djangorestframework's [RendererMixin](http://django-rest-framework.org/library/renderers.html). 4. A generic object store API. 5. A code highlighting API. From 51f43170587a1d679ab99f9c0e96fee566469d0c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 2 Sep 2012 09:30:20 +0200 Subject: [PATCH 042/196] Drop link to Jenkins --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index aa1bda4b7..84d1edadb 100644 --- a/README.rst +++ b/README.rst @@ -26,8 +26,6 @@ Full documentation for the project is available at http://django-rest-framework. Issue tracking is on `GitHub `_. General questions should be taken to the `discussion group `_. -We also have a `Jenkins service `_ which runs our test suite. - Requirements: * Python 2.6+ From 0fc5a49a114ca65aab90dcf039da1569dde3c499 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Sep 2012 10:04:45 +0100 Subject: [PATCH 043/196] Add trailing slash to auth url. Refs: #248 --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index ec12a7b00..d906120eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,7 +68,7 @@ To add Django REST framework to a Django project: urlpatterns = patterns('', ... - url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework')) + url(r'^api-auth/', include('djangorestframework.urls', namespace='djangorestframework')) ) For more information on settings take a look at the :ref:`setup` section. From 943bc073d986b1d5bc10c6d17d3601d4311c3681 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Wed, 19 Sep 2012 08:05:16 -0700 Subject: [PATCH 044/196] Fixing Issue #265. https://github.com/tomchristie/django-rest-framework/issues/265 --- djangorestframework/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index 3f05903b5..5d77c461e 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -182,7 +182,7 @@ class Serializer(object): else: depth = self.depth - 1 - if any([obj is elem for elem in self.stack]): + if obj in self.stack: return self.serialize_recursion(obj) else: stack = self.stack[:] From f741bab709d2ec9e457c81f72743750d9a03963a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Sep 2012 17:05:09 +0100 Subject: [PATCH 045/196] Added @phobologic. Thanks\! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 127e83aa7..e0840961a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -40,6 +40,7 @@ Adam Ness Max Arnold Ralph Broenink Simon Pantzare +Michael Barrett THANKS TO: From f3834aa241ba6b6e922e324092dc2d6e7e343e72 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Wed, 19 Sep 2012 13:43:36 -0700 Subject: [PATCH 046/196] Stop serialization from going back to base object Without this patch the base object will be recursed back into with each related object at least once. --- djangorestframework/serializer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py index 5d77c461e..d2349b534 100644 --- a/djangorestframework/serializer.py +++ b/djangorestframework/serializer.py @@ -210,6 +210,9 @@ class Serializer(object): Given a model instance or dict, serialize it to a dict.. """ data = {} + # Append the instance itself to the stack so that you never iterate + # back into the first object. + self.stack.append(instance) fields = self.get_fields(instance) From 689d2afd97006cff102eddabe20e7f04c2c958c0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 13:46:20 +0100 Subject: [PATCH 047/196] Breadcrumbs play nicely when app not installed at root URL. Fixes #211 --- djangorestframework/utils/breadcrumbs.py | 16 ++-- rest_framework.egg-info/PKG-INFO | 19 +++++ rest_framework.egg-info/SOURCES.txt | 81 ++++++++++++++++++++ rest_framework.egg-info/dependency_links.txt | 1 + rest_framework.egg-info/top_level.txt | 7 ++ 5 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 rest_framework.egg-info/PKG-INFO create mode 100644 rest_framework.egg-info/SOURCES.txt create mode 100644 rest_framework.egg-info/dependency_links.txt create mode 100644 rest_framework.egg-info/top_level.txt diff --git a/djangorestframework/utils/breadcrumbs.py b/djangorestframework/utils/breadcrumbs.py index 85e13a5a4..becdcad0a 100644 --- a/djangorestframework/utils/breadcrumbs.py +++ b/djangorestframework/utils/breadcrumbs.py @@ -1,11 +1,12 @@ -from django.core.urlresolvers import resolve +from django.core.urlresolvers import resolve, get_script_prefix + def get_breadcrumbs(url): """Given a url returns a list of breadcrumbs, which are each a tuple of (name, url).""" from djangorestframework.views import View - def breadcrumbs_recursive(url, breadcrumbs_list): + def breadcrumbs_recursive(url, breadcrumbs_list, prefix): """Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url.""" try: @@ -15,7 +16,7 @@ def get_breadcrumbs(url): else: # Check if this is a REST framework view, and if so add it to the breadcrumbs if isinstance(getattr(view, 'cls_instance', None), View): - breadcrumbs_list.insert(0, (view.cls_instance.get_name(), url)) + breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url)) if url == '': # All done @@ -23,10 +24,11 @@ def get_breadcrumbs(url): elif url.endswith('/'): # Drop trailing slash off the end and continue to try to resolve more breadcrumbs - return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list) + return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list, prefix) # Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs - return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list) - - return breadcrumbs_recursive(url, []) + return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list, prefix) + prefix = get_script_prefix().rstrip('/') + url = url[len(prefix):] + return breadcrumbs_recursive(url, [], prefix) diff --git a/rest_framework.egg-info/PKG-INFO b/rest_framework.egg-info/PKG-INFO new file mode 100644 index 000000000..148d4757e --- /dev/null +++ b/rest_framework.egg-info/PKG-INFO @@ -0,0 +1,19 @@ +Metadata-Version: 1.0 +Name: rest-framework +Version: 2.0.0 +Summary: A lightweight REST framework for Django. +Home-page: http://django-rest-framework.org +Author: Tom Christie +Author-email: tom@tomchristie.com +License: BSD +Download-URL: http://pypi.python.org/pypi/rest_framework/ +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP diff --git a/rest_framework.egg-info/SOURCES.txt b/rest_framework.egg-info/SOURCES.txt new file mode 100644 index 000000000..f3ab66b0f --- /dev/null +++ b/rest_framework.egg-info/SOURCES.txt @@ -0,0 +1,81 @@ +MANIFEST.in +setup.py +rest_framework/__init__.py +rest_framework/authentication.py +rest_framework/compat.py +rest_framework/decorators.py +rest_framework/exceptions.py +rest_framework/fields.py +rest_framework/generics.py +rest_framework/mixins.py +rest_framework/models.py +rest_framework/negotiation.py +rest_framework/parsers.py +rest_framework/permissions.py +rest_framework/renderers.py +rest_framework/request.py +rest_framework/resources.py +rest_framework/response.py +rest_framework/reverse.py +rest_framework/serializers.py +rest_framework/settings.py +rest_framework/status.py +rest_framework/throttling.py +rest_framework/urlpatterns.py +rest_framework/urls.py +rest_framework/views.py +rest_framework.egg-info/PKG-INFO +rest_framework.egg-info/SOURCES.txt +rest_framework.egg-info/dependency_links.txt +rest_framework.egg-info/top_level.txt +rest_framework/authtoken/__init__.py +rest_framework/authtoken/models.py +rest_framework/authtoken/views.py +rest_framework/authtoken/migrations/0001_initial.py +rest_framework/authtoken/migrations/__init__.py +rest_framework/runtests/__init__.py +rest_framework/runtests/runcoverage.py +rest_framework/runtests/runtests.py +rest_framework/runtests/settings.py +rest_framework/runtests/urls.py +rest_framework/static/rest_framework/css/bootstrap-tweaks.css +rest_framework/static/rest_framework/css/bootstrap.min.css +rest_framework/static/rest_framework/css/default.css +rest_framework/static/rest_framework/css/prettify.css +rest_framework/static/rest_framework/img/glyphicons-halflings-white.png +rest_framework/static/rest_framework/img/glyphicons-halflings.png +rest_framework/static/rest_framework/js/bootstrap.min.js +rest_framework/static/rest_framework/js/default.js +rest_framework/static/rest_framework/js/jquery-1.8.1-min.js +rest_framework/static/rest_framework/js/prettify-min.js +rest_framework/templates/rest_framework/api.html +rest_framework/templates/rest_framework/base.html +rest_framework/templates/rest_framework/login.html +rest_framework/templatetags/__init__.py +rest_framework/templatetags/rest_framework.py +rest_framework/tests/__init__.py +rest_framework/tests/authentication.py +rest_framework/tests/breadcrumbs.py +rest_framework/tests/decorators.py +rest_framework/tests/description.py +rest_framework/tests/files.py +rest_framework/tests/methods.py +rest_framework/tests/mixins.py +rest_framework/tests/models.py +rest_framework/tests/modelviews.py +rest_framework/tests/oauthentication.py +rest_framework/tests/parsers.py +rest_framework/tests/renderers.py +rest_framework/tests/request.py +rest_framework/tests/response.py +rest_framework/tests/reverse.py +rest_framework/tests/serializer.py +rest_framework/tests/status.py +rest_framework/tests/testcases.py +rest_framework/tests/throttling.py +rest_framework/tests/validators.py +rest_framework/tests/views.py +rest_framework/utils/__init__.py +rest_framework/utils/breadcrumbs.py +rest_framework/utils/encoders.py +rest_framework/utils/mediatypes.py \ No newline at end of file diff --git a/rest_framework.egg-info/dependency_links.txt b/rest_framework.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/rest_framework.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/rest_framework.egg-info/top_level.txt b/rest_framework.egg-info/top_level.txt new file mode 100644 index 000000000..7e18534d8 --- /dev/null +++ b/rest_framework.egg-info/top_level.txt @@ -0,0 +1,7 @@ +rest_framework/authtoken +rest_framework/utils +rest_framework/tests +rest_framework/runtests +rest_framework/templatetags +rest_framework +rest_framework/authtoken/migrations From 49d2ea7cc0fa960ca3a98c90e8829ffebdcc1570 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 13:47:06 +0100 Subject: [PATCH 048/196] Remove erronous checkins --- rest_framework.egg-info/PKG-INFO | 19 ----- rest_framework.egg-info/SOURCES.txt | 81 -------------------- rest_framework.egg-info/dependency_links.txt | 1 - rest_framework.egg-info/top_level.txt | 7 -- 4 files changed, 108 deletions(-) delete mode 100644 rest_framework.egg-info/PKG-INFO delete mode 100644 rest_framework.egg-info/SOURCES.txt delete mode 100644 rest_framework.egg-info/dependency_links.txt delete mode 100644 rest_framework.egg-info/top_level.txt diff --git a/rest_framework.egg-info/PKG-INFO b/rest_framework.egg-info/PKG-INFO deleted file mode 100644 index 148d4757e..000000000 --- a/rest_framework.egg-info/PKG-INFO +++ /dev/null @@ -1,19 +0,0 @@ -Metadata-Version: 1.0 -Name: rest-framework -Version: 2.0.0 -Summary: A lightweight REST framework for Django. -Home-page: http://django-rest-framework.org -Author: Tom Christie -Author-email: tom@tomchristie.com -License: BSD -Download-URL: http://pypi.python.org/pypi/rest_framework/ -Description: UNKNOWN -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Environment :: Web Environment -Classifier: Framework :: Django -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Topic :: Internet :: WWW/HTTP diff --git a/rest_framework.egg-info/SOURCES.txt b/rest_framework.egg-info/SOURCES.txt deleted file mode 100644 index f3ab66b0f..000000000 --- a/rest_framework.egg-info/SOURCES.txt +++ /dev/null @@ -1,81 +0,0 @@ -MANIFEST.in -setup.py -rest_framework/__init__.py -rest_framework/authentication.py -rest_framework/compat.py -rest_framework/decorators.py -rest_framework/exceptions.py -rest_framework/fields.py -rest_framework/generics.py -rest_framework/mixins.py -rest_framework/models.py -rest_framework/negotiation.py -rest_framework/parsers.py -rest_framework/permissions.py -rest_framework/renderers.py -rest_framework/request.py -rest_framework/resources.py -rest_framework/response.py -rest_framework/reverse.py -rest_framework/serializers.py -rest_framework/settings.py -rest_framework/status.py -rest_framework/throttling.py -rest_framework/urlpatterns.py -rest_framework/urls.py -rest_framework/views.py -rest_framework.egg-info/PKG-INFO -rest_framework.egg-info/SOURCES.txt -rest_framework.egg-info/dependency_links.txt -rest_framework.egg-info/top_level.txt -rest_framework/authtoken/__init__.py -rest_framework/authtoken/models.py -rest_framework/authtoken/views.py -rest_framework/authtoken/migrations/0001_initial.py -rest_framework/authtoken/migrations/__init__.py -rest_framework/runtests/__init__.py -rest_framework/runtests/runcoverage.py -rest_framework/runtests/runtests.py -rest_framework/runtests/settings.py -rest_framework/runtests/urls.py -rest_framework/static/rest_framework/css/bootstrap-tweaks.css -rest_framework/static/rest_framework/css/bootstrap.min.css -rest_framework/static/rest_framework/css/default.css -rest_framework/static/rest_framework/css/prettify.css -rest_framework/static/rest_framework/img/glyphicons-halflings-white.png -rest_framework/static/rest_framework/img/glyphicons-halflings.png -rest_framework/static/rest_framework/js/bootstrap.min.js -rest_framework/static/rest_framework/js/default.js -rest_framework/static/rest_framework/js/jquery-1.8.1-min.js -rest_framework/static/rest_framework/js/prettify-min.js -rest_framework/templates/rest_framework/api.html -rest_framework/templates/rest_framework/base.html -rest_framework/templates/rest_framework/login.html -rest_framework/templatetags/__init__.py -rest_framework/templatetags/rest_framework.py -rest_framework/tests/__init__.py -rest_framework/tests/authentication.py -rest_framework/tests/breadcrumbs.py -rest_framework/tests/decorators.py -rest_framework/tests/description.py -rest_framework/tests/files.py -rest_framework/tests/methods.py -rest_framework/tests/mixins.py -rest_framework/tests/models.py -rest_framework/tests/modelviews.py -rest_framework/tests/oauthentication.py -rest_framework/tests/parsers.py -rest_framework/tests/renderers.py -rest_framework/tests/request.py -rest_framework/tests/response.py -rest_framework/tests/reverse.py -rest_framework/tests/serializer.py -rest_framework/tests/status.py -rest_framework/tests/testcases.py -rest_framework/tests/throttling.py -rest_framework/tests/validators.py -rest_framework/tests/views.py -rest_framework/utils/__init__.py -rest_framework/utils/breadcrumbs.py -rest_framework/utils/encoders.py -rest_framework/utils/mediatypes.py \ No newline at end of file diff --git a/rest_framework.egg-info/dependency_links.txt b/rest_framework.egg-info/dependency_links.txt deleted file mode 100644 index 8b1378917..000000000 --- a/rest_framework.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/rest_framework.egg-info/top_level.txt b/rest_framework.egg-info/top_level.txt deleted file mode 100644 index 7e18534d8..000000000 --- a/rest_framework.egg-info/top_level.txt +++ /dev/null @@ -1,7 +0,0 @@ -rest_framework/authtoken -rest_framework/utils -rest_framework/tests -rest_framework/runtests -rest_framework/templatetags -rest_framework -rest_framework/authtoken/migrations From 9094f93d188859f5db9198a170bbb65d5b9e9286 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 11:21:50 +0100 Subject: [PATCH 049/196] Sanitise JSON error messages --- rest_framework/tests/views.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 3746d7c8c..43365e07a 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,3 +1,4 @@ +import copy from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status @@ -27,6 +28,17 @@ def basic_view(request): return {'method': 'PUT', 'data': request.DATA} +def sanitise_json_error(error_dict): + """ + Exact contents of JSON error messages depend on the installed version + of json. + """ + ret = copy.copy(error_dict) + chop = len('JSON parse error - No JSON object could be decoded') + ret['detail'] = ret['detail'][:chop] + return ret + + class ClassBasedViewIntegrationTests(TestCase): def setUp(self): self.view = BasicView.as_view() @@ -38,7 +50,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -53,7 +65,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) class FunctionBasedViewIntegrationTests(TestCase): @@ -67,7 +79,7 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -82,4 +94,4 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) From d15e31b28d8a98c5df969fa8ebaa01b05897f2f5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 13:05:01 +0100 Subject: [PATCH 050/196] Drop coverage in travis tests --- .travis.yml | 1 - setup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dfb084961..3c8bb4723 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ install: - pip install $DJANGO - pip install -e . --use-mirrors - pip install -r requirements.txt --use-mirrors - - pip install coverage==3.5.1 --use-mirrors script: - python setup.py test diff --git a/setup.py b/setup.py index 19cf6d4c7..8ac2f03f5 100755 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ setup( author_email='tom@tomchristie.com', packages=get_packages('rest_framework'), package_data=get_package_data('rest_framework'), - test_suite='rest_framework.runtests.runcoverage.main', + test_suite='rest_framework.runtests.runtests.main', install_requires=[], classifiers=[ 'Development Status :: 4 - Beta', From 3c9fb0429973282e59e0ae8d4d0fb38ac56f495f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 13:09:53 +0100 Subject: [PATCH 051/196] Don't bother to setup.py test, just test directly --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3c8bb4723..2439bd787 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,6 @@ env: install: - pip install $DJANGO - - pip install -e . --use-mirrors - - pip install -r requirements.txt --use-mirrors script: - - python setup.py test + - python rest_framework/runtests/runtests.py From db0a9acfd7aca21f4ec65c84e74db5034660e2ff Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 13:12:43 +0100 Subject: [PATCH 052/196] Add PYTHONPATH in travis config --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2439bd787..0e177a95a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ env: - DJANGO=django==1.3.3 --use-mirrors install: - - pip install $DJANGO + - pip install $DJANGO + - export PYTHONPATH=. script: - python rest_framework/runtests/runtests.py From 7367bd53a908474f14f0f44f3744f2fcb1f4111c Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 12 Oct 2012 10:02:21 +0200 Subject: [PATCH 053/196] Fix tiny typo --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 854113785..851db3c7e 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -27,7 +27,7 @@ Notice that we're using hyperlinked relations in this case, with `HyperlinkedMod ## Views -Right, we'd better right some views then. Open `quickstart/views.py` and get typing. +Right, we'd better write some views then. Open `quickstart/views.py` and get typing. from django.contrib.auth.models import User, Group from rest_framework import generics From 7608cf1193fd555f31eb9a3d98c6f258720f8022 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 13 Oct 2012 15:07:43 +0100 Subject: [PATCH 054/196] Improve documentation for Requests --- docs/api-guide/requests.md | 111 +++++++++++++----- docs/index.md | 4 +- docs/template.html | 2 +- ...rowserhacks.md => browser-enhancements.md} | 4 +- rest_framework/request.py | 2 +- 5 files changed, 86 insertions(+), 37 deletions(-) rename docs/topics/{browserhacks.md => browser-enhancements.md} (96%) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 36513cd9f..919e410a4 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -6,63 +6,112 @@ > > — Malcom Tredinnick, [Django developers group][cite] -REST framework's `Request` class extends the standard `HttpRequest`, adding support for parsing multiple content types, allowing browser-based `PUT`, `DELETE` and other methods, and adding flexible per-request authentication. +REST framework's `Request` class extends the standard `HttpRequest`, adding support for REST framework's flexible request parsing and request authentication. -## .method - -`request.method` returns the uppercased string representation of the request's HTTP method. - -Browser-based `PUT`, `DELETE` and other requests are supported, and can be made by using a hidden form field named `_method` in a regular `POST` form. - - - -## .content_type - -`request.content`, returns a string object representing the mimetype of the HTTP request's body, if one exists. +--- +# Request parsing +REST framework's Request objects provide flexible request parsing that allows you to treat requests with JSON data or other media types in the same way that you would normally deal with form data. ## .DATA -`request.DATA` returns the parsed content of the request body. This is similar to the standard `HttpRequest.POST` attribute except that: +`request.DATA` returns the parsed content of the request body. This is similar to the standard `request.POST` attribute except that: -1. It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests. -2. It supports parsing multiple content types, rather than just form data. For example you can handle incoming json data in the same way that you handle incoming form data. +* It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests. +* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data. + +For more details see the [parsers documentation]. ## .FILES `request.FILES` returns any uploaded files that may be present in the content of the request body. This is the same as the standard `HttpRequest` behavior, except that the same flexible request parsing that is used for `request.DATA`. -This allows you to support file uploads from multiple content-types. For example you can write a parser that supports `POST`ing the raw content of a file, instead of using form-encoded file uploads. +For more details see the [parsers documentation]. -## .user +## .QUERY_PARAMS -`request.user` returns a `django.contrib.auth.models.User` instance. +`request.QUERY_PARAMS` is a more correcly named synonym for `request.GET`. -## .auth - -`request.auth` returns any additional authentication context that may not be contained in `request.user`. The exact behavior of `request.auth` depends on what authentication has been set in `request.authentication`. For many types of authentication this will simply be `None`, but it may also be an object representing a permission scope, an expiry time, or any other information that might be contained in a token-based authentication scheme. +For clarity inside your code, we recommend using `request.QUERY_PARAMS` instead of the usual `request.GET`, as *any* HTTP method type may include query parameters. ## .parsers -`request.parsers` should be set to a list of `Parser` instances that can be used to parse the content of the request body. +The `APIView` class or `@api_view` decorator will ensure that this property is automatically to a list of `Parser` instances, based on the `parser_classes` set on the view or based on the `DEFAULT_PARSERS` setting. -`request.parsers` may no longer be altered once `request.DATA`, `request.FILES` or `request.POST` have been accessed. +You won't typically need to access this property. -If you're using the `rest_framework.views.View` class... **[TODO]** +--- + +**Note:** If a client sends malformed content, then accessing `request.DATA` or `request.FILES` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response. + +--- + +# Authentication + +REST framework provides flexbile, per-request authentication, that gives you the abilty to: + +* Use different authentication policies for different parts of your API. +* Support the use of multiple authentication policies. +* Provide both user and token information associated with the incoming request. + +## .user + +`request.user` typically returns an instance of `django.contrib.auth.models.User`, although the behavior depends on the authentication policy being used. + +If the request is unauthenticated the default value of `request.user` is an instance of `django.contrib.auth.models.AnonymousUser`. + +For more details see the [authentication documentation]. + +## .auth + +`request.auth` returns any additional authentication context. The exact behavior of `request.auth` depends on the authentication policy being used, but it may typically be an instance of the token that the request was authenticated against. + +If the request is unauthenticated, or if no additional context is present, the default value of `request.auth` is `None`. + +For more details see the [authentication documentation]. + +## .authenticators + +The `APIView` class or `@api_view` decorator will ensure that this property is automatically to a list of `Authentication` instances, based on the `authentication_classes` set on the view or based on the `DEFAULT_AUTHENTICATORS` setting. + +You won't typically need to access this property. + +--- + +# Browser enhancments + +REST framework supports a few browser enhancments such as broser-based `PUT` and `DELETE` forms. + +## .method + +`request.method` returns the **uppercased** string representation of the request's HTTP method. + +Browser-based `PUT` and `DELETE` forms are transparently supported. + +For more information see the [browser enhancements documentation]. + +## .content_type + +`request.content_type`, returns a string object representing the media type of the HTTP request's body, or an empty string if no media type was provided. + +You won't typically need to directly access the request's content type, as you'll normally rely on REST framework's default request parsing behavior. + +If you do need to access the content type of the request you should use the `.content_type` property in preference to using `request.META.get('HTTP_CONTENT_TYPE')`, as it provides transparent support for browser-based non-form content. + +For more information see the [browser enhancements documentation]. ## .stream `request.stream` returns a stream representing the content of the request body. -You will not typically need to access `request.stream`, unless you're writing a `Parser` class. +You won't typically need to directly access the request's content, as you'll normally rely on REST framework's default request parsing behavior. -## .authentication +If you do need to access the raw content directly, you should use the `.stream` property in preference to using `request.content`, as it provides transparent support for browser-based non-form content. -`request.authentication` should be set to a list of `Authentication` instances that can be used to authenticate the request. - -`request.authentication` may no longer be altered once `request.user` or `request.auth` have been accessed. - -If you're using the `rest_framework.views.View` class... **[TODO]** +For more information see the [browser enhancements documentation]. [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion +[parsers documentation]: parsers.md +[authentication documentation]: authentication.md +[browser enhancements documentation]: ../topics/browser-enhancements.md \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index b3845521d..322a7b27c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -99,7 +99,7 @@ The API guide is your complete reference manual to all the functionality provide General guides to using REST framework. * [CSRF][csrf] -* [Browser hacks][browserhacks] +* [Browser enhancements][browser-enhancements] * [Working with the Browsable API][browsableapi] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] * [Contributing to REST framework][contributing] @@ -185,7 +185,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [settings]: api-guide/settings.md [csrf]: topics/csrf.md -[browserhacks]: topics/browserhacks.md +[browser-enhancements]: topics/browser-enhancements.md [browsableapi]: topics/browsable-api.md [rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md [contributing]: topics/contributing.md diff --git a/docs/template.html b/docs/template.html index 016de151a..2cc52819c 100644 --- a/docs/template.html +++ b/docs/template.html @@ -73,7 +73,7 @@ Topics diff --git a/docs/topics/rest-framework-2-announcement.md b/docs/topics/rest-framework-2-announcement.md new file mode 100644 index 000000000..98e66228b --- /dev/null +++ b/docs/topics/rest-framework-2-announcement.md @@ -0,0 +1,88 @@ +# Django REST framework 2 + +What it is, and why you should care + +> Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result. +> +> — [Roy Fielding][cite] + +REST framework 2 is an almost complete reworking of the original framework, which comprehensivly addresses some of the original design issues. + +Because the latest version should be considered a re-release, rather than an incremental improvement, we've skipped a version, and called this release Django REST framework 2.0. + +This article is intended to give you a flavor of what REST framework 2 is, and why you might want to give it a try. + +## User feedback + +Before we get cracking, let's start with the hard sell, with a few bits of feedback from some early adopters… + +"Django REST framework 2 is beautiful. Some of the API design is worthy of @kennethreitz." - [Kit La Touche][quote1] + +"Since it's pretty much just Django, controlling things like URLs has been a breeze... I think [REST framework 2] has definitely got the right approach here; even simple things like being able to override a function called post to do custom work during rather than having to intimately know what happens during a post make a huge difference to your productivity." - [Ian Strachan][quote2] + +"I switched to the 2.0 branch and I don't regret it - fully refactored my code in another ½ day and it's *much* more to my tastes" - [Bruno Desthuilliers][quote3] + +Sounds good, right? Let's get into some details... + +## Serialization + +REST framework 2 includes a totally re-worked serialization engine, that was initially intended as a replacement for Django's existing inflexible fixture serialization, and which meets the following design goals: + +* A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API. +* Structural concerns are decoupled from encoding concerns. +* Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms. +* Validation that can be mapped to obvious and comprehensive error responses. +* Serializers that support both nested, flat, and partially-nested representations. +* Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations. + +Mapping between the internal state of the system and external representations of that state is the core concern of building Web APIs. Designing serializers that allow the developer to do so in a flexible and obvious way is a deceptively difficult design task, and with the new serialization API we think we've pretty much nailed it. + +## Generic views + +When REST framework was initially released at the start of 2011, the current Django release was version 1.2. REST framework included a backport of Django 1.3's upcoming `View` class, but it didn't take full advantage of the generic view implementations. + +With the new release the generic views in REST framework now tie in with Django's generic views. The end result is that framework is clean, lightweight and easy to use. + +## Requests, Responses & Views + +REST framework 2 includes `Request` and `Response` classes, than are used in place of Django's existing `HttpRequest` and `HttpResponse` classes. Doing so allows logic such as parsing the incoming request or rendering the outgoing response to be supported transparently by the framework. + +The `Request`/`Response` approach leads to a much cleaner API, less logic in the view itself, and a simple, obvious request-response cycle. + +REST framework 2 also allows you to work with both function-based and class-based views. For simple API views all you need is a single `@api_view` decorator, and you're good to go. + + +## API Design + +Pretty much every aspect of REST framework has been reworked, with the aim of ironing out some of the design flaws of the previous versions. Each of the components of REST framework are cleanly decoupled, and can be used independantly of each-other, and there are no monolithic resource classes, overcomplicated mixin combinations, or opinionated serialization or URL routing decisions. + +## Documentation + +As you can see the documentation for REST framework has been radically improved. It gets a completely new style, using markdown for the documentation source, and a bootstrap-based theme for the styling. + +In the author's opinion, using `markdown` for documentation is a much better option that `rst`. It is intuitive and readable, and there is great tooling available, such as the [Mou][mou] editor for Mac OS X, which makes it easy and plesant to work. + +We're really pleased with how the docs style looks - it's simple and clean, and the docs build much more quickly than with the previous sphinx setup. We'll miss being able to use the wonderful [Read the Docs][readthedocs] service, but we think it's a trade-off worth making. + +Developing REST framework's documentation builder into a fully-fledged reusable project is something that we have planned for a future date. + +## The Browseable API + +Django REST framework's most unique feature is the way it is able to serve up both machine-readable representations, and a fully browsable HTML representation to the same endpoints. + +Browseable Web APIs are easier to work with, visualize and debug, and generally makes it easier and more frictionless to inspect and work with. + +With REST framework 2, the browseable API gets a snazzy new bootstrap-based theme that looks great and is even nicer to work with. + +There are also some functionality improvments - actions such as as `POST` and `DELETE` will only display if the user has the appropriate permissions. + +## Summary + +In short, we've engineered the hell outta this thing, and we're incredibly proud of the result. + +[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-724 +[quote1]: https://twitter.com/kobutsu/status/261689665952833536 +[quote2]: https://groups.google.com/d/msg/django-rest-framework/heRGHzG6BWQ/ooVURgpwVC0J +[quote3]: https://groups.google.com/d/msg/django-rest-framework/flsXbvYqRoY/9lSyntOf5cUJ +[mou]: http://mouapp.com/ +[readthedocs]: https://readthedocs.org/ From b9e576f16ef7cc98f671e9c18ff8ae1a95bfe3ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:44:23 +0100 Subject: [PATCH 128/196] Push tests into a seperate app namespace 'rest_framework.test' Prevents tests from running by default when rest_framework is installed as 3rd party app. Fixes #316, #185 --- rest_framework/runtests/runtests.py | 2 +- rest_framework/runtests/settings.py | 9 +-------- rest_framework/tests/__init__.py | 13 ------------- rest_framework/tests/models.py | 2 +- rest_framework/tests/tests.py | 13 +++++++++++++ 5 files changed, 16 insertions(+), 23 deletions(-) create mode 100644 rest_framework/tests/tests.py diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index b2438c9b2..1bd0a5fc8 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -32,7 +32,7 @@ def main(): else: print usage() sys.exit(1) - failures = test_runner.run_tests(['rest_framework' + test_case]) + failures = test_runner.run_tests(['tests' + test_case]) sys.exit(failures) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 67de82c81..951b1e72b 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -91,6 +91,7 @@ INSTALLED_APPS = ( # 'django.contrib.admindocs', 'rest_framework', 'rest_framework.authtoken', + 'rest_framework.tests' ) STATIC_URL = '/static/' @@ -100,14 +101,6 @@ import django if django.VERSION < (1, 3): INSTALLED_APPS += ('staticfiles',) -# OAuth support is optional, so we only test oauth if it's installed. -try: - import oauth_provider -except ImportError: - pass -else: - INSTALLED_APPS += ('oauth_provider',) - # If we're running on the Jenkins server we want to archive the coverage reports as XML. import os if os.environ.get('HUDSON_URL', None): diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py index adeaf6da3..e69de29bb 100644 --- a/rest_framework/tests/__init__.py +++ b/rest_framework/tests/__init__.py @@ -1,13 +0,0 @@ -""" -Force import of all modules in this package in order to get the standard test -runner to pick up the tests. Yowzers. -""" -import os - -modules = [filename.rsplit('.', 1)[0] - for filename in os.listdir(os.path.dirname(__file__)) - if filename.endswith('.py') and not filename.startswith('_')] -__test__ = dict() - -for module in modules: - exec("from rest_framework.tests.%s import *" % module) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 0ee18c697..d4ea729b4 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -40,7 +40,7 @@ class RESTFrameworkModel(models.Model): Base for test models that sets app_label, so they play nicely. """ class Meta: - app_label = 'rest_framework' + app_label = 'tests' abstract = True diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py new file mode 100644 index 000000000..adeaf6da3 --- /dev/null +++ b/rest_framework/tests/tests.py @@ -0,0 +1,13 @@ +""" +Force import of all modules in this package in order to get the standard test +runner to pick up the tests. Yowzers. +""" +import os + +modules = [filename.rsplit('.', 1)[0] + for filename in os.listdir(os.path.dirname(__file__)) + if filename.endswith('.py') and not filename.startswith('_')] +__test__ = dict() + +for module in modules: + exec("from rest_framework.tests.%s import *" % module) From 51a64019260d99e3c615b407353e344cf615da1e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:47:17 +0100 Subject: [PATCH 129/196] Added @madisvain. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 27a563262..a317afdeb 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -49,6 +49,7 @@ The following people have helped make REST framework great. * Tomi Pajunen - [eofs] * Rob Dobson - [rdobson] * Daniel Vaca Araujo - [diviei] +* Madis Väin - [madisvain] Many thanks to everyone who's contributed to the project. @@ -129,3 +130,4 @@ To contact the author directly: [eofs]: https://github.com/eofs [rdobson]: https://github.com/rdobson [diviei]: https://github.com/diviei +[madisvain]: https://github.com/madisvain From d995742afc09ff8d387751a6fe47b9686845740b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:04:33 +0100 Subject: [PATCH 130/196] Add AllowAny permission --- docs/api-guide/permissions.md | 12 ++++++++++++ docs/api-guide/settings.md | 6 +++++- rest_framework/settings.py | 9 ++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 0b7b32e93..d43b7bedc 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -33,6 +33,12 @@ The default permission policy may be set globally, using the `DEFAULT_PERMISSION ) } +If not specified, this setting defaults to allowing unrestricted access: + + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ) + You can also set the authentication policy on a per-view basis, using the `APIView` class based views. class ExampleView(APIView): @@ -58,6 +64,12 @@ Or, if you're using the `@api_view` decorator with function based views. # API Reference +## AllowAny + +The `AllowAny` permission class will allow unrestricted access, **regardless of if the request was authenticated or unauthenticated**. + +This permission is not strictly required, since you can achieve the same result by using an empty list or tuple for the permissions setting, but you may find it useful to specify this class because it makes the intention explicit. + ## IsAuthenticated The `IsAuthenticated` permission class will deny permission to any unauthenticated user, and allow permission otherwise. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 21efc853e..3556a5b14 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -72,7 +72,11 @@ Default: A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. -Default: `()` +Default: + + ( + 'rest_framework.permissions.AllowAny', + ) ## DEFAULT_THROTTLE_CLASSES diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 3c5082943..9c40a2144 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -37,11 +37,14 @@ DEFAULTS = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication' ), - 'DEFAULT_PERMISSION_CLASSES': (), - 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ), + 'DEFAULT_THROTTLE_CLASSES': ( + ), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', - 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', 'DEFAULT_PAGINATION_SERIALIZER_CLASS': From af96fe05d0138c34128fc3944fc2701cbad5bd01 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:17:49 +0100 Subject: [PATCH 131/196] Add AllowAny class --- rest_framework/permissions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 51e961963..655b78a34 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -18,6 +18,17 @@ class BasePermission(object): raise NotImplementedError(".has_permission() must be overridden.") +class AllowAny(BasePermission): + """ + Allow any access. + This isn't strictly required, since you could use an empty + permission_classes list, but it's useful because it makes the intention + more explicit. + """ + def has_permission(self, request, view, obj=None): + return True + + class IsAuthenticated(BasePermission): """ Allows access only to authenticated users. From 12c363c1fe237d0357e6020b44890926856b9191 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 18:12:56 +0000 Subject: [PATCH 132/196] TemplateHTMLRenderer, StaticHTMLRenderer --- docs/api-guide/renderers.md | 40 +++++++++++++++++++++------ rest_framework/renderers.py | 41 ++++++++++++++++++++++++---- rest_framework/tests/htmlrenderer.py | 6 ++-- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b5e2fe8f8..5efb3610b 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -106,12 +106,12 @@ If you are considering using `XML` for your API, you may want to consider implem **.format**: `'.xml'` -## HTMLRenderer +## TemplateHTMLRenderer Renders data to HTML, using Django's standard template rendering. Unlike other renderers, the data passed to the `Response` does not need to be serialized. Also, unlike other renderers, you may want to include a `template_name` argument when creating the `Response`. -The HTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context. +The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context. The template name is determined by (in order of preference): @@ -119,27 +119,49 @@ The template name is determined by (in order of preference): 2. An explicit `.template_name` attribute set on this class. 3. The return result of calling `view.get_template_names()`. -An example of a view that uses `HTMLRenderer`: +An example of a view that uses `TemplateHTMLRenderer`: class UserInstance(generics.RetrieveUserAPIView): """ A view that returns a templated HTML representations of a given user. """ model = Users - renderer_classes = (HTMLRenderer,) + renderer_classes = (TemplateHTMLRenderer,) def get(self, request, *args, **kwargs) self.object = self.get_object() return Response({'user': self.object}, template_name='user_detail.html') -You can use `HTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint. +You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint. -If you're building websites that use `HTMLRenderer` along with other renderer classes, you should consider listing `HTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers. +If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers. **.media_type**: `text/html` **.format**: `'.html'` +See also: `StaticHTMLRenderer` + +## StaticHTMLRenderer + +A simple renderer that simply returns pre-rendered HTML. Unlike other renderers, the data passed to the response object should be a string representing the content to be returned. + +An example of a view that uses `TemplateHTMLRenderer`: + + @api_view(('GET',)) + @renderer_classes((StaticHTMLRenderer,)) + def simple_html_view(request): + data = '

Hello, world

' + return Response(data) + +You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint. + +**.media_type**: `text/html` + +**.format**: `'.html'` + +See also: `TemplateHTMLRenderer` + ## BrowsableAPIRenderer Renders data into HTML for the Browseable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page. @@ -207,7 +229,7 @@ In some cases you might want your view to use different serialization styles dep For example: @api_view(('GET',)) - @renderer_classes((HTMLRenderer, JSONRenderer)) + @renderer_classes((TemplateHTMLRenderer, JSONRenderer)) def list_users(request): """ A view that can return JSON or HTML representations @@ -215,9 +237,9 @@ For example: """ queryset = Users.objects.filter(active=True) - if request.accepted_media_type == 'text/html': + if request.accepted_renderer.format == 'html': # TemplateHTMLRenderer takes a context dict, - # and additionally requiresa 'template_name'. + # and additionally requires a 'template_name'. # It does not require serialization. data = {'users': queryset} return Response(data, template_name='list_users.html') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1a8b1d978..cfe4df6dd 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -139,13 +139,24 @@ class YAMLRenderer(BaseRenderer): return yaml.dump(data, stream=None, Dumper=self.encoder) -class HTMLRenderer(BaseRenderer): +class TemplateHTMLRenderer(BaseRenderer): """ - A Base class provided for convenience. + An HTML renderer for use with templates. - Render the object simply by using the given template. - To create a template renderer, subclass this class, and set - the :attr:`media_type` and :attr:`template` attributes. + The data supplied to the Response object should be a dictionary that will + be used as context for the template. + + The template name is determined by (in order of preference): + + 1. An explicit `.template_name` attribute set on the response. + 2. An explicit `.template_name` attribute set on this class. + 3. The return result of calling `view.get_template_names()`. + + For example: + data = {'users': User.objects.all()} + return Response(data, template_name='users.html') + + For pre-rendered HTML, see StaticHTMLRenderer. """ media_type = 'text/html' @@ -188,6 +199,26 @@ class HTMLRenderer(BaseRenderer): raise ConfigurationError('Returned a template response with no template_name') +class StaticHTMLRenderer(BaseRenderer): + """ + An HTML renderer class that simply returns pre-rendered HTML. + + The data supplied to the Response object should be a string representing + the pre-rendered HTML content. + + For example: + data = 'example' + return Response(data) + + For template rendered HTML, see TemplateHTMLRenderer. + """ + media_type = 'text/html' + format = 'html' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data + + class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index da2f83c3d..10d7e31dc 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -3,12 +3,12 @@ from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import HTMLRenderer +from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response @api_view(('GET',)) -@renderer_classes((HTMLRenderer,)) +@renderer_classes((TemplateHTMLRenderer,)) def example(request): """ A view that can returns an HTML representation. @@ -22,7 +22,7 @@ urlpatterns = patterns('', ) -class HTMLRendererTests(TestCase): +class TemplateHTMLRendererTests(TestCase): urls = 'rest_framework.tests.htmlrenderer' def setUp(self): From bc99142c7dc1ebf84ca0858ce32b400a537e1908 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 19:35:50 +0100 Subject: [PATCH 133/196] Added wo tests. One for PUTing on a non-existing id-based url. And another for PUTing on a non-existing slug-based url. Fix doctoring for 'test_put_cannot_set_id'. --- rest_framework/tests/generics.py | 44 ++++++++++++++++++++++++++++++-- rest_framework/tests/models.py | 5 ++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f4263478e..151532a72 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status -from rest_framework.tests.models import BasicModel, Comment +from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel factory = RequestFactory() @@ -22,6 +22,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class SlugBasedInstanceView(InstanceView): + """ + A model with a slug-field. + """ + model = SlugBasedModel + + class TestRootView(TestCase): def setUp(self): """ @@ -129,6 +136,7 @@ class TestInstanceView(TestCase): for obj in self.objects.all() ] self.view = InstanceView.as_view() + self.slug_based_view = SlugBasedInstanceView.as_view() def test_get_instance_view(self): """ @@ -198,7 +206,7 @@ class TestInstanceView(TestCase): def test_put_cannot_set_id(self): """ - POST requests to create a new object should not be able to set the id. + PUT requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), @@ -224,6 +232,38 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + def test_put_as_create_on_id_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if it doesn't exist, if creation is not possible, + e.g. the pk for an id-field is determined by the database, + a HTTP_403_FORBIDDEN error-response must be returned. + """ + content = {'text': 'foobar'} + # pk fields can not be created on demand, only the database can set th pk for a new object + request = factory.put('/5', json.dumps(content), + content_type='application/json') + response = self.view(request, pk=5).render() + expected = { + 'detail': u'A resource could not be created at the requested URI' + } + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEquals(response.data, expected) + + def test_put_as_create_on_slug_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. + """ + content = {'text': 'foobar'} + request = factory.put('/test_slug', json.dumps(content), + content_type='application/json') + response = self.slug_based_view(request, pk='test_slug').render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) + updated = self.objects.get(slug='test_slug') + self.assertEquals(updated.text, 'foobar') + # Regression test for #285 diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index d4ea729b4..ac73a4bbb 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) +class SlugBasedModel(RESTFrameworkModel): + text = models.CharField(max_length=100) + slug = models.SlugField(max_length=32) + + class DefaultValueModel(RESTFrameworkModel): text = models.CharField(default='foobar', max_length=100) From fde79376f323708d9f7b80ee830fe63060fb335f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 19:25:51 +0000 Subject: [PATCH 134/196] Pastebin tutorial --- docs/tutorial/1-serialization.md | 176 ++++++++++------- docs/tutorial/2-requests-and-responses.md | 45 +++-- docs/tutorial/3-class-based-views.md | 88 ++++----- .../4-authentication-and-permissions.md | 183 ++++++++++++++++++ ...thentication-permissions-and-throttling.md | 5 - .../5-relationships-and-hyperlinked-apis.md | 158 ++++++++++++++- 6 files changed, 510 insertions(+), 145 deletions(-) create mode 100644 docs/tutorial/4-authentication-and-permissions.md delete mode 100644 docs/tutorial/4-authentication-permissions-and-throttling.md diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index d1ae0ba54..fc052202a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -2,7 +2,9 @@ ## 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. +This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together. + +The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started. If you just want a quick overview, you should head over to the [quickstart] documentation instead. ## Setting up a new environment @@ -17,6 +19,7 @@ Now that we're inside a virtualenv environment, we can install our package requi pip install django pip install djangorestframework + pip install pygments # We'll be using this for the code highlighting **Note:** To exit the virtualenv environment at any time, just type `deactivate`. For more information see the [virtualenv documentation][virtualenv]. @@ -30,8 +33,9 @@ To get started, let's create a new project to work with. cd tutorial Once that's done we can create an app that we'll use to create a simple Web API. +We're going to create a project that - python manage.py startapp blog + python manage.py startapp snippets 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"`. @@ -46,32 +50,48 @@ The simplest way to get up and running will probably be to use an `sqlite3` data } } -We'll also need to add our new `blog` app and the `rest_framework` app to `INSTALLED_APPS`. +We'll also need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`. INSTALLED_APPS = ( ... 'rest_framework', - 'blog' + 'snippets' ) -We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our blog views. +We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet views. urlpatterns = patterns('', - url(r'^', include('blog.urls')), + url(r'^', include('snippets.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. +For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. from django.db import models - - class Comment(models.Model): - email = models.EmailField() - content = models.CharField(max_length=200) + from pygments.lexers import get_all_lexers + from pygments.styles import get_all_styles + + LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in get_all_lexers()]) + STYLE_CHOICES = sorted((item, item) for item in list(get_all_styles())) + + + class Snippet(models.Model): created = models.DateTimeField(auto_now_add=True) + title = models.CharField(max_length=100, default='') + code = models.TextField() + linenos = models.BooleanField(default=False) + language = models.CharField(choices=LANGUAGE_CHOICES, + default='python', + max_length=100) + style = models.CharField(choices=STYLE_CHOICES, + default='friendly', + max_length=100) + + class Meta: + ordering = ('created',) Don't forget to sync the database for the first time. @@ -79,28 +99,40 @@ Don't forget to sync the database for the first time. ## 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 `blog` directory named `serializers.py` and add the following. +The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similarly to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. - from blog import models + from django.forms import widgets from rest_framework import serializers + from snippets import models - class CommentSerializer(serializers.Serializer): - id = serializers.IntegerField(readonly=True) - email = serializers.EmailField() - content = serializers.CharField(max_length=200) - created = serializers.DateTimeField(readonly=True) - + class SnippetSerializer(serializers.Serializer): + pk = serializers.Field() # Note: `Field` is an untyped read-only field. + title = serializers.CharField(required=False, + max_length=100) + code = serializers.CharField(widget=widgets.Textarea, + max_length=100000) + linenos = serializers.BooleanField(required=False) + language = serializers.ChoiceField(choices=models.LANGUAGE_CHOICES, + default='python') + style = serializers.ChoiceField(choices=models.STYLE_CHOICES, + default='friendly') + def restore_object(self, attrs, instance=None): """ - Create or update a new comment instance. + Create or update a new snippet instance. """ if instance: - instance.email = attrs['email'] - instance.content = attrs['content'] - instance.created = attrs['created'] + # Update existing instance + instance.title = attrs['title'] + instance.code = attrs['code'] + instance.linenos = attrs['linenos'] + instance.language = attrs['language'] + instance.style = attrs['style'] return instance - return models.Comment(**attrs) + + # Create new instance + return models.Snippet(**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. @@ -112,31 +144,27 @@ Before we go any further we'll familiarise ourselves with using our new Serializ 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. +Okay, once we've got a few imports out of the way, let's create a code snippet to work with. - from blog.models import Comment - from blog.serializers import CommentSerializer + from snippets.models import Snippet + from snippets.serializers import SnippetSerializer from rest_framework.renderers import JSONRenderer from rest_framework.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() + snippet = Snippet(code='print "hello, world"\n') + snippet.save() -We've now got a few comment instances to play with. Let's take a look at serializing one of those instances. +We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances. - serializer = CommentSerializer(instance=c1) + serializer = SnippetSerializer(instance=snippet) serializer.data - # {'id': 1, 'email': u'leila@example.com', 'content': u'nothing to say', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774, tzinfo=)} + # {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`. content = JSONRenderer().render(serializer.data) content - # '{"id": 1, "email": "leila@example.com", "content": "nothing to say", "created": "2012-08-22T16:20:09.822"}' + # '{"pk": 1, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into python native datatypes... @@ -147,28 +175,45 @@ Deserialization is similar. First we parse a stream into python native datatype ...then we restore those native datatypes into to a fully populated object instance. - serializer = CommentSerializer(data) + serializer = SnippetSerializer(data) serializer.is_valid() # True serializer.object - # + # 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 +## Using ModelSerializers + +Our `SnippetSerializer` class is replicating a lot of information that's also contained in the `Snippet` model. It would be nice if we could keep out code a bit more concise. + +In the same way that Django provides both `Form` classes and `ModelForm` classes, REST framework includes both `Serializer` classes, and `ModelSerializer` classes. + +Let's look at refactoring our serializer using the `ModelSerializer` class. +Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` class. + + class SnippetSerializer(serializers.ModelSerializer): + class Meta: + model = models.Snippet + fields = ('pk', 'title', 'code', 'linenos', 'language', 'style') + + + +## Writing regular Django views using our Serializer Let's see how we can write some API views using our new Serializer class. +For the moment we won't use any of REST framework's other features, we'll just write the views as regular Django views. + 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. +Edit the `snippet/views.py` file, and add the following. - from blog.models import Comment - from blog.serializers import CommentSerializer from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser - + from snippets.models import Snippet + from snippets.serializers import SnippetSerializer class JSONResponse(HttpResponse): """ @@ -181,67 +226,65 @@ Edit the `blog/views.py` file, and add the following. 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. +The root of our API is going to be a view that supports listing all the existing snippets, or creating a new snippet. @csrf_exempt - def comment_root(request): + def snippet_list(request): """ - List all comments, or create a new comment. + List all code snippets, or create a new snippet. """ if request.method == 'GET': - comments = Comment.objects.all() - serializer = CommentSerializer(instance=comments) + snippets = Snippet.objects.all() + serializer = SnippetSerializer(instance=snippets) return JSONResponse(serializer.data) elif request.method == 'POST': data = JSONParser().parse(request) - serializer = CommentSerializer(data) + serializer = SnippetSerializer(data) if serializer.is_valid(): - comment = serializer.object - comment.save() + serializer.save() return JSONResponse(serializer.data, status=201) else: return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. -We'll also need a view which corresponds to an individual comment, and can be used to retrieve, update or delete the comment. +We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet. @csrf_exempt - def comment_instance(request, pk): + def snippet_detail(request, pk): """ - Retrieve, update or delete a comment instance. + Retrieve, update or delete a code snippet. """ try: - comment = Comment.objects.get(pk=pk) - except Comment.DoesNotExist: + snippet = Snippet.objects.get(pk=pk) + except Snippet.DoesNotExist: return HttpResponse(status=404) if request.method == 'GET': - serializer = CommentSerializer(instance=comment) + serializer = SnippetSerializer(instance=snippet) return JSONResponse(serializer.data) elif request.method == 'PUT': data = JSONParser().parse(request) - serializer = CommentSerializer(data, instance=comment) + serializer = SnippetSerializer(data, instance=snippet) if serializer.is_valid(): - comment = serializer.object - comment.save() + serializer.save() return JSONResponse(serializer.data) else: return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': - comment.delete() + snippet.delete() return HttpResponse(status=204) -Finally we need to wire these views up. Create the `blog/urls.py` file: +Finally we need to wire these views up. Create the `snippet/urls.py` file: from django.conf.urls import patterns, url - urlpatterns = patterns('blog.views', - url(r'^$', 'comment_root'), - url(r'^(?P[0-9]+)$', 'comment_instance') + urlpatterns = patterns('snippets.views', + url(r'^snippet/$', 'snippet_list'), + url(r'^snippet/(?P[0-9]+)/$', 'snippet_detail') ) 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. @@ -260,5 +303,6 @@ Our API views don't do anything particularly special at the moment, beyond serve We'll see how we can start to improve things in [part 2 of the tutorial][tut-2]. +[quickstart]: quickstart.md [virtualenv]: http://www.virtualenv.org/en/latest/index.html [tut-2]: 2-requests-and-responses.md diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index fc37322ae..76803d24d 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -38,27 +38,27 @@ Okay, let's go ahead and start using these new components to write a few views. We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. - from blog.models import Comment - from blog.serializers import CommentSerializer from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response + from snippet.models import Snippet + from snippet.serializers import SnippetSerializer + @api_view(['GET', 'POST']) - def comment_root(request): + def snippet_list(request): """ - List all comments, or create a new comment. + List all snippets, or create a new snippet. """ if request.method == 'GET': - comments = Comment.objects.all() - serializer = CommentSerializer(instance=comments) + snippets = Snippet.objects.all() + serializer = SnippetSerializer(instance=snippets) return Response(serializer.data) elif request.method == 'POST': - serializer = CommentSerializer(request.DATA) + serializer = SnippetSerializer(request.DATA) if serializer.is_valid(): - comment = serializer.object - comment.save() + serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -67,30 +67,29 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On 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']) - def comment_instance(request, pk): + def snippet_detail(request, pk): """ - Retrieve, update or delete a comment instance. + Retrieve, update or delete a snippet instance. """ try: - comment = Comment.objects.get(pk=pk) - except Comment.DoesNotExist: + snippet = Snippet.objects.get(pk=pk) + except Snippet.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': - serializer = CommentSerializer(instance=comment) + serializer = SnippetSerializer(instance=snippet) return Response(serializer.data) elif request.method == 'PUT': - serializer = CommentSerializer(request.DATA, instance=comment) + serializer = SnippetSerializer(request.DATA, instance=snippet) if serializer.is_valid(): - comment = serializer.object - comment.save() + serializer.save() return Response(serializer.data) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': - comment.delete() + snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) This should all feel very familiar - there's not a lot different to working with regular Django views. @@ -103,20 +102,20 @@ To take advantage of the fact that our responses are no longer hardwired to a si Start by adding a `format` keyword argument to both of the views, like so. - def comment_root(request, format=None): + def snippet_list(request, format=None): and - def comment_instance(request, pk, format=None): + def snippet_detail(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 django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns - urlpatterns = patterns('blog.views', - url(r'^$', 'comment_root'), - url(r'^(?P[0-9]+)$', 'comment_instance') + urlpatterns = patterns('snippet.views', + url(r'^$', 'snippet_list'), + url(r'^(?P[0-9]+)$', 'snippet_detail') ) urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 0ee81ea38..3d58fe8e7 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -6,61 +6,59 @@ We can also write our API views using class based views, rather than function ba 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 CommentSerializer + from snippet.models import Snippet + from snippet.serializers import SnippetSerializer from django.http import Http404 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status - class CommentRoot(APIView): + class SnippetList(APIView): """ - List all comments, or create a new comment. + List all snippets, or create a new snippet. """ def get(self, request, format=None): - comments = Comment.objects.all() - serializer = CommentSerializer(instance=comments) + snippets = Snippet.objects.all() + serializer = SnippetSerializer(instance=snippets) return Response(serializer.data) def post(self, request, format=None): - serializer = CommentSerializer(request.DATA) + serializer = SnippetSerializer(request.DATA) if serializer.is_valid(): - comment = serializer.object - comment.save() + serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view. - class CommentInstance(APIView): + class SnippetDetail(APIView): """ - Retrieve, update or delete a comment instance. + Retrieve, update or delete a snippet instance. """ def get_object(self, pk): try: - return Comment.objects.get(pk=pk) - except Comment.DoesNotExist: + return Snippet.objects.get(pk=pk) + except Snippet.DoesNotExist: raise Http404 def get(self, request, pk, format=None): - comment = self.get_object(pk) - serializer = CommentSerializer(instance=comment) + snippet = self.get_object(pk) + serializer = SnippetSerializer(instance=snippet) return Response(serializer.data) def put(self, request, pk, format=None): - comment = self.get_object(pk) - serializer = CommentSerializer(request.DATA, instance=comment) + snippet = self.get_object(pk) + serializer = SnippetSerializer(request.DATA, instance=snippet) if serializer.is_valid(): - comment = serializer.object - comment.save() + serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, pk, format=None): - comment = self.get_object(pk) - comment.delete() + snippet = self.get_object(pk) + snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) That's looking good. Again, it's still pretty similar to the function based view right now. @@ -69,11 +67,11 @@ We'll also need to refactor our URLconf slightly now we're using class based vie from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns - from blogpost import views + from snippetpost import views urlpatterns = patterns('', - url(r'^$', views.CommentRoot.as_view()), - url(r'^(?P[0-9]+)$', views.CommentInstance.as_view()) + url(r'^$', views.SnippetList.as_view()), + url(r'^(?P[0-9]+)$', views.SnippetDetail.as_view()) ) urlpatterns = format_suffix_patterns(urlpatterns) @@ -88,16 +86,16 @@ The create/retrieve/update/delete operations that we've been using so far are go Let's take a look at how we can compose our views by using the mixin classes. - from blog.models import Comment - from blog.serializers import CommentSerializer + from snippet.models import Snippet + from snippet.serializers import SnippetSerializer from rest_framework import mixins from rest_framework import generics - class CommentRoot(mixins.ListModelMixin, + class SnippetList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.MultipleObjectBaseView): - model = Comment - serializer_class = CommentSerializer + model = Snippet + serializer_class = SnippetSerializer def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) @@ -109,12 +107,12 @@ We'll take a moment to examine exactly what's happening here - We're building ou The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far. - class CommentInstance(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.SingleObjectBaseView): - model = Comment - serializer_class = CommentSerializer + class SnippetDetail(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.SingleObjectBaseView): + model = Snippet + serializer_class = SnippetSerializer def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) @@ -131,23 +129,23 @@ Pretty similar. This time we're using the `SingleObjectBaseView` class to provi Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use. - from blog.models import Comment - from blog.serializers import CommentSerializer + from snippet.models import Snippet + from snippet.serializers import SnippetSerializer from rest_framework import generics - class CommentRoot(generics.ListCreateAPIView): - model = Comment - serializer_class = CommentSerializer + class SnippetList(generics.ListCreateAPIView): + model = Snippet + serializer_class = SnippetSerializer - class CommentInstance(generics.RetrieveUpdateDestroyAPIView): - model = Comment - serializer_class = CommentSerializer + class SnippetDetail(generics.RetrieveUpdateDestroyAPIView): + model = Snippet + serializer_class = SnippetSerializer Wow, that's pretty concise. We've got a huge amount for free, and our code looks like good, clean, idiomatic Django. -Next we'll move onto [part 4 of the tutorial][tut-4], 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. +Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API. [dry]: http://en.wikipedia.org/wiki/Don't_repeat_yourself -[tut-4]: 4-authentication-permissions-and-throttling.md +[tut-4]: 4-authentication-and-permissions.md diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md new file mode 100644 index 000000000..79436ad41 --- /dev/null +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -0,0 +1,183 @@ +# Tutorial 4: Authentication & Permissions + +Currently our API doesn't have any restrictions on who can + + +## Adding information to our model + +We're going to make a couple of changes to our `Snippet` model class. +First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code. + +Add the following two fields to the model. + + owner = models.ForeignKey('auth.User', related_name='snippets') + highlighted = models.TextField() + +We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code higlighting library. + +We'll ned some extra imports: + + from pygments.lexers import get_lexer_by_name + from pygments.formatters import HtmlFormatter + from pygments import highlight + +And now we can add a `.save()` method to our model class: + + def save(self, *args, **kwargs): + """ + Use the `pygments` library to create an highlighted HTML + representation of the code snippet. + """ + lexer = get_lexer_by_name(self.language) + linenos = self.linenos and 'table' or False + options = self.title and {'title': self.title} or {} + formatter = HtmlFormatter(style=self.style, linenos=linenos, + full=True, **options) + self.highlighted = highlight(self.code, lexer, formatter) + super(Snippet, self).save(*args, **kwargs) + +When that's all done we'll need to update our database tables. +Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again. + + rm tmp.db + python ./manage.py syncdb + +You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command. + + python ./manage.py createsuperuser + +## Adding endpoints for our User models + +Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy: + + class UserSerializer(serializers.ModelSerializer): + snippets = serializers.ManyPrimaryKeyRelatedField() + + class Meta: + model = User + fields = ('pk', 'username', 'snippets') + +Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we've needed to add an explicit field for it. + +We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. + + class UserList(generics.ListAPIView): + model = User + serializer_class = UserSerializer + + + class UserInstance(generics.RetrieveAPIView): + model = User + serializer_class = UserSerializer + +Finally we need to add those views into the API, by referencing them from the URL conf. + + url(r'^users/$', views.UserList.as_view()), + url(r'^users/(?P[0-9]+)/$', views.UserInstance.as_view()) + +## Associating Snippets with Users + +Right now, if we created a code snippet, there'd be no way of associating the user that created the snippet, with the snippet instance. The user isn't sent as part of the serialized representation, but is instead a property of the incoming request. + +The way we deal with that is by overriding a `.pre_save()` method on our snippet views, that allows us to handle any information that is implicit in the incoming request or requested URL. + +On **both** the `SnippetList` and `SnippetInstance` view classes, add the following method: + + def pre_save(self, obj): + obj.owner = self.request.user + +## Updating our serializer + +Now that snippets are associated with the user that created them, let's update our SnippetSerializer to reflect that. + +Add the following field to the serializer definition: + + owner = serializers.Field(source='owner.username') + +**Note**: Make sure you also add `'owner',` to the list of fields in the inner `Meta` class. + +This field is doing something quite interesting. The `source` argument controls which attribtue is used to populate a field, and can point at any attribute on the serialized instance. It can also take the dotted notation shown above, in which case it will traverse the given attributes, in a similar way as is used with Django's template language. + +The field we've added is the untyped `Field` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `Field` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized. + +## Adding required permissions to views + +Now that code snippets are associated with users we want to make sure that only authenticated users are able to create, update and delete code snippets. + +REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access. + +Add the following property to **both** the `SnippetList` and `SnippetInstance` view classes. + + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + +**TODO: Now that the permissions are restricted, demonstrate using HTTP basic authentication with curl requests** + +## Adding login to the Browseable API + +If you open a browser and navigate to the browseable API at the moment, you'll find you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user. + +We can add a login view for use with the browseable API, by editing our URLconf once more. + +Add the following import at the top of the file: + + from django.conf.urls import include + +And, at the end of the file, add a pattern to include the login and logout views for the browseable API. + + urlpatterns += patterns('', + url(r'^api-auth/', include('rest_framework.urls', + namespace='rest_framework')) + ) + +The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace. + +Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earier, you'll be able to create code snippets again. + +Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field. + +## Object level permissions + +Really we'd like all code snippets to be visible to anyone, but also make sure that only the user that created a code snippet is able update or delete it. + +To do that we're going to need to create a custom permission. + +In the snippets app, create a new file, `permissions.py` + + from rest_framework import permissions + + + class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to edit it. + """ + + def has_permission(self, request, view, obj=None): + # Skip the check unless this is an object-level test + if obj is None: + return True + + # Read permissions are allowed to any request + if request.method in permissions.SAFE_METHODS: + return True + + # Write permissions are only allowed to the owner of the snippet + return obj.owner == request.user + +Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetInstance` class: + + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrReadOnly,) + +Make sure to also import the `IsOwnerOrReadOnly` class. + + from snippets.permissions import IsOwnerOrReadOnly + +Now, if you open a browser again, you find that the 'DELETE' and 'PUT' actions only appear on a snippet instance endpoint if you're logged in as the same user that created the code snippet. + +## Summary + +We've now got a fairly fine-grained set of permissions on our Web API, and end points for users of the system and for the code snippets that they have created. + +In [part 5][tut-5] of the tutorial we'll look at how we can tie everything together by creating an HTML endpoint for our hightlighted snippets, and improve the cohesion of our API by using hyperlinking for the relationships within the system. + +[tut-5]: 5-relationships-and-hyperlinked-apis.md \ No newline at end of file diff --git a/docs/tutorial/4-authentication-permissions-and-throttling.md b/docs/tutorial/4-authentication-permissions-and-throttling.md deleted file mode 100644 index c8d7cbd34..000000000 --- a/docs/tutorial/4-authentication-permissions-and-throttling.md +++ /dev/null @@ -1,5 +0,0 @@ -# Tutorial 4: Authentication & Permissions - -Nothing to see here. Onwards to [part 5][tut-5]. - -[tut-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 index 8600d5edf..84d02a534 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -1,11 +1,157 @@ # Tutorial 5 - Relationships & Hyperlinked APIs -**TODO** +At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships. -* Create BlogPost model -* Demonstrate nested relationships -* Demonstrate and describe hyperlinked relationships +## Creating an endpoint for the root of our API - + from rest_framework import renderers + from rest_framework.decorators import api_view + from rest_framework.response import Response + from rest_framework.reverse import reverse + + + @api_view(('GET',)) + def api_root(request, format=None): + return Response({ + 'users': reverse('user-list', request=request), + 'snippets': reverse('snippet-list', request=request) + }) + +Notice that we're using REST framework's `reverse` function in order to return fully-qualified URLs. + +## Creating an endpoint for the highlighted snippets + +The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints. + +Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two style of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint. + +The other thing we need to consider when creating the code highlight view is that there's no existing concreate generic view that we can use. We're not returning an object instance, but instead a property of an object instance. + +Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. + + class SnippetHighlight(generics.SingleObjectAPIView): + model = Snippet + renderer_classes = (renderers.StaticHTMLRenderer,) + + def get(self, request, *args, **kwargs): + snippet = self.get_object() + return Response(snippet.highlighted) + +As usual we need to add the new views that we've created in to our URLconf. +We'll add a url pattern for our new API root: + + url(r'^$', 'api_root'), + +And then add a url pattern for the snippet highlights: + + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), + +## Hyperlinking our API + +Dealing with relationships between entities is one of the more challenging aspects of Web API design. There are a number of different ways that we might choose to represent a relationship: + +* Using primary keys. +* Using hyperlinking between entities. +* Using a unique identifying slug field on the related entity. +* Using the default string representation of the related entity. +* Nesting the related entity inside the parent representation. +* Some other custom representation. + +REST framework supports all of these styles, and can apply them across forward or reverse relationships, or apply them across custom managers such as generic foreign keys. + +In this case we'd like to use a hyperlinked style between entities. In order to do so, we'll modify our serializers to extend `HyperlinkedModelSerializer` instead of the existing `ModelSerializer`. + +The `HyperlinkedModelSerializer` has the following differences from `ModelSerializer`: + +* It does not include the `pk` field by default. +* It includes a `url` field, using `HyperlinkedIdentityField`. +* Relationships use `HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField`, + instead of `PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField`. + +We can easily re-write our existing serializers to use hyperlinking. + + class SnippetSerializer(serializers.HyperlinkedModelSerializer): + owner = serializers.Field(source='owner.username') + highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight') + + class Meta: + model = models.Snippet + fields = ('url', 'highlight', 'owner', + 'title', 'code', 'linenos', 'language', 'style') + + + class UserSerializer(serializers.HyperlinkedModelSerializer): + snippets = serializers.ManyHyperlinkedRelatedField(view_name='snippet-detail') + + class Meta: + model = User + fields = ('url', 'username', 'snippets') + +Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern. + +## Making sure our URL patterns are named + +If we're going to have a hyperlinked API, we need to make sure we name our URL patterns. Let's take a look at which URL patterns we need to name. + +* The root of our API refers to `'user-list'` and `'snippet-list'`. +* Our snippet serializer includes a field that refers to `'snippet-highlight'`. +* Our user serializer includes a field that refers to `'snippet-detail'`. +* Our snippet and user serializers include `'url'` fields that by default will refer to `'{model_name}-detail'`, which in this case will be `'snippet-detail'` and `'user-detail'`. + +After adding all those names into our URLconf, our final `'urls.py'` file should look something like this: + + # API endpoints + urlpatterns = format_suffix_patterns(patterns('snippets.views', + url(r'^$', 'api_root'), + url(r'^snippets/$', + views.SnippetList.as_view(), + name='snippet-list'), + url(r'^snippets/(?P[0-9]+)/$', + views.SnippetInstance.as_view(), + name='snippet-detail'), + url(r'^snippets/(?P[0-9]+)/highlight/$' + views.SnippetHighlight.as_view(), + name='snippet-highlight'), + url(r'^users/$', + views.UserList.as_view(), + name='user-list'), + url(r'^users/(?P[0-9]+)/$', + views.UserInstance.as_view(), + name='user-detail') + )) + + # Login and logout views for the browsable API + urlpatterns += patterns('', + url(r'^api-auth/', include('rest_framework.urls', + namespace='rest_framework')) + ) + +## Reviewing our work + +If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links. + +You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the hightlighted code HTML representations. + +We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats. + +We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. + +You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox]. + +## 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][github] by reviewing and subitting issues, and making pull requests. +* Join the [REST framework discussion group][group], and help build the community. +* Follow the author [on Twitter][twitter] and say hi. + +**Now go build some awesome things.** + +[repo]: https://github.com/tomchristie/rest-framework-tutorial +[sandbox]: http://sultry-coast-6726.herokuapp.com/ +[github]: https://github.com/tomchristie/django-rest-framework +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[twitter]: https://twitter.com/_tomchristie \ No newline at end of file From db635fa6327d8d3ac3b06886c5f459b5c5a5cd04 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 19:37:27 +0000 Subject: [PATCH 135/196] Minor fixes --- docs/index.md | 6 ++---- docs/template.html | 3 +-- docs/tutorial/1-serialization.md | 6 +++--- docs/tutorial/2-requests-and-responses.md | 8 ++++---- docs/tutorial/4-authentication-and-permissions.md | 2 ++ 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index f66ba7f42..bd0c007d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,9 +67,8 @@ The tutorial will walk you through the building blocks that make up REST framewo * [1 - Serialization][tut-1] * [2 - Requests & Responses][tut-2] * [3 - Class based views][tut-3] -* [4 - Authentication, permissions & throttling][tut-4] +* [4 - Authentication & permissions][tut-4] * [5 - Relationships & hyperlinked APIs][tut-5] - ## API Guide @@ -161,9 +160,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [tut-1]: tutorial/1-serialization.md [tut-2]: tutorial/2-requests-and-responses.md [tut-3]: tutorial/3-class-based-views.md -[tut-4]: tutorial/4-authentication-permissions-and-throttling.md +[tut-4]: tutorial/4-authentication-and-permissions.md [tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md -[tut-6]: tutorial/6-resource-orientated-projects.md [request]: api-guide/requests.md [response]: api-guide/responses.md diff --git a/docs/template.html b/docs/template.html index 56c5d8326..557b45b3b 100644 --- a/docs/template.html +++ b/docs/template.html @@ -57,9 +57,8 @@
  • 1 - Serialization
  • 2 - Requests and responses
  • 3 - Class based views
  • -
  • 4 - Authentication, permissions and throttling
  • +
  • 4 - Authentication and permissions
  • 5 - Relationships and hyperlinked APIs
  • -