From 4687db680cda52e9836743940e4cf7279b307294 Mon Sep 17 00:00:00 2001 From: "tom christie tom@tomchristie.com" Date: Sat, 2 Apr 2011 16:32:37 +0100 Subject: [PATCH] Refactor to use self.CONTENT to access request body. Get file upload working --- .hgignore | 1 + djangorestframework/content.py | 57 ----- djangorestframework/emitters.py | 3 +- djangorestframework/mediatypes.py | 78 +++++++ djangorestframework/methods.py | 35 --- djangorestframework/parsers.py | 123 ++++++----- djangorestframework/request.py | 128 +++++++++++ djangorestframework/resource.py | 61 ++---- djangorestframework/templates/emitter.html | 4 +- djangorestframework/tests/__init__.py | 1 - djangorestframework/tests/content.py | 241 +++++++++++---------- djangorestframework/tests/files.py | 37 ++++ djangorestframework/tests/methods.py | 105 ++++----- djangorestframework/tests/parsers.py | 23 +- djangorestframework/tests/validators.py | 2 +- djangorestframework/validators.py | 13 +- 16 files changed, 540 insertions(+), 372 deletions(-) delete mode 100644 djangorestframework/content.py create mode 100644 djangorestframework/mediatypes.py delete mode 100644 djangorestframework/methods.py create mode 100644 djangorestframework/request.py create mode 100644 djangorestframework/tests/files.py diff --git a/.hgignore b/.hgignore index 49cc62366..5e9c03980 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 cfdd33be5..000000000 --- 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 be1d7ef30..4cd462cb1 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 000000000..d1641a8fc --- /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 "" % (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 088c563c6..000000000 --- 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 3bd020ad4..1503342c7 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. +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 MultipartData(dict): + def __init__(self, data, files): + dict.__init__(self, data) + self.FILES = files + class MultipartParser(BaseParser, DataFlatener): - media_type = 'multipart/form-data' + media_type = MediaType('multipart/form-data') + RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',) - def parse(self, input): - - 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 000000000..9c6f8f300 --- /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 15c1d7c88..80e5df2a2 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 798c5fb97..1931ad39e 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 %} -
+

POST {{ name }}

{% csrf_token %} @@ -86,7 +86,7 @@ {% endif %} {% if 'PUT' in resource.allowed_methods %} - +

PUT {{ name }}

diff --git a/djangorestframework/tests/__init__.py b/djangorestframework/tests/__init__.py index 5d5b652ab..f664c5c12 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 ee7af486d..c5eae2f97 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 000000000..e155f181f --- /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 64e2c1212..f19bb3e5a 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 d4cd1e87f..4753f6f39 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 8e6497643..b5d2d566c 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 3d0a7794c..d96e8d9ef 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 +