cleaned a bit Response/ResponseMixin code, added some documentation + renamed ErrorResponse to ImmediateResponse

This commit is contained in:
Sébastien Piquemal 2012-02-07 13:15:30 +02:00
parent a0dc0b10e5
commit ca96b4523b
13 changed files with 155 additions and 113 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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]'

View File

@ -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)

View File

@ -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}.
"""

View File

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

View File

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

View File

@ -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):

View File

@ -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])),
)

View File

@ -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):
"""
@ -33,6 +37,13 @@ class TestResponseDetermineRenderer(TestCase):
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):
"""
Test Accept header overriding.
@ -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<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
url(r'^.*\.(?P<format>.+)$', 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

View File

@ -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."""

View File

@ -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):