mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-13 18:11:05 +03:00
Rename mixins into Mixin class, rename ResponseException to ErrorResponse, remove NoContent
This commit is contained in:
parent
a1ed565081
commit
349ffcaf5f
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user