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 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
  • -