diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index dc2cfd277..c30ef10b7 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -11,7 +11,7 @@ from urlobject import URLObject from djangorestframework import status from djangorestframework.renderers import BaseRenderer from djangorestframework.resources import Resource, FormResource, ModelResource -from djangorestframework.response import Response, ErrorResponse +from djangorestframework.response import Response, ImmediateResponse from djangorestframework.request import request_class_factory from djangorestframework.utils import as_tuple, allowed_methods @@ -80,28 +80,37 @@ class RequestMixin(object): class ResponseMixin(object): """ - Adds behavior for pluggable `Renderers` to a :class:`views.View` class. + Adds behavior for pluggable `renderers` to a :class:`views.View` class. Default behavior is to use standard HTTP Accept header content negotiation. Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL. Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead. """ - renderers = () + renderer_classes = () """ The set of response renderers that the view can handle. Should be a tuple/list of classes as described in the :mod:`renderers` module. """ - response_class = Response + def get_renderers(self): + """ + Instantiates and returns the list of renderers that will be used to render + the response. + """ + if not hasattr(self, '_renderers'): + self._renderers = [r(self) for r in self.renderer_classes] + return self._renderers def prepare_response(self, response): """ - Prepares response for the response cycle. Sets some headers, sets renderers, ... + Prepares the response for the response cycle. This has no effect if the + response is not an instance of :class:`response.Response`. """ if hasattr(response, 'request') and response.request is None: response.request = self.request + # Always add these headers. response['Allow'] = ', '.join(allowed_methods(self)) # sample to allow caching using Vary http header @@ -109,10 +118,9 @@ class ResponseMixin(object): # merge with headers possibly set at some point in the view for name, value in self.headers.items(): response[name] = value + # set the views renderers on the response - response.renderers = self.renderers - # TODO: must disappear - response.view = self + response.renderers = self.get_renderers() self.response = response return response @@ -121,21 +129,21 @@ class ResponseMixin(object): """ Return an list of all the media types that this view can render. """ - return [renderer.media_type for renderer in self.renderers] + return [renderer.media_type for renderer in self.get_renderers()] @property def _rendered_formats(self): """ Return a list of all the formats that this view can render. """ - return [renderer.format for renderer in self.renderers] + return [renderer.format for renderer in self.get_renderers()] @property def _default_renderer(self): """ Return the view's default renderer class. """ - return self.renderers[0] + return self.get_renderers()[0] @property def headers(self): @@ -195,7 +203,7 @@ class AuthMixin(object): # TODO: wrap this behavior around dispatch() def _check_permissions(self): """ - Check user permissions and either raise an ``ErrorResponse`` or return. + Check user permissions and either raise an ``ImmediateResponse`` or return. """ user = self.user for permission_cls in self.permissions: @@ -223,7 +231,7 @@ class ResourceMixin(object): """ Returns the cleaned, validated request content. - May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request). + May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request). """ if not hasattr(self, '_content'): self._content = self.validate_request(self.request.DATA, self.request.FILES) @@ -234,7 +242,7 @@ class ResourceMixin(object): """ Returns the cleaned, validated query parameters. - May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request). + May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request). """ return self.validate_request(self.request.GET) @@ -253,7 +261,7 @@ class ResourceMixin(object): def validate_request(self, data, files=None): """ Given the request *data* and optional *files*, return the cleaned, validated content. - May raise an :class:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request) on failure. """ return self._resource.validate_request(data, files) @@ -384,7 +392,7 @@ class ReadModelMixin(ModelMixin): try: self.model_instance = self.get_instance(**query_kwargs) except model.DoesNotExist: - raise ErrorResponse(status=status.HTTP_404_NOT_FOUND) + raise ImmediateResponse(status=status.HTTP_404_NOT_FOUND) return self.model_instance @@ -463,7 +471,7 @@ class DeleteModelMixin(ModelMixin): try: instance = self.get_instance(**query_kwargs) except model.DoesNotExist: - raise ErrorResponse(status=status.HTTP_404_NOT_FOUND) + raise ImmediateResponse(status=status.HTTP_404_NOT_FOUND) instance.delete() return Response() @@ -570,12 +578,12 @@ class PaginatorMixin(object): try: page_num = int(self.request.GET.get('page', '1')) except ValueError: - raise ErrorResponse( + raise ImmediateResponse( content={'detail': 'That page contains no results'}, status=status.HTTP_404_NOT_FOUND) if page_num not in paginator.page_range: - raise ErrorResponse( + raise ImmediateResponse( content={'detail': 'That page contains no results'}, status=status.HTTP_404_NOT_FOUND) diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py index 7732a2939..5fc5c71ee 100644 --- a/djangorestframework/parsers.py +++ b/djangorestframework/parsers.py @@ -17,7 +17,7 @@ from django.http.multipartparser import MultiPartParserError from django.utils import simplejson as json from djangorestframework import status from djangorestframework.compat import yaml -from djangorestframework.response import ErrorResponse +from djangorestframework.response import ImmediateResponse from djangorestframework.utils.mediatypes import media_type_matches from xml.etree import ElementTree as ET import datetime @@ -88,7 +88,7 @@ class JSONParser(BaseParser): try: return (json.load(stream), None) except ValueError, exc: - raise ErrorResponse( + raise ImmediateResponse( content={'detail': 'JSON parse error - %s' % unicode(exc)}, status=status.HTTP_400_BAD_REQUEST) @@ -111,7 +111,7 @@ if yaml: try: return (yaml.safe_load(stream), None) except ValueError, exc: - raise ErrorResponse( + raise ImmediateResponse( content={'detail': 'YAML parse error - %s' % unicode(exc)}, status=status.HTTP_400_BAD_REQUEST) else: @@ -172,7 +172,7 @@ class MultiPartParser(BaseParser): try: django_parser = DjangoMultiPartParser(self.view.META, stream, upload_handlers) except MultiPartParserError, exc: - raise ErrorResponse( + raise ImmediateResponse( content={'detail': 'multipart parse error - %s' % unicode(exc)}, status=status.HTTP_400_BAD_REQUEST) return django_parser.parse() diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index bce03cabc..4ddc35cb9 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -6,7 +6,7 @@ class to your view by setting your View's :attr:`permissions` class attribute. from django.core.cache import cache from djangorestframework import status -from djangorestframework.response import ErrorResponse +from djangorestframework.response import ImmediateResponse import time __all__ = ( @@ -21,12 +21,12 @@ __all__ = ( ) -_403_FORBIDDEN_RESPONSE = ErrorResponse( +_403_FORBIDDEN_RESPONSE = ImmediateResponse( content={'detail': 'You do not have permission to access this resource. ' + 'You may need to login or otherwise authenticate the request.'}, status=status.HTTP_403_FORBIDDEN) -_503_SERVICE_UNAVAILABLE = ErrorResponse( +_503_SERVICE_UNAVAILABLE = ImmediateResponse( content={'detail': 'request was throttled'}, status=status.HTTP_503_SERVICE_UNAVAILABLE) @@ -43,7 +43,7 @@ class BasePermission(object): def check_permission(self, auth): """ - Should simply return, or raise an :exc:`response.ErrorResponse`. + Should simply return, or raise an :exc:`response.ImmediateResponse`. """ pass @@ -116,7 +116,7 @@ class BaseThrottle(BasePermission): def check_permission(self, auth): """ Check the throttling. - Return `None` or raise an :exc:`.ErrorResponse`. + Return `None` or raise an :exc:`.ImmediateResponse`. """ num, period = getattr(self.view, self.attr_name, self.default).split('/') self.num_requests = int(num) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 929ed0738..4e8158aa7 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -45,7 +45,7 @@ class BaseRenderer(object): media_type = None format = None - def __init__(self, view): + def __init__(self, view=None): self.view = view def can_handle_response(self, accept): @@ -218,7 +218,8 @@ class DocumentingTemplateRenderer(BaseRenderer): """ # Find the first valid renderer and render the content. (Don't use another documenting renderer.) - renderers = [renderer for renderer in view.renderers if not issubclass(renderer, DocumentingTemplateRenderer)] + renderers = [renderer for renderer in view.renderer_classes + if not issubclass(renderer, DocumentingTemplateRenderer)] if not renderers: return '[No renderers were found]' diff --git a/djangorestframework/request.py b/djangorestframework/request.py index ee43857ed..21538aec0 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -11,7 +11,7 @@ This enhanced request object offers the following : from django.http import HttpRequest -from djangorestframework.response import ErrorResponse +from djangorestframework.response import ImmediateResponse from djangorestframework import status from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence from djangorestframework.utils import as_tuple @@ -194,7 +194,7 @@ class Request(object): """ Parse the request content. - May raise a 415 ErrorResponse (Unsupported Media Type), or a 400 ErrorResponse (Bad Request). + May raise a 415 ImmediateResponse (Unsupported Media Type), or a 400 ImmediateResponse (Bad Request). """ if stream is None or content_type is None: return (None, None) @@ -206,7 +206,7 @@ class Request(object): if parser.can_handle_request(content_type): return parser.parse(stream) - raise ErrorResponse(content={'error': + raise ImmediateResponse(content={'error': 'Unsupported media type in request \'%s\'.' % content_type}, status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index a20e477ec..f478bd521 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -2,7 +2,7 @@ from django import forms from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch from django.db import models -from djangorestframework.response import ErrorResponse +from djangorestframework.response import ImmediateResponse from djangorestframework.serializer import Serializer, _SkipField from djangorestframework.utils import as_tuple @@ -22,7 +22,7 @@ class BaseResource(Serializer): def validate_request(self, data, files=None): """ Given the request content return the cleaned, validated content. - Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + Typically raises a :exc:`response.ImmediateResponse` with status code 400 (Bad Request) on failure. """ return data @@ -73,19 +73,19 @@ class FormResource(Resource): """ Flag to check for unknown fields when validating a form. If set to false and we receive request data that is not expected by the form it raises an - :exc:`response.ErrorResponse` with status code 400. If set to true, only + :exc:`response.ImmediateResponse` with status code 400. If set to true, only expected fields are validated. """ def validate_request(self, data, files=None): """ Given some content as input return some cleaned, validated content. - Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + Raises a :exc:`response.ImmediateResponse` with status code 400 (Bad Request) on failure. Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied if :attr:`self.allow_unknown_form_fields` is ``False``. - On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys. + On failure the :exc:`response.ImmediateResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys. If the :obj:`'errors'` key exists it is a list of strings of non-field errors. If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``. """ @@ -174,7 +174,7 @@ class FormResource(Resource): detail[u'field_errors'] = field_errors # Return HTTP 400 response (BAD REQUEST) - raise ErrorResponse(content=detail, status=400) + raise ImmediateResponse(content=detail, status=400) def get_form_class(self, method=None): """ @@ -273,14 +273,14 @@ class ModelResource(FormResource): def validate_request(self, data, files=None): """ Given some content as input return some cleaned, validated content. - Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + Raises a :exc:`response.ImmediateResponse` with status code 400 (Bad Request) on failure. Validation is standard form or model form validation, with an additional constraint that no extra unknown fields may be supplied, and that all fields specified by the fields class attribute must be supplied, even if they are not validated by the form/model form. - On failure the ErrorResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys. + On failure the ImmediateResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys. If the :obj:`'errors'` key exists it is a list of strings of non-field errors. If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}. """ diff --git a/djangorestframework/response.py b/djangorestframework/response.py index 4f9b3a625..3b692b24e 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -1,8 +1,18 @@ """ -The :mod:`response` module provides Response classes you can use in your -views to return a certain HTTP response. Typically a response is *rendered* -into a HTTP response depending on what renderers are set on your view and -als depending on the accept header of the request. +The :mod:`response` module provides :class:`Response` and :class:`ImmediateResponse` classes. + +`Response` is a subclass of `HttpResponse`, and can be similarly instantiated and returned +from any view. It is a bit smarter than Django's `HttpResponse` though, for it knows how +to use :mod:`renderers` to automatically render its content to a serial format. +This is achieved by : + + - determining the accepted types by checking for an overload or an `Accept` header in the request + - looking for a suitable renderer and using it on the content given at instantiation + + +`ImmediateResponse` is an exception that inherits from `Response`. It can be used +to abort the request handling (i.e. ``View.get``, ``View.put``, ...), +and immediately returning a response. """ from django.template.response import SimpleTemplateResponse @@ -13,7 +23,7 @@ from djangorestframework.utils import MSIE_USER_AGENT_REGEX from djangorestframework import status -__all__ = ('Response', 'ErrorResponse') +__all__ = ('Response', 'ImmediateResponse') class Response(SimpleTemplateResponse): @@ -26,15 +36,16 @@ class Response(SimpleTemplateResponse): def __init__(self, content=None, status=None, request=None, renderers=None): """ - content is the raw content. + `content` is the raw content, not yet serialized. This must be simple Python + data that renderers can handle (cf: dict, str, ...) - The set of renderers that the response can handle. - - Should be a tuple/list of classes as described in the :mod:`renderers` module. + `renderers` is a list/tuple of renderer instances and represents the set of renderers + that the response can handle. """ # First argument taken by `SimpleTemplateResponse.__init__` is template_name, # which we don't need super(Response, self).__init__(None, status=status) + # We need to store our content in raw content to avoid overriding HttpResponse's # `content` property self.raw_content = content @@ -42,17 +53,14 @@ class Response(SimpleTemplateResponse): self.request = request if renderers is not None: self.renderers = renderers - # TODO: must go - self.view = None - # TODO: wrap this behavior around dispatch(), ensuring it works - # out of the box with existing Django classes that use render_to_response. @property def rendered_content(self): """ + The final rendered content. Accessing this attribute triggers the complete rendering cycle : + selecting suitable renderer, setting response's actual content type, rendering data. """ renderer, media_type = self._determine_renderer() - # TODO: renderer *could* override media_type in .render() if required. # Set the media type of the response self['Content-Type'] = renderer.media_type @@ -65,12 +73,20 @@ class Response(SimpleTemplateResponse): @property def status_text(self): """ - Return reason text corresponding to our HTTP response status code. + Returns reason text corresponding to our HTTP response status code. Provided for convenience. """ return STATUS_CODE_TEXT.get(self.status, '') def _determine_accept_list(self): + """ + Returns a list of accepted media types. This list is determined from : + + 1. overload with `_ACCEPT_QUERY_PARAM` + 2. `Accept` header of the request + + If those are useless, a default value is returned instead. + """ request = self.request if request is None: return ['*/*'] @@ -92,7 +108,7 @@ class Response(SimpleTemplateResponse): def _determine_renderer(self): """ - Determines the appropriate renderer for the output, given the client's 'Accept' header, + Determines the appropriate renderer for the output, given the list of accepted media types, and the :attr:`renderers` set on this class. Returns a 2-tuple of `(renderer, media_type)` @@ -103,16 +119,14 @@ class Response(SimpleTemplateResponse): # attempting more specific media types first # NB. The inner loop here isn't as bad as it first looks :) # Worst case is we're looping over len(accept_list) * len(self.renderers) - renderers = [renderer_cls(self.view) for renderer_cls in self.renderers] - for media_type_list in order_by_precedence(self._determine_accept_list()): - for renderer in renderers: + for renderer in self.renderers: for media_type in media_type_list: if renderer.can_handle_response(media_type): return renderer, media_type # No acceptable renderers were found - raise ErrorResponse(content={'detail': 'Could not satisfy the client\'s Accept header', + raise ImmediateResponse(content={'detail': 'Could not satisfy the client\'s Accept header', 'available_types': self._rendered_media_types}, status=status.HTTP_406_NOT_ACCEPTABLE, renderers=self.renderers) @@ -152,10 +166,9 @@ class Response(SimpleTemplateResponse): return self.renderers[0] -class ErrorResponse(Response, BaseException): +class ImmediateResponse(Response, BaseException): """ - An exception representing an Response that should be returned immediately. - Any content should be serialized as-is, without being filtered. + A subclass of :class:`Response` used to abort the current request handling. """ pass diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index 2a02e04d2..e7dfc3038 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -23,9 +23,10 @@ class UserAgentMungingTest(TestCase): class MockView(View): permissions = () + response_class = Response def get(self, request): - return Response({'a':1, 'b':2, 'c':3}) + return self.response_class({'a':1, 'b':2, 'c':3}) self.req = RequestFactory() self.MockView = MockView diff --git a/djangorestframework/tests/mixins.py b/djangorestframework/tests/mixins.py index 7a1d27694..187ce7193 100644 --- a/djangorestframework/tests/mixins.py +++ b/djangorestframework/tests/mixins.py @@ -6,7 +6,7 @@ from djangorestframework.compat import RequestFactory from django.contrib.auth.models import Group, User from djangorestframework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin from djangorestframework.resources import ModelResource -from djangorestframework.response import Response, ErrorResponse +from djangorestframework.response import Response, ImmediateResponse from djangorestframework.tests.models import CustomUser from djangorestframework.tests.testcases import TestModelsTestCase from djangorestframework.views import View @@ -41,7 +41,7 @@ class TestModelRead(TestModelsTestCase): mixin = ReadModelMixin() mixin.resource = GroupResource - self.assertRaises(ErrorResponse, mixin.get, request, id=12345) + self.assertRaises(ImmediateResponse, mixin.get, request, id=12345) class TestModelCreation(TestModelsTestCase): diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py index 461bc877a..cc211dce2 100644 --- a/djangorestframework/tests/renderers.py +++ b/djangorestframework/tests/renderers.py @@ -73,8 +73,8 @@ class MockGETView(View): urlpatterns = patterns('', - url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])), - url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])), + url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), + url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), ) diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py index 5a01e356f..b8cc5c1b6 100644 --- a/djangorestframework/tests/response.py +++ b/djangorestframework/tests/response.py @@ -1,9 +1,10 @@ import json +import unittest from django.conf.urls.defaults import patterns, url from django.test import TestCase -from djangorestframework.response import Response, ErrorResponse +from djangorestframework.response import Response, ImmediateResponse from djangorestframework.mixins import ResponseMixin from djangorestframework.views import View from djangorestframework.compat import View as DjangoView @@ -17,13 +18,16 @@ from djangorestframework.renderers import BaseRenderer, JSONRenderer, YAMLRender class TestResponseDetermineRenderer(TestCase): def get_response(self, url='', accept_list=[], renderers=[]): - request = RequestFactory().get(url, HTTP_ACCEPT=','.join(accept_list)) + kwargs = {} + if accept_list is not None: + kwargs['HTTP_ACCEPT'] = HTTP_ACCEPT=','.join(accept_list) + request = RequestFactory().get(url, **kwargs) return Response(request=request, renderers=renderers) def get_renderer_mock(self, media_type): return type('RendererMock', (BaseRenderer,), { 'media_type': media_type, - }) + })() def test_determine_accept_list_accept_header(self): """ @@ -32,6 +36,13 @@ class TestResponseDetermineRenderer(TestCase): accept_list = ['application/pickle', 'application/json'] response = self.get_response(accept_list=accept_list) self.assertEqual(response._determine_accept_list(), accept_list) + + def test_determine_accept_list_default(self): + """ + Test that determine_accept_list takes the default renderer if Accept is not specified. + """ + response = self.get_response(accept_list=None) + self.assertEqual(response._determine_accept_list(), ['*/*']) def test_determine_accept_list_overriden_header(self): """ @@ -47,38 +58,46 @@ class TestResponseDetermineRenderer(TestCase): Test that right renderer is chosen, in the order of Accept list. """ accept_list = ['application/pickle', 'application/json'] - PRenderer = self.get_renderer_mock('application/pickle') - JRenderer = self.get_renderer_mock('application/json') + prenderer = self.get_renderer_mock('application/pickle') + jrenderer = self.get_renderer_mock('application/json') - renderers = (PRenderer, JRenderer) - response = self.get_response(accept_list=accept_list, renderers=renderers) + response = self.get_response(accept_list=accept_list, renderers=(prenderer, jrenderer)) renderer, media_type = response._determine_renderer() self.assertEqual(media_type, 'application/pickle') - self.assertTrue(isinstance(renderer, PRenderer)) + self.assertTrue(renderer, prenderer) - renderers = (JRenderer,) - response = self.get_response(accept_list=accept_list, renderers=renderers) + response = self.get_response(accept_list=accept_list, renderers=(jrenderer,)) renderer, media_type = response._determine_renderer() self.assertEqual(media_type, 'application/json') - self.assertTrue(isinstance(renderer, JRenderer)) + self.assertTrue(renderer, jrenderer) + + def test_determine_renderer_default(self): + """ + Test determine renderer when Accept was not specified. + """ + prenderer = self.get_renderer_mock('application/pickle') + + response = self.get_response(accept_list=None, renderers=(prenderer,)) + renderer, media_type = response._determine_renderer() + self.assertEqual(media_type, '*/*') + self.assertTrue(renderer, prenderer) def test_determine_renderer_no_renderer(self): """ Test determine renderer when no renderer can satisfy the Accept list. """ accept_list = ['application/json'] - PRenderer = self.get_renderer_mock('application/pickle') + prenderer = self.get_renderer_mock('application/pickle') - renderers = (PRenderer,) - response = self.get_response(accept_list=accept_list, renderers=renderers) - self.assertRaises(ErrorResponse, response._determine_renderer) + response = self.get_response(accept_list=accept_list, renderers=(prenderer,)) + self.assertRaises(ImmediateResponse, response._determine_renderer) class TestResponseRenderContent(TestCase): def get_response(self, url='', accept_list=[], content=None): request = RequestFactory().get(url, HTTP_ACCEPT=','.join(accept_list)) - return Response(request=request, content=content, renderers=DEFAULT_RENDERERS) + return Response(request=request, content=content, renderers=[r() for r in DEFAULT_RENDERERS]) def test_render(self): """ @@ -116,7 +135,7 @@ class RendererB(BaseRenderer): class MockView(ResponseMixin, DjangoView): - renderers = (RendererA, RendererB) + renderer_classes = (RendererA, RendererB) def get(self, request, **kwargs): response = Response(DUMMYCONTENT, status=DUMMYSTATUS) @@ -124,22 +143,22 @@ class MockView(ResponseMixin, DjangoView): class HTMLView(View): - renderers = (DocumentingHTMLRenderer, ) + renderer_classes = (DocumentingHTMLRenderer, ) def get(self, request, **kwargs): return Response('text') class HTMLView1(View): - renderers = (DocumentingHTMLRenderer, JSONRenderer) + renderer_classes = (DocumentingHTMLRenderer, JSONRenderer) def get(self, request, **kwargs): return Response('text') urlpatterns = patterns('', - url(r'^.*\.(?P.+)$', MockView.as_view(renderers=[RendererA, RendererB])), - url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])), + url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), ) @@ -197,11 +216,11 @@ class RendererIntegrationTests(TestCase): self.assertEquals(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEquals(resp.status_code, DUMMYSTATUS) -# TODO: can't pass because view is a simple Django view and response is an ErrorResponse -# def test_unsatisfiable_accept_header_on_request_returns_406_status(self): -# """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" -# resp = self.client.get('/', HTTP_ACCEPT='foo/bar') -# self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE) + @unittest.skip('can\'t pass because view is a simple Django view and response is an ImmediateResponse') + def test_unsatisfiable_accept_header_on_request_returns_406_status(self): + """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" + resp = self.client.get('/', HTTP_ACCEPT='foo/bar') + self.assertEquals(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE) def test_specified_renderer_serializes_content_on_format_query(self): """If a 'format' query is specified, the renderer with the matching diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py index 1f384b4c8..771b31256 100644 --- a/djangorestframework/tests/validators.py +++ b/djangorestframework/tests/validators.py @@ -2,7 +2,7 @@ from django import forms from django.db import models from django.test import TestCase from djangorestframework.resources import FormResource, ModelResource -from djangorestframework.response import ErrorResponse +from djangorestframework.response import ImmediateResponse from djangorestframework.views import View @@ -81,10 +81,10 @@ class TestNonFieldErrors(TestCase): content = {'field1': 'example1', 'field2': 'example2'} try: MockResource(view).validate_request(content, None) - except ErrorResponse, response: + except ImmediateResponse, response: self.assertEqual(response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) else: - self.fail('ErrorResponse was not raised') + self.fail('ImmediateResponse was not raised') class TestFormValidation(TestCase): @@ -120,14 +120,14 @@ class TestFormValidation(TestCase): def validation_failure_raises_response_exception(self, validator): """If form validation fails a ResourceException 400 (Bad Request) should be raised.""" content = {} - self.assertRaises(ErrorResponse, validator.validate_request, content, None) + self.assertRaises(ImmediateResponse, validator.validate_request, content, None) def validation_does_not_allow_extra_fields_by_default(self, validator): """If some (otherwise valid) content includes fields that are not in the form then validation should fail. It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up broken clients more easily (eg submitting content with a misnamed field)""" content = {'qwerty': 'uiop', 'extra': 'extra'} - self.assertRaises(ErrorResponse, validator.validate_request, content, None) + self.assertRaises(ImmediateResponse, validator.validate_request, content, None) def validation_allows_extra_fields_if_explicitly_set(self, validator): """If we include an allowed_extra_fields paramater on _validate, then allow fields with those names.""" @@ -154,7 +154,7 @@ class TestFormValidation(TestCase): content = {} try: validator.validate_request(content, None) - except ErrorResponse, response: + except ImmediateResponse, response: self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) else: self.fail('ResourceException was not raised') @@ -164,7 +164,7 @@ class TestFormValidation(TestCase): content = {'qwerty': ''} try: validator.validate_request(content, None) - except ErrorResponse, response: + except ImmediateResponse, response: self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}}) else: self.fail('ResourceException was not raised') @@ -174,7 +174,7 @@ class TestFormValidation(TestCase): content = {'qwerty': 'uiop', 'extra': 'extra'} try: validator.validate_request(content, None) - except ErrorResponse, response: + except ImmediateResponse, response: self.assertEqual(response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}}) else: self.fail('ResourceException was not raised') @@ -184,7 +184,7 @@ class TestFormValidation(TestCase): content = {'qwerty': '', 'extra': 'extra'} try: validator.validate_request(content, None) - except ErrorResponse, response: + except ImmediateResponse, response: self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.'], 'extra': ['This field does not exist.']}}) else: @@ -307,14 +307,14 @@ class TestModelFormValidator(TestCase): It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up broken clients more easily (eg submitting content with a misnamed field)""" content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'} - self.assertRaises(ErrorResponse, self.validator.validate_request, content, None) + self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) def test_validate_requires_fields_on_model_forms(self): """If some (otherwise valid) content includes fields that are not in the form then validation should fail. It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up broken clients more easily (eg submitting content with a misnamed field)""" content = {'readonly': 'read only'} - self.assertRaises(ErrorResponse, self.validator.validate_request, content, None) + self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) def test_validate_does_not_require_blankable_fields_on_model_forms(self): """Test standard ModelForm validation behaviour - fields with blank=True are not required.""" diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 44d68641f..8ba05e35a 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -13,7 +13,7 @@ from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_exempt from djangorestframework.compat import View as DjangoView, apply_markdown -from djangorestframework.response import Response, ErrorResponse +from djangorestframework.response import Response, ImmediateResponse from djangorestframework.mixins import * from djangorestframework import resources, renderers, parsers, authentication, permissions, status @@ -81,7 +81,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): or `None` to use default behaviour. """ - renderers = renderers.DEFAULT_RENDERERS + renderer_classes = renderers.DEFAULT_RENDERERS """ List of renderers the resource can serialize the response with, ordered by preference. """ @@ -172,7 +172,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ Return an HTTP 405 error if an operation is called which does not have a handler method. """ - raise ErrorResponse(content= + raise ImmediateResponse(content= {'detail': 'Method \'%s\' not allowed on this resource.' % request.method}, status=status.HTTP_405_METHOD_NOT_ALLOWED) @@ -232,13 +232,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): self.prepare_response(response) # Pre-serialize filtering (eg filter complex objects into natively serializable types) - # TODO: ugly + # TODO: ugly hack to handle both HttpResponse and Response. if hasattr(response, 'raw_content'): response.raw_content = self.filter_response(response.raw_content) else: response.content = self.filter_response(response.content) - except ErrorResponse, response: + except ImmediateResponse, response: # Prepare response for the response cycle. self.prepare_response(response) @@ -259,10 +259,10 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): for name, field in form.fields.iteritems(): field_name_types[name] = field.__class__.__name__ content['fields'] = field_name_types - # Note 'ErrorResponse' is misleading, it's just any response + # Note 'ImmediateResponse' is misleading, it's just any response # that should be rendered and returned immediately, without any # response filtering. - raise ErrorResponse(content=content, status=status.HTTP_200_OK) + raise ImmediateResponse(content=content, status=status.HTTP_200_OK) class ModelView(View):