Rename mixins into Mixin class, rename ResponseException to ErrorResponse, remove NoContent

This commit is contained in:
Tom Christie 2011-04-11 16:38:00 +01:00
parent a1ed565081
commit 349ffcaf5f
11 changed files with 64 additions and 191 deletions

View File

@ -4,11 +4,10 @@ by serializing the output along with documentation regarding the Resource, outpu
and providing forms and links depending on the allowed methods, emitters and parsers on the Resource. and providing forms and links depending on the allowed methods, emitters and parsers on the Resource.
""" """
from django.conf import settings from django.conf import settings
from django.http import HttpResponse
from django.template import RequestContext, loader from django.template import RequestContext, loader
from django import forms from django import forms
from djangorestframework.response import NoContent, ResponseException from djangorestframework.response import ErrorResponse
from djangorestframework.utils import dict2xml, url_resolves from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.markdownwrapper import apply_markdown from djangorestframework.markdownwrapper import apply_markdown
from djangorestframework.breadcrumbs import get_breadcrumbs from djangorestframework.breadcrumbs import get_breadcrumbs
@ -18,7 +17,6 @@ from djangorestframework import status
from urllib import quote_plus from urllib import quote_plus
import string import string
import re import re
from decimal import Decimal
try: try:
import json import json
@ -26,132 +24,9 @@ except ImportError:
import simplejson as json import simplejson as json
_MSIE_USER_AGENT = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
class EmitterMixin(object):
"""Adds behaviour for pluggable Emitters to a :class:`.Resource` or Django :class:`View`. class.
Default behaviour is to use standard HTTP Accept header content negotiation.
Also supports overidding 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."""
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
REWRITE_IE_ACCEPT_HEADER = True
request = None
response = None
emitters = ()
def emit(self, response):
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
self.response = response
try:
emitter = self._determine_emitter(self.request)
except ResponseException, exc:
emitter = self.default_emitter
response = exc.response
# Serialize the response content
if response.has_content_body:
content = emitter(self).emit(output=response.cleaned_content)
else:
content = emitter(self).emit()
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
if response.status == 204:
response.status = 200
# Build the HTTP Response
# TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
resp = HttpResponse(content, mimetype=emitter.media_type, status=response.status)
for (key, val) in response.headers.items():
resp[key] = val
return resp
def _determine_emitter(self, request):
"""Return the appropriate emitter for the output, given the client's 'Accept' header,
and the content types that this Resource knows how to serve.
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
# Use _accept parameter override
accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
elif self.REWRITE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and _MSIE_USER_AGENT.match(request.META['HTTP_USER_AGENT']):
accept_list = ['text/html', '*/*']
elif request.META.has_key('HTTP_ACCEPT'):
# Use standard HTTP Accept negotiation
accept_list = request.META["HTTP_ACCEPT"].split(',')
else:
# No accept header specified
return self.default_emitter
# Parse the accept header into a dict of {qvalue: set of media types}
# We ignore mietype parameters
accept_dict = {}
for token in accept_list:
components = token.split(';')
mimetype = components[0].strip()
qvalue = Decimal('1.0')
if len(components) > 1:
# Parse items that have a qvalue eg text/html;q=0.9
try:
(q, num) = components[-1].split('=')
if q == 'q':
qvalue = Decimal(num)
except:
# Skip malformed entries
continue
if accept_dict.has_key(qvalue):
accept_dict[qvalue].add(mimetype)
else:
accept_dict[qvalue] = set((mimetype,))
# Convert to a list of sets ordered by qvalue (highest first)
accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
for accept_set in accept_sets:
# Return any exact match
for emitter in self.emitters:
if emitter.media_type in accept_set:
return emitter
# Return any subtype match
for emitter in self.emitters:
if emitter.media_type.split('/')[0] + '/*' in accept_set:
return emitter
# Return default
if '*/*' in accept_set:
return self.default_emitter
raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
{'detail': 'Could not statisfy the client\'s Accept header',
'available_types': self.emitted_media_types})
@property
def emitted_media_types(self):
"""Return an list of all the media types that this resource can emit."""
return [emitter.media_type for emitter in self.emitters]
@property
def default_emitter(self):
"""Return the resource's most prefered emitter.
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
return self.emitters[0]
# TODO: Rename verbose to something more appropriate # TODO: Rename verbose to something more appropriate
# TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default, # TODO: Maybe None could be handled more cleanly. It'd be nice if it was handled by default,
# and only have an emitter output anything if it explicitly provides support for that. # and only have an emitter output anything if it explicitly provides support for that.
class BaseEmitter(object): class BaseEmitter(object):
@ -162,10 +37,10 @@ class BaseEmitter(object):
def __init__(self, resource): def __init__(self, resource):
self.resource = resource self.resource = resource
def emit(self, output=NoContent, verbose=False): def emit(self, output=None, verbose=False):
"""By default emit simply returns the ouput as-is. """By default emit simply returns the ouput as-is.
Override this method to provide for other behaviour.""" Override this method to provide for other behaviour."""
if output is NoContent: if output is None:
return '' return ''
return output return output
@ -177,8 +52,8 @@ class TemplateEmitter(BaseEmitter):
media_type = None media_type = None
template = None template = None
def emit(self, output=NoContent, verbose=False): def emit(self, output=None, verbose=False):
if output is NoContent: if output is None:
return '' return ''
context = RequestContext(self.request, output) context = RequestContext(self.request, output)
@ -276,7 +151,7 @@ class DocumentingTemplateEmitter(BaseEmitter):
return GenericContentForm(resource) return GenericContentForm(resource)
def emit(self, output=NoContent): def emit(self, output=None):
content = self._get_content(self.resource, self.resource.request, output) content = self._get_content(self.resource, self.resource.request, output)
form_instance = self._get_form_instance(self.resource) form_instance = self._get_form_instance(self.resource)
@ -324,8 +199,8 @@ class JSONEmitter(BaseEmitter):
"""Emitter which serializes to JSON""" """Emitter which serializes to JSON"""
media_type = 'application/json' media_type = 'application/json'
def emit(self, output=NoContent, verbose=False): def emit(self, output=None, verbose=False):
if output is NoContent: if output is None:
return '' return ''
if verbose: if verbose:
return json.dumps(output, indent=4, sort_keys=True) return json.dumps(output, indent=4, sort_keys=True)
@ -336,8 +211,8 @@ class XMLEmitter(BaseEmitter):
"""Emitter which serializes to XML.""" """Emitter which serializes to XML."""
media_type = 'application/xml' media_type = 'application/xml'
def emit(self, output=NoContent, verbose=False): def emit(self, output=None, verbose=False):
if output is NoContent: if output is None:
return '' return ''
return dict2xml(output) return dict2xml(output)

View File

@ -3,7 +3,7 @@ from django.db.models import Model
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models.fields.related import RelatedField from django.db.models.fields.related import RelatedField
from djangorestframework.response import Response, ResponseException from djangorestframework.response import Response, ErrorResponse
from djangorestframework.resource import Resource from djangorestframework.resource import Resource
from djangorestframework import status, validators from djangorestframework import status, validators
@ -370,7 +370,7 @@ class ModelResource(Resource):
# Otherwise assume the kwargs uniquely identify the model # Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs) instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist: except self.model.DoesNotExist:
raise ResponseException(status.HTTP_404_NOT_FOUND) raise ErrorResponse(status.HTTP_404_NOT_FOUND)
return instance return instance
@ -402,7 +402,7 @@ class ModelResource(Resource):
# Otherwise assume the kwargs uniquely identify the model # Otherwise assume the kwargs uniquely identify the model
instance = self.model.objects.get(**kwargs) instance = self.model.objects.get(**kwargs)
except self.model.DoesNotExist: except self.model.DoesNotExist:
raise ResponseException(status.HTTP_404_NOT_FOUND, None, {}) raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
instance.delete() instance.delete()
return return

View File

@ -9,7 +9,7 @@ We need a method to be able to:
and multipart/form-data. (eg also handle multipart/json) and multipart/form-data. (eg also handle multipart/json)
""" """
from django.http.multipartparser import MultiPartParser as DjangoMPParser from django.http.multipartparser import MultiPartParser as DjangoMPParser
from djangorestframework.response import ResponseException from djangorestframework.response import ErrorResponse
from djangorestframework import status from djangorestframework import status
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple
from djangorestframework.mediatypes import MediaType from djangorestframework.mediatypes import MediaType
@ -59,7 +59,7 @@ class JSONParser(BaseParser):
try: try:
return json.load(stream) return json.load(stream)
except ValueError, exc: except ValueError, exc:
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)}) raise ErrorResponse(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class DataFlatener(object): class DataFlatener(object):

View File

@ -2,9 +2,8 @@ from django.core.urlresolvers import set_script_prefix
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View from djangorestframework.compat import View
from djangorestframework.emitters import EmitterMixin from djangorestframework.response import Response, ErrorResponse
from djangorestframework.response import Response, ResponseException from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
from djangorestframework.request import RequestMixin, AuthMixin
from djangorestframework import emitters, parsers, authenticators, validators, status from djangorestframework import emitters, parsers, authenticators, validators, status
@ -16,7 +15,7 @@ from djangorestframework import emitters, parsers, authenticators, validators, s
__all__ = ['Resource'] __all__ = ['Resource']
class Resource(EmitterMixin, AuthMixin, RequestMixin, View): class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
"""Handles incoming requests and maps them to REST operations, """Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization.""" performing authentication, input deserialization, input validation, output serialization."""
@ -81,7 +80,7 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
def not_implemented(self, operation): def not_implemented(self, operation):
"""Return an HTTP 500 server error if an operation is called which has been allowed by """Return an HTTP 500 server error if an operation is called which has been allowed by
allowed_methods, but which has not been implemented.""" allowed_methods, but which has not been implemented."""
raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR, raise ErrorResponse(status.HTTP_500_INTERNAL_SERVER_ERROR,
{'detail': '%s operation on this resource has not been implemented' % (operation, )}) {'detail': '%s operation on this resource has not been implemented' % (operation, )})
@ -89,15 +88,15 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
"""Ensure the request method is permitted for this resource, raising a ResourceException if it is not.""" """Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
if not method in self.callmap.keys(): if not method in self.callmap.keys():
raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED, raise ErrorResponse(status.HTTP_501_NOT_IMPLEMENTED,
{'detail': 'Unknown or unsupported method \'%s\'' % method}) {'detail': 'Unknown or unsupported method \'%s\'' % method})
if not method in self.allowed_methods: if not method in self.allowed_methods:
raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED, raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % method}) {'detail': 'Method \'%s\' not allowed on this resource.' % method})
if auth is None and not method in self.anon_allowed_methods: if auth is None and not method in self.anon_allowed_methods:
raise ResponseException(status.HTTP_403_FORBIDDEN, raise ErrorResponse(status.HTTP_403_FORBIDDEN,
{'detail': 'You do not have permission to access this resource. ' + {'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'}) 'You may need to login or otherwise authenticate the request.'})
@ -172,7 +171,7 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
# Pre-serialize filtering (eg filter complex objects into natively serializable types) # Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.cleanup_response(response.raw_content) response.cleaned_content = self.cleanup_response(response.raw_content)
except ResponseException, exc: except ErrorResponse, exc:
response = exc.response response = exc.response
except: except:
@ -183,8 +182,12 @@ class Resource(EmitterMixin, AuthMixin, RequestMixin, View):
# #
# TODO - this isn't actually the correct way to set the vary header, # TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out. # also it's currently sub-obtimal for HTTP caching - need to sort that out.
try:
response.headers['Allow'] = ', '.join(self.allowed_methods) response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept' response.headers['Vary'] = 'Authenticate, Accept'
return self.emit(response) return self.emit(response)
except:
import traceback
traceback.print_exc()

View File

@ -1,24 +1,16 @@
from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.core.handlers.wsgi import STATUS_CODE_TEXT
__all__ =['NoContent', 'Response', ] __all__ =['Response', 'ErrorResponse']
class NoContent(object):
"""Used to indicate no body in http response.
(We cannot just use None, as that is a valid, serializable response object.)
TODO: On reflection I'm going to get rid of this and just not support serialized 'None' responses.
"""
pass
# TODO: remove raw_content/cleaned_content and just use content?
class Response(object): class Response(object):
def __init__(self, status=200, content=NoContent, headers={}): """An HttpResponse that may include content that hasn't yet been serialized."""
def __init__(self, status=200, content=None, headers={}):
self.status = status self.status = status
self.has_content_body = not content is NoContent # TODO: remove and just use content self.has_content_body = content is not None
self.raw_content = content # content prior to filtering - TODO: remove and just use content self.raw_content = content # content prior to filtering
self.cleaned_content = content # content after filtering TODO: remove and just use content self.cleaned_content = content # content after filtering
self.headers = headers self.headers = headers
@property @property
@ -28,6 +20,7 @@ class Response(object):
return STATUS_CODE_TEXT.get(self.status, '') return STATUS_CODE_TEXT.get(self.status, '')
class ResponseException(BaseException): class ErrorResponse(BaseException):
def __init__(self, status, content=NoContent, headers={}): """An exception representing an HttpResponse that should be returned immediatley."""
def __init__(self, status, content=None, headers={}):
self.response = Response(status, content=content, headers=headers) self.response = Response(status, content=content, headers=headers)

View File

@ -3,7 +3,7 @@ Tests for content parsing, and form-overloaded content parsing.
""" """
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin from djangorestframework.mixins import RequestMixin
from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser from djangorestframework.parsers import FormParser, MultipartParser, PlainTextParser

View File

@ -2,7 +2,8 @@ from django.conf.urls.defaults import patterns, url
from django import http from django import http
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import View from djangorestframework.compat import View
from djangorestframework.emitters import EmitterMixin, BaseEmitter from djangorestframework.emitters import BaseEmitter
from djangorestframework.mixins import ResponseMixin
from djangorestframework.response import Response from djangorestframework.response import Response
DUMMYSTATUS = 200 DUMMYSTATUS = 200
@ -11,7 +12,7 @@ DUMMYCONTENT = 'dummycontent'
EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x EMITTER_A_SERIALIZER = lambda x: 'Emitter A: %s' % x
EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x EMITTER_B_SERIALIZER = lambda x: 'Emitter B: %s' % x
class MockView(EmitterMixin, View): class MockView(ResponseMixin, View):
def get(self, request): def get(self, request):
response = Response(DUMMYSTATUS, DUMMYCONTENT) response = Response(DUMMYSTATUS, DUMMYCONTENT)
return self.emit(response) return self.emit(response)

View File

@ -1,6 +1,6 @@
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.request import RequestMixin from djangorestframework.mixins import RequestMixin
class TestMethodOverloading(TestCase): class TestMethodOverloading(TestCase):

View File

@ -3,7 +3,7 @@ from django.db import models
from django.test import TestCase from django.test import TestCase
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
from djangorestframework.response import ResponseException from djangorestframework.response import ErrorResponse
class TestValidatorMixinInterfaces(TestCase): class TestValidatorMixinInterfaces(TestCase):
@ -81,7 +81,7 @@ class TestNonFieldErrors(TestCase):
content = {'field1': 'example1', 'field2': 'example2'} content = {'field1': 'example1', 'field2': 'example2'}
try: try:
FormValidator(view).validate(content) FormValidator(view).validate(content)
except ResponseException, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -115,14 +115,14 @@ class TestFormValidation(TestCase):
def validation_failure_raises_response_exception(self, validator): def validation_failure_raises_response_exception(self, validator):
"""If form validation fails a ResourceException 400 (Bad Request) should be raised.""" """If form validation fails a ResourceException 400 (Bad Request) should be raised."""
content = {} content = {}
self.assertRaises(ResponseException, validator.validate, content) self.assertRaises(ErrorResponse, validator.validate, content)
def validation_does_not_allow_extra_fields_by_default(self, validator): 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. """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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'uiop', 'extra': 'extra'} content = {'qwerty': 'uiop', 'extra': 'extra'}
self.assertRaises(ResponseException, validator.validate, content) self.assertRaises(ErrorResponse, validator.validate, content)
def validation_allows_extra_fields_if_explicitly_set(self, validator): 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.""" """If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
@ -139,7 +139,7 @@ class TestFormValidation(TestCase):
content = {} content = {}
try: try:
validator.validate(content) validator.validate(content)
except ResponseException, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -149,7 +149,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': ''} content = {'qwerty': ''}
try: try:
validator.validate(content) validator.validate(content)
except ResponseException, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}}) self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -159,7 +159,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': 'uiop', 'extra': 'extra'} content = {'qwerty': 'uiop', 'extra': 'extra'}
try: try:
validator.validate(content) validator.validate(content)
except ResponseException, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}}) self.assertEqual(exc.response.raw_content, {'field-errors': {'extra': ['This field does not exist.']}})
else: else:
self.fail('ResourceException was not raised') #pragma: no cover self.fail('ResourceException was not raised') #pragma: no cover
@ -169,7 +169,7 @@ class TestFormValidation(TestCase):
content = {'qwerty': '', 'extra': 'extra'} content = {'qwerty': '', 'extra': 'extra'}
try: try:
validator.validate(content) validator.validate(content)
except ResponseException, exc: except ErrorResponse, exc:
self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'], self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.'],
'extra': ['This field does not exist.']}}) 'extra': ['This field does not exist.']}})
else: else:
@ -286,14 +286,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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'} content = {'qwerty': 'example', 'uiop':'example', 'readonly': 'read only', 'extra': 'extra'}
self.assertRaises(ResponseException, self.validator.validate, content) self.assertRaises(ErrorResponse, self.validator.validate, content)
def test_validate_requires_fields_on_model_forms(self): 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. """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 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)""" broken clients more easily (eg submitting content with a misnamed field)"""
content = {'readonly': 'read only'} content = {'readonly': 'read only'}
self.assertRaises(ResponseException, self.validator.validate, content) self.assertRaises(ErrorResponse, self.validator.validate, content)
def test_validate_does_not_require_blankable_fields_on_model_forms(self): def test_validate_does_not_require_blankable_fields_on_model_forms(self):
"""Test standard ModelForm validation behaviour - fields with blank=True are not required.""" """Test standard ModelForm validation behaviour - fields with blank=True are not required."""

View File

@ -14,6 +14,7 @@ except ImportError:
# """Adds the ADMIN_MEDIA_PREFIX to the request context.""" # """Adds the ADMIN_MEDIA_PREFIX to the request context."""
# return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX} # return {'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX}
MSIE_USER_AGENT_REGEX = re.compile(r'^Mozilla/[0-9]+\.[0-9]+ \([^)]*; MSIE [0-9]+\.[0-9]+[a-z]?;[^)]*\)(?!.* Opera )')
def as_tuple(obj): def as_tuple(obj):
"""Given obj return a tuple""" """Given obj return a tuple"""

View File

@ -1,7 +1,7 @@
"""Mixin classes that provide a validate(content) function to validate and cleanup request content""" """Mixin classes that provide a validate(content) function to validate and cleanup request content"""
from django import forms from django import forms
from django.db import models from django.db import models
from djangorestframework.response import ResponseException from djangorestframework.response import ErrorResponse
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple
@ -13,7 +13,7 @@ class BaseValidator(object):
def validate(self, content): def validate(self, content):
"""Given some content as input return some cleaned, validated content. """Given some content as input return some cleaned, validated content.
Typically raises a ResponseException with status code 400 (Bad Request) on failure. Typically raises a ErrorResponse with status code 400 (Bad Request) on failure.
Must be overridden to be implemented.""" Must be overridden to be implemented."""
raise NotImplementedError() raise NotImplementedError()
@ -32,11 +32,11 @@ class FormValidator(BaseValidator):
def validate(self, content): def validate(self, content):
"""Given some content as input return some cleaned, validated content. """Given some content as input return some cleaned, validated content.
Raises a ResponseException with status code 400 (Bad Request) on failure. Raises a ErrorResponse 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. Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys. On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors. If the '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}.""" If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
return self._validate(content) return self._validate(content)
@ -97,7 +97,7 @@ class FormValidator(BaseValidator):
detail[u'field-errors'] = field_errors detail[u'field-errors'] = field_errors
# Return HTTP 400 response (BAD REQUEST) # Return HTTP 400 response (BAD REQUEST)
raise ResponseException(400, detail) raise ErrorResponse(400, detail)
def get_bound_form(self, content=None): def get_bound_form(self, content=None):
@ -139,14 +139,14 @@ class ModelFormValidator(FormValidator):
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.) # TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
def validate(self, content): def validate(self, content):
"""Given some content as input return some cleaned, validated content. """Given some content as input return some cleaned, validated content.
Raises a ResponseException with status code 400 (Bad Request) on failure. Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form or model form validation, Validation is standard form or model form validation,
with an additional constraint that no extra unknown fields may be supplied, 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, and that all fields specified by the fields class attribute must be supplied,
even if they are not validated by the form/model form. even if they are not validated by the form/model form.
On failure the ResponseException content is a dict which may contain 'errors' and 'field-errors' keys. On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors. If the '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}.""" If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
return self._validate(content, allowed_extra_fields=self._property_fields_set) return self._validate(content, allowed_extra_fields=self._property_fields_set)