From 2d2e2f95b0268c3282edfca5c047bdc831133016 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Feb 2012 16:02:16 +0000 Subject: 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 3dd13ef8..01ba19b9 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 73b8fa4d..0a7af0e9 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 -- cgit v1.2.3 From 54a19105f051113085efe9f87783acb77127c1d1 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Thu, 1 Mar 2012 12:46:38 +0000 Subject: 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 5dea37e8..e2d860a2 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) -- cgit v1.2.3 From 0a57cf98766ebbf22900024608faefbca3bdde6b Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Thu, 1 Mar 2012 12:51:23 +0000 Subject: 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 e2d860a2..f30667f1 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: -- cgit v1.2.3 From 537fa19bacd97743555b3cac2a3e3c6e14786124 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Thu, 1 Mar 2012 13:17:29 +0000 Subject: 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 f30667f1..d000ad96 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) -- cgit v1.2.3 From e3d7c361051bde7b6d712ca975b4fe14f6449c15 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Tue, 20 Mar 2012 13:21:24 +0000 Subject: 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 f170eb45..5e350268 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 -- cgit v1.2.3 From a07212389d86081353c9702880dd0da23d9579d5 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 11 Apr 2012 23:13:04 +0200 Subject: 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 83d26f1f..c9ae3b93 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): """ -- cgit v1.2.3 From 64a49905a40f5718d721f9ea300a1d42c8886392 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 11 Apr 2012 23:51:00 +0200 Subject: 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 ddc3ac17..6471edbe 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 5cd2a28f..79bd7353 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', -- cgit v1.2.3 From d88ae359b8da330cff97518176aaba2de7b252a8 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 12 Apr 2012 01:06:35 +0300 Subject: 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 dc8d1ea6..0f668335 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 `_ -- cgit v1.2.3 From 0c82e4b57549ed2d08bfb782cd99d4b71fd91eb7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 11 May 2012 09:46:00 +0200 Subject: 1.4 Ain't alpha. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 23a8075e..d5fc0ce8 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ We also have a `Jenkins service Daniel Izquierdo Can Yavuz Shawn Lewis +Adam Ness THANKS TO: -- cgit v1.2.3 From 0e3a2e6fdd800465b07817d780a981a18cb79880 Mon Sep 17 00:00:00 2001 From: Ralph Broenink Date: Fri, 6 Jul 2012 15:43:02 +0300 Subject: 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 0f292b4e..4a453957 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 -- cgit v1.2.3 From b0004c439860a1289f20c2175802d0bd4d2661c5 Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Thu, 12 Jul 2012 15:07:04 +0200 Subject: 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 4cf0133b..143d7b3f 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 -- cgit v1.2.3 From 36686cad13e7483699f224b16c9b7722c969f355 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 12 Jul 2012 22:40:24 +0700 Subject: 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 83d26f1f..11f24b86 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 -- cgit v1.2.3 From fe262ef3537fa67ecda374825a295ff854f027a3 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 12 Jul 2012 23:12:09 +0700 Subject: 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 11f24b86..b5858a82 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): -- cgit v1.2.3 From 650c04662dc4b47f285601fefad3b71afc7f6461 Mon Sep 17 00:00:00 2001 From: Max Arnold Date: Thu, 12 Jul 2012 23:13:04 +0700 Subject: remove remaining head() method --- djangorestframework/compat.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py index b5858a82..b8681022 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 -- cgit v1.2.3 From 2deb31d0968c77f40c63e0ffb9f655e69fbe1d96 Mon Sep 17 00:00:00 2001 From: yetist Date: Fri, 27 Jul 2012 11:39:24 +0800 Subject: support utf8 description --- djangorestframework/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 3657fd64..4aa6ca0c 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 -- cgit v1.2.3 From cb7a8155608e6a3a801343ec965270eed5acb42f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 28 Jul 2012 21:50:58 +0200 Subject: Added @yetist --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ad94b672..2b042344 100644 --- a/AUTHORS +++ b/AUTHORS @@ -36,6 +36,7 @@ Daniel Izquierdo Can Yavuz Shawn Lewis Adam Ness + THANKS TO: -- cgit v1.2.3 From abd3c7b46dcc76a042789f5c6d87ebdc9e8c980c Mon Sep 17 00:00:00 2001 From: Simon Pantzare Date: Fri, 10 Aug 2012 19:32:55 +0200 Subject: 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 d9aa4028..3d01582c 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. -- cgit v1.2.3 From 2f9775c12d172199c2a915062bfba3a13f5cadc4 Mon Sep 17 00:00:00 2001 From: Alen Mujezinovic Date: Mon, 13 Aug 2012 15:58:23 +0100 Subject: 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 d000ad96..f2f89f6c 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): """ -- cgit v1.2.3 From e9f67d3afcfab0b0754a0b2a49de46f4e890e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Aug 2012 10:40:43 +0200 Subject: Update AUTHORS Added @max-arnold, @ralphje. Thanks folks!--- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 2b042344..1b9431b1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -37,6 +37,8 @@ Can Yavuz Shawn Lewis Adam Ness +Max Arnold +Ralph Broenink THANKS TO: -- cgit v1.2.3 From acbc2d176825c024fcb5745a38ef506e915c00a1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 21 Aug 2012 17:39:40 +0200 Subject: Added @pilt - Thanks! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 1b9431b1..127e83aa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Adam Ness Max Arnold Ralph Broenink +Simon Pantzare THANKS TO: -- cgit v1.2.3 From 239d8c6c4dfea135dd622528b616c4fc572ed469 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sat, 25 Aug 2012 04:02:12 +0300 Subject: 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 a6745fca..5297bd99 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: --------- -- cgit v1.2.3 From 00d3aa21ba3bd5524932a692b75053a24ecbebd2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 19:53:10 +0100 Subject: 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 3d01582c..772002ba 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 64088345..29e6a24c 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 11e376ef..1d191af3 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 b67d50d9..9bd27045 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 0939fe9c..81c1fcb6 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 eafc3255..a806d751 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 4e72f754..4d1bb6ca 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 5297bd99..486c934f 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 8e3b3a10..86b3c28b 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: -- cgit v1.2.3 From 46ecf8d86fe05e536a131a77a7379c5538c0e577 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 20:07:53 +0100 Subject: 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 c7bf0a8f..40c68bd5 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 00000000..7cc2a31f --- /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 -- cgit v1.2.3 From 9d5f37d9b6900d5fb5989d3343c59c868db731d2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 20:13:43 +0100 Subject: 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 7cc2a31f..8638eac0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - "2.5" - "2.6" - "2.7" env: -- cgit v1.2.3 From 9449dcdd0682b76c009acbcd47abc933e6819713 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 20:37:20 +0100 Subject: 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 00ecf8c3..f84efb38 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 248744df..6c328d58 100644 --- a/djangorestframework/templates/djangorestframework/login.html +++ b/djangorestframework/templates/djangorestframework/login.html @@ -17,7 +17,7 @@
-
+ {% csrf_token %}
{{ form.username }} -- cgit v1.2.3 From 1a2186cd6e5b2af3c5f726353a12c2bfe0c2fd6a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 22:00:21 +0100 Subject: 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 f84efb38..2bc988de 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 6c328d58..ce14db5b 100644 --- a/djangorestframework/templates/djangorestframework/login.html +++ b/djangorestframework/templates/djangorestframework/login.html @@ -1,4 +1,5 @@ {% load static %} +{% load url from future %} -- cgit v1.2.3 From 03c6ccb45f6a9586cf4c6ffe271608b39870d640 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 22:09:47 +0100 Subject: 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 d5fc0ce8..901f34a8 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) -- cgit v1.2.3 From 70d9c747e786e191c8a7f659e52a7cfe65dd9045 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 22:24:24 +0100 Subject: 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 8638eac0..3ce1ee7b 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 -- cgit v1.2.3 From 7533c1027f6f8de257d9486ce98bb8ba1edfa1a7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 23:26:23 +0100 Subject: Django 1.5 breaks test client form encoding --- djangorestframework/tests/authentication.py | 14 +++++----- djangorestframework/tests/content.py | 6 ++--- djangorestframework/tests/serializer.py | 40 ++++++++++++++--------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py index 303bf96b..3dac7ee3 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 6bae8feb..e5d98e32 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 834a60d0..1a4eaa10 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', ) - - class NestedM3(Serializer): - fields = ('field2', ) - - class SerializerM2(Serializer): - include = [('field', NestedM2)] - exclude = ('id', ) - - 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'}}) + # 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 SerializerM2(Serializer): + # include = [('field', NestedM2)] + # exclude = ('id', ) + + # 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'}}) def test_serializer_classname_nesting(self): """ -- cgit v1.2.3 From ec868418073fe8cc778e5b81246a149fc9bd9a35 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 25 Aug 2012 23:33:17 +0100 Subject: Add build status --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 901f34a8..13fe23e8 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 ======== -- cgit v1.2.3 From 96dd1b50e6744ecdb11475ae7f78cbe95e23e517 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 26 Aug 2012 00:40:36 +0200 Subject: Add travis image --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 13fe23e8..3ed08775 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 ======== -- cgit v1.2.3 From 8bc04317b071048326df8391910d5186c3195fb8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 26 Aug 2012 00:43:35 +0200 Subject: 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 3ed08775..aa1bda4b 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 ======== -- cgit v1.2.3 From b93994c44456e16bde34bdd40122e9f0d489465f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 29 Aug 2012 19:27:01 +0100 Subject: 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 46dd608f..ee050f0d 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 -- cgit v1.2.3 From 7fa082489bb384422d129f2d7906ed2e550d3784 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 29 Aug 2012 21:39:12 +0100 Subject: 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 6471edbe..dab69c6f 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 ----- -- cgit v1.2.3 From a0504391e22e15d4e1dc77b0a98a0241db82f78b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 1 Sep 2012 22:56:11 +0100 Subject: 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 640b3567..ec12a7b0 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:: -- cgit v1.2.3 From 0f5dfb62b1f8648d222970fd2c39958fe3b16f16 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 1 Sep 2012 23:04:14 +0100 Subject: 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 86b3c28b..11f68945 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. -- cgit v1.2.3 From 51f43170587a1d679ab99f9c0e96fee566469d0c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 2 Sep 2012 09:30:20 +0200 Subject: Drop link to Jenkins --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index aa1bda4b..84d1edad 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+ -- cgit v1.2.3 From 0fc5a49a114ca65aab90dcf039da1569dde3c499 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 5 Sep 2012 10:04:45 +0100 Subject: 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 ec12a7b0..d906120e 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. -- cgit v1.2.3 From 943bc073d986b1d5bc10c6d17d3601d4311c3681 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Wed, 19 Sep 2012 08:05:16 -0700 Subject: 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 3f05903b..5d77c461 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[:] -- cgit v1.2.3 From f741bab709d2ec9e457c81f72743750d9a03963a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Sep 2012 17:05:09 +0100 Subject: Added @phobologic. Thanks\! --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 127e83aa..e0840961 100644 --- a/AUTHORS +++ b/AUTHORS @@ -40,6 +40,7 @@ Adam Ness Max Arnold Ralph Broenink Simon Pantzare +Michael Barrett THANKS TO: -- cgit v1.2.3 From f3834aa241ba6b6e922e324092dc2d6e7e343e72 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Wed, 19 Sep 2012 13:43:36 -0700 Subject: 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 5d77c461..d2349b53 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) -- cgit v1.2.3 From 689d2afd97006cff102eddabe20e7f04c2c958c0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 13:46:20 +0100 Subject: 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 85e13a5a..becdcad0 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 00000000..148d4757 --- /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 00000000..f3ab66b0 --- /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 00000000..8b137891 --- /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 00000000..7e18534d --- /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 -- cgit v1.2.3 From 49d2ea7cc0fa960ca3a98c90e8829ffebdcc1570 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 13:47:06 +0100 Subject: 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 148d4757..00000000 --- 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 f3ab66b0..00000000 --- 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 8b137891..00000000 --- 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 7e18534d..00000000 --- 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 -- cgit v1.2.3 From 1e9ece0f9353515265da9b6266dc4b39775a0257 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Mon, 8 Oct 2012 22:00:55 +0200 Subject: First attempt at adding filter support. The filter support uses django-filter to work its magic. --- requirements.txt | 1 + rest_framework/generics.py | 35 +++++++- rest_framework/mixins.py | 2 +- rest_framework/tests/filterset.py | 160 +++++++++++++++++++++++++++++++++++++ rest_framework/tests/models.py | 7 ++ rest_framework/tests/pagination.py | 69 +++++++++++++++- 6 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 rest_framework/tests/filterset.py diff --git a/requirements.txt b/requirements.txt index 730c1d07..b37b6185 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Django>=1.3 +-e git+https://github.com/alex/django-filter.git#egg=django-filter diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 59739d01..3b2bea3b 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -1,12 +1,12 @@ """ -Generic views that provide commmonly needed behaviour. +Generic views that provide commonly needed behaviour. """ from rest_framework import views, mixins from rest_framework.settings import api_settings from django.views.generic.detail import SingleObjectMixin from django.views.generic.list import MultipleObjectMixin - +import django_filters ### Base classes for the generic views ### @@ -58,6 +58,37 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView): pagination_serializer_class = api_settings.PAGINATION_SERIALIZER paginate_by = api_settings.PAGINATE_BY + filter_class = None + filter_fields = None + + def get_filter_class(self): + """ + Return the django-filters `FilterSet` used to filter the queryset. + """ + if self.filter_class: + return self.filter_class + + if self.filter_fields: + class AutoFilterSet(django_filters.FilterSet): + class Meta: + model = self.model + fields = self.filter_fields + return AutoFilterSet + + return None + + def filter_queryset(self, queryset): + filter_class = self.get_filter_class() + + if filter_class: + assert issubclass(filter_class.Meta.model, self.model), \ + "%s is not a subclass of %s" % (filter_class.Meta.model, self.model) + return filter_class(self.request.GET, queryset=queryset) + + return queryset + + def get_filtered_queryset(self): + return self.filter_queryset(self.get_queryset()) def get_pagination_serializer_class(self): """ diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 29153e18..04626fb0 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -33,7 +33,7 @@ class ListModelMixin(object): empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." def list(self, request, *args, **kwargs): - self.object_list = self.get_queryset() + self.object_list = self.get_filtered_queryset() # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py new file mode 100644 index 00000000..8c857f3f --- /dev/null +++ b/rest_framework/tests/filterset.py @@ -0,0 +1,160 @@ +import datetime +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import generics, status +from rest_framework.tests.models import FilterableItem, BasicModel +import django_filters + +factory = RequestFactory() + +# Basic filter on a list view. +class FilterFieldsRootView(generics.ListCreateAPIView): + model = FilterableItem + filter_fields = ['decimal', 'date'] + + +# These class are used to test a filter class. +class SeveralFieldsFilter(django_filters.FilterSet): + text = django_filters.CharFilter(lookup_type='icontains') + decimal = django_filters.NumberFilter(lookup_type='lt') + date = django_filters.DateFilter(lookup_type='gt') + class Meta: + model = FilterableItem + fields = ['text', 'decimal', 'date'] + + +class FilterClassRootView(generics.ListCreateAPIView): + model = FilterableItem + filter_class = SeveralFieldsFilter + + +# These classes are used to test a misconfigured filter class. +class MisconfiguredFilter(django_filters.FilterSet): + text = django_filters.CharFilter(lookup_type='icontains') + class Meta: + model = BasicModel + fields = ['text'] + + +class IncorrectlyConfiguredRootView(generics.ListCreateAPIView): + model = FilterableItem + filter_class = MisconfiguredFilter + + +class IntegrationTestFiltering(TestCase): + """ + Integration tests for filtered list views. + """ + + def setUp(self): + """ + Create 10 FilterableItem instances. + """ + base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + for i in range(10): + text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. + decimal = base_data[1] + i + date = base_data[2] - datetime.timedelta(days=i * 2) + FilterableItem(text=text, decimal=decimal, date=date).save() + + self.objects = FilterableItem.objects + self.data = [ + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} + for obj in self.objects.all() + ] + + def test_get_filtered_fields_root_view(self): + """ + GET requests to paginated ListCreateAPIView should return paginated results. + """ + view = FilterFieldsRootView.as_view() + + # Basic test with no filter. + request = factory.get('/') + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) + + # Tests that the decimal filter works. + search_decimal = 2.25 + request = factory.get('/?decimal=%s' % search_decimal) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['decimal'] == search_decimal ] + self.assertEquals(response.data, expected_data) + + # Tests that the date filter works. + search_date = datetime.date(2012, 9, 22) + request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22' + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['date'] == search_date ] + self.assertEquals(response.data, expected_data) + + def test_get_filtered_class_root_view(self): + """ + GET requests to filtered ListCreateAPIView that have a filter_class set + should return filtered results. + """ + view = FilterClassRootView.as_view() + + # Basic test with no filter. + request = factory.get('/') + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) + + # Tests that the decimal filter set with 'lt' in the filter class works. + search_decimal = 4.25 + request = factory.get('/?decimal=%s' % search_decimal) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['decimal'] < search_decimal ] + self.assertEquals(response.data, expected_data) + + # Tests that the date filter set with 'gt' in the filter class works. + search_date = datetime.date(2012, 10, 2) + request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02' + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['date'] > search_date ] + self.assertEquals(response.data, expected_data) + + # Tests that the text filter set with 'icontains' in the filter class works. + search_text = 'ff' + request = factory.get('/?text=%s' % search_text) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if search_text in f['text'].lower() ] + self.assertEquals(response.data, expected_data) + + # Tests that multiple filters works. + search_decimal = 5.25 + search_date = datetime.date(2012, 10, 2) + request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['date'] > search_date and + f['decimal'] < search_decimal ] + self.assertEquals(response.data, expected_data) + + def test_incorrectly_configured_filter(self): + """ + An error should be displayed when the filter class is misconfigured. + """ + view = IncorrectlyConfiguredRootView.as_view() + + request = factory.get('/') + self.assertRaises(AssertionError, view, request) + + # TODO Return 400 filter paramater requested that hasn't been configured. + def test_bad_request(self): + """ + GET requests with filters that aren't configured should return 400. + """ + view = FilterFieldsRootView.as_view() + + search_integer = 10 + request = factory.get('/?integer=%s' % search_integer) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6a758f0c..780c9dba 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -85,6 +85,13 @@ class Bookmark(RESTFrameworkModel): tags = GenericRelation(TaggedItem) +# Model to test filtering. +class FilterableItem(RESTFrameworkModel): + text = models.CharField(max_length=100) + decimal = models.DecimalField(max_digits=4, decimal_places=2) + date = models.DateField() + + # Model for regression test for #285 class Comment(RESTFrameworkModel): diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index a939c9ef..729bbfc2 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,8 +1,10 @@ +import datetime from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, pagination -from rest_framework.tests.models import BasicModel +from rest_framework.tests.models import BasicModel, FilterableItem +import django_filters factory = RequestFactory() @@ -15,6 +17,19 @@ class RootView(generics.ListCreateAPIView): paginate_by = 10 +class DecimalFilter(django_filters.FilterSet): + decimal = django_filters.NumberFilter(lookup_type='lt') + class Meta: + model = FilterableItem + fields = ['text', 'decimal', 'date'] + + +class FilterFieldsRootView(generics.ListCreateAPIView): + model = FilterableItem + paginate_by = 10 + filter_class = DecimalFilter + + class IntegrationTestPagination(TestCase): """ Integration tests for paginated list views. @@ -22,7 +37,7 @@ class IntegrationTestPagination(TestCase): def setUp(self): """ - Create 26 BasicModel intances. + Create 26 BasicModel instances. """ for char in 'abcdefghijklmnopqrstuvwxyz': BasicModel(text=char * 3).save() @@ -61,6 +76,56 @@ class IntegrationTestPagination(TestCase): self.assertEquals(response.data['next'], None) self.assertNotEquals(response.data['previous'], None) +class IntegrationTestPaginationAndFiltering(TestCase): + + def setUp(self): + """ + Create 50 FilterableItem instances. + """ + base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + for i in range(26): + text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. + decimal = base_data[1] + i + date = base_data[2] - datetime.timedelta(days=i * 2) + FilterableItem(text=text, decimal=decimal, date=date).save() + + self.objects = FilterableItem.objects + self.data = [ + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} + for obj in self.objects.all() + ] + self.view = FilterFieldsRootView.as_view() + + def test_get_paginated_filtered_root_view(self): + """ + GET requests to paginated filtered ListCreateAPIView should return + paginated results. The next and previous links should preserve the + filtered parameters. + """ + request = factory.get('/?decimal=15.20') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 15) + self.assertEquals(response.data['results'], self.data[:10]) + self.assertNotEquals(response.data['next'], None) + self.assertEquals(response.data['previous'], None) + + request = factory.get(response.data['next']) + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 15) + self.assertEquals(response.data['results'], self.data[10:15]) + self.assertEquals(response.data['next'], None) + self.assertNotEquals(response.data['previous'], None) + + request = factory.get(response.data['previous']) + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 15) + self.assertEquals(response.data['results'], self.data[:10]) + self.assertNotEquals(response.data['next'], None) + self.assertEquals(response.data['previous'], None) + class UnitTestPagination(TestCase): """ -- cgit v1.2.3 From 692203f933b77cc3b18e15434002169b642fbd84 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Tue, 9 Oct 2012 08:22:00 +0200 Subject: Check for 200 status when unknown filter requested. This changes the test from the failing checking for status 400. See discussion here: https://github.com/tomchristie/django-rest-framework/pull/169#issuecomment-9240480 --- rest_framework/tests/filterset.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 8c857f3f..b21abacb 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -147,14 +147,13 @@ class IntegrationTestFiltering(TestCase): request = factory.get('/') self.assertRaises(AssertionError, view, request) - # TODO Return 400 filter paramater requested that hasn't been configured. - def test_bad_request(self): + def test_unknown_filter(self): """ - GET requests with filters that aren't configured should return 400. + GET requests with filters that aren't configured should return 200. """ view = FilterFieldsRootView.as_view() search_integer = 10 request = factory.get('/?integer=%s' % search_integer) response = view(request).render() - self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) \ No newline at end of file + self.assertEquals(response.status_code, status.HTTP_200_OK) \ No newline at end of file -- cgit v1.2.3 From e295f616ec2cfee9c24b22d4be1a605a93d9544d Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 11:32:51 +0200 Subject: Fix small PEP8 problem. --- rest_framework/tests/pagination.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 729bbfc2..054b7ee3 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -76,6 +76,7 @@ class IntegrationTestPagination(TestCase): self.assertEquals(response.data['next'], None) self.assertNotEquals(response.data['previous'], None) + class IntegrationTestPaginationAndFiltering(TestCase): def setUp(self): -- cgit v1.2.3 From 6fbd411254089c86baca65b08a89d239e5b804a9 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 11:35:00 +0200 Subject: Make query filters work with pagination. --- rest_framework/pagination.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 131718fd..616c7674 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -14,6 +14,9 @@ class NextPageField(serializers.Field): request = self.context.get('request') relative_url = '?page=%d' % page if request: + for field, value in request.QUERY_PARAMS.iteritems(): + if field != 'page': + relative_url += '&%s=%s' % (field, value) return request.build_absolute_uri(relative_url) return relative_url @@ -29,7 +32,10 @@ class PreviousPageField(serializers.Field): request = self.context.get('request') relative_url = '?page=%d' % page if request: - return request.build_absolute_uri('?page=%d' % page) + for field, value in request.QUERY_PARAMS.iteritems(): + if field != 'page': + relative_url += '&%s=%s' % (field, value) + return request.build_absolute_uri(relative_url) return relative_url -- cgit v1.2.3 From 5454162b0419ab6564f37ea56b1852d686b0a11e Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 11:39:23 +0200 Subject: Define 'page' query field name in one place. --- rest_framework/pagination.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 616c7674..c77a1005 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -3,7 +3,11 @@ from rest_framework import serializers # TODO: Support URLconf kwarg-style paging -class NextPageField(serializers.Field): +class PageField(serializers.Field): + page_field = 'page' + + +class NextPageField(PageField): """ Field that returns a link to the next page in paginated results. """ @@ -12,16 +16,16 @@ class NextPageField(serializers.Field): return None page = value.next_page_number() request = self.context.get('request') - relative_url = '?page=%d' % page + relative_url = '?%s=%d' % (self.page_field, page) if request: for field, value in request.QUERY_PARAMS.iteritems(): - if field != 'page': + if field != self.page_field: relative_url += '&%s=%s' % (field, value) return request.build_absolute_uri(relative_url) return relative_url -class PreviousPageField(serializers.Field): +class PreviousPageField(PageField): """ Field that returns a link to the previous page in paginated results. """ @@ -30,10 +34,10 @@ class PreviousPageField(serializers.Field): return None page = value.previous_page_number() request = self.context.get('request') - relative_url = '?page=%d' % page + relative_url = '?%s=%d' % (self.page_field, page) if request: for field, value in request.QUERY_PARAMS.iteritems(): - if field != 'page': + if field != self.page_field: relative_url += '&%s=%s' % (field, value) return request.build_absolute_uri(relative_url) return relative_url -- cgit v1.2.3 From 6300334acaef8fe66e03557f089fb335ac861a57 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 11:21:50 +0100 Subject: 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 3746d7c8..43365e07 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) -- cgit v1.2.3 From 6f736a682369e003e4ae4b8d587f9168d4196986 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 13:55:16 +0200 Subject: Explicitly use Decimal for creating filter test data. This fixes a Travis build failures on python 2.6: https://travis-ci.org/#!/tomchristie/django-rest-framework/builds/2746628 --- rest_framework/tests/filterset.py | 3 ++- rest_framework/tests/pagination.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index b21abacb..5b2721ff 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -1,4 +1,5 @@ import datetime +from decimal import Decimal from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status @@ -50,7 +51,7 @@ class IntegrationTestFiltering(TestCase): """ Create 10 FilterableItem instances. """ - base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8)) for i in range(10): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 054b7ee3..8c5e6ad7 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,4 +1,5 @@ import datetime +from decimal import Decimal from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory @@ -83,7 +84,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): """ Create 50 FilterableItem instances. """ - base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + base_data = ('a', Decimal(0.25), datetime.date(2012, 10, 8)) for i in range(26): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i -- cgit v1.2.3 From 1d054f95725e5bec7d4ba9d23717897ef80b7388 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 14:19:29 +0200 Subject: Use Decimal (properly) everywhere. --- rest_framework/tests/filterset.py | 6 +++--- rest_framework/tests/pagination.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 5b2721ff..5374eefc 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -77,7 +77,7 @@ class IntegrationTestFiltering(TestCase): self.assertEquals(response.data, self.data) # Tests that the decimal filter works. - search_decimal = 2.25 + search_decimal = Decimal('2.25') request = factory.get('/?decimal=%s' % search_decimal) response = view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) @@ -106,7 +106,7 @@ class IntegrationTestFiltering(TestCase): self.assertEquals(response.data, self.data) # Tests that the decimal filter set with 'lt' in the filter class works. - search_decimal = 4.25 + search_decimal = Decimal('4.25') request = factory.get('/?decimal=%s' % search_decimal) response = view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) @@ -130,7 +130,7 @@ class IntegrationTestFiltering(TestCase): self.assertEquals(response.data, expected_data) # Tests that multiple filters works. - search_decimal = 5.25 + search_decimal = Decimal('5.25') search_date = datetime.date(2012, 10, 2) request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) response = view(request).render() diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 8c5e6ad7..170515a7 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -84,7 +84,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): """ Create 50 FilterableItem instances. """ - base_data = ('a', Decimal(0.25), datetime.date(2012, 10, 8)) + base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8)) for i in range(26): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i -- cgit v1.2.3 From c24997df3b943e5d7a3b2e101508e4b79ee82dc4 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 16:39:25 +0200 Subject: Change django-filter to version that supports Django 1.3. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b37b6185..de752639 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ Django>=1.3 --e git+https://github.com/alex/django-filter.git#egg=django-filter +-e git+https://github.com/onepercentclub/django-filter.git@django-1.3-compat#egg=django-filter -- cgit v1.2.3 From cab4a2a5ad17545ac435785bf55b0b3a6c8f932c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 15:41:57 +0100 Subject: Split up doc sections more cleanly --- docs/api-guide/exceptions.md | 4 ++++ docs/api-guide/permissions.md | 5 +++++ docs/api-guide/settings.md | 4 ++++ docs/api-guide/throttling.md | 4 ++++ docs/api-guide/views.md | 4 ++-- 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index c3bdb7b9..33cf1ca8 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -33,6 +33,10 @@ Might recieve an error response indicating that the `DELETE` method is not allow {"detail": "Method 'DELETE' not allowed."} +--- + +# API Reference + ## APIException **Signature:** `APIException(detail=None)` diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index eb290849..b25b52be 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -54,6 +54,8 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) +--- + # API Reference ## IsAuthenticated @@ -88,12 +90,15 @@ To use custom model permissions, override `DjangoModelPermissions` and set the ` The `DjangoModelPermissions` class also supports object-level permissions. Third-party authorization backends such as [django-guardian][guardian] that provide object-level permissions should work just fine with `DjangoModelPermissions` without any custom configuration required. +--- + # Custom permissions To implement a custom permission, override `BasePermission` and implement the `.has_permission(self, request, view, obj=None)` method. The method should return `True` if the request should be granted access, and `False` otherwise. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index f473128e..84acd797 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -30,6 +30,10 @@ you should use the `api_settings` object. For example. The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. +--- + +# API Reference + ## DEFAULT_RENDERERS A list or tuple of renderer classes, that determines the default set of renderers that may be used when returning a `Response` object. diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 3fb95ae3..22e34187 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -63,6 +63,8 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) +--- + # API Reference ## AnonRateThrottle @@ -144,6 +146,8 @@ For example, given the following views... User requests to either `ContactListView` or `ContactDetailView` would be restricted to a total of 1000 requests per-day. User requests to `UploadView` would be restricted to 20 requests per day. +--- + # Custom throttles To create a custom throttle, override `BaseThrottle` and implement `.allow_request(request, view)`. The method should return `True` if the request should be allowed, and `False` otherwise. diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index cbfa2e28..77349252 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -33,8 +33,8 @@ For example: """ Return a list of all users. """ - users = [user.username for user in User.objects.all()] - return Response(users) + usernames = [user.username for user in User.objects.all()] + return Response(usernames) ## API policy attributes -- cgit v1.2.3 From 38673c35d4aa5487e175ac7c917c66c45ddb6ba4 Mon Sep 17 00:00:00 2001 From: Rob Dobson Date: Wed, 17 Oct 2012 19:12:34 +0100 Subject: Make default field check safe for boolean values whereby 'False' may be an acceptable default value --- rest_framework/serializers.py | 2 +- rest_framework/tests/models.py | 4 ++++ rest_framework/tests/serializer.py | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 13f8cde2..6724bbdf 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -393,7 +393,7 @@ class ModelSerializer(Serializer): except KeyError: ret = ModelField(model_field=model_field) - if model_field.default: + if model_field.default is not None: ret.required = False return ret diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6a758f0c..75dab2f7 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -91,3 +91,7 @@ class Comment(RESTFrameworkModel): email = models.EmailField() content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + +class ActionItem(RESTFrameworkModel): + title = models.CharField(max_length=200) + done = models.BooleanField(default=False) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 256987ad..610ed85f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,6 +28,10 @@ class CommentSerializer(serializers.Serializer): return instance +class ActionItemSerializer(serializers.ModelSerializer): + class Meta: + model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -81,7 +85,9 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } + self.actionitem = ActionItem('Some to do item', + ) def test_create(self): serializer = CommentSerializer(self.data) @@ -102,6 +108,17 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + def test_missing_bool_with_default(self): + """Make sure that a boolean value with a 'False' value is not + mistaken for not having a default.""" + data = { + 'title':'Some action item', + #No 'done' value. + } + serializer = ActionItemSerializer(data, instance=self.actionitem) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.errors, {}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 6717d654d0bbfdfca4aaea84a5b4814c4e5f7567 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 21:57:23 +0100 Subject: Added @rdobson. Thanks! --- docs/topics/credits.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 3f4b9f0d..6df99237 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -47,6 +47,7 @@ The following people have helped make REST framework great. * Mattbo - [mattbo] * Max Hurl - [maximilianhurl] * Tomi Pajunen - [eofs] +* Rob Dobson - [rdobson] Many thanks to everyone who's contributed to the project. @@ -124,4 +125,5 @@ To contact the author directly: [j4mie]: https://github.com/j4mie [mattbo]: https://github.com/mattbo [maximilianhurl]: https://github.com/maximilianhurl -[eofs]: https://github.com/eofs \ No newline at end of file +[eofs]: https://github.com/eofs +[rdobson]: https://github.com/rdobson -- cgit v1.2.3 From 99d48f90030d174ef80498b48f56af6489865f0d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:07:56 +0100 Subject: Drop .parse_string_or_stream() - keep API minimal. --- docs/api-guide/parsers.md | 10 +++++----- docs/tutorial/1-serialization.md | 7 +++++-- rest_framework/parsers.py | 28 ++++++++-------------------- rest_framework/tests/request.py | 2 +- rest_framework/views.py | 2 +- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 4f145ba3..a950c0e0 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -91,11 +91,11 @@ You will typically want to use both `FormParser` and `MultiPartParser` together # Custom parsers -To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse_stream(self, stream, parser_context)` method. +To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, parser_context)` method. The method should return the data that will be used to populate the `request.DATA` property. -The arguments passed to `.parse_stream()` are: +The arguments passed to `.parse()` are: ### stream @@ -116,7 +116,7 @@ The following is an example plaintext parser that will populate the `request.DAT media_type = 'text/plain' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Simply return a string representing the body of the request. """ @@ -124,7 +124,7 @@ The following is an example plaintext parser that will populate the `request.DAT ## Uploading file content -If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse_stream()` method. `DataAndFiles` should be instantiated with two arguments. The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property. +If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse()` method. `DataAndFiles` should be instantiated with two arguments. The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property. For example: @@ -133,7 +133,7 @@ For example: A naive raw file upload parser. """ - def parse_stream(self, stream, parser_context): + def parse(self, stream, parser_context): content = stream.read() name = 'example.dat' content_type = 'application/octet-stream' diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e21433ba..5b58f293 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -134,12 +134,15 @@ We've now got a few comment instances to play with. Let's take a look at serial At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`. - stream = JSONRenderer().render(serializer.data) - stream + content = JSONRenderer().render(serializer.data) + content # '{"id": 1, "email": "leila@example.com", "content": "nothing to say", "created": "2012-08-22T16:20:09.822"}' Deserialization is similar. First we parse a stream into python native datatypes... + import StringIO + + stream = StringIO.StringIO(content) data = JSONParser().parse(stream) ...then we restore those native datatypes into to a fully populated object instance. diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 048b71e1..6287b842 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -21,7 +21,6 @@ from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError import datetime import decimal -from io import BytesIO class DataAndFiles(object): @@ -33,29 +32,18 @@ class DataAndFiles(object): class BaseParser(object): """ All parsers should extend `BaseParser`, specifying a `media_type` - attribute, and overriding the `.parse_stream()` method. + attribute, and overriding the `.parse()` method. """ media_type = None - def parse(self, string_or_stream, parser_context=None): - """ - The main entry point to parsers. This is a light wrapper around - `parse_stream`, that instead handles both string and stream objects. - """ - if isinstance(string_or_stream, basestring): - stream = BytesIO(string_or_stream) - else: - stream = string_or_stream - return self.parse_stream(stream, parser_context) - - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Given a stream to read from, return the deserialized output. Should return parsed data, or a DataAndFiles object consisting of the parsed data and files. """ - raise NotImplementedError(".parse_stream() must be overridden.") + raise NotImplementedError(".parse() must be overridden.") class JSONParser(BaseParser): @@ -65,7 +53,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -85,7 +73,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -105,7 +93,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -123,7 +111,7 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a DataAndFiles object. @@ -148,7 +136,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f90bebf4..f698e845 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. diff --git a/rest_framework/views.py b/rest_framework/views.py index 62fc92f9..1be2593c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -158,7 +158,7 @@ class APIView(View): def get_parser_context(self, http_request): """ - Returns a dict that is passed through to Parser.parse_stream(), + Returns a dict that is passed through to Parser.parse(), as the `parser_context` keyword argument. """ return { -- cgit v1.2.3 From 4231995fbd80e45991975ab81d9e570a9f4b72d0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:19:59 +0100 Subject: parser_context includes `view`, `request`, `args`, `kwargs`. (Not `meta` and `upload_handlers`) Consistency with renderer API. --- docs/api-guide/parsers.md | 4 +++- docs/api-guide/renderers.md | 5 ++++- rest_framework/parsers.py | 6 ++++-- rest_framework/request.py | 9 ++------- rest_framework/views.py | 15 +++++++++------ 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index a950c0e0..70abad9b 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -103,7 +103,9 @@ A stream-like object representing the body of the request. ### parser_context -If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. By default it includes the keys `'upload_handlers'` and `'meta'`, which contain the values of the `request.upload_handlers` and `request.meta` properties. +Optional. If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. + +By default this will include the following keys: `view`, `request`, `args`, `kwargs`. ## Example diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index c8addb32..24cca181 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -162,11 +162,14 @@ The request data, as set by the `Response()` instantiation. ### `media_type=None` -Optional. If provided, this is the accepted media type, as determined by the content negotiation stage. Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"application/json; nested=true"`. +Optional. If provided, this is the accepted media type, as determined by the content negotiation stage. + +Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"application/json; nested=true"`. ### `renderer_context=None` Optional. If provided, this is a dictionary of contextual information provided by the view. + By default this will include the following keys: `view`, `request`, `response`, `args`, `kwargs`. ## Example diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 6287b842..7e13c3d8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -119,8 +119,10 @@ class MultiPartParser(BaseParser): `.files` will be a `QueryDict` containing all the form files. """ parser_context = parser_context or {} - meta = parser_context['meta'] - upload_handlers = parser_context['upload_handlers'] + request = parser_context['request'] + meta = request.META + upload_handlers = request.upload_handlers + try: parser = DjangoMultiPartParser(meta, stream, upload_handlers) data, files = parser.parse() diff --git a/rest_framework/request.py b/rest_framework/request.py index 6f9cf09a..d739d27d 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -88,17 +88,12 @@ class Request(object): self._stream = Empty if self.parser_context is None: - self.parser_context = self._default_parser_context(request) + self.parser_context = {} + self.parser_context['request'] = self def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION() - def _default_parser_context(self, request): - return { - 'upload_handlers': request.upload_handlers, - 'meta': request.META, - } - @property def method(self): """ diff --git a/rest_framework/views.py b/rest_framework/views.py index 1be2593c..066c0bb9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -161,9 +161,12 @@ class APIView(View): Returns a dict that is passed through to Parser.parse(), as the `parser_context` keyword argument. """ + # Note: Additionally `request` will also be added to the context + # by the Request object. return { - 'upload_handlers': http_request.upload_handlers, - 'meta': http_request.META, + 'view': self, + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}) } def get_renderer_context(self): @@ -171,13 +174,13 @@ class APIView(View): Returns a dict that is passed through to Renderer.render(), as the `renderer_context` keyword argument. """ - # Note: Additionally 'response' will also be set on the context, + # Note: Additionally 'response' will also be added to the context, # by the Response object. return { 'view': self, - 'request': self.request, - 'args': self.args, - 'kwargs': self.kwargs + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}), + 'request': getattr(self, 'request', None) } # API policy instantiation methods -- cgit v1.2.3 From fb56f215ae50da0aebe99e05036ece259fd3e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:39:07 +0100 Subject: Added `media_type` to `.parse()` - Consistency with renderer API. --- docs/api-guide/parsers.md | 10 ++++++++-- rest_framework/parsers.py | 28 +++++++++++----------------- rest_framework/renderers.py | 13 +++++++------ rest_framework/request.py | 12 ++++++++---- rest_framework/tests/request.py | 2 +- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 70abad9b..18a5872c 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -101,6 +101,12 @@ The arguments passed to `.parse()` are: A stream-like object representing the body of the request. +### media_type + +Optional. If provided, this is the media type of the incoming request. + +Depending on the request's `Content-Type:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"text/plain; charset=utf-8"`. + ### parser_context Optional. If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. @@ -118,7 +124,7 @@ The following is an example plaintext parser that will populate the `request.DAT media_type = 'text/plain' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Simply return a string representing the body of the request. """ @@ -135,7 +141,7 @@ For example: A naive raw file upload parser. """ - def parse(self, stream, parser_context): + def parse(self, stream, media_type=None, parser_context=None): content = stream.read() name = 'example.dat' content_type = 'application/octet-stream' diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 7e13c3d8..4841676c 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -1,14 +1,8 @@ """ -Django supports parsing the content of an HTTP request, but only for form POST requests. -That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well -to general HTTP requests. +Parsers are used to parse the content of incoming HTTP requests. -We need a method to be able to: - -1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT) - -2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded - and multipart/form-data. (eg also handle multipart/json) +They give us a generic way of being able to handle various media types +on the request, such as form content or json encoded data. """ from django.http import QueryDict @@ -37,10 +31,10 @@ class BaseParser(object): media_type = None - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ - Given a stream to read from, return the deserialized output. - Should return parsed data, or a DataAndFiles object consisting of the + Given a stream to read from, return the parsed representation. + Should return parsed data, or a `DataAndFiles` object consisting of the parsed data and files. """ raise NotImplementedError(".parse() must be overridden.") @@ -53,7 +47,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -73,7 +67,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -93,7 +87,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -111,7 +105,7 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a DataAndFiles object. @@ -138,7 +132,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 94d253c9..23fd961b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1,9 +1,10 @@ """ -Renderers are used to serialize a View's output into specific media types. +Renderers are used to serialize a response into specific media types. -Django REST framework also provides HTML and PlainText renderers that help self-document the API, -by serializing the output along with documentation regarding the View, output status and headers, -and providing forms and links depending on the allowed methods, renderers and parsers on the View. +They give us a generic way of being able to handle various media types +on the response, such as JSON encoded data or HTML output. + +REST framework also provides an HTML renderer the renders the browseable API. """ import string from django import forms @@ -23,8 +24,8 @@ from rest_framework import serializers, parsers class BaseRenderer(object): """ - All renderers must extend this class, set the :attr:`media_type` attribute, - and override the :meth:`render` method. + All renderers should extend this class, setting the `media_type` + and `format` attributes, and override the `.render()` method. """ media_type = None diff --git a/rest_framework/request.py b/rest_framework/request.py index d739d27d..b9d55de4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -260,15 +260,19 @@ class Request(object): May raise an `UnsupportedMediaType`, or `ParseError` exception. """ - if self.stream is None or self.content_type is None: + stream = self.stream + media_type = self.content_type + + if stream is None or media_type is None: return (None, None) - parser = self.negotiator.select_parser(self.parsers, self.content_type) + parser = self.negotiator.select_parser(self.parsers, media_type) if not parser: - raise exceptions.UnsupportedMediaType(self.content_type) + raise exceptions.UnsupportedMediaType(media_type) + + parsed = parser.parse(stream, media_type, self.parser_context) - parsed = parser.parse(self.stream, self.parser_context) # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. try: diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f698e845..ff48f3fa 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. -- cgit v1.2.3 From e126b615420fed12af58675cb4bb52e749b006bd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:58:18 +0100 Subject: Negotiation API finalized. .select_renderers and .select_parsers --- docs/api-guide/content-negotiation.md | 6 ++++++ rest_framework/negotiation.py | 33 +++++++++++---------------------- rest_framework/request.py | 2 +- rest_framework/tests/negotiation.py | 10 +++++----- rest_framework/views.py | 8 +++++++- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md index ad98de3b..b95091c5 100644 --- a/docs/api-guide/content-negotiation.md +++ b/docs/api-guide/content-negotiation.md @@ -7,3 +7,9 @@ > — [RFC 2616][cite], Fielding et al. [cite]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html + +**TODO**: Describe content negotiation style used by REST framework. + +## Custom content negotiation + +It's unlikley that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme, override `BaseContentNegotiation`, and implement the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` \ No newline at end of file diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 8b22f669..444f8056 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -4,45 +4,34 @@ from rest_framework.utils.mediatypes import order_by_precedence, media_type_matc class BaseContentNegotiation(object): - def negotiate(self, request, renderers, format=None, force=False): - raise NotImplementedError('.negotiate() must be implemented') + def select_parser(self, request, parsers): + raise NotImplementedError('.select_parser() must be implemented') + def select_renderer(self, request, renderers, format_suffix=None): + raise NotImplementedError('.select_renderer() must be implemented') -class DefaultContentNegotiation(object): + +class DefaultContentNegotiation(BaseContentNegotiation): settings = api_settings - def select_parser(self, parsers, media_type): + def select_parser(self, request, parsers): """ Given a list of parsers and a media type, return the appropriate parser to handle the incoming request. """ for parser in parsers: - if media_type_matches(parser.media_type, media_type): + if media_type_matches(parser.media_type, request.content_type): return parser return None - def negotiate(self, request, renderers, format=None, force=False): + def select_renderer(self, request, renderers, format_suffix=None): """ Given a request and a list of renderers, return a two-tuple of: (renderer, media type). - - If force is set, then suppress exceptions, and forcibly return a - fallback renderer and media_type. - """ - try: - return self.unforced_negotiate(request, renderers, format) - except (exceptions.InvalidFormat, exceptions.NotAcceptable): - if force: - return (renderers[0], renderers[0].media_type) - raise - - def unforced_negotiate(self, request, renderers, format=None): - """ - As `.negotiate()`, but does not take the optional `force` agument, - or suppress exceptions. """ # Allow URL style format override. eg. "?format=json - format = format or request.GET.get(self.settings.URL_FORMAT_OVERRIDE) + format_query_param = self.settings.URL_FORMAT_OVERRIDE + format = format_suffix or request.GET.get(format_query_param) if format: renderers = self.filter_renderers(renderers, format) diff --git a/rest_framework/request.py b/rest_framework/request.py index b9d55de4..b212680f 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -266,7 +266,7 @@ class Request(object): if stream is None or media_type is None: return (None, None) - parser = self.negotiator.select_parser(self.parsers, media_type) + parser = self.negotiator.select_parser(self, self.parsers) if not parser: raise exceptions.UnsupportedMediaType(media_type) diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index d8265b43..e06354ea 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -18,20 +18,20 @@ class TestAcceptedMediaType(TestCase): self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] self.negotiator = DefaultContentNegotiation() - def negotiate(self, request): - return self.negotiator.negotiate(request, self.renderers) + def select_renderer(self, request): + return self.negotiator.select_renderer(request, self.renderers) def test_client_without_accept_use_renderer(self): request = factory.get('/') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_underspecifies_accept_use_renderer(self): request = factory.get('/', HTTP_ACCEPT='*/*') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_overspecifies_accept_use_client(self): request = factory.get('/', HTTP_ACCEPT='application/json; indent=8') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json; indent=8') diff --git a/rest_framework/views.py b/rest_framework/views.py index 066c0bb9..357d8939 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -238,7 +238,13 @@ class APIView(View): """ renderers = self.get_renderers() conneg = self.get_content_negotiator() - return conneg.negotiate(request, renderers, self.format_kwarg, force) + + try: + return conneg.select_renderer(request, renderers, self.format_kwarg) + except: + if force: + return (renderers[0], renderers[0].media_type) + raise def has_permission(self, request, obj=None): """ -- cgit v1.2.3 From fed235dd0135c3eb98bb218a51f01ace5ddd3782 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 23:09:11 +0100 Subject: Make settings consistent with corrosponding view attributes --- docs/api-guide/authentication.md | 4 ++-- docs/api-guide/parsers.md | 4 ++-- docs/api-guide/permissions.md | 4 ++-- docs/api-guide/renderers.md | 4 ++-- docs/api-guide/requests.md | 4 ++-- docs/api-guide/settings.md | 20 +++++++++---------- docs/api-guide/throttling.md | 8 ++++---- docs/css/default.css | 4 ++++ docs/tutorial/quickstart.md | 4 ++-- rest_framework/generics.py | 4 ++-- rest_framework/request.py | 2 +- rest_framework/settings.py | 43 +++++++++++++++++++++------------------- rest_framework/views.py | 12 +++++------ 13 files changed, 62 insertions(+), 55 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 71f48163..5e5ee4ed 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -26,10 +26,10 @@ The value of `request.user` and `request.auth` for unauthenticated requests can ## Setting the authentication policy -The default authentication policy may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example. +The default authentication policy may be set globally, using the `DEFAULT_AUTHENTICATION_CLASSES` setting. For example. REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION': ( + 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.UserBasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 18a5872c..f7a62d3d 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -16,10 +16,10 @@ The set of valid parsers for a view is always defined as a list of classes. Whe ## Setting the parsers -The default set of parsers may be set globally, using the `DEFAULT_PARSERS` setting. For example, the following settings would allow requests with `YAML` content. +The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content. REST_FRAMEWORK = { - 'DEFAULT_PARSERS': ( + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.YAMLParser', ) } diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index b25b52be..249f3938 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -25,10 +25,10 @@ Object level permissions are run by REST framework's generic views when `.get_ob ## Setting the permission policy -The default permission policy may be set globally, using the `DEFAULT_PERMISSIONS` setting. For example. +The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. REST_FRAMEWORK = { - 'DEFAULT_PERMISSIONS': ( + 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ) } diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 24cca181..b3b8d5bc 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -18,10 +18,10 @@ For more information see the documentation on [content negotation][conneg]. ## Setting the renderers -The default set of renderers may be set globally, using the `DEFAULT_RENDERERS` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API. +The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API. REST_FRAMEWORK = { - 'DEFAULT_RENDERERS': ( + 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.YAMLRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', ) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 439c97bc..2770c6dd 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -37,7 +37,7 @@ For clarity inside your code, we recommend using `request.QUERY_PARAMS` instead ## .parsers -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. +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_PARSER_CLASSES` setting. You won't typically need to access this property. @@ -125,4 +125,4 @@ Note that due to implementation reasons the `Request` class does not inherit fro [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 +[browser enhancements documentation]: ../topics/browser-enhancements.md diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 84acd797..21efc853 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -11,10 +11,10 @@ Configuration for REST framework is all namespaced inside a single Django settin For example your project's `settings.py` file might include something like this: REST_FRAMEWORK = { - 'DEFAULT_RENDERERS': ( + 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.YAMLRenderer', ) - 'DEFAULT_PARSERS': ( + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.YAMLParser', ) } @@ -26,7 +26,7 @@ you should use the `api_settings` object. For example. from rest_framework.settings import api_settings - print api_settings.DEFAULT_AUTHENTICATION + print api_settings.DEFAULT_AUTHENTICATION_CLASSES The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. @@ -34,7 +34,7 @@ The `api_settings` object will check for any user-defined settings, and otherwis # API Reference -## DEFAULT_RENDERERS +## DEFAULT_RENDERER_CLASSES A list or tuple of renderer classes, that determines the default set of renderers that may be used when returning a `Response` object. @@ -46,7 +46,7 @@ Default: 'rest_framework.renderers.TemplateHTMLRenderer' ) -## DEFAULT_PARSERS +## DEFAULT_PARSER_CLASSES A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.DATA` property. @@ -57,7 +57,7 @@ Default: 'rest_framework.parsers.FormParser' ) -## DEFAULT_AUTHENTICATION +## DEFAULT_AUTHENTICATION_CLASSES A list or tuple of authentication classes, that determines the default set of authenticators used when accessing the `request.user` or `request.auth` properties. @@ -68,25 +68,25 @@ Default: 'rest_framework.authentication.UserBasicAuthentication' ) -## DEFAULT_PERMISSIONS +## DEFAULT_PERMISSION_CLASSES A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. Default: `()` -## DEFAULT_THROTTLES +## DEFAULT_THROTTLE_CLASSES A list or tuple of throttle classes, that determines the default set of throttles checked at the start of a view. Default: `()` -## DEFAULT_MODEL_SERIALIZER +## DEFAULT_MODEL_SERIALIZER_CLASS **TODO** Default: `rest_framework.serializers.ModelSerializer` -## DEFAULT_PAGINATION_SERIALIZER +## DEFAULT_PAGINATION_SERIALIZER_CLASS **TODO** diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 22e34187..435b20ce 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -27,10 +27,10 @@ If any throttle check fails an `exceptions.Throttled` exception will be raised, ## Setting the throttling policy -The default throttling policy may be set globally, using the `DEFAULT_THROTTLES` and `DEFAULT_THROTTLE_RATES` settings. For example. +The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_CLASSES` and `DEFAULT_THROTTLE_RATES` settings. For example. REST_FRAMEWORK = { - 'DEFAULT_THROTTLES': ( + 'DEFAULT_THROTTLE_CLASSES': ( 'rest_framework.throttles.AnonThrottle', 'rest_framework.throttles.UserThrottle', ) @@ -100,7 +100,7 @@ For example, multiple user throttle rates could be implemented by using the foll ...and the following settings. REST_FRAMEWORK = { - 'DEFAULT_THROTTLES': ( + 'DEFAULT_THROTTLE_CLASSES': ( 'example.throttles.BurstRateThrottle', 'example.throttles.SustainedRateThrottle', ) @@ -135,7 +135,7 @@ For example, given the following views... ...and the following settings. REST_FRAMEWORK = { - 'DEFAULT_THROTTLES': ( + 'DEFAULT_THROTTLE_CLASSES': ( 'rest_framework.throttles.ScopedRateThrottle', ) 'DEFAULT_THROTTLE_RATES': { diff --git a/docs/css/default.css b/docs/css/default.css index c1d2e885..57446ff9 100644 --- a/docs/css/default.css +++ b/docs/css/default.css @@ -88,6 +88,10 @@ pre { font-weight: bold; } +.nav-list a { + overflow: hidden; +} + /* Set the table of contents to static so it flows back into the content when viewed on tablets and smaller. */ @media (max-width: 767px) { diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 851db3c7..6bde725b 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -126,7 +126,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a ) REST_FRAMEWORK = { - 'DEFAULT_PERMISSIONS': ('rest_framework.permissions.IsAdminUser',), + 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), 'PAGINATE_BY': 10 } @@ -169,4 +169,4 @@ If you want to get a more in depth understanding of how REST framework fits toge [image]: ../img/quickstart.png [tutorial]: 1-serialization.md -[guide]: ../#api-guide \ No newline at end of file +[guide]: ../#api-guide diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 59739d01..18c1033d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -15,7 +15,7 @@ class BaseView(views.APIView): Base class for all other generic views. """ serializer_class = None - model_serializer_class = api_settings.MODEL_SERIALIZER + model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS def get_serializer_context(self): """ @@ -56,7 +56,7 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView): Base class for generic views onto a queryset. """ - pagination_serializer_class = api_settings.PAGINATION_SERIALIZER + pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS paginate_by = api_settings.PAGINATE_BY def get_pagination_serializer_class(self): diff --git a/rest_framework/request.py b/rest_framework/request.py index b212680f..5870be82 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -92,7 +92,7 @@ class Request(object): self.parser_context['request'] = self def _default_negotiator(self): - return api_settings.DEFAULT_CONTENT_NEGOTIATION() + return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() @property def method(self): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8bbb2f75..3c508294 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -3,11 +3,11 @@ Settings for REST framework are all namespaced in the REST_FRAMEWORK setting. For example your project's `settings.py` file might look like this: REST_FRAMEWORK = { - 'DEFAULT_RENDERERS': ( + 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.YAMLRenderer', ) - 'DEFAULT_PARSERS': ( + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.YAMLParser', ) @@ -24,30 +24,33 @@ from django.utils import importlib USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) DEFAULTS = { - 'DEFAULT_RENDERERS': ( + 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', ), - 'DEFAULT_PARSERS': ( + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser' ), - 'DEFAULT_AUTHENTICATION': ( + 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication' ), - 'DEFAULT_PERMISSIONS': (), - 'DEFAULT_THROTTLES': (), - 'DEFAULT_CONTENT_NEGOTIATION': + 'DEFAULT_PERMISSION_CLASSES': (), + 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', + + 'DEFAULT_MODEL_SERIALIZER_CLASS': + 'rest_framework.serializers.ModelSerializer', + 'DEFAULT_PAGINATION_SERIALIZER_CLASS': + 'rest_framework.pagination.PaginationSerializer', + 'DEFAULT_THROTTLE_RATES': { 'user': None, 'anon': None, }, - - 'MODEL_SERIALIZER': 'rest_framework.serializers.ModelSerializer', - 'PAGINATION_SERIALIZER': 'rest_framework.pagination.PaginationSerializer', 'PAGINATE_BY': None, 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', @@ -65,14 +68,14 @@ DEFAULTS = { # List of settings that may be in string import notation. IMPORT_STRINGS = ( - 'DEFAULT_RENDERERS', - 'DEFAULT_PARSERS', - 'DEFAULT_AUTHENTICATION', - 'DEFAULT_PERMISSIONS', - 'DEFAULT_THROTTLES', - 'DEFAULT_CONTENT_NEGOTIATION', - 'MODEL_SERIALIZER', - 'PAGINATION_SERIALIZER', + 'DEFAULT_RENDERER_CLASSES', + 'DEFAULT_PARSER_CLASSES', + 'DEFAULT_AUTHENTICATION_CLASSES', + 'DEFAULT_PERMISSION_CLASSES', + 'DEFAULT_THROTTLE_CLASSES', + 'DEFAULT_CONTENT_NEGOTIATION_CLASS', + 'DEFAULT_MODEL_SERIALIZER_CLASS', + 'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_TOKEN', ) @@ -111,7 +114,7 @@ class APISettings(object): For example: from rest_framework.settings import api_settings - print api_settings.DEFAULT_RENDERERS + print api_settings.DEFAULT_RENDERER_CLASSES Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. diff --git a/rest_framework/views.py b/rest_framework/views.py index 357d8939..c721be3c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -54,12 +54,12 @@ def _camelcase_to_spaces(content): class APIView(View): settings = api_settings - renderer_classes = api_settings.DEFAULT_RENDERERS - parser_classes = api_settings.DEFAULT_PARSERS - authentication_classes = api_settings.DEFAULT_AUTHENTICATION - throttle_classes = api_settings.DEFAULT_THROTTLES - permission_classes = api_settings.DEFAULT_PERMISSIONS - content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS @classmethod def as_view(cls, **initkwargs): -- cgit v1.2.3 From e8f542aac88677cd95c473d56511cadbc0c67813 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 09:19:38 +0100 Subject: Minor docs fix --- docs/api-guide/parsers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 18a5872c..0985b2ca 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -91,7 +91,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together # Custom parsers -To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, parser_context)` method. +To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method. The method should return the data that will be used to populate the `request.DATA` property. @@ -103,7 +103,7 @@ A stream-like object representing the body of the request. ### media_type -Optional. If provided, this is the media type of the incoming request. +Optional. If provided, this is the media type of the incoming request content. Depending on the request's `Content-Type:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"text/plain; charset=utf-8"`. -- cgit v1.2.3 From d1746e2f3c3c2250ffdf7b71f2a77df3edccea61 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 22:02:44 +0100 Subject: Allow callables in dotted notation like Field(source='foo.bar') --- rest_framework/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb9a523d..663a168d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -70,6 +70,8 @@ class Field(object): value = obj for component in self.source.split('.'): value = getattr(value, component) + if is_simple_callable(value): + value = value() else: value = getattr(obj, field_name) return self.to_native(value) -- cgit v1.2.3 From c341799344ab394322a589ce44328f245910e651 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 22:19:54 +0100 Subject: Apply readonly on RelatedField --- rest_framework/fields.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 663a168d..162eed26 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -224,6 +224,9 @@ class RelatedField(WritableField): return self.to_native(value) def field_from_native(self, data, field_name, into): + if self.readonly: + return + value = data.get(field_name) into[(self.source or field_name) + '_id'] = self.from_native(value) -- cgit v1.2.3 From d70e387f106c269d5d8c447c77ba26bdb1aafc8f Mon Sep 17 00:00:00 2001 From: Ian Strachan Date: Thu, 18 Oct 2012 23:45:16 +0100 Subject: Added tests for dotted notation in serializer field source --- rest_framework/tests/serializer.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 610ed85f..bd1f07da 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,6 +4,11 @@ from rest_framework import serializers from rest_framework.tests.models import * +class SubComment(object): + def __init__(self, sub_comment): + self.sub_comment = sub_comment + + class Comment(object): def __init__(self, email, content, created): self.email = email @@ -13,13 +18,18 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) + + def get_sub_comment(self): + sub_comment = SubComment('And Merry Christmas!') + return sub_comment class CommentSerializer(serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() - + sub_comment = serializers.Field(source='get_sub_comment.sub_comment') + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,7 +52,14 @@ class BasicTests(TestCase): self.data = { 'email': 'tom@example.com', 'content': 'Happy new year!', - 'created': datetime.datetime(2012, 1, 1) + 'created': datetime.datetime(2012, 1, 1), + 'sub_comment': 'This wont change' + } + self.expected = { + 'email': 'tom@example.com', + 'content': 'Happy new year!', + 'created': datetime.datetime(2012, 1, 1), + 'sub_comment': 'And Merry Christmas!' } def test_empty(self): @@ -50,14 +67,14 @@ class BasicTests(TestCase): expected = { 'email': '', 'content': '', - 'created': None + 'created': None, + 'sub_comment': '' } self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) - expected = self.data - self.assertEquals(serializer.data, expected) + serializer = CommentSerializer(instance=self.comment) + self.assertEquals(serializer.data, self.expected) def test_create(self): serializer = CommentSerializer(self.data) @@ -65,6 +82,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertFalse(serializer.object is expected) + self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') def test_update(self): serializer = CommentSerializer(self.data, instance=self.comment) @@ -72,6 +90,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertTrue(serializer.object is expected) + self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') class ValidationTests(TestCase): -- cgit v1.2.3 From 643d3491a65237fef6932ef8833472c243ad7ee8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 23:48:52 +0100 Subject: First pass at pastebin tutorial --- rest_framework/fields.py | 80 ++++++++++++++++++++++++++++++++++++++++----- rest_framework/renderers.py | 13 ++++++-- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb9a523d..14422b27 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -7,6 +7,7 @@ from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve from django.conf import settings +from django.forms import widgets from django.utils.encoding import is_protected_type, smart_unicode from django.utils.translation import ugettext_lazy as _ from rest_framework.reverse import reverse @@ -105,10 +106,14 @@ class WritableField(Field): 'required': _('This field is required.'), 'invalid': _('Invalid value.'), } + widget = widgets.TextInput def __init__(self, source=None, readonly=False, required=None, - validators=[], error_messages=None): + validators=[], error_messages=None, widget=None, + help_text=None, initial=None): + super(WritableField, self).__init__(source=source) + self.readonly = readonly if required is None: self.required = not(readonly) @@ -124,6 +129,15 @@ class WritableField(Field): self.validators = self.default_validators + validators + # These attributes are ony used for HTML forms. + self.initial = initial + self.help_text = help_text and smart_unicode(help_text) or '' + + widget = widget or self.widget + if isinstance(widget, type): + widget = widget() + self.widget = widget + def validate(self, value): if value in validators.EMPTY_VALUES and self.required: raise ValidationError(self.error_messages['required']) @@ -157,9 +171,12 @@ class WritableField(Field): try: native = data[field_name] except KeyError: - if self.required: - raise ValidationError(self.error_messages['required']) - return + if getattr(self, 'missing_value', None) is not None: + native = self.missing_value + else: + if self.required: + raise ValidationError(self.error_messages['required']) + return value = self.from_native(native) if self.source == '*': @@ -394,20 +411,19 @@ class HyperlinkedIdentityField(Field): class BooleanField(WritableField): type_name = 'BooleanField' + widget = widgets.CheckboxInput default_error_messages = { 'invalid': _(u"'%s' value must be either True or False."), } + empty = False + missing_value = False # Fill in missing value not supplied by html form def from_native(self, value): - if value in (True, False): - # if value is 1 or 0 than it's equal to True or False, but we want - # to return a true bool for semantic reasons. - return bool(value) if value in ('t', 'True', '1'): return True if value in ('f', 'False', '0'): return False - raise ValidationError(self.error_messages['invalid'] % value) + return bool(value) class CharField(WritableField): @@ -427,6 +443,52 @@ class CharField(WritableField): return smart_unicode(value) +class ChoiceField(WritableField): + type_name = 'ChoiceField' + widget = widgets.Select + default_error_messages = { + 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), + } + + def __init__(self, choices=(), *args, **kwargs): + super(ChoiceField, self).__init__(*args, **kwargs) + self.choices = choices + + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + # choices can be any iterable, but we call list() on it because + # it will be consumed more than once. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + def validate(self, value): + """ + Validates that the input is in self.choices. + """ + super(ChoiceField, self).validate(value) + if value and not self.valid_value(value): + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + + def valid_value(self, value): + """ + Check to see if the provided value is a valid choice. + """ + for k, v in self.choices: + if isinstance(v, (list, tuple)): + # This is an optgroup, so look inside the group for options + for k2, v2 in v: + if value == smart_unicode(k2): + return True + else: + if value == smart_unicode(k): + return True + return False + + class EmailField(CharField): type_name = 'EmailField' diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b..936bec36 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -279,13 +279,22 @@ class BrowsableAPIRenderer(BaseRenderer): continue kwargs = {} + kwargs['required'] = v.required if getattr(v, 'queryset', None): - kwargs['queryset'] = getattr(v, 'queryset', None) + kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): + kwargs['widget'] = v.widget + if getattr(v, 'initial', None): + kwargs['initial'] = v.initial + if getattr(v, 'help_text', None): + kwargs['help_text'] = v.help_text + kwargs['label'] = k + print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: - fields[k] = forms.CharField() + fields[k] = forms.CharField(**kwargs) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted -- cgit v1.2.3 From dab177e29e45b657bb43705979c8e601d5a1b31b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:20:54 +0100 Subject: Drop help_text --- rest_framework/fields.py | 3 +-- rest_framework/renderers.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 14422b27..0990eadc 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -110,7 +110,7 @@ class WritableField(Field): def __init__(self, source=None, readonly=False, required=None, validators=[], error_messages=None, widget=None, - help_text=None, initial=None): + initial=None): super(WritableField, self).__init__(source=source) @@ -131,7 +131,6 @@ class WritableField(Field): # These attributes are ony used for HTML forms. self.initial = initial - self.help_text = help_text and smart_unicode(help_text) or '' widget = widget or self.widget if isinstance(widget, type): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 936bec36..de2276ad 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -286,8 +286,6 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs['widget'] = v.widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial - if getattr(v, 'help_text', None): - kwargs['help_text'] = v.help_text kwargs['label'] = k print kwargs -- cgit v1.2.3 From a7390fe7044c4f10d15fdbe9de9e594d0ffa2e05 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:47:01 +0100 Subject: Fix up widget choices --- rest_framework/renderers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index de2276ad..ba5489bc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -6,6 +6,7 @@ on the response, such as JSON encoded data or HTML output. REST framework also provides an HTML renderer the renders the browseable API. """ +import copy import string from django import forms from django.http.multipartparser import parse_header @@ -283,11 +284,19 @@ class BrowsableAPIRenderer(BaseRenderer): if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset if getattr(v, 'widget', None): - kwargs['widget'] = v.widget + widget = copy.deepcopy(v.widget) + # If choices have friendly readable names, + # then add in the identities too + if getattr(widget, 'choices', None): + choices = widget.choices + if any([ident != desc for (ident, desc) in choices]): + choices = [(ident, "%s (%s)" % (desc, ident)) + for (ident, desc) in choices] + widget.choices = choices + kwargs['widget'] = widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial kwargs['label'] = k - print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) -- cgit v1.2.3 From efabd2bb1b762fbdee2b48fa3a6ccb8f23c7e8dc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 19:59:46 +0100 Subject: docs, docs, docs, docs, docs, docs, docs --- docs/api-guide/content-negotiation.md | 57 +++++++++++++++++++++++++++++++++-- docs/api-guide/format-suffixes.md | 52 +++++++++++++++++++++++++++++++- docs/api-guide/pagination.md | 6 +++- rest_framework/urlpatterns.py | 13 +++----- 4 files changed, 115 insertions(+), 13 deletions(-) diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md index b95091c5..78dc0747 100644 --- a/docs/api-guide/content-negotiation.md +++ b/docs/api-guide/content-negotiation.md @@ -8,8 +8,59 @@ [cite]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html -**TODO**: Describe content negotiation style used by REST framework. +Content negotiation is the process of selecting one of multiple possible representations to return to a client, based on client or server preferences. -## Custom content negotiation +## Determining the accepted renderer -It's unlikley that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme, override `BaseContentNegotiation`, and implement the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` \ No newline at end of file +REST framework uses a simple style of content negotition to determine which media type should be returned to a client, based on the available renderers, the priorities of each of those renderers, and the client's `Accept:` header. The style used is partly client-driven, and partly server-driven. + +1. More specific media types are given preference to less specific media types. +2. If multiple media types have the same specificity, then preference is given to based on the ordering of the renderers configured for the given view. + +For example, given the following `Accept` header: + + application/json; indent=4, application/json, application/yaml, text/html, */* + +The priorities for each of the given media types would be: + +* `application/json; indent=4` +* `application/json`, `application/yaml` and `text/html` +* `*/*` + +If the requested view was only configured with renderers for `YAML` and `HTML`, then REST framework would select whichever renderer was listed first in the `renderer_classes` list or `DEFAULT_RENDERER_CLASSES` setting. + +For more information on the `HTTP Accept` header, see [RFC 2616][accept-header] + +--- + +**Note**: "q" values are not taken into account by REST framework when determining preference. The use of "q" values negatively impacts caching, and in the author's opinion they are an unnecessary and overcomplicated approach to content negotiation. + +This is a valid approach as the HTTP spec deliberately underspecifies how a server should weight server-based preferences against client-based preferences. + +--- + +# Custom content negotiation + +It's unlikley that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`. + +REST framework's content negotiation classes handle selection of both the approprate parser for the requesr, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods. + +## Example + +The following is a custom content negotiation class which ignores the client +request when selecting the appropriate parser or renderer. + + class IgnoreClientContentNegotiation(BaseContentNegotiation): + def select_parser(self, request, parsers): + """ + Select the first parser in the `.parser_classes` list. + """ + return parsers[0] + + def select_renderer(self, request, renderers, format_suffix): + """ + Select the first renderer in the `.renderer_classes` list. + """ + return renderers[0] + +[accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html \ No newline at end of file diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md index 7d72d9f8..6d5feba4 100644 --- a/docs/api-guide/format-suffixes.md +++ b/docs/api-guide/format-suffixes.md @@ -7,5 +7,55 @@ used all the time. > > — Roy Fielding, [REST discuss mailing list][cite] -[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857 +A common pattern for Web APIs is to use filename extensions on URLs to provide an endpoint for a given media type. For example, 'http://example.com/api/users.json' to serve a JSON representation. + +Adding format-suffix patterns to each individual entry in the URLconf for your API is error-prone and non-DRY, so REST framework provides a shortcut to adding these patterns to your URLConf. + +## format_suffix_patterns + +**Signature**: format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None) + +Returns a URL pattern list which includes format suffix patterns appended to each of the URL patterns provided. + +Arguments: + +* **urlpatterns**: Required. A URL pattern list. +* **suffix_required**: Optional. A boolean indicating if suffixes in the URLs should be optional or mandatory. Defaults to `False`, meaning that suffixes are optional by default. +* **allowed**: Optional. A list or tuple of valid format suffixes. If not provided, a wildcard format suffix pattern will be used. + +Example: + + from rest_framework.urlpatterns import format_suffix_patterns + + urlpatterns = patterns('blog.views', + url(r'^/$', 'api_root'), + url(r'^comment/$', 'comment_root'), + url(r'^comment/(?P[0-9]+)/$', 'comment_instance') + ) + + urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html']) + +When using `format_suffix_patterns`, you must make sure to add the `'format'` keyword argument to the corresponding views. For example. + @api_view(('GET',)) + def api_root(request, format=None): + # do stuff... + +The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` setting. + +Also note that `format_suffix_patterns` does not support descending into `include` URL patterns. + +--- + +## Accept headers vs. format suffixes + +There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead. + +It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators: + +“That's why I always prefer extensions. Neither choice has anything to do with REST.” — Roy Fielding, [REST discuss mailing list][cite2] + +The quote does not mention Accept headers, but it does make it clear that format suffixes should be considered an acceptable pattern. + +[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857 +[cite2]: http://tech.groups.yahoo.com/group/rest-discuss/message/14844 \ No newline at end of file diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index e416de02..597baba4 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -100,12 +100,16 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. -## Custom pagination serializers +--- + +# Custom pagination serializers To create a custom pagination serializer class you should override `pagination.BasePaginationSerializer` and set the fields that you want the serializer to return. You can also override the name used for the object list field, by setting the `results_field` attribute, which defaults to `'results'`. +## Example + For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this. class LinksSerializer(serializers.Serializer): diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 386c78a2..316ccd19 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -2,26 +2,23 @@ from django.conf.urls.defaults import url from rest_framework.settings import api_settings -def format_suffix_patterns(urlpatterns, suffix_required=False, - suffix_kwarg=None, allowed=None): +def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): """ Supplement existing urlpatterns with corrosponding patterns that also include a '.format' suffix. Retains urlpattern ordering. + urlpatterns: + A list of URL patterns. + suffix_required: If `True`, only suffixed URLs will be generated, and non-suffixed URLs will not be used. Defaults to `False`. - suffix_kwarg: - The name of the kwarg that will be passed to the view. - Defaults to 'format'. - allowed: An optional tuple/list of allowed suffixes. eg ['json', 'api'] Defaults to `None`, which allows any suffix. - """ - suffix_kwarg = suffix_kwarg or api_settings.FORMAT_SUFFIX_KWARG + suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG if allowed: if len(allowed) == 1: allowed_pattern = allowed[0] -- cgit v1.2.3 From 71a93930fd4df7a1f5f92c67633b813a26a5e938 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 16:34:07 +0200 Subject: Fixing spelling errors. --- docs/api-guide/authentication.md | 2 +- docs/api-guide/content-negotiation.md | 8 ++++---- docs/api-guide/exceptions.md | 4 ++-- docs/api-guide/fields.md | 8 ++++---- docs/api-guide/parsers.md | 4 ++-- docs/api-guide/permissions.md | 2 +- docs/api-guide/renderers.md | 4 ++-- docs/api-guide/requests.md | 12 ++++++------ docs/api-guide/responses.md | 2 +- docs/api-guide/reverse.md | 2 +- docs/api-guide/serializers.md | 4 ++-- docs/api-guide/throttling.md | 6 +++--- docs/index.md | 4 ++-- docs/topics/csrf.md | 4 ++-- docs/topics/rest-hypermedia-hateoas.md | 6 +++--- docs/tutorial/2-requests-and-responses.md | 2 +- docs/tutorial/3-class-based-views.md | 2 +- docs/tutorial/6-resource-orientated-projects.md | 2 +- rest_framework/throttling.py | 4 ++-- 19 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 5e5ee4ed..7bad4867 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -86,7 +86,7 @@ You'll also need to create tokens for your users. token = Token.objects.create(user=...) print token.key -For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace seperating the two strings. For example: +For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace separating the two strings. For example: Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md index 78dc0747..10288c94 100644 --- a/docs/api-guide/content-negotiation.md +++ b/docs/api-guide/content-negotiation.md @@ -12,7 +12,7 @@ Content negotiation is the process of selecting one of multiple possible represe ## Determining the accepted renderer -REST framework uses a simple style of content negotition to determine which media type should be returned to a client, based on the available renderers, the priorities of each of those renderers, and the client's `Accept:` header. The style used is partly client-driven, and partly server-driven. +REST framework uses a simple style of content negotiation to determine which media type should be returned to a client, based on the available renderers, the priorities of each of those renderers, and the client's `Accept:` header. The style used is partly client-driven, and partly server-driven. 1. More specific media types are given preference to less specific media types. 2. If multiple media types have the same specificity, then preference is given to based on the ordering of the renderers configured for the given view. @@ -41,9 +41,9 @@ This is a valid approach as the HTTP spec deliberately underspecifies how a serv # Custom content negotiation -It's unlikley that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`. +It's unlikely that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`. -REST framework's content negotiation classes handle selection of both the approprate parser for the requesr, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods. +REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods. ## Example @@ -63,4 +63,4 @@ request when selecting the appropriate parser or renderer. """ return renderers[0] -[accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html \ No newline at end of file +[accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 33cf1ca8..ba57fde8 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -25,7 +25,7 @@ For example, the following request: DELETE http://api.example.com/foo/bar HTTP/1.1 Accept: application/json -Might recieve an error response indicating that the `DELETE` method is not allowed on that resource: +Might receive an error response indicating that the `DELETE` method is not allowed on that resource: HTTP/1.1 405 Method Not Allowed Content-Type: application/json; charset=utf-8 @@ -85,4 +85,4 @@ Raised when an incoming request fails the throttling checks. By default this exception results in a response with the HTTP status code "429 Too Many Requests". -[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html \ No newline at end of file +[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index dc9ab045..b3cf186c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -42,7 +42,7 @@ A serializer definition that looked like this: class Meta: fields = ('url', 'owner', 'name', 'expired') -Would produced output similar to: +Would produce output similar to: { 'url': 'http://example.com/api/accounts/3/', @@ -51,7 +51,7 @@ Would produced output similar to: 'expired': True } -Be default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when neccesary. +By default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when necessary. You can customize this behaviour by overriding the `.to_native(self, value)` method. @@ -84,7 +84,7 @@ or `django.db.models.fields.TextField`. ## EmailField -A text representation, validates the text to be a valid e-mail adress. Corresponds to `django.db.models.fields.EmailField` +A text representation, validates the text to be a valid e-mail address. Corresponds to `django.db.models.fields.EmailField` ## DateField @@ -165,7 +165,7 @@ And a model serializer defined like this: model = Bookmark exclude = ('id',) -The an example output format for a Bookmark instance would be: +Then an example output format for a Bookmark instance would be: { 'tags': [u'django', u'python'], diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index c35dfd05..ac904720 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -8,7 +8,7 @@ sending more complex data than simple forms > > — Malcom Tredinnick, [Django developers group][cite] -REST framework includes a number of built in Parser classes, that allow you to accept requests with various media types. There is also support for defining your own custom parsers, which gives you the flexiblity to design the media types that your API accepts. +REST framework includes a number of built in Parser classes, that allow you to accept requests with various media types. There is also support for defining your own custom parsers, which gives you the flexibility to design the media types that your API accepts. ## How the parser is determined @@ -65,7 +65,7 @@ Parses `YAML` request content. Parses REST framework's default style of `XML` request content. -Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`. +Note that the `XML` markup language is typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`. If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type. diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 249f3938..0b7b32e9 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -6,7 +6,7 @@ > > — [Apple Developer Documentation][cite] -Together with [authentication] and [throttling], permissions determine wheter a request should be granted or denied access. +Together with [authentication] and [throttling], permissions determine whether a request should be granted or denied access. Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted. diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b3b8d5bc..b6db376c 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -6,7 +6,7 @@ > > — [Django documentation][cite] -REST framework includes a number of built in Renderer classes, that allow you to return responses with various media types. There is also support for defining your own custom renderers, which gives you the flexiblity to design your own media types. +REST framework includes a number of built in Renderer classes, that allow you to return responses with various media types. There is also support for defining your own custom renderers, which gives you the flexibility to design your own media types. ## How the renderer is determined @@ -229,7 +229,7 @@ For example: ## Designing your media types -For the purposes of many Web APIs, simple `JSON` responses with hyperlinked relations may be sufficient. If you want to fully embrace RESTful design and [HATEOAS] you'll neeed to consider the design and usage of your media types in more detail. +For the purposes of many Web APIs, simple `JSON` responses with hyperlinked relations may be sufficient. If you want to fully embrace RESTful design and [HATEOAS] you'll need to consider the design and usage of your media types in more detail. In [the words of Roy Fielding][quote], "A REST API should spend almost all of its descriptive effort in defining the media type(s) used for representing resources and driving application state, or in defining extended relation names and/or hypertext-enabled mark-up for existing standard media types.". diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 2770c6dd..72932f5d 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -25,19 +25,19 @@ 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`. +`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 is used for `request.DATA`. For more details see the [parsers documentation]. ## .QUERY_PARAMS -`request.QUERY_PARAMS` is a more correcly named synonym for `request.GET`. +`request.QUERY_PARAMS` is a more correctly named synonym for `request.GET`. 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 -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_PARSER_CLASSES` setting. +The `APIView` class or `@api_view` decorator will ensure that this property is automatically set to a list of `Parser` instances, based on the `parser_classes` set on the view or based on the `DEFAULT_PARSER_CLASSES` setting. You won't typically need to access this property. @@ -51,7 +51,7 @@ If a client sends a request with a content-type that cannot be parsed then a `Un # Authentication -REST framework provides flexbile, per-request authentication, that gives you the abilty to: +REST framework provides flexible, per-request authentication, that gives you the ability to: * Use different authentication policies for different parts of your API. * Support the use of multiple authentication policies. @@ -75,7 +75,7 @@ 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. +The `APIView` class or `@api_view` decorator will ensure that this property is automatically set 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. @@ -83,7 +83,7 @@ You won't typically need to access this property. # Browser enhancements -REST framework supports a few browser enhancments such as browser-based `PUT` and `DELETE` forms. +REST framework supports a few browser enhancements such as browser-based `PUT` and `DELETE` forms. ## .method diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index 395decda..794f9377 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -86,7 +86,7 @@ The `Response` class extends `SimpleTemplateResponse`, and all the usual attribu **Signature:** `.render()` -As with any other `TemplateResponse`, this methd is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance. +As with any other `TemplateResponse`, this method is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance. You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle. diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 12346eb4..19930dc3 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -6,7 +6,7 @@ > > — Roy Fielding, [Architectural Styles and the Design of Network-based Software Architectures][cite] -As a rule, it's probably better practice to return absolute URIs from you Web APIs, such as `http://example.com/foobar`, rather than returning relative URIs, such as `/foobar`. +As a rule, it's probably better practice to return absolute URIs from your Web APIs, such as `http://example.com/foobar`, rather than returning relative URIs, such as `/foobar`. The advantages of doing so are: diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 47958fe3..c10a3f44 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -175,7 +175,7 @@ You can add extra fields to a `ModelSerializer` or override the default fields b class Meta: model = Account -Extra fields can corrospond to any property or callable on the model. +Extra fields can correspond to any property or callable on the model. ## Relational fields @@ -187,7 +187,7 @@ The `PrimaryKeyRelatedField` and `HyperlinkedRelatedField` fields provide altern The `ModelSerializer` class can itself be used as a field, in order to serialize relationships using nested representations. -The `RelatedField` class may be subclassed to create a custom represenation of a relationship. The subclass should override `.to_native()`, and optionally `.from_native()` if deserialization is supported. +The `RelatedField` class may be subclassed to create a custom representation of a relationship. The subclass should override `.to_native()`, and optionally `.from_native()` if deserialization is supported. All the relational fields may be used for any relationship or reverse relationship on a model. diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 435b20ce..d54433b1 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -80,7 +80,7 @@ The allowed request rate is determined from one of the following (in order of pr ## UserRateThrottle -The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticted requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against. +The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against. The allowed request rate is determined from one of the following (in order of preference). @@ -114,7 +114,7 @@ For example, multiple user throttle rates could be implemented by using the foll ## ScopedRateThrottle -The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unqiue user id or IP address. +The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address. The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope". @@ -152,6 +152,6 @@ User requests to either `ContactListView` or `ContactDetailView` would be restri To create a custom throttle, override `BaseThrottle` and implement `.allow_request(request, view)`. The method should return `True` if the request should be allowed, and `False` otherwise. -Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recomended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`. +Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`. [permissions]: permissions.md diff --git a/docs/index.md b/docs/index.md index 02bda081..f66ba7f4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,7 @@ Add `rest_framework` to your `INSTALLED_APPS`. 'rest_framework', ) -If you're intending to use the browserable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file. +If you're intending to use the browseable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file. urlpatterns = patterns('', ... @@ -195,4 +195,4 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [DabApps]: http://dabapps.com -[email]: mailto:tom@tomchristie.com \ No newline at end of file +[email]: mailto:tom@tomchristie.com diff --git a/docs/topics/csrf.md b/docs/topics/csrf.md index a2ee1b9c..043144c1 100644 --- a/docs/topics/csrf.md +++ b/docs/topics/csrf.md @@ -5,8 +5,8 @@ > — [Jeff Atwood][cite] * Explain need to add CSRF token to AJAX requests. -* Explain defered CSRF style used by REST framework +* Explain deferred CSRF style used by REST framework * Why you should use Django's standard login/logout views, and not REST framework view -[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html \ No newline at end of file +[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html diff --git a/docs/topics/rest-hypermedia-hateoas.md b/docs/topics/rest-hypermedia-hateoas.md index 8b0309b9..d7646892 100644 --- a/docs/topics/rest-hypermedia-hateoas.md +++ b/docs/topics/rest-hypermedia-hateoas.md @@ -4,7 +4,7 @@ > > — Mike Amundsen, [REST fest 2012 keynote][cite]. -First off, the disclaimer. The name "Django REST framework" was choosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs". +First off, the disclaimer. The name "Django REST framework" was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs". If you are serious about designing a Hypermedia APIs, you should look to resources outside of this documentation to help inform your design choices. @@ -22,7 +22,7 @@ For a more thorough background, check out Klabnik's [Hypermedia API reading list ## Building Hypermedia APIs with REST framework -REST framework is an agnositic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style. +REST framework is an agnostic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style. ## What REST framework provides. @@ -50,4 +50,4 @@ What REST framework doesn't do is give you is machine readable hypermedia format [parser]: ../api-guide/parsers.md [renderer]: ../api-guide/renderers.md [fields]: ../api-guide/fields.md -[conneg]: ../api-guide/content-negotiation.md \ No newline at end of file +[conneg]: ../api-guide/content-negotiation.md diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 7c8fc044..fc37322a 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -5,7 +5,7 @@ Let's introduce a couple of essential building blocks. ## Request objects -REST framework intoduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs. +REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs. request.POST # Only handles form data. Only works for 'POST' method. request.DATA # Handles arbitrary data. Works any HTTP request with content. diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 2f273364..0ee81ea3 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -107,7 +107,7 @@ Let's take a look at how we can compose our views by using the mixin classes. We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectBaseView`, and adding in `ListModelMixin` and `CreateModelMixin`. -The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explictly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far. +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, diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index e7190a77..9ee599ae 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -5,7 +5,7 @@ Resource classes are just View classes that don't have any handler methods bound This allows us to: * Encapsulate common behaviour across a class of views, in a single Resource class. -* Separate out the actions of a Resource from the specfics of how those actions should be bound to a particular set of URLs. +* Separate out the actions of a Resource from the specifics of how those actions should be bound to a particular set of URLs. ## Refactoring to use Resources, not Views diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 6e7a0b72..6860e6b9 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -60,7 +60,7 @@ class SimpleRateThrottle(BaseThrottle): Determine the string representation of the allowed request rate. """ if not getattr(self, 'scope', None): - msg = ("You must set either `.scope` or `.rate` for '%s' thottle" % + msg = ("You must set either `.scope` or `.rate` for '%s' throttle" % self.__class__.__name__) raise exceptions.ConfigurationError(msg) @@ -137,7 +137,7 @@ class AnonRateThrottle(SimpleRateThrottle): """ Limits the rate of API calls that may be made by a anonymous users. - The IP address of the request will be used as the unqiue cache key. + The IP address of the request will be used as the unique cache key. """ scope = 'anon' -- cgit v1.2.3 From 65d4970bf71d31669f10dc0cecd4a2a00acd7393 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 16:34:50 +0200 Subject: Changed IsAdmin -> IsAdminUser in example --- docs/api-guide/views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 77349252..e3fbadb2 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -27,7 +27,7 @@ For example: * Only admin users are able to access this view. """ authentication_classes = (authentication.TokenAuthentication,) - permission_classes = (permissions.IsAdmin,) + permission_classes = (permissions.IsAdminUser,) def get(self, request, format=None): """ @@ -123,4 +123,4 @@ REST framework also gives you to work with regular function based views... **[TODO]** [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html -[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html \ No newline at end of file +[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html -- cgit v1.2.3 From 13d0a829390105aa53602be7dc713092ead5a66c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 21 Oct 2012 17:40:49 +0100 Subject: Minor docs tweaks --- docs/api-guide/fields.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index dc9ab045..234c65ad 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -73,34 +73,52 @@ These fields represent basic datatypes, and support both reading and writing val ## BooleanField -A Boolean representation, corresponds to `django.db.models.fields.BooleanField`. +A Boolean representation. + +Corresponds to `django.db.models.fields.BooleanField`. ## CharField -A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`, corresponds to `django.db.models.fields.CharField` +A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`. + +Corresponds to `django.db.models.fields.CharField` or `django.db.models.fields.TextField`. -**Signature:** `CharField([max_length=[, min_length=]])` +**Signature:** `CharField(max_length=None, min_length=None)` + +## ChoiceField + +A field that can accept on of a limited set of choices. ## EmailField -A text representation, validates the text to be a valid e-mail adress. Corresponds to `django.db.models.fields.EmailField` +A text representation, validates the text to be a valid e-mail adress. + +Corresponds to `django.db.models.fields.EmailField` ## DateField -A date representation. Corresponds to `django.db.models.fields.DateField` +A date representation. + +Corresponds to `django.db.models.fields.DateField` ## DateTimeField -A date and time representation. Corresponds to `django.db.models.fields.DateTimeField` +A date and time representation. + +Corresponds to `django.db.models.fields.DateTimeField` ## IntegerField -An integer representation. Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField` +An integer representation. + +Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField` ## FloatField -A floating point representation. Corresponds to `django.db.models.fields.FloatField`. +A floating point representation. + +Corresponds to `django.db.models.fields.FloatField`. --- -- cgit v1.2.3 From 93f1aa4f69df85add114c9730a01b50d013a844a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 21 Oct 2012 17:41:05 +0100 Subject: Remove `initial` kwarg, add `default`. --- rest_framework/fields.py | 18 +++++++++++------- rest_framework/renderers.py | 8 ++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0990eadc..29940946 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -107,10 +107,11 @@ class WritableField(Field): 'invalid': _('Invalid value.'), } widget = widgets.TextInput + default = None def __init__(self, source=None, readonly=False, required=None, validators=[], error_messages=None, widget=None, - initial=None): + default=None): super(WritableField, self).__init__(source=source) @@ -128,10 +129,9 @@ class WritableField(Field): self.error_messages = messages self.validators = self.default_validators + validators + self.default = default or self.default - # These attributes are ony used for HTML forms. - self.initial = initial - + # Widgets are ony used for HTML forms. widget = widget or self.widget if isinstance(widget, type): widget = widget() @@ -170,8 +170,8 @@ class WritableField(Field): try: native = data[field_name] except KeyError: - if getattr(self, 'missing_value', None) is not None: - native = self.missing_value + if self.default is not None: + native = self.default else: if self.required: raise ValidationError(self.error_messages['required']) @@ -415,7 +415,11 @@ class BooleanField(WritableField): 'invalid': _(u"'%s' value must be either True or False."), } empty = False - missing_value = False # Fill in missing value not supplied by html form + + # Note: we set default to `False` in order to fill in missing value not + # supplied by html form. TODO: Fix so that only html form input gets + # this behavior. + default = False def from_native(self, value): if value in ('t', 'True', '1'): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba5489bc..b2dbffd2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -281,8 +281,10 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs = {} kwargs['required'] = v.required + if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): widget = copy.deepcopy(v.widget) # If choices have friendly readable names, @@ -294,8 +296,10 @@ class BrowsableAPIRenderer(BaseRenderer): for (ident, desc) in choices] widget.choices = choices kwargs['widget'] = widget - if getattr(v, 'initial', None): - kwargs['initial'] = v.initial + + if getattr(v, 'default', None) is not None: + kwargs['initial'] = v.default + kwargs['label'] = k try: -- cgit v1.2.3 From c30712a5c8e89c7d3e235c72867288a4cb5c8c85 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 22:23:54 +0200 Subject: Remove redundant check if method=='DELETE' --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b..ba07f6cd 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -288,7 +288,7 @@ class BrowsableAPIRenderer(BaseRenderer): fields[k] = forms.CharField() OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + if obj: data = serializer.data form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From ab1a12bfecf49061cb31dff05add26d96078771e Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 23:04:12 +0200 Subject: Refactoring BrowsableAPIRenderer --- rest_framework/renderers.py | 52 +++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba07f6cd..43175861 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -223,11 +223,9 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def get_form(self, view, method, request): + def show_form_for_method(self, view, method, request): """ - Get a form, possibly bound to either the input or output data. - In the absence on of the Resource having an associated form then - provide a form that can be used to submit arbitrary content. + Returns True if a form should be shown for this method. """ if not method in view.allowed_methods: return # Not a valid method @@ -241,20 +239,9 @@ class BrowsableAPIRenderer(BaseRenderer): return # Don't have permission except: return # Don't have permission and exception explicitly raise + return True - if method == 'DELETE' or method == 'OPTIONS': - return True # Don't actually need to return a form - - if (not getattr(view, 'get_serializer', None) or - not parsers.FormParser in getattr(view, 'parser_classes')): - media_types = [parser.media_type for parser in view.parser_classes] - return self.get_generic_content_form(media_types) - - ##### - # TODO: This is a little bit of a hack. Actually we'd like to remove - # this and just render serializer fields to html directly. - - # We need to map our Fields to Django's Fields. + def serializer_to_form_fields(self, serializer): field_mapping = { serializers.FloatField: forms.FloatField, serializers.IntegerField: forms.IntegerField, @@ -267,13 +254,7 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField } - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - - serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): if getattr(v, 'readonly', True): continue @@ -286,6 +267,31 @@ class BrowsableAPIRenderer(BaseRenderer): fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: fields[k] = forms.CharField() + return fields + + def get_form(self, view, method, request): + """ + Get a form, possibly bound to either the input or output data. + In the absence on of the Resource having an associated form then + provide a form that can be used to submit arbitrary content. + """ + if not self.show_form_for_method(view, method, request): + return + + if method == 'DELETE' or method == 'OPTIONS': + return True # Don't actually need to return a form + + if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: + media_types = [parser.media_type for parser in view.parser_classes] + return self.get_generic_content_form(media_types) + + # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python + obj, data = None, None + if getattr(view, 'object', None): + obj = view.object + + serializer = view.get_serializer(instance=obj) + fields = self.serializer_to_form_fields(serializer) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj: -- cgit v1.2.3 From 45d4622f090f8d81a04b4d3e888017419676bbc0 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 22 Oct 2012 15:12:25 +0100 Subject: Fix serialization of reverse relationships --- rest_framework/serializers.py | 23 +++++++++++++---------- rest_framework/tests/models.py | 11 +++++++++++ rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6724bbdf..221cbf2f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -247,6 +247,19 @@ class BaseSerializer(Field): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def field_to_native(self, obj, field_name): + """ + Override default so that we can apply ModelSerializer as a nested + field to relationships. + """ + obj = getattr(obj, self.source or field_name) + + # If the object has an "all" method, assume it's a relationship + if is_simple_callable(getattr(obj, 'all', None)): + return [self.to_native(item) for item in obj.all()] + + return self.to_native(obj) + @property def errors(self): """ @@ -295,16 +308,6 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions - def field_to_native(self, obj, field_name): - """ - Override default so that we can apply ModelSerializer as a nested - field to relationships. - """ - obj = getattr(obj, self.source or field_name) - if obj.__class__.__name__ in ('RelatedManager', 'ManyRelatedManager'): - return [self.to_native(item) for item in obj.all()] - return self.to_native(obj) - def default_fields(self, serialize, obj=None, data=None, nested=False): """ Return all the fields that should be serialized for the model. diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 75dab2f7..8e721737 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -92,6 +92,17 @@ class Comment(RESTFrameworkModel): content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) done = models.BooleanField(default=False) + + +# Models for reverse relations +class BlogPost(RESTFrameworkModel): + title = models.CharField(max_length=100) + + +class BlogPostComment(RESTFrameworkModel): + text = models.TextField() + blog_post = models.ForeignKey(BlogPost) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index bd1f07da..2dfc04e1 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -302,3 +302,32 @@ class CallableDefaultValueTests(TestCase): self.assertEquals(len(self.objects.all()), 1) self.assertEquals(instance.pk, 1) self.assertEquals(instance.text, 'overridden') + + +class ManyRelatedTests(TestCase): + def setUp(self): + + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField() + comments = BlogPostCommentSerializer(source='blogpostcomment_set') + + self.serializer_class = BlogPostSerializer + + def test_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + serializer = self.serializer_class(instance=post) + expected = { + 'title': 'Test blog post', + 'comments': [ + {'text': 'I hate this blog post'}, + {'text': 'I love this blog post'} + ] + } + + self.assertEqual(serializer.data, expected) -- cgit v1.2.3 From aba0172f5c988af145113678fe3d4f411111d4ff Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Mon, 22 Oct 2012 21:31:15 +0300 Subject: Update docs/api-guide/fields.md Fix typo.--- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 189ed76f..7e117df7 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -88,7 +88,7 @@ or `django.db.models.fields.TextField`. ## ChoiceField -A field that can accept on of a limited set of choices. +A field that can accept a value out of a limited set of choices. ## EmailField -- cgit v1.2.3 From c7a0d52fd7e22fbc4a01ff900bd3b2c1215e984d Mon Sep 17 00:00:00 2001 From: Ian Strachan Date: Mon, 22 Oct 2012 22:24:26 +0100 Subject: #314 Fix for manytomany field being required in the payload even though the field is specified as readonly in the serializer --- rest_framework/fields.py | 3 +++ rest_framework/tests/models.py | 5 ++++ rest_framework/tests/serializer.py | 54 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f610d6aa..6ed37823 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -256,6 +256,9 @@ class ManyRelatedMixin(object): return [self.to_native(item) for item in value.all()] def field_from_native(self, data, field_name, into): + if self.readonly: + return + try: # Form data value = data.getlist(self.source or field_name) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 8e721737..97cd0849 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,7 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) + +class ReadOnlyManyToManyModel(RESTFrameworkModel): + text = models.CharField(max_length=100, default='anchor') + rel = models.ManyToManyField(Anchor) + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 2dfc04e1..c614b66a 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -246,6 +246,60 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) + +class ReadOnlyManyToManyTests(TestCase): + def setUp(self): + class ReadOnlyManyToManySerializer(serializers.ModelSerializer): + rel = serializers.ManyRelatedField(readonly=True) + class Meta: + model = ReadOnlyManyToManyModel + + self.serializer_class = ReadOnlyManyToManySerializer + + # An anchor instance to use for the relationship + self.anchor = Anchor() + self.anchor.save() + + # A model instance with a many to many relationship to the anchor + self.instance = ReadOnlyManyToManyModel() + self.instance.save() + self.instance.rel.add(self.anchor) + + # A serialized representation of the model instance + self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} + + + def test_update(self): + """ + Attempt to update an instance of a model with a ManyToMany + relationship. Not updated due to readonly=True + """ + new_anchor = Anchor() + new_anchor.save() + data = {'rel': [self.anchor.id, new_anchor.id]} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + # rel is still as original (1 entry) + self.assertEquals(list(instance.rel.all()), [self.anchor]) + + def test_update_without_relationship(self): + """ + Attempt to update an instance of a model where many to ManyToMany + relationship is not supplied. Not updated due to readonly=True + """ + new_anchor = Anchor() + new_anchor.save() + data = {} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + # rel is still as original (1 entry) + self.assertEquals(list(instance.rel.all()), [self.anchor]) class DefaultValueTests(TestCase): -- cgit v1.2.3 From 51fae73f3d565e2702c72ff9841cc072d6490804 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 09:28:10 +0100 Subject: Implement per-field validation on Serializers --- docs/api-guide/serializers.md | 17 +++++++++++++++++ rest_framework/serializers.py | 18 ++++++++++++++++++ rest_framework/tests/serializer.py | 25 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index c10a3f44..e1e12e74 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -78,6 +78,23 @@ When deserializing data, you always need to call `is_valid()` before attempting **TODO: Describe validation in more depth** +## Custom field validation + +Like Django forms, you can specify custom field-level validation by adding `clean_()` methods to your `Serializer` subclass. This method takes a dictionary of deserialized data as a first argument, and the field name in that data as a second argument (which will be either the name of the field or the value of the `source` argument, if one was provided.) It should either return the data dictionary or raise a `ValidationError`. For example: + + class BlogPostSerializer(Serializer): + title = serializers.CharField(max_length=100) + content = serializers.CharField() + + def clean_title(self, data, source): + """ + Check that the blog post is about Django + """ + value = data[source] + if "Django" not in value: + raise ValidationError("Blog post is not about Django") + return data + ## Dealing with nested objects The previous example is fine for dealing with objects that only have simple datatypes, but sometimes we also need to be able to represent more complex objects, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 221cbf2f..c9c4faa3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,6 +208,23 @@ class BaseSerializer(Field): return reverted_data + def clean_fields(self, data): + """ + Run clean_ validators on the serializer + """ + fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) + + for field_name, field in fields.items(): + try: + clean_method = getattr(self, 'clean_%s' % field_name, None) + if clean_method: + source = field.source or field_name + data = clean_method(data, source) + except ValidationError as err: + self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) + + return data + def restore_object(self, attrs, instance=None): """ Deserialize a dictionary of attributes into an object instance. @@ -241,6 +258,7 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) + attrs = self.clean_fields(attrs) else: self._errors['non_field_errors'] = 'No input provided' diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index c614b66a..35908449 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -138,6 +138,31 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.errors, {}) + def test_field_validation(self): + + class CommentSerializerWithFieldValidator(CommentSerializer): + + def clean_content(self, attrs, source): + value = attrs[source] + if "test" not in value: + raise serializers.ValidationError("Test not in value") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A test comment', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'This should not validate' + + serializer = CommentSerializerWithFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 388a807f64f60d84556288e2ade4f0fe57a8e66b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:27:01 +0100 Subject: Switch from clean_ to validate_, clarify documentation --- docs/api-guide/serializers.md | 10 ++++++---- rest_framework/serializers.py | 4 ++-- rest_framework/tests/serializer.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index e1e12e74..9011d31f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -80,19 +80,21 @@ When deserializing data, you always need to call `is_valid()` before attempting ## Custom field validation -Like Django forms, you can specify custom field-level validation by adding `clean_()` methods to your `Serializer` subclass. This method takes a dictionary of deserialized data as a first argument, and the field name in that data as a second argument (which will be either the name of the field or the value of the `source` argument, if one was provided.) It should either return the data dictionary or raise a `ValidationError`. For example: +You can specify custom field-level validation by adding `validate_()` methods to your `Serializer` subclass. These are analagous to `clean_` methods on Django forms, but accept slightly different arguments. They take a dictionary of deserialized data as a first argument, and the field name in that data as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided). Your `validate_` methods should either just return the data dictionary or raise a `ValidationError`. For example: - class BlogPostSerializer(Serializer): + from rest_framework import serializers + + class BlogPostSerializer(serializers.Serializer): title = serializers.CharField(max_length=100) content = serializers.CharField() - def clean_title(self, data, source): + def validate_title(self, data, source): """ Check that the blog post is about Django """ value = data[source] if "Django" not in value: - raise ValidationError("Blog post is not about Django") + raise serializers.ValidationError("Blog post is not about Django") return data ## Dealing with nested objects diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c9c4faa3..802ca55f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -210,13 +210,13 @@ class BaseSerializer(Field): def clean_fields(self, data): """ - Run clean_ validators on the serializer + Run validate_ methods on the serializer """ fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) for field_name, field in fields.items(): try: - clean_method = getattr(self, 'clean_%s' % field_name, None) + clean_method = getattr(self, 'validate_%s' % field_name, None) if clean_method: source = field.source or field_name data = clean_method(data, source) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 35908449..a32de80d 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -142,7 +142,7 @@ class ValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): - def clean_content(self, attrs, source): + def validate_content(self, attrs, source): value = attrs[source] if "test" not in value: raise serializers.ValidationError("Test not in value") -- cgit v1.2.3 From ac2d39892d6b3fbbe5cd53b9ef83367249ba4880 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:39:17 +0100 Subject: Add cross-field validate method --- docs/api-guide/serializers.md | 8 +++++--- rest_framework/serializers.py | 13 +++++++++++++ rest_framework/tests/serializer.py | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 9011d31f..40f8a170 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -76,9 +76,7 @@ Deserialization is similar. First we parse a stream into python native datatype When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages. -**TODO: Describe validation in more depth** - -## Custom field validation +### Field-level validation You can specify custom field-level validation by adding `validate_()` methods to your `Serializer` subclass. These are analagous to `clean_` methods on Django forms, but accept slightly different arguments. They take a dictionary of deserialized data as a first argument, and the field name in that data as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided). Your `validate_` methods should either just return the data dictionary or raise a `ValidationError`. For example: @@ -97,6 +95,10 @@ You can specify custom field-level validation by adding `validate_()` raise serializers.ValidationError("Blog post is not about Django") return data +### Final cross-field validation + +To do any other validation that requires access to multiple fields, add a method called `validate` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. + ## Dealing with nested objects The previous example is fine for dealing with objects that only have simple datatypes, but sometimes we also need to be able to represent more complex objects, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 802ca55f..15fe26ee 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -225,6 +225,18 @@ class BaseSerializer(Field): return data + def clean_all(self, attrs): + """ + Run the `validate` method on the serializer, if it exists + """ + try: + validate_method = getattr(self, 'validate', None) + if validate_method: + attrs = validate_method(attrs) + except ValidationError as err: + self._errors['non_field_errors'] = err.messages + return attrs + def restore_object(self, attrs, instance=None): """ Deserialize a dictionary of attributes into an object instance. @@ -259,6 +271,7 @@ class BaseSerializer(Field): if data is not None: attrs = self.restore_fields(data) attrs = self.clean_fields(attrs) + attrs = self.clean_all(attrs) else: self._errors['non_field_errors'] = 'No input provided' diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a32de80d..936f15aa 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -163,6 +163,30 @@ class ValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + def test_cross_field_validation(self): + + class CommentSerializerWithCrossFieldValidator(CommentSerializer): + + def validate(self, attrs): + if attrs["email"] not in attrs["content"]: + raise serializers.ValidationError("Email address not in content") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A comment from tom@example.com', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'A comment from foo@bar.com' + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From d60d598e0255fb3d55a1213d1025447d83523658 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:43:30 +0100 Subject: Clean up internal names and documentation --- docs/api-guide/serializers.md | 8 ++++---- rest_framework/serializers.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 40f8a170..50505d30 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -78,7 +78,7 @@ When deserializing data, you always need to call `is_valid()` before attempting ### Field-level validation -You can specify custom field-level validation by adding `validate_()` methods to your `Serializer` subclass. These are analagous to `clean_` methods on Django forms, but accept slightly different arguments. They take a dictionary of deserialized data as a first argument, and the field name in that data as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided). Your `validate_` methods should either just return the data dictionary or raise a `ValidationError`. For example: +You can specify custom field-level validation by adding `validate_()` methods to your `Serializer` subclass. These are analagous to `clean_` methods on Django forms, but accept slightly different arguments. They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided). Your `validate_` methods should either just return the attrs dictionary or raise a `ValidationError`. For example: from rest_framework import serializers @@ -86,14 +86,14 @@ You can specify custom field-level validation by adding `validate_()` title = serializers.CharField(max_length=100) content = serializers.CharField() - def validate_title(self, data, source): + def validate_title(self, attrs, source): """ Check that the blog post is about Django """ - value = data[source] + value = attrs[source] if "Django" not in value: raise serializers.ValidationError("Blog post is not about Django") - return data + return attrs ### Final cross-field validation diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 15fe26ee..2f8108d1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,24 +208,24 @@ class BaseSerializer(Field): return reverted_data - def clean_fields(self, data): + def validate_fields(self, attrs): """ Run validate_ methods on the serializer """ - fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) + fields = self.get_fields(serialize=False, data=attrs, nested=self.opts.nested) for field_name, field in fields.items(): try: clean_method = getattr(self, 'validate_%s' % field_name, None) if clean_method: source = field.source or field_name - data = clean_method(data, source) + attrs = clean_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) - return data + return attrs - def clean_all(self, attrs): + def validate_all(self, attrs): """ Run the `validate` method on the serializer, if it exists """ @@ -270,10 +270,10 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) - attrs = self.clean_fields(attrs) - attrs = self.clean_all(attrs) + attrs = self.validate_fields(attrs) + attrs = self.validate_all(attrs) else: - self._errors['non_field_errors'] = 'No input provided' + self._errors['non_field_errors'] = ['No input provided'] if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) -- cgit v1.2.3 From 607c31c6d880501e5dc524fc5a5e1fc136b162fc Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 12:12:27 +0100 Subject: Move per-field and cross-field validation into a single method --- rest_framework/serializers.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2f8108d1..c9f025bc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,33 +208,32 @@ class BaseSerializer(Field): return reverted_data - def validate_fields(self, attrs): + def perform_validation(self, attrs): """ - Run validate_ methods on the serializer + Run `validate_()` and `validate()` methods on the serializer """ fields = self.get_fields(serialize=False, data=attrs, nested=self.opts.nested) for field_name, field in fields.items(): try: - clean_method = getattr(self, 'validate_%s' % field_name, None) - if clean_method: + validate_method = getattr(self, 'validate_%s' % field_name, None) + if validate_method: source = field.source or field_name - attrs = clean_method(attrs, source) + attrs = validate_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) + try: + attrs = self.validate(attrs) + except ValidationError as err: + self._errors['non_field_errors'] = err.messages + return attrs - def validate_all(self, attrs): + def validate(self, attrs): """ - Run the `validate` method on the serializer, if it exists + Stub method, to be overridden in Serializer subclasses """ - try: - validate_method = getattr(self, 'validate', None) - if validate_method: - attrs = validate_method(attrs) - except ValidationError as err: - self._errors['non_field_errors'] = err.messages return attrs def restore_object(self, attrs, instance=None): @@ -270,8 +269,7 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) - attrs = self.validate_fields(attrs) - attrs = self.validate_all(attrs) + attrs = self.perform_validation(attrs) else: self._errors['non_field_errors'] = ['No input provided'] -- cgit v1.2.3 From 32ebf96ef661533a9bb69124ec9cef4af2393014 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 18:22:29 +0100 Subject: Split concrete generic views up into separate bits of functionality --- rest_framework/generics.py | 68 ++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 18c1033d..cfb3f29e 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -95,27 +95,25 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView): ### Concrete view classes that provide method handlers ### ### by composing the mixin classes with a base view. ### -class ListAPIView(mixins.ListModelMixin, - MultipleObjectBaseView): + +class CreateAPIView(mixins.CreateModelMixin, + BaseView): + """ - Concrete view for listing a queryset. + Concrete view for creating a model instance. """ - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) -class ListCreateAPIView(mixins.ListModelMixin, - mixins.CreateModelMixin, - MultipleObjectBaseView): +class ListAPIView(mixins.ListModelMixin, + MultipleObjectBaseView): """ - Concrete view for listing a queryset or creating a model instance. + Concrete view for listing a queryset. """ def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) - class RetrieveAPIView(mixins.RetrieveModelMixin, SingleObjectBaseView): @@ -126,31 +124,43 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, return self.retrieve(request, *args, **kwargs) -class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - SingleObjectBaseView): +class DestroyAPIView(mixins.DestroyModelMixin, + SingleObjectBaseView): + """ - Concrete view for retrieving or deleting a model instance. + Concrete view for deleting a model instance. """ - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) -class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - SingleObjectBaseView): +class UpdateAPIView(mixins.UpdateModelMixin, + SingleObjectBaseView): + """ - Concrete view for retrieving, updating or deleting a model instance. + Concrete view for updating a model instance. """ - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) + +class ListCreateAPIView(ListAPIView, + CreateAPIView): + """ + Concrete view for listing a queryset or creating a model instance. + """ + + +class RetrieveDestroyAPIView(RetrieveAPIView, + DestroyAPIView): + """ + Concrete view for retrieving or deleting a model instance. + """ + + +class RetrieveUpdateDestroyAPIView(RetrieveAPIView, + UpdateAPIView, + DestroyAPIView): + """ + Concrete view for retrieving, updating or deleting a model instance. + """ -- cgit v1.2.3 From d97c71212423dd79abafc5aab21afc4eccfc4529 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Oct 2012 20:49:09 +0200 Subject: Fix typo reported by @diviei --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 50505d30..057827d3 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -254,7 +254,7 @@ When serializing objects using a nested representation any occurances of recursi def get_related_field(self, model_field, to_many=False): queryset = model_field.rel.to._default_manager if to_many: - return return serializers.ManyRelatedField(queryset=queryset) + return serializers.ManyRelatedField(queryset=queryset) return serializers.RelatedField(queryset=queryset) def get_field(self, model_field): -- cgit v1.2.3 From 0aed70dc8b9bf2bf6c6bbd540ffadfae9f74bbaa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Oct 2012 20:50:45 +0200 Subject: Added @diviei - Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 6df99237..27a56326 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -48,6 +48,7 @@ The following people have helped make REST framework great. * Max Hurl - [maximilianhurl] * Tomi Pajunen - [eofs] * Rob Dobson - [rdobson] +* Daniel Vaca Araujo - [diviei] Many thanks to everyone who's contributed to the project. @@ -127,3 +128,4 @@ To contact the author directly: [maximilianhurl]: https://github.com/maximilianhurl [eofs]: https://github.com/eofs [rdobson]: https://github.com/rdobson +[diviei]: https://github.com/diviei -- cgit v1.2.3 From 3e751ccd8aa2870c125a17de6af6e1909aa2b35e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Oct 2012 20:58:10 +0100 Subject: Fix ModelSerializer logic for fields with default value, which should have required=False set --- rest_framework/serializers.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c9f025bc..8ee9a0ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -406,6 +406,10 @@ class ModelSerializer(Serializer): """ Creates a default instance of a basic non-relational field. """ + kwargs = {} + if model_field.has_default(): + kwargs['required'] = False + field_mapping = { models.FloatField: FloatField, models.IntegerField: IntegerField, @@ -421,14 +425,9 @@ class ModelSerializer(Serializer): models.BooleanField: BooleanField, } try: - ret = field_mapping[model_field.__class__]() + return field_mapping[model_field.__class__](**kwargs) except KeyError: - ret = ModelField(model_field=model_field) - - if model_field.default is not None: - ret.required = False - - return ret + return ModelField(model_field=model_field, **kwargs) def restore_object(self, attrs, instance=None): """ -- cgit v1.2.3 From 8c360770c18ac38a2f4da81a3553fb40592558c4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Oct 2012 12:15:31 +0100 Subject: Add pre_save hook in generic views --- rest_framework/mixins.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 29153e18..8873e4ae 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -20,10 +20,14 @@ class CreateModelMixin(object): def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.DATA) if serializer.is_valid(): + self.pre_save(serializer.object) self.object = serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def pre_save(self, obj): + pass + class ListModelMixin(object): """ @@ -46,7 +50,8 @@ class ListModelMixin(object): # which may be `None` to disable pagination. page_size = self.get_paginate_by(self.object_list) if page_size: - paginator, page, queryset, is_paginated = self.paginate_queryset(self.object_list, page_size) + packed = self.paginate_queryset(self.object_list, page_size) + paginator, page, queryset, is_paginated = packed serializer = self.get_pagination_serializer(page) else: serializer = self.get_serializer(instance=self.object_list) @@ -79,20 +84,17 @@ class UpdateModelMixin(object): serializer = self.get_serializer(data=request.DATA, instance=self.object) if serializer.is_valid(): - if self.object is None: - # If PUT occurs to a non existant object, we need to set any - # attributes on the object that are implicit in the URL. - self.update_urlconf_attributes(serializer.object) + self.pre_save(serializer.object) self.object = serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def update_urlconf_attributes(self, obj): + def pre_save(self, obj): """ - When update (re)creates an object, we need to set any attributes that - are tied to the URLconf. + Set any attributes on the object that are implicit in the request. """ + # pk and/or slug attributes are implicit in the URL. pk = self.kwargs.get(self.pk_url_kwarg, None) if pk: setattr(obj, 'pk', pk) -- cgit v1.2.3 From d6e10b50fc6f1735d7dd6ee8bfd9d5d39b635b49 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Thu, 25 Oct 2012 12:26:08 +0100 Subject: Re-add implementation of multiple-operation generic views to remove diamond inheritance --- rest_framework/generics.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index cfb3f29e..7a36f36a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -144,23 +144,44 @@ class UpdateAPIView(mixins.UpdateModelMixin, return self.update(request, *args, **kwargs) -class ListCreateAPIView(ListAPIView, - CreateAPIView): +class ListCreateAPIView(mixins.ListModelMixin, + mixins.CreateModelMixin, + MultipleObjectBaseView): """ Concrete view for listing a queryset or creating a model instance. """ + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) -class RetrieveDestroyAPIView(RetrieveAPIView, - DestroyAPIView): +class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + SingleObjectBaseView): """ Concrete view for retrieving or deleting a model instance. """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) -class RetrieveUpdateDestroyAPIView(RetrieveAPIView, - UpdateAPIView, - DestroyAPIView): +class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + SingleObjectBaseView): """ Concrete view for retrieving, updating or deleting a model instance. """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) -- cgit v1.2.3 From 27935f6f6652871c5ed1a2ab879fac22d5257549 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Thu, 25 Oct 2012 13:50:39 +0100 Subject: Rework generic view class names --- rest_framework/generics.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7a36f36a..81014026 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -10,7 +10,7 @@ from django.views.generic.list import MultipleObjectMixin ### Base classes for the generic views ### -class BaseView(views.APIView): +class GenericAPIView(views.APIView): """ Base class for all other generic views. """ @@ -51,7 +51,7 @@ class BaseView(views.APIView): return serializer_class(data, instance=instance, context=context) -class MultipleObjectBaseView(MultipleObjectMixin, BaseView): +class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): """ Base class for generic views onto a queryset. """ @@ -75,7 +75,7 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView): return pagination_serializer_class(instance=page, context=context) -class SingleObjectBaseView(SingleObjectMixin, BaseView): +class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): """ Base class for generic views onto a model instance. """ @@ -86,7 +86,7 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView): """ Override default to add support for object-level permissions. """ - obj = super(SingleObjectBaseView, self).get_object() + obj = super(SingleObjectAPIView, self).get_object() if not self.has_permission(self.request, obj): self.permission_denied(self.request) return obj @@ -97,7 +97,7 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView): class CreateAPIView(mixins.CreateModelMixin, - BaseView): + GenericAPIView): """ Concrete view for creating a model instance. @@ -107,7 +107,7 @@ class CreateAPIView(mixins.CreateModelMixin, class ListAPIView(mixins.ListModelMixin, - MultipleObjectBaseView): + MultipleObjectAPIView): """ Concrete view for listing a queryset. """ @@ -116,7 +116,7 @@ class ListAPIView(mixins.ListModelMixin, class RetrieveAPIView(mixins.RetrieveModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for retrieving a model instance. """ @@ -125,7 +125,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for deleting a model instance. @@ -135,7 +135,7 @@ class DestroyAPIView(mixins.DestroyModelMixin, class UpdateAPIView(mixins.UpdateModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for updating a model instance. @@ -146,7 +146,7 @@ class UpdateAPIView(mixins.UpdateModelMixin, class ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, - MultipleObjectBaseView): + MultipleObjectAPIView): """ Concrete view for listing a queryset or creating a model instance. """ @@ -159,7 +159,7 @@ class ListCreateAPIView(mixins.ListModelMixin, class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for retrieving or deleting a model instance. """ @@ -173,7 +173,7 @@ class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for retrieving, updating or deleting a model instance. """ -- cgit v1.2.3 From 1ceca69e5fa64344f1a039526fb653bf6bbd8a9d Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Thu, 25 Oct 2012 13:50:48 +0100 Subject: Update generic view documentation --- docs/api-guide/generic-views.md | 62 ++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 8bf7a7e2..7ca0f905 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -49,21 +49,21 @@ For very simple cases you might want to pass through any class attributes using The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior. -## ListAPIView +## CreateAPIView -Used for **read-only** endpoints to represent a **collection of model instances**. +Used for **create-only** endpoints. -Provides a `get` method handler. +Provides `post` method handlers. -Extends: [MultipleObjectBaseAPIView], [ListModelMixin] +Extends: [GenericAPIView], [CreateModelMixin] -## ListCreateAPIView +## ListAPIView -Used for **read-write** endpoints to represent a **collection of model instances**. +Used for **read-only** endpoints to represent a **collection of model instances**. -Provides `get` and `post` method handlers. +Provides a `get` method handler. -Extends: [MultipleObjectBaseAPIView], [ListModelMixin], [CreateModelMixin] +Extends: [MultipleObjectAPIView], [ListModelMixin] ## RetrieveAPIView @@ -71,7 +71,31 @@ Used for **read-only** endpoints to represent a **single model instance**. Provides a `get` method handler. -Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin] +Extends: [SingleObjectAPIView], [RetrieveModelMixin] + +## DestroyAPIView + +Used for **delete-only** endpoints for a **single model instance**. + +Provides a `delete` method handler. + +Extends: [SingleObjectAPIView], [DestroyModelMixin] + +## UpdateAPIView + +Used for **update-only** endpoints for a **single model instance**. + +Provides a `put` method handler. + +Extends: [SingleObjectAPIView], [UpdateModelMixin] + +## ListCreateAPIView + +Used for **read-write** endpoints to represent a **collection of model instances**. + +Provides `get` and `post` method handlers. + +Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin] ## RetrieveDestroyAPIView @@ -79,15 +103,15 @@ Used for **read or delete** endpoints to represent a **single model instance**. Provides `get` and `delete` method handlers. -Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin], [DestroyModelMixin] +Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin] ## RetrieveUpdateDestroyAPIView -Used for **read-write** endpoints to represent a **single model instance**. +Used for **read-write-delete** endpoints to represent a **single model instance**. Provides `get`, `put` and `delete` method handlers. -Extends: [SingleObjectBaseAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin] +Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin] --- @@ -121,7 +145,7 @@ The mixin classes provide the actions that are used to provide the basic view be Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset. -Should be mixed in with [MultipleObjectBaseAPIView]. +Should be mixed in with [MultipleObjectAPIView]. ## CreateModelMixin @@ -133,19 +157,19 @@ Should be mixed in with any [BaseAPIView]. Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response. -Should be mixed in with [SingleObjectBaseAPIView]. +Should be mixed in with [SingleObjectAPIView]. ## UpdateModelMixin Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance. -Should be mixed in with [SingleObjectBaseAPIView]. +Should be mixed in with [SingleObjectAPIView]. ## DestroyModelMixin Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance. -Should be mixed in with [SingleObjectBaseAPIView]. +Should be mixed in with [SingleObjectAPIView]. [cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views [MultipleObjectMixin]: https://docs.djangoproject.com/en/dev/ref/class-based-views/mixins-multiple-object/ @@ -153,9 +177,9 @@ Should be mixed in with [SingleObjectBaseAPIView]. [multiple-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.list/MultipleObjectMixin/ [single-object-mixin-classy]: http://ccbv.co.uk/projects/Django/1.4/django.views.generic.detail/SingleObjectMixin/ -[BaseAPIView]: #baseapiview -[SingleObjectBaseAPIView]: #singleobjectbaseapiview -[MultipleObjectBaseAPIView]: #multipleobjectbaseapiview +[GenericAPIView]: #genericapiview +[SingleObjectAPIView]: #singleobjectapiview +[MultipleObjectAPIView]: #multipleobjectapiview [ListModelMixin]: #listmodelmixin [CreateModelMixin]: #createmodelmixin [RetrieveModelMixin]: #retrievemodelmixin -- cgit v1.2.3 From 41d27b1a307132fea42fb104a8d7d266ea9e90d1 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Thu, 25 Oct 2012 14:22:36 +0100 Subject: Fix section headings in generic views docs --- docs/api-guide/generic-views.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 7ca0f905..97b4441f 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -119,17 +119,17 @@ Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [Destr Each of the generic views provided is built by combining one of the base views below, with one or more mixin classes. -## BaseAPIView +## GenericAPIView Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets. -## MultipleObjectBaseAPIView +## MultipleObjectAPIView Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin]. **See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy]. -## SingleObjectBaseAPIView +## SingleObjectAPIView Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin]. @@ -151,7 +151,7 @@ Should be mixed in with [MultipleObjectAPIView]. Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. -Should be mixed in with any [BaseAPIView]. +Should be mixed in with any [GenericAPIView]. ## RetrieveModelMixin -- cgit v1.2.3 From 04ae32c9340b0782014bd61ef9ee3196af22ebce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Oct 2012 17:01:00 +0100 Subject: remove no-site-packages since that's now the default --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 5b58f293..d1ae0ba5 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -10,7 +10,7 @@ Before we do anything else we'll create a new virtual environment, using [virtua :::bash mkdir ~/env - virtualenv --no-site-packages ~/env/tutorial + virtualenv ~/env/tutorial source ~/env/tutorial/bin/activate Now that we're inside a virtualenv environment, we can install our package requirements. -- cgit v1.2.3 From 8a7b2cd68ac73550088c00880cdea55d4713cace Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Oct 2012 19:39:47 +0200 Subject: 2.0 Note --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 84d1edad..8ca4f572 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,10 @@ 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 +**Important!** REST framework 2.0 is due to be released to PyPI by the end of October. +If you are considering building a new API using REST framework you should start working +with the `restframework2` branch, end refering to the upcoming [REST framework 2 docs][docs]. + Overview ======== @@ -84,3 +88,5 @@ To run the tests against the full set of supported configurations:: To create the sdist packages:: python setup.py sdist --formats=gztar,zip + +[docs]: tomchristie.github.com/django-rest-framework \ No newline at end of file -- cgit v1.2.3 From 13f89320b2b462d2d8c78fa4a05611a06e55c0d5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Oct 2012 19:40:39 +0200 Subject: update link --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8ca4f572..c377eeb3 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Django REST framework **Important!** REST framework 2.0 is due to be released to PyPI by the end of October. If you are considering building a new API using REST framework you should start working -with the `restframework2` branch, end refering to the upcoming [REST framework 2 docs][docs]. +with the `restframework2` branch, end refering to the upcoming `REST framework 2 docs `_. Overview ======== @@ -88,5 +88,3 @@ To run the tests against the full set of supported configurations:: To create the sdist packages:: python setup.py sdist --formats=gztar,zip - -[docs]: tomchristie.github.com/django-rest-framework \ No newline at end of file -- cgit v1.2.3 From 75ee194e970ae644e4f692178458adc14b9722b1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 02:02:18 +0200 Subject: Typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c377eeb3..ed30f2bb 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Django REST framework **Important!** REST framework 2.0 is due to be released to PyPI by the end of October. If you are considering building a new API using REST framework you should start working -with the `restframework2` branch, end refering to the upcoming `REST framework 2 docs `_. +with the `restframework2` branch, and refering to the upcoming `REST framework 2 docs `_. Overview ======== -- cgit v1.2.3 From 195006bbc36c21f0154fe1ab7c46f339b2efe559 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 09:27:59 +0100 Subject: Drop resources from codebase since implementation is only partial (Created resoorces-routers branch for future reference) --- docs/tutorial/6-resource-orientated-projects.md | 76 -------------------- rest_framework/resources.py | 96 ------------------------- 2 files changed, 172 deletions(-) delete mode 100644 docs/tutorial/6-resource-orientated-projects.md delete mode 100644 rest_framework/resources.py diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md deleted file mode 100644 index 9ee599ae..00000000 --- a/docs/tutorial/6-resource-orientated-projects.md +++ /dev/null @@ -1,76 +0,0 @@ -# Tutorial 6 - Resources - -Resource classes are just View classes that don't have any handler methods bound to them. The actions on a resource are defined, - -This allows us to: - -* Encapsulate common behaviour across a class of views, in a single Resource class. -* Separate out the actions of a Resource from the specifics of how those actions should be bound to a particular set of URLs. - -## Refactoring to use Resources, not Views - -For instance, we can re-write our 4 sets of views into something more compact... - -resources.py - - class BlogPostResource(ModelResource): - serializer_class = BlogPostSerializer - model = BlogPost - permissions_classes = (permissions.IsAuthenticatedOrReadOnly,) - throttle_classes = (throttles.UserRateThrottle,) - - class CommentResource(ModelResource): - serializer_class = CommentSerializer - model = Comment - permissions_classes = (permissions.IsAuthenticatedOrReadOnly,) - throttle_classes = (throttles.UserRateThrottle,) - -## Binding Resources to URLs explicitly -The handler methods only get bound to the actions when we define the URLConf. Here's our urls.py: - - comment_root = CommentResource.as_view(actions={ - 'get': 'list', - 'post': 'create' - }) - comment_instance = CommentInstance.as_view(actions={ - 'get': 'retrieve', - 'put': 'update', - 'delete': 'destroy' - }) - ... # And for blog post - - urlpatterns = patterns('blogpost.views', - url(r'^$', comment_root), - url(r'^(?P[0-9]+)$', comment_instance) - ... # And for blog post - ) - -## Using Routers - -Right now that hasn't really saved us a lot of code. However, now that we're using Resources rather than Views, we actually don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file. - - from blog import resources - from rest_framework.routers import DefaultRouter - - router = DefaultRouter() - router.register(resources.BlogPostResource) - router.register(resources.CommentResource) - urlpatterns = router.urlpatterns - -## Trade-offs between views vs resources. - -Writing resource-oriented code can be a good thing. It helps ensure that URL conventions will be consistent across your APIs, and minimises the amount of code you need to write. - -The trade-off is that the behaviour is less explict. It can be more difficult to determine what code path is being followed, or where to override some behaviour. - -## Onwards and upwards. - -We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start: - -* Contribute on GitHub by reviewing issues, and submitting issues or pull requests. -* Join the REST framework group, and help build the community. -* Follow me [on Twitter][twitter] and say hi. - -**Now go build some awesome things.** - -[twitter]: https://twitter.com/_tomchristie diff --git a/rest_framework/resources.py b/rest_framework/resources.py deleted file mode 100644 index dd8a5471..00000000 --- a/rest_framework/resources.py +++ /dev/null @@ -1,96 +0,0 @@ -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -from functools import update_wrapper -import inspect -from django.utils.decorators import classonlymethod -from rest_framework import views, generics - - -def wrapped(source, dest): - """ - Copy public, non-method attributes from source to dest, and return dest. - """ - for attr in [attr for attr in dir(source) - if not attr.startswith('_') and not inspect.ismethod(attr)]: - setattr(dest, attr, getattr(source, attr)) - return dest - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ResourceMixin(object): - """ - Clone Django's `View.as_view()` behaviour *except* using REST framework's - 'method -> action' binding for resources. - """ - - @classonlymethod - def as_view(cls, actions, **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("You tried to pass in the %s method name as a " - "keyword argument to %s(). Don't do that." - % (key, cls.__name__)) - if not hasattr(cls, key): - raise TypeError("%s() received an invalid keyword %r" % ( - cls.__name__, key)) - - def view(request, *args, **kwargs): - self = cls(**initkwargs) - - # Bind methods to actions - for method, action in actions.items(): - handler = getattr(self, action) - setattr(self, method, handler) - - # As you were, solider. - 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 - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class Resource(ResourceMixin, views.APIView): - pass - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ModelResource(ResourceMixin, views.APIView): - # TODO: Actually delegation won't work - root_class = generics.ListCreateAPIView - detail_class = generics.RetrieveUpdateDestroyAPIView - - def root_view(self): - return wrapped(self, self.root_class()) - - def detail_view(self): - return wrapped(self, self.detail_class()) - - def list(self, request, *args, **kwargs): - return self.root_view().list(request, args, kwargs) - - def create(self, request, *args, **kwargs): - return self.root_view().create(request, args, kwargs) - - def retrieve(self, request, *args, **kwargs): - return self.detail_view().retrieve(request, args, kwargs) - - def update(self, request, *args, **kwargs): - return self.detail_view().update(request, args, kwargs) - - def destroy(self, request, *args, **kwargs): - return self.detail_view().destroy(request, args, kwargs) -- cgit v1.2.3 From 365d20652eb7b56c9d1d4b63b52c056f03cab514 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 09:30:46 +0100 Subject: Add analytics --- docs/template.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/template.html b/docs/template.html index 45bada54..56c5d832 100644 --- a/docs/template.html +++ b/docs/template.html @@ -1,5 +1,6 @@ - + + Django REST framework @@ -17,6 +18,21 @@ + + +
-- cgit v1.2.3 From 32d602880fc88e2b3e8d8f2a82132bed224f8b49 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:45:52 +0100 Subject: Choice fields from ModelSerializer. --- rest_framework/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8ee9a0ec..d4fcddd5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -3,6 +3,7 @@ import datetime import types from decimal import Decimal from django.db import models +from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model from rest_framework.fields import * @@ -409,6 +410,15 @@ class ModelSerializer(Serializer): kwargs = {} if model_field.has_default(): kwargs['required'] = False + kwargs['default'] = model_field.default + + if model_field.__class__ == models.TextField: + kwargs['widget'] = widgets.Textarea + + # TODO: TypedChoiceField? + if model_field.flatchoices: # This ModelField contains choices + kwargs['choices'] = model_field.flatchoices + return ChoiceField(**kwargs) field_mapping = { models.FloatField: FloatField, -- cgit v1.2.3 From 2efb5f8a14ffc321a1a9e88548abfa8b0782aae4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:15 +0100 Subject: Object-level permissions respected by Browseable API --- rest_framework/renderers.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c64fb517..1a8b1d97 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -224,7 +224,7 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def show_form_for_method(self, view, method, request): + def show_form_for_method(self, view, method, request, obj): """ Returns True if a form should be shown for this method. """ @@ -236,7 +236,7 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) try: - if not view.has_permission(request): + if not view.has_permission(request, obj): return # Don't have permission except: return # Don't have permission and exception explicitly raise @@ -295,7 +295,8 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence on of the Resource having an associated form then provide a form that can be used to submit arbitrary content. """ - if not self.show_form_for_method(view, method, request): + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): return if method == 'DELETE' or method == 'OPTIONS': @@ -305,17 +306,13 @@ class BrowsableAPIRenderer(BaseRenderer): media_types = [parser.media_type for parser in view.parser_classes] return self.get_generic_content_form(media_types) - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - serializer = view.get_serializer(instance=obj) fields = self.serializer_to_form_fields(serializer) + # Creating an on the fly form see: + # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj: - data = serializer.data + data = (obj is not None) and serializer.data or None form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From d4063eb02e31401252ca15b3aae50ed951d7869f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:31 +0100 Subject: Fix incorrect method signature in docs --- docs/api-guide/generic-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 97b4441f..360ef1a2 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -30,7 +30,7 @@ For more complex cases you might also want to override various methods on the vi serializer_class = UserSerializer permission_classes = (IsAdminUser,) - def get_paginate_by(self): + def get_paginate_by(self, queryset): """ Use smaller pagination for HTML representations. """ -- cgit v1.2.3 From fc4614a89c5c2bbdeb3626e2f16e3a2cd6445e3e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:41 +0100 Subject: Whitespace --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6ed37823..85e6ee31 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -258,7 +258,7 @@ class ManyRelatedMixin(object): def field_from_native(self, data, field_name, into): if self.readonly: return - + try: # Form data value = data.getlist(self.source or field_name) -- cgit v1.2.3 From 67f1265e493adc35239d90aeb3bfeb8492fbd741 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 13:20:30 +0100 Subject: Fix failing 'default' on ModelSerializer --- rest_framework/serializers.py | 2 +- rest_framework/tests/models.py | 4 ++-- rest_framework/tests/serializer.py | 20 +++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d4fcddd5..db6f9f61 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -410,7 +410,7 @@ class ModelSerializer(Serializer): kwargs = {} if model_field.has_default(): kwargs['required'] = False - kwargs['default'] = model_field.default + kwargs['default'] = model_field.get_default() if model_field.__class__ == models.TextField: kwargs['widget'] = widgets.Textarea diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 97cd0849..0ee18c69 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,12 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) - + class ReadOnlyManyToManyModel(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') rel = models.ManyToManyField(Anchor) - + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 936f15aa..67c97f0f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -7,7 +7,7 @@ from rest_framework.tests.models import * class SubComment(object): def __init__(self, sub_comment): self.sub_comment = sub_comment - + class Comment(object): def __init__(self, email, content, created): @@ -18,7 +18,7 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) - + def get_sub_comment(self): sub_comment = SubComment('And Merry Christmas!') return sub_comment @@ -29,7 +29,7 @@ class CommentSerializer(serializers.Serializer): content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() sub_comment = serializers.Field(source='get_sub_comment.sub_comment') - + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,6 +42,7 @@ class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -73,7 +74,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) + serializer = CommentSerializer(instance=self.comment) self.assertEquals(serializer.data, self.expected) def test_create(self): @@ -104,7 +105,7 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } self.actionitem = ActionItem('Some to do item', ) @@ -131,7 +132,7 @@ class ValidationTests(TestCase): """Make sure that a boolean value with a 'False' value is not mistaken for not having a default.""" data = { - 'title':'Some action item', + 'title': 'Some action item', #No 'done' value. } serializer = ActionItemSerializer(data, instance=self.actionitem) @@ -295,11 +296,13 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) - + + class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(readonly=True) + class Meta: model = ReadOnlyManyToManyModel @@ -317,7 +320,6 @@ class ReadOnlyManyToManyTests(TestCase): # A serialized representation of the model instance self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} - def test_update(self): """ Attempt to update an instance of a model with a ManyToMany -- cgit v1.2.3 From c221bc6f6f239d958e89523c00686da595c68578 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 17:30:45 +0200 Subject: Use context dict in HTMLRenderer --- docs/api-guide/renderers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b6db376c..b5e2fe8f 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -130,7 +130,7 @@ An example of a view that uses `HTMLRenderer`: def get(self, request, *args, **kwargs) self.object = self.get_object() - return Response(self.object, template_name='user_detail.html') + 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. -- cgit v1.2.3 From 2026d5f1d93da7c16a58590a561563cfea3b9ad1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 20:55:23 +0200 Subject: Fix PyPI package name in install instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8ffce94..d74340ef 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ For more information, check out [the documentation][docs], in particular, the tu Install using `pip`... - pip install rest_framework + pip install djangorestframework ...or clone the project from github. -- cgit v1.2.3 From 44207a347ac765d900f15b65bdd24dbf8eb9ead2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 10:32:49 +0100 Subject: pep8 --- rest_framework/compat.py | 4 +++- rest_framework/generics.py | 2 +- rest_framework/permissions.py | 2 +- rest_framework/request.py | 4 ++-- rest_framework/serializers.py | 8 ++++---- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7664c400..b0367a32 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -1,6 +1,8 @@ """ -The :mod:`compat` module provides support for backwards compatibility with older versions of django/python. +The `compat` module provides support for backwards compatibility with older +versions of django/python, and compatbility wrappers around optional packages. """ +# flake8: noqa import django # cStringIO only if it's available, otherwise StringIO diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 81014026..190a5f79 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -125,7 +125,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectAPIView): + SingleObjectAPIView): """ Concrete view for deleting a model instance. diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6f848cee..51e96196 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -85,7 +85,7 @@ class DjangoModelPermissions(BasePermission): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': model_cls._meta.module_name } return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/request.py b/rest_framework/request.py index 5870be82..a1827ba4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -21,8 +21,8 @@ def is_form_media_type(media_type): Return True if the media type is a valid form media type. """ base_media_type, params = parse_header(media_type) - return base_media_type == 'application/x-www-form-urlencoded' or \ - base_media_type == 'multipart/form-data' + return (base_media_type == 'application/x-www-form-urlencoded' or + base_media_type == 'multipart/form-data') class Empty(object): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index db6f9f61..8a895343 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -34,10 +34,10 @@ def _is_protected_type(obj): """ return isinstance(obj, ( types.NoneType, - int, long, - datetime.datetime, datetime.date, datetime.time, - float, Decimal, - basestring) + int, long, + datetime.datetime, datetime.date, datetime.time, + float, Decimal, + basestring) ) -- cgit v1.2.3 From 5180b725655e84712b103fbc280353d25048068b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Sat, 27 Oct 2012 13:53:07 +0100 Subject: Documentation for function-based view decorators --- docs/api-guide/views.md | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index e3fbadb2..c51860b5 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -118,9 +118,48 @@ You won't typically need to override this method. > > — [Nick Coghlan][cite2] -REST framework also gives you to work with regular function based views... +REST framework also allows you to work with regular function based views. It provides a set of simple decorators that wrap your function based views to ensure they receive an instance of `Request` (rather than the usual Django `HttpRequest`) and allows them to return a `Response` (instead of a Django `HttpResponse`), and allow you to configure how the request is processed. + +### api_view(http_method_names) + +The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data: + + from rest_framework.decorators import api_view + + @api_view(['GET']) + def hello_world(request): + return Response({"message": "Hello, world!"}) + + +This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings). + +To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `throttle_classes` decorator, passing a list of throttle classes: + + from rest_framework.decorators import api_view, throttle_classes + from rest_framework.throttling import UserRateThrottle + + class OncePerDayUserThrottle(UserRateThrottle): + rate = '1/day' + + @api_view(['GET']) + @throttle_classes([OncePerDayUserThrottle]) + def view(request): + return Response({"message": "Hello for today! See you tomorrow!"}) + +These decorators correspond to the attributes set on `APIView` subclasses, described above. + +### renderer_classes(renderer_classes) + +### parser_classes(parser_classes) + +### authentication_classes(authentication_classes) + +### throttle_classes(throttle_classes) + +### permission_classes(permission_classes) -**[TODO]** [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html +[settings]: api-guide/settings.md +[throttling]: api-guide/throttling.md -- cgit v1.2.3 From cf09dbe655525f02bddab90038319d2b52c431d1 Mon Sep 17 00:00:00 2001 From: Madis Väin Date: Sat, 27 Oct 2012 15:56:37 +0300 Subject: Update README.rst Fix documentation link.--- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ed30f2bb..2c8dabd4 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Django REST framework **Important!** REST framework 2.0 is due to be released to PyPI by the end of October. If you are considering building a new API using REST framework you should start working -with the `restframework2` branch, and refering to the upcoming `REST framework 2 docs `_. +with the `restframework2` branch, and refering to the upcoming `REST framework 2 docs `_. Overview ======== -- cgit v1.2.3 From ec1429ffc8079e2f0fcc5af05882360689fca5bf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 17:27:12 +0200 Subject: Tweaks --- docs/api-guide/views.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index c51860b5..9e661532 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -148,15 +148,15 @@ To override the default settings, REST framework provides a set of additional de These decorators correspond to the attributes set on `APIView` subclasses, described above. -### renderer_classes(renderer_classes) +### @renderer_classes() -### parser_classes(parser_classes) +### @parser_classes() -### authentication_classes(authentication_classes) +### @authentication_classes() -### throttle_classes(throttle_classes) +### @throttle_classes() -### permission_classes(permission_classes) +### @permission_classes() [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html -- cgit v1.2.3 From cef379db065711bd2f1b0805d28a56f7a80cef37 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:39:17 +0100 Subject: 2.0 Announcement --- docs/api-guide/views.md | 23 ++++---- docs/index.md | 4 +- docs/template.html | 2 +- docs/topics/rest-framework-2-announcement.md | 88 ++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 docs/topics/rest-framework-2-announcement.md diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 9e661532..96ce3be7 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -120,7 +120,9 @@ You won't typically need to override this method. REST framework also allows you to work with regular function based views. It provides a set of simple decorators that wrap your function based views to ensure they receive an instance of `Request` (rather than the usual Django `HttpRequest`) and allows them to return a `Response` (instead of a Django `HttpResponse`), and allow you to configure how the request is processed. -### api_view(http_method_names) +## @api_view() + +**Signature:** `@api_view(http_method_names) The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data: @@ -133,7 +135,9 @@ The core of this functionality is the `api_view` decorator, which takes a list o This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings). -To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `throttle_classes` decorator, passing a list of throttle classes: +## API policy decorators + +To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes: from rest_framework.decorators import api_view, throttle_classes from rest_framework.throttling import UserRateThrottle @@ -148,16 +152,15 @@ To override the default settings, REST framework provides a set of additional de These decorators correspond to the attributes set on `APIView` subclasses, described above. -### @renderer_classes() - -### @parser_classes() - -### @authentication_classes() - -### @throttle_classes() +The available decorators are: -### @permission_classes() +* `@renderer_classes(...)` +* `@parser_classes(...)` +* `@authentication_classes(...)` +* `@throttle_classes(...)` +* `@permission_classes(...)` +Each of these decorators takes a single argument which must be a list or tuple of classes. [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html diff --git a/docs/index.md b/docs/index.md index f66ba7f4..a75dbc58 100644 --- a/docs/index.md +++ b/docs/index.md @@ -103,7 +103,7 @@ General guides to using REST framework. * [The Browsable API][browsableapi] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] * [Contributing to REST framework][contributing] -* [2.0 Migration Guide][migration] +* [2.0 Announcement][rest-framework-2-announcement] * [Release Notes][release-notes] * [Credits][credits] @@ -189,7 +189,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [browsableapi]: topics/browsable-api.md [rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md [contributing]: topics/contributing.md -[migration]: topics/migration.md +[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md [release-notes]: topics/release-notes.md [credits]: topics/credits.md diff --git a/docs/template.html b/docs/template.html index 56c5d832..91a7a731 100644 --- a/docs/template.html +++ b/docs/template.html @@ -93,7 +93,7 @@
  • The Browsable API
  • REST, Hypermedia & HATEOAS
  • Contributing to REST framework
  • -
  • 2.0 Migration Guide
  • +
  • 2.0 Announcement
  • Release Notes
  • Credits
  • diff --git a/docs/topics/rest-framework-2-announcement.md b/docs/topics/rest-framework-2-announcement.md new file mode 100644 index 00000000..98e66228 --- /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/ -- cgit v1.2.3 From b9e576f16ef7cc98f671e9c18ff8ae1a95bfe3ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:44:23 +0100 Subject: 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 b2438c9b..1bd0a5fc 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 67de82c8..951b1e72 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 adeaf6da..e69de29b 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 0ee18c69..d4ea729b 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 00000000..adeaf6da --- /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) -- cgit v1.2.3 From 51a64019260d99e3c615b407353e344cf615da1e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:47:17 +0100 Subject: 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 27a56326..a317afde 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 -- cgit v1.2.3 From d995742afc09ff8d387751a6fe47b9686845740b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:04:33 +0100 Subject: 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 0b7b32e9..d43b7bed 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 21efc853..3556a5b1 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 3c508294..9c40a214 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': -- cgit v1.2.3 From af96fe05d0138c34128fc3944fc2701cbad5bd01 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:17:49 +0100 Subject: 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 51e96196..655b78a3 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. -- cgit v1.2.3 From 12c363c1fe237d0357e6020b44890926856b9191 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 18:12:56 +0000 Subject: 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 b5e2fe8f..5efb3610 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 1a8b1d97..cfe4df6d 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 da2f83c3..10d7e31d 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): -- cgit v1.2.3 From bc99142c7dc1ebf84ca0858ce32b400a537e1908 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 19:35:50 +0100 Subject: 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 f4263478..151532a7 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 d4ea729b..ac73a4bb 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) -- cgit v1.2.3 From fde79376f323708d9f7b80ee830fe63060fb335f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 19:25:51 +0000 Subject: Pastebin tutorial --- docs/tutorial/1-serialization.md | 180 ++++++++++++-------- docs/tutorial/2-requests-and-responses.md | 45 +++-- docs/tutorial/3-class-based-views.md | 88 +++++----- docs/tutorial/4-authentication-and-permissions.md | 183 +++++++++++++++++++++ .../4-authentication-permissions-and-throttling.md | 5 - .../5-relationships-and-hyperlinked-apis.md | 158 +++++++++++++++++- 6 files changed, 512 insertions(+), 147 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 d1ae0ba5..fc052202 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 - - - class CommentSerializer(serializers.Serializer): - id = serializers.IntegerField(readonly=True) - email = serializers.EmailField() - content = serializers.CharField(max_length=200) - created = serializers.DateTimeField(readonly=True) - + from snippets import models + + + 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 fc37322a..76803d24 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 0ee81ea3..3d58fe8e 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 00000000..79436ad4 --- /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 c8d7cbd3..00000000 --- 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 8600d5ed..84d02a53 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 -- cgit v1.2.3 From db635fa6327d8d3ac3b06886c5f459b5c5a5cd04 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 19:37:27 +0000 Subject: 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 f66ba7f4..bd0c007d 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 56c5d832..557b45b3 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
  • -