aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortom christie tom@tomchristie.com2011-04-02 16:32:37 +0100
committertom christie tom@tomchristie.com2011-04-02 16:32:37 +0100
commit4687db680cda52e9836743940e4cf7279b307294 (patch)
tree23b9b22eee3c08f6de09295b3c6630f5fb0730fa
parent8845b281fe9aafbc9f9b2a283fafbde9787f4734 (diff)
downloaddjango-rest-framework-4687db680cda52e9836743940e4cf7279b307294.tar.bz2
Refactor to use self.CONTENT to access request body. Get file upload working
-rw-r--r--.hgignore1
-rw-r--r--djangorestframework/content.py57
-rw-r--r--djangorestframework/emitters.py3
-rw-r--r--djangorestframework/mediatypes.py78
-rw-r--r--djangorestframework/methods.py35
-rw-r--r--djangorestframework/parsers.py123
-rw-r--r--djangorestframework/request.py128
-rw-r--r--djangorestframework/resource.py61
-rw-r--r--djangorestframework/templates/emitter.html4
-rw-r--r--djangorestframework/tests/__init__.py1
-rw-r--r--djangorestframework/tests/content.py241
-rw-r--r--djangorestframework/tests/files.py37
-rw-r--r--djangorestframework/tests/methods.py105
-rw-r--r--djangorestframework/tests/parsers.py23
-rw-r--r--djangorestframework/tests/validators.py2
-rw-r--r--djangorestframework/validators.py13
16 files changed, 540 insertions, 372 deletions
diff --git a/.hgignore b/.hgignore
index 49cc6236..5e9c0398 100644
--- a/.hgignore
+++ b/.hgignore
@@ -21,3 +21,4 @@ MANIFEST
.cache
.coverage
.tox
+.DS_Store
diff --git a/djangorestframework/content.py b/djangorestframework/content.py
deleted file mode 100644
index cfdd33be..00000000
--- a/djangorestframework/content.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""Mixin classes that provide a determine_content(request) method to return the content type and content of a request.
-We use this more generic behaviour to allow for overloaded content in POST forms.
-"""
-
-class ContentMixin(object):
- """Base class for all ContentMixin classes, which simply defines the interface they provide."""
-
- def determine_content(self, request):
- """If the request contains content return a tuple of (content_type, content) otherwise return None.
- Note that content_type may be None if it is unset.
- Must be overridden to be implemented."""
- raise NotImplementedError()
-
-
-class StandardContentMixin(ContentMixin):
- """Standard HTTP request content behaviour.
- See RFC 2616 sec 4.3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3"""
-
- def determine_content(self, request):
- """If the request contains content return a tuple of (content_type, content) otherwise return None.
- Note that content_type may be None if it is unset."""
-
- if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
- return None
- return (request.META.get('CONTENT_TYPE', None), request.raw_post_data)
-
-
-class OverloadedContentMixin(ContentMixin):
- """HTTP request content behaviour that also allows arbitrary content to be tunneled in form data."""
-
- """The name to use for the content override field in the POST form.
- Set this to *None* to desactivate content overloading."""
- CONTENT_PARAM = '_content'
-
- """The name to use for the content-type override field in the POST form.
- Taken into account only if content overloading is activated."""
- CONTENTTYPE_PARAM = '_contenttype'
-
- def determine_content(self, request):
- """If the request contains content, returns a tuple of (content_type, content) otherwise returns None.
- Note that content_type may be None if it is unset."""
- if not request.META.get('CONTENT_LENGTH', None) and not request.META.get('TRANSFER_ENCODING', None):
- return None
- content_type = request.META.get('CONTENT_TYPE', None)
-
- if (request.method == 'POST' and self.CONTENT_PARAM and
- request.POST.get(self.CONTENT_PARAM, None) is not None):
-
- # Set content type if form contains a non-empty CONTENTTYPE_PARAM field
- content_type = None
- if self.CONTENTTYPE_PARAM and request.POST.get(self.CONTENTTYPE_PARAM, None):
- content_type = request.POST.get(self.CONTENTTYPE_PARAM, None)
- request.META['CONTENT_TYPE'] = content_type # TODO : VERY BAD, avoid modifying original request.
-
- return (content_type, request.POST[self.CONTENT_PARAM])
- else:
- return (content_type, request.raw_post_data)
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
index be1d7ef3..4cd462cb 100644
--- a/djangorestframework/emitters.py
+++ b/djangorestframework/emitters.py
@@ -13,7 +13,6 @@ from djangorestframework.validators import FormValidatorMixin
from djangorestframework.utils import dict2xml, url_resolves
from djangorestframework.markdownwrapper import apply_markdown
from djangorestframework.breadcrumbs import get_breadcrumbs
-from djangorestframework.content import OverloadedContentMixin
from djangorestframework.description import get_name, get_description
from djangorestframework import status
@@ -254,7 +253,7 @@ class DocumentingTemplateEmitter(BaseEmitter):
# If we're not using content overloading there's no point in supplying a generic form,
# as the resource won't treat the form's value as the content of the request.
- if not isinstance(resource, OverloadedContentMixin):
+ if not getattr(resource, 'USE_FORM_OVERLOADING', False):
return None
# NB. http://jacobian.org/writing/dynamic-form-generation/
diff --git a/djangorestframework/mediatypes.py b/djangorestframework/mediatypes.py
new file mode 100644
index 00000000..d1641a8f
--- /dev/null
+++ b/djangorestframework/mediatypes.py
@@ -0,0 +1,78 @@
+"""
+Handling of media types, as found in HTTP Content-Type and Accept headers.
+
+See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
+"""
+
+from django.http.multipartparser import parse_header
+
+
+class MediaType(object):
+ def __init__(self, media_type_str):
+ self.orig = media_type_str
+ self.media_type, self.params = parse_header(media_type_str)
+ self.main_type, sep, self.sub_type = self.media_type.partition('/')
+
+ def match(self, other):
+ """Return true if this MediaType satisfies the constraint of the given MediaType."""
+ for key in other.params.keys():
+ if key != 'q' and other.params[key] != self.params.get(key, None):
+ return False
+
+ if other.sub_type != '*' and other.sub_type != self.sub_type:
+ return False
+
+ if other.main_type != '*' and other.main_type != self.main_type:
+ return False
+
+ return True
+
+ def precedence(self):
+ """
+ Return a precedence level for the media type given how specific it is.
+ """
+ if self.main_type == '*':
+ return 1
+ elif self.sub_type == '*':
+ return 2
+ elif not self.params or self.params.keys() == ['q']:
+ return 3
+ return 4
+
+ def quality(self):
+ """
+ Return a quality level for the media type.
+ """
+ try:
+ return Decimal(self.params.get('q', '1.0'))
+ except:
+ return Decimal(0)
+
+ def score(self):
+ """
+ Return an overall score for a given media type given it's quality and precedence.
+ """
+ # NB. quality values should only have up to 3 decimal points
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
+ return self.quality * 10000 + self.precedence
+
+ def is_form(self):
+ """
+ Return True if the MediaType is a valid form media type as defined by the HTML4 spec.
+ (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
+ """
+ return self.media_type == 'application/x-www-form-urlencoded' or \
+ self.media_type == 'multipart/form-data'
+
+ def as_tuple(self):
+ return (self.main_type, self.sub_type, self.params)
+
+ def __repr__(self):
+ return "<MediaType %s>" % (self.as_tuple(),)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ def __unicode__(self):
+ return self.orig
+
diff --git a/djangorestframework/methods.py b/djangorestframework/methods.py
deleted file mode 100644
index 088c563c..00000000
--- a/djangorestframework/methods.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""Mixin classes that provide a determine_method(request) function to determine the HTTP
-method that a given request should be treated as. We use this more generic behaviour to
-allow for overloaded methods in POST forms.
-
-See Richardson & Ruby's RESTful Web Services for justification.
-"""
-
-class MethodMixin(object):
- """Base class for all MethodMixin classes, which simply defines the interface they provide."""
- def determine_method(self, request):
- """Simply return GET, POST etc... as appropriate."""
- raise NotImplementedError()
-
-
-class StandardMethodMixin(MethodMixin):
- """Provide for standard HTTP behaviour, with no overloaded POST."""
-
- def determine_method(self, request):
- """Simply return GET, POST etc... as appropriate."""
- return request.method.upper()
-
-
-class OverloadedPOSTMethodMixin(MethodMixin):
- """Provide for overloaded POST behaviour."""
-
- """The name to use for the method override field in the POST form."""
- METHOD_PARAM = '_method'
-
- def determine_method(self, request):
- """Simply return GET, POST etc... as appropriate, allowing for POST overloading
- by setting a form field with the requested method name."""
- method = request.method.upper()
- if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
- method = request.POST[self.METHOD_PARAM].upper()
- return method \ No newline at end of file
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
index 3bd020ad..1503342c 100644
--- a/djangorestframework/parsers.py
+++ b/djangorestframework/parsers.py
@@ -1,9 +1,18 @@
-from StringIO import StringIO
+"""Django supports parsing the content of an HTTP request, but only for form POST requests.
+That behaviour is sufficient for dealing with standard HTML forms, but it doesn't map well
+to general HTTP requests.
-from django.http.multipartparser import MultiPartParser as DjangoMPParser
+We need a method to be able to:
+1) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
+2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
+ and multipart/form-data. (eg also handle multipart/json)
+"""
+from django.http.multipartparser import MultiPartParser as DjangoMPParser
from djangorestframework.response import ResponseException
from djangorestframework import status
+from djangorestframework.utils import as_tuple
+from djangorestframework.mediatypes import MediaType
try:
import json
@@ -18,22 +27,27 @@ except ImportError:
class ParserMixin(object):
parsers = ()
- def parse(self, content_type, content):
- # See RFC 2616 sec 3 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
- split = content_type.split(';', 1)
- if len(split) > 1:
- content_type = split[0]
- content_type = content_type.strip()
+ def parse(self, stream, content_type):
+ """
+ Parse the request content.
- media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
+ May raise a 415 ResponseException (Unsupported Media Type),
+ or a 400 ResponseException (Bad Request).
+ """
+ parsers = as_tuple(self.parsers)
- try:
- parser = media_type_to_parser[content_type]
- except KeyError:
+ parser = None
+ for parser_cls in parsers:
+ if parser_cls.handles(content_type):
+ parser = parser_cls(self)
+ break
+
+ if parser is None:
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- {'error': 'Unsupported media type in request \'%s\'.' % content_type})
-
- return parser(self).parse(content)
+ {'error': 'Unsupported media type in request \'%s\'.' %
+ content_type.media_type})
+
+ return parser.parse(stream)
@property
def parsed_media_types(self):
@@ -48,36 +62,41 @@ class ParserMixin(object):
class BaseParser(object):
- """All parsers should extend BaseParser, specifing a media_type attribute,
+ """All parsers should extend BaseParser, specifying a media_type attribute,
and overriding the parse() method."""
-
media_type = None
- def __init__(self, resource):
- """Initialise the parser with the Resource instance as state,
- in case the parser needs to access any metadata on the Resource object."""
- self.resource = resource
+ def __init__(self, view):
+ """
+ Initialise the parser with the View instance as state,
+ in case the parser needs to access any metadata on the View object.
+
+ """
+ self.view = view
- def parse(self, input):
- """Given some serialized input, return the deserialized output.
- The input will be the raw request content body. The return value may be of
- any type, but for many parsers/inputs it might typically be a dict."""
- return input
+ @classmethod
+ def handles(self, media_type):
+ """
+ Returns `True` if this parser is able to deal with the given MediaType.
+ """
+ return media_type.match(self.media_type)
+
+ def parse(self, stream):
+ """Given a stream to read from, return the deserialized output.
+ The return value may be of any type, but for many parsers it might typically be a dict-like object."""
+ raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")
class JSONParser(BaseParser):
- media_type = 'application/json'
+ media_type = MediaType('application/json')
- def parse(self, input):
+ def parse(self, stream):
try:
- return json.loads(input)
+ return json.load(stream)
except ValueError, exc:
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
-class XMLParser(BaseParser):
- media_type = 'application/xml'
-
class DataFlatener(object):
"""Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data."""
@@ -102,6 +121,7 @@ class DataFlatener(object):
*val_list* which is the received value for parameter *key* can be used to guess the answer."""
return False
+
class FormParser(BaseParser, DataFlatener):
"""The default parser for form data.
Return a dict containing a single value for each non-reserved parameter.
@@ -109,16 +129,17 @@ class FormParser(BaseParser, DataFlatener):
In order to handle select multiple (and having possibly more than a single value for each parameter),
you can customize the output by subclassing the method 'is_a_list'."""
- media_type = 'application/x-www-form-urlencoded'
+ media_type = MediaType('application/x-www-form-urlencoded')
"""The value of the parameter when the select multiple is empty.
Browsers are usually stripping the select multiple that have no option selected from the parameters sent.
A common hack to avoid this is to send the parameter with a value specifying that the list is empty.
This value will always be stripped before the data is returned."""
EMPTY_VALUE = '_empty'
+ RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
- def parse(self, input):
- data = parse_qs(input, keep_blank_values=True)
+ def parse(self, stream):
+ data = parse_qs(stream.read(), keep_blank_values=True)
# removing EMPTY_VALUEs from the lists and flatening the data
for key, val_list in data.items():
@@ -127,8 +148,9 @@ class FormParser(BaseParser, DataFlatener):
# Strip any parameters that we are treating as reserved
for key in data.keys():
- if key in self.resource.RESERVED_FORM_PARAMS:
+ if key in self.RESERVED_FORM_PARAMS:
data.pop(key)
+
return data
def remove_empty_val(self, val_list):
@@ -141,27 +163,28 @@ class FormParser(BaseParser, DataFlatener):
else:
val_list.pop(ind)
-# TODO: Allow parsers to specify multiple media_types
-class MultipartParser(BaseParser, DataFlatener):
- media_type = 'multipart/form-data'
- def parse(self, input):
+class MultipartData(dict):
+ def __init__(self, data, files):
+ dict.__init__(self, data)
+ self.FILES = files
+
+class MultipartParser(BaseParser, DataFlatener):
+ media_type = MediaType('multipart/form-data')
+ RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)
- request = self.resource.request
- #TODO : that's pretty dumb : files are loaded with
- #upload_handlers, but as we read the request body completely (input),
- #then it kind of misses the point. Why not input as a stream ?
- upload_handlers = request._get_upload_handlers()
- django_mpp = DjangoMPParser(request.META, StringIO(input), upload_handlers)
+ def parse(self, stream):
+ upload_handlers = self.view.request._get_upload_handlers()
+ django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers)
data, files = django_mpp.parse()
# Flatening data, files and combining them
data = self.flatten_data(dict(data.iterlists()))
files = self.flatten_data(dict(files.iterlists()))
- data.update(files)
-
+
# Strip any parameters that we are treating as reserved
for key in data.keys():
- if key in self.resource.RESERVED_FORM_PARAMS:
+ if key in self.RESERVED_FORM_PARAMS:
data.pop(key)
- return data
+
+ return MultipartData(data, files)
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
new file mode 100644
index 00000000..9c6f8f30
--- /dev/null
+++ b/djangorestframework/request.py
@@ -0,0 +1,128 @@
+from djangorestframework.mediatypes import MediaType
+#from djangorestframework.requestparsing import parse, load_parser
+from StringIO import StringIO
+
+class RequestMixin(object):
+ """Delegate class that supplements an HttpRequest object with additional behaviour."""
+
+ USE_FORM_OVERLOADING = True
+ METHOD_PARAM = "_method"
+ CONTENTTYPE_PARAM = "_content_type"
+ CONTENT_PARAM = "_content"
+
+ def _get_method(self):
+ """
+ Returns the HTTP method for the current view.
+ """
+ if not hasattr(self, '_method'):
+ self._method = self.request.method
+ return self._method
+
+
+ def _set_method(self, method):
+ """
+ Set the method for the current view.
+ """
+ self._method = method
+
+
+ def _get_content_type(self):
+ """
+ Returns a MediaType object, representing the request's content type header.
+ """
+ if not hasattr(self, '_content_type'):
+ content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', ''))
+ self._content_type = MediaType(content_type)
+ return self._content_type
+
+
+ def _set_content_type(self, content_type):
+ """
+ Set the content type. Should be a MediaType object.
+ """
+ self._content_type = content_type
+
+
+ def _get_accept(self):
+ """
+ Returns a list of MediaType objects, representing the request's accept header.
+ """
+ if not hasattr(self, '_accept'):
+ accept = self.request.META.get('HTTP_ACCEPT', '*/*')
+ self._accept = [MediaType(elem) for elem in accept.split(',')]
+ return self._accept
+
+
+ def _set_accept(self):
+ """
+ Set the acceptable media types. Should be a list of MediaType objects.
+ """
+ self._accept = accept
+
+
+ def _get_stream(self):
+ """
+ Returns an object that may be used to stream the request content.
+ """
+ if not hasattr(self, '_stream'):
+ if hasattr(self.request, 'read'):
+ self._stream = self.request
+ else:
+ self._stream = StringIO(self.request.raw_post_data)
+ return self._stream
+
+
+ def _set_stream(self, stream):
+ """
+ Set the stream representing the request body.
+ """
+ self._stream = stream
+
+
+ def _get_raw_content(self):
+ """
+ Returns the parsed content of the request
+ """
+ if not hasattr(self, '_raw_content'):
+ self._raw_content = self.parse(self.stream, self.content_type)
+ return self._raw_content
+
+
+ def _get_content(self):
+ """
+ Returns the parsed and validated content of the request
+ """
+ if not hasattr(self, '_content'):
+ self._content = self.validate(self.RAW_CONTENT)
+
+ return self._content
+
+
+ def perform_form_overloading(self):
+ """
+ Check the request to see if it is using form POST '_method'/'_content'/'_content_type' overrides.
+ If it is then alter self.method, self.content_type, self.CONTENT to reflect that rather than simply
+ delegating them to the original request.
+ """
+ if not self.USE_FORM_OVERLOADING or self.method != 'POST' or not self.content_type.is_form():
+ return
+
+ content = self.RAW_CONTENT
+ if self.METHOD_PARAM in content:
+ self.method = content[self.METHOD_PARAM].upper()
+ del self._raw_content[self.METHOD_PARAM]
+
+ if self.CONTENT_PARAM in content and self.CONTENTTYPE_PARAM in content:
+ self._content_type = MediaType(content[self.CONTENTTYPE_PARAM])
+ self._stream = StringIO(content[self.CONTENT_PARAM])
+ del(self._raw_content)
+
+ method = property(_get_method, _set_method)
+ content_type = property(_get_content_type, _set_content_type)
+ accept = property(_get_accept, _set_accept)
+ stream = property(_get_stream, _set_stream)
+ RAW_CONTENT = property(_get_raw_content)
+ CONTENT = property(_get_content)
+
+
+
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
index 15c1d7c8..80e5df2a 100644
--- a/djangorestframework/resource.py
+++ b/djangorestframework/resource.py
@@ -6,47 +6,42 @@ from djangorestframework.emitters import EmitterMixin
from djangorestframework.parsers import ParserMixin
from djangorestframework.authenticators import AuthenticatorMixin
from djangorestframework.validators import FormValidatorMixin
-from djangorestframework.content import OverloadedContentMixin
-from djangorestframework.methods import OverloadedPOSTMethodMixin
from djangorestframework.response import Response, ResponseException
+from djangorestframework.request import RequestMixin
from djangorestframework import emitters, parsers, authenticators, status
-import re
# TODO: Figure how out references and named urls need to work nicely
# TODO: POST on existing 404 URL, PUT on existing 404 URL
#
# NEXT: Exceptions on func() -> 500, tracebacks emitted if settings.DEBUG
-#
__all__ = ['Resource']
-
-
-class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin,
- OverloadedContentMixin, OverloadedPOSTMethodMixin, View):
+class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin, RequestMixin, View):
"""Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization."""
# List of RESTful operations which may be performed on this resource.
+ # These are going to get dropped at some point, the allowable methods will be defined simply by
+ # which methods are present on the request (in the same way as Django's generic View)
allowed_methods = ('GET',)
anon_allowed_methods = ()
- # List of emitters the resource can serialize the response with, ordered by preference
+ # List of emitters the resource can serialize the response with, ordered by preference.
emitters = ( emitters.JSONEmitter,
emitters.DocumentingHTMLEmitter,
emitters.DocumentingXHTMLEmitter,
emitters.DocumentingPlainTextEmitter,
emitters.XMLEmitter )
- # List of content-types the resource can read from
+ # List of parsers the resource can parse the request with.
parsers = ( parsers.JSONParser,
- parsers.XMLParser,
parsers.FormParser,
parsers.MultipartParser )
- # List of all authenticating methods to attempt
+ # List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator,
authenticators.BasicAuthenticator )
@@ -63,12 +58,6 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
callmap = { 'GET': 'get', 'POST': 'post',
'PUT': 'put', 'DELETE': 'delete' }
-
- # Some reserved parameters to allow us to use standard HTML forms with our resource
- # Override any/all of these with None to disable them, or override them with another value to rename them.
- CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token used in form params
-
-
def get(self, request, auth, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('GET')
@@ -137,24 +126,14 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
4. cleanup the response data
5. serialize response data into response content, using standard HTTP content negotiation
"""
-
+
self.request = request
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix)
- # These sets are determined now so that overridding classes can modify the various parameter names,
- # or set them to None to disable them.
- self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
- self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
- self.RESERVED_FORM_PARAMS.discard(None)
- self.RESERVED_QUERY_PARAMS.discard(None)
-
- method = self.determine_method(request)
-
try:
-
# Authenticate the request, and store any context so that the resource operations can
# do more fine grained authentication if required.
#
@@ -163,19 +142,21 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
# has been signed against a particular set of permissions)
auth_context = self.authenticate(request)
+ # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
+ # self.method, self.content_type, self.CONTENT appropriately.
+ self.perform_form_overloading()
+
# Ensure the requested operation is permitted on this resource
- self.check_method_allowed(method, auth_context)
+ self.check_method_allowed(self.method, auth_context)
# Get the appropriate create/read/update/delete function
- func = getattr(self, self.callmap.get(method, None))
+ func = getattr(self, self.callmap.get(self.method, None))
# Either generate the response data, deserializing and validating any request data
- # TODO: Add support for message bodys on other HTTP methods, as it is valid (although non-conventional).
- if method in ('PUT', 'POST'):
- (content_type, content) = self.determine_content(request)
- parser_content = self.parse(content_type, content)
- cleaned_content = self.validate(parser_content)
- response_obj = func(request, auth_context, cleaned_content, *args, **kwargs)
+ # TODO: This is going to change to: func(request, *args, **kwargs)
+ # That'll work out now that we have the lazily evaluated self.CONTENT property.
+ if self.method in ('PUT', 'POST'):
+ response_obj = func(request, auth_context, self.CONTENT, *args, **kwargs)
else:
response_obj = func(request, auth_context, *args, **kwargs)
@@ -191,11 +172,13 @@ class Resource(EmitterMixin, ParserMixin, AuthenticatorMixin, FormValidatorMixin
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.cleanup_response(response.raw_content)
-
except ResponseException, exc:
response = exc.response
- # Always add these headers
+ # Always add these headers.
+ #
+ # 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.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
diff --git a/djangorestframework/templates/emitter.html b/djangorestframework/templates/emitter.html
index 798c5fb9..1931ad39 100644
--- a/djangorestframework/templates/emitter.html
+++ b/djangorestframework/templates/emitter.html
@@ -65,7 +65,7 @@
{% if resource.METHOD_PARAM and form %}
{% if 'POST' in resource.allowed_methods %}
- <form action="{{ request.path }}" method="post">
+ <form action="{{ request.path }}" method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
<fieldset class='module aligned'>
<h2>POST {{ name }}</h2>
{% csrf_token %}
@@ -86,7 +86,7 @@
{% endif %}
{% if 'PUT' in resource.allowed_methods %}
- <form action="{{ request.path }}" method="post">
+ <form action="{{ request.path }}" method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
<fieldset class='module aligned'>
<h2>PUT {{ name }}</h2>
<input type="hidden" name="{{ resource.METHOD_PARAM }}" value="PUT" />
diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py
index 5d5b652a..f664c5c1 100644
--- a/djangorestframework/tests/__init__.py
+++ b/djangorestframework/tests/__init__.py
@@ -4,7 +4,6 @@ import os
modules = [filename.rsplit('.', 1)[0]
for filename in os.listdir(os.path.dirname(__file__))
if filename.endswith('.py') and not filename.startswith('_')]
-
__test__ = dict()
for module in modules:
diff --git a/djangorestframework/tests/content.py b/djangorestframework/tests/content.py
index ee7af486..c5eae2f9 100644
--- a/djangorestframework/tests/content.py
+++ b/djangorestframework/tests/content.py
@@ -1,121 +1,122 @@
-from django.test import TestCase
-from djangorestframework.compat import RequestFactory
-from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
-
-
-class TestContentMixins(TestCase):
- def setUp(self):
- self.req = RequestFactory()
-
- # Interface tests
-
- def test_content_mixin_interface(self):
- """Ensure the ContentMixin interface is as expected."""
- self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
-
- def test_standard_content_mixin_interface(self):
- """Ensure the OverloadedContentMixin interface is as expected."""
- self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
- getattr(StandardContentMixin, 'determine_content')
-
- def test_overloaded_content_mixin_interface(self):
- """Ensure the OverloadedContentMixin interface is as expected."""
- self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
- getattr(OverloadedContentMixin, 'CONTENT_PARAM')
- getattr(OverloadedContentMixin, 'CONTENTTYPE_PARAM')
- getattr(OverloadedContentMixin, 'determine_content')
-
-
- # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
-
- def ensure_determines_no_content_GET(self, mixin):
- """Ensure determine_content(request) returns None for GET request with no content."""
- request = self.req.get('/')
- self.assertEqual(mixin.determine_content(request), None)
-
- def ensure_determines_form_content_POST(self, mixin):
- """Ensure determine_content(request) returns content for POST request with content."""
- form_data = {'qwerty': 'uiop'}
- request = self.req.post('/', data=form_data)
- self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
-
- def ensure_determines_non_form_content_POST(self, mixin):
- """Ensure determine_content(request) returns (content type, content) for POST request with content."""
- content = 'qwerty'
- content_type = 'text/plain'
- request = self.req.post('/', content, content_type=content_type)
- self.assertEqual(mixin.determine_content(request), (content_type, content))
-
- def ensure_determines_form_content_PUT(self, mixin):
- """Ensure determine_content(request) returns content for PUT request with content."""
- form_data = {'qwerty': 'uiop'}
- request = self.req.put('/', data=form_data)
- self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
-
- def ensure_determines_non_form_content_PUT(self, mixin):
- """Ensure determine_content(request) returns (content type, content) for PUT request with content."""
- content = 'qwerty'
- content_type = 'text/plain'
- request = self.req.put('/', content, content_type=content_type)
- self.assertEqual(mixin.determine_content(request), (content_type, content))
-
- # StandardContentMixin behavioural tests
-
- def test_standard_behaviour_determines_no_content_GET(self):
- """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
- self.ensure_determines_no_content_GET(StandardContentMixin())
-
- def test_standard_behaviour_determines_form_content_POST(self):
- """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
- self.ensure_determines_form_content_POST(StandardContentMixin())
-
- def test_standard_behaviour_determines_non_form_content_POST(self):
- """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
- self.ensure_determines_non_form_content_POST(StandardContentMixin())
-
- def test_standard_behaviour_determines_form_content_PUT(self):
- """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
- self.ensure_determines_form_content_PUT(StandardContentMixin())
-
- def test_standard_behaviour_determines_non_form_content_PUT(self):
- """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
- self.ensure_determines_non_form_content_PUT(StandardContentMixin())
-
- # OverloadedContentMixin behavioural tests
-
- def test_overloaded_behaviour_determines_no_content_GET(self):
- """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
- self.ensure_determines_no_content_GET(OverloadedContentMixin())
-
- def test_overloaded_behaviour_determines_form_content_POST(self):
- """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
- self.ensure_determines_form_content_POST(OverloadedContentMixin())
-
- def test_overloaded_behaviour_determines_non_form_content_POST(self):
- """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
- self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
-
- def test_overloaded_behaviour_determines_form_content_PUT(self):
- """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
- self.ensure_determines_form_content_PUT(OverloadedContentMixin())
-
- def test_overloaded_behaviour_determines_non_form_content_PUT(self):
- """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
- self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
-
- def test_overloaded_behaviour_allows_content_tunnelling(self):
- """Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
- content = 'qwerty'
- content_type = 'text/plain'
- form_data = {OverloadedContentMixin.CONTENT_PARAM: content,
- OverloadedContentMixin.CONTENTTYPE_PARAM: content_type}
- request = self.req.post('/', form_data)
- self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
- self.assertEqual(request.META['CONTENT_TYPE'], content_type)
-
- def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
- """Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
- content = 'qwerty'
- request = self.req.post('/', {OverloadedContentMixin.CONTENT_PARAM: content})
- self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
+# TODO: refactor these tests
+#from django.test import TestCase
+#from djangorestframework.compat import RequestFactory
+#from djangorestframework.content import ContentMixin, StandardContentMixin, OverloadedContentMixin
+#
+#
+#class TestContentMixins(TestCase):
+# def setUp(self):
+# self.req = RequestFactory()
+#
+# # Interface tests
+#
+# def test_content_mixin_interface(self):
+# """Ensure the ContentMixin interface is as expected."""
+# self.assertRaises(NotImplementedError, ContentMixin().determine_content, None)
+#
+# def test_standard_content_mixin_interface(self):
+# """Ensure the OverloadedContentMixin interface is as expected."""
+# self.assertTrue(issubclass(StandardContentMixin, ContentMixin))
+# getattr(StandardContentMixin, 'determine_content')
+#
+# def test_overloaded_content_mixin_interface(self):
+# """Ensure the OverloadedContentMixin interface is as expected."""
+# self.assertTrue(issubclass(OverloadedContentMixin, ContentMixin))
+# getattr(OverloadedContentMixin, 'CONTENT_PARAM')
+# getattr(OverloadedContentMixin, 'CONTENTTYPE_PARAM')
+# getattr(OverloadedContentMixin, 'determine_content')
+#
+#
+# # Common functionality to test with both StandardContentMixin and OverloadedContentMixin
+#
+# def ensure_determines_no_content_GET(self, mixin):
+# """Ensure determine_content(request) returns None for GET request with no content."""
+# request = self.req.get('/')
+# self.assertEqual(mixin.determine_content(request), None)
+#
+# def ensure_determines_form_content_POST(self, mixin):
+# """Ensure determine_content(request) returns content for POST request with content."""
+# form_data = {'qwerty': 'uiop'}
+# request = self.req.post('/', data=form_data)
+# self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
+#
+# def ensure_determines_non_form_content_POST(self, mixin):
+# """Ensure determine_content(request) returns (content type, content) for POST request with content."""
+# content = 'qwerty'
+# content_type = 'text/plain'
+# request = self.req.post('/', content, content_type=content_type)
+# self.assertEqual(mixin.determine_content(request), (content_type, content))
+#
+# def ensure_determines_form_content_PUT(self, mixin):
+# """Ensure determine_content(request) returns content for PUT request with content."""
+# form_data = {'qwerty': 'uiop'}
+# request = self.req.put('/', data=form_data)
+# self.assertEqual(mixin.determine_content(request), (request.META['CONTENT_TYPE'], request.raw_post_data))
+#
+# def ensure_determines_non_form_content_PUT(self, mixin):
+# """Ensure determine_content(request) returns (content type, content) for PUT request with content."""
+# content = 'qwerty'
+# content_type = 'text/plain'
+# request = self.req.put('/', content, content_type=content_type)
+# self.assertEqual(mixin.determine_content(request), (content_type, content))
+#
+# # StandardContentMixin behavioural tests
+#
+# def test_standard_behaviour_determines_no_content_GET(self):
+# """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
+# self.ensure_determines_no_content_GET(StandardContentMixin())
+#
+# def test_standard_behaviour_determines_form_content_POST(self):
+# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
+# self.ensure_determines_form_content_POST(StandardContentMixin())
+#
+# def test_standard_behaviour_determines_non_form_content_POST(self):
+# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
+# self.ensure_determines_non_form_content_POST(StandardContentMixin())
+#
+# def test_standard_behaviour_determines_form_content_PUT(self):
+# """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
+# self.ensure_determines_form_content_PUT(StandardContentMixin())
+#
+# def test_standard_behaviour_determines_non_form_content_PUT(self):
+# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
+# self.ensure_determines_non_form_content_PUT(StandardContentMixin())
+#
+# # OverloadedContentMixin behavioural tests
+#
+# def test_overloaded_behaviour_determines_no_content_GET(self):
+# """Ensure StandardContentMixin.determine_content(request) returns None for GET request with no content."""
+# self.ensure_determines_no_content_GET(OverloadedContentMixin())
+#
+# def test_overloaded_behaviour_determines_form_content_POST(self):
+# """Ensure StandardContentMixin.determine_content(request) returns content for POST request with content."""
+# self.ensure_determines_form_content_POST(OverloadedContentMixin())
+#
+# def test_overloaded_behaviour_determines_non_form_content_POST(self):
+# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for POST request with content."""
+# self.ensure_determines_non_form_content_POST(OverloadedContentMixin())
+#
+# def test_overloaded_behaviour_determines_form_content_PUT(self):
+# """Ensure StandardContentMixin.determine_content(request) returns content for PUT request with content."""
+# self.ensure_determines_form_content_PUT(OverloadedContentMixin())
+#
+# def test_overloaded_behaviour_determines_non_form_content_PUT(self):
+# """Ensure StandardContentMixin.determine_content(request) returns (content type, content) for PUT request with content."""
+# self.ensure_determines_non_form_content_PUT(OverloadedContentMixin())
+#
+# def test_overloaded_behaviour_allows_content_tunnelling(self):
+# """Ensure determine_content(request) returns (content type, content) for overloaded POST request"""
+# content = 'qwerty'
+# content_type = 'text/plain'
+# form_data = {OverloadedContentMixin.CONTENT_PARAM: content,
+# OverloadedContentMixin.CONTENTTYPE_PARAM: content_type}
+# request = self.req.post('/', form_data)
+# self.assertEqual(OverloadedContentMixin().determine_content(request), (content_type, content))
+# self.assertEqual(request.META['CONTENT_TYPE'], content_type)
+#
+# def test_overloaded_behaviour_allows_content_tunnelling_content_type_not_set(self):
+# """Ensure determine_content(request) returns (None, content) for overloaded POST request with content type not set"""
+# content = 'qwerty'
+# request = self.req.post('/', {OverloadedContentMixin.CONTENT_PARAM: content})
+# self.assertEqual(OverloadedContentMixin().determine_content(request), (None, content))
diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py
new file mode 100644
index 00000000..e155f181
--- /dev/null
+++ b/djangorestframework/tests/files.py
@@ -0,0 +1,37 @@
+from django.test import TestCase
+from django import forms
+from djangorestframework.compat import RequestFactory
+from djangorestframework.resource import Resource
+import StringIO
+
+class UploadFilesTests(TestCase):
+ """Check uploading of files"""
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def test_upload_file(self):
+
+
+ class FileForm(forms.Form):
+ file = forms.FileField
+
+ class MockResource(Resource):
+ allowed_methods = anon_allowed_methods = ('POST',)
+ form = FileForm
+
+ def post(self, request, auth, content, *args, **kwargs):
+ #self.uploaded = content.file
+ return {'FILE_NAME': content['file'].name,
+ 'FILE_CONTENT': content['file'].read()}
+
+ file = StringIO.StringIO('stuff')
+ file.name = 'stuff.txt'
+ request = self.factory.post('/', {'file': file})
+ view = MockResource.as_view()
+ response = view(request)
+ self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
+
+
+
+
+
diff --git a/djangorestframework/tests/methods.py b/djangorestframework/tests/methods.py
index 64e2c121..f19bb3e5 100644
--- a/djangorestframework/tests/methods.py
+++ b/djangorestframework/tests/methods.py
@@ -1,52 +1,53 @@
-from django.test import TestCase
-from djangorestframework.compat import RequestFactory
-from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
-
-
-class TestMethodMixins(TestCase):
- def setUp(self):
- self.req = RequestFactory()
-
- # Interface tests
-
- def test_method_mixin_interface(self):
- """Ensure the base ContentMixin interface is as expected."""
- self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
-
- def test_standard_method_mixin_interface(self):
- """Ensure the StandardMethodMixin interface is as expected."""
- self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
- getattr(StandardMethodMixin, 'determine_method')
-
- def test_overloaded_method_mixin_interface(self):
- """Ensure the OverloadedPOSTMethodMixin interface is as expected."""
- self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
- getattr(OverloadedPOSTMethodMixin, 'METHOD_PARAM')
- getattr(OverloadedPOSTMethodMixin, 'determine_method')
-
- # Behavioural tests
-
- def test_standard_behaviour_determines_GET(self):
- """GET requests identified as GET method with StandardMethodMixin"""
- request = self.req.get('/')
- self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
-
- def test_standard_behaviour_determines_POST(self):
- """POST requests identified as POST method with StandardMethodMixin"""
- request = self.req.post('/')
- self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
-
- def test_overloaded_POST_behaviour_determines_GET(self):
- """GET requests identified as GET method with OverloadedPOSTMethodMixin"""
- request = self.req.get('/')
- self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
-
- def test_overloaded_POST_behaviour_determines_POST(self):
- """POST requests identified as POST method with OverloadedPOSTMethodMixin"""
- request = self.req.post('/')
- self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
-
- def test_overloaded_POST_behaviour_determines_overloaded_method(self):
- """POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
- request = self.req.post('/', {OverloadedPOSTMethodMixin.METHOD_PARAM: 'DELETE'})
- self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
+# TODO: Refactor these tests
+#from django.test import TestCase
+#from djangorestframework.compat import RequestFactory
+#from djangorestframework.methods import MethodMixin, StandardMethodMixin, OverloadedPOSTMethodMixin
+#
+#
+#class TestMethodMixins(TestCase):
+# def setUp(self):
+# self.req = RequestFactory()
+#
+# # Interface tests
+#
+# def test_method_mixin_interface(self):
+# """Ensure the base ContentMixin interface is as expected."""
+# self.assertRaises(NotImplementedError, MethodMixin().determine_method, None)
+#
+# def test_standard_method_mixin_interface(self):
+# """Ensure the StandardMethodMixin interface is as expected."""
+# self.assertTrue(issubclass(StandardMethodMixin, MethodMixin))
+# getattr(StandardMethodMixin, 'determine_method')
+#
+# def test_overloaded_method_mixin_interface(self):
+# """Ensure the OverloadedPOSTMethodMixin interface is as expected."""
+# self.assertTrue(issubclass(OverloadedPOSTMethodMixin, MethodMixin))
+# getattr(OverloadedPOSTMethodMixin, 'METHOD_PARAM')
+# getattr(OverloadedPOSTMethodMixin, 'determine_method')
+#
+# # Behavioural tests
+#
+# def test_standard_behaviour_determines_GET(self):
+# """GET requests identified as GET method with StandardMethodMixin"""
+# request = self.req.get('/')
+# self.assertEqual(StandardMethodMixin().determine_method(request), 'GET')
+#
+# def test_standard_behaviour_determines_POST(self):
+# """POST requests identified as POST method with StandardMethodMixin"""
+# request = self.req.post('/')
+# self.assertEqual(StandardMethodMixin().determine_method(request), 'POST')
+#
+# def test_overloaded_POST_behaviour_determines_GET(self):
+# """GET requests identified as GET method with OverloadedPOSTMethodMixin"""
+# request = self.req.get('/')
+# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'GET')
+#
+# def test_overloaded_POST_behaviour_determines_POST(self):
+# """POST requests identified as POST method with OverloadedPOSTMethodMixin"""
+# request = self.req.post('/')
+# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'POST')
+#
+# def test_overloaded_POST_behaviour_determines_overloaded_method(self):
+# """POST requests can be overloaded to another method by setting a reserved form field with OverloadedPOSTMethodMixin"""
+# request = self.req.post('/', {OverloadedPOSTMethodMixin.METHOD_PARAM: 'DELETE'})
+# self.assertEqual(OverloadedPOSTMethodMixin().determine_method(request), 'DELETE')
diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py
index d4cd1e87..4753f6f3 100644
--- a/djangorestframework/tests/parsers.py
+++ b/djangorestframework/tests/parsers.py
@@ -1,12 +1,13 @@
"""
..
>>> from djangorestframework.parsers import FormParser
- >>> from djangorestframework.resource import Resource
>>> from djangorestframework.compat import RequestFactory
+ >>> from djangorestframework.resource import Resource
+ >>> from StringIO import StringIO
>>> from urllib import urlencode
>>> req = RequestFactory().get('/')
>>> some_resource = Resource()
- >>> trash = some_resource.dispatch(req)# Some variables are set only when calling dispatch
+ >>> some_resource.request = req # Make as if this request had been dispatched
FormParser
============
@@ -23,7 +24,7 @@ Here is some example data, which would eventually be sent along with a post requ
Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
- >>> FormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': 'blo1'}
+ >>> FormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
True
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
@@ -35,7 +36,7 @@ However, you can customize this behaviour by subclassing :class:`parsers.FormPar
This new parser only flattens the lists of parameters that contain a single value.
- >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
+ >>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
True
.. note:: The same functionality is available for :class:`parsers.MultipartParser`.
@@ -60,7 +61,7 @@ The browsers usually strip the parameter completely. A hack to avoid this, and t
:class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
- >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1'}
+ >>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1'}
True
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
@@ -70,7 +71,7 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis
... def is_a_list(self, key, val_list):
... return key == 'key2'
...
- >>> MyFormParser(some_resource).parse(inpt) == {'key1': 'blo1', 'key2': []}
+ >>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
True
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
@@ -81,6 +82,8 @@ from django.test import TestCase
from djangorestframework.compat import RequestFactory
from djangorestframework.parsers import MultipartParser
from djangorestframework.resource import Resource
+from djangorestframework.mediatypes import MediaType
+from StringIO import StringIO
def encode_multipart_formdata(fields, files):
"""For testing multipart parser.
@@ -119,9 +122,9 @@ class TestMultipartParser(TestCase):
def test_multipartparser(self):
"""Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters."""
post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
- some_resource = Resource()
- some_resource.dispatch(post_req)
- parsed = MultipartParser(some_resource).parse(self.body)
+ resource = Resource()
+ resource.request = post_req
+ parsed = MultipartParser(resource).parse(StringIO(self.body))
self.assertEqual(parsed['key1'], 'val1')
- self.assertEqual(parsed['file1'].read(), 'blablabla')
+ self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py
index 8e649764..b5d2d566 100644
--- a/djangorestframework/tests/validators.py
+++ b/djangorestframework/tests/validators.py
@@ -143,7 +143,7 @@ class TestFormValidation(TestCase):
try:
validator.validate(content)
except ResponseException, exc:
- self.assertEqual(exc.response.raw_content, {'errors': ['No content was supplied.']})
+ self.assertEqual(exc.response.raw_content, {'field-errors': {'qwerty': ['This field is required.']}})
else:
self.fail('ResourceException was not raised') #pragma: no cover
diff --git a/djangorestframework/validators.py b/djangorestframework/validators.py
index 3d0a7794..d96e8d9e 100644
--- a/djangorestframework/validators.py
+++ b/djangorestframework/validators.py
@@ -58,6 +58,8 @@ class FormValidatorMixin(ValidatorMixin):
# Validation succeeded...
cleaned_data = bound_form.cleaned_data
+ cleaned_data.update(bound_form.files)
+
# Add in any extra fields to the cleaned content...
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
cleaned_data[key] = content[key]
@@ -95,7 +97,9 @@ class FormValidatorMixin(ValidatorMixin):
if not self.form:
return None
- if content:
+ if not content is None:
+ if hasattr(content, 'FILES'):
+ return self.form(content, content.FILES)
return self.form(content)
return self.form()
@@ -157,8 +161,11 @@ class ModelFormValidatorMixin(FormValidatorMixin):
# Instantiate the ModelForm as appropriate
if content and isinstance(content, models.Model):
+ # Bound to an existing model instance
return OnTheFlyModelForm(instance=content)
- elif content:
+ elif not content is None:
+ if hasattr(content, 'FILES'):
+ return OnTheFlyModelForm(content, content.FILES)
return OnTheFlyModelForm(content)
return OnTheFlyModelForm()
@@ -189,4 +196,4 @@ class ModelFormValidatorMixin(FormValidatorMixin):
return property_fields - set(as_tuple(self.exclude_fields))
- \ No newline at end of file
+