diff options
-rw-r--r-- | djangorestframework/mixins.py | 124 | ||||
-rw-r--r-- | djangorestframework/renderers.py | 79 | ||||
-rw-r--r-- | djangorestframework/templates/renderer.html | 18 | ||||
-rw-r--r-- | djangorestframework/tests/accept.py | 4 | ||||
-rw-r--r-- | djangorestframework/tests/files.py | 2 | ||||
-rw-r--r-- | djangorestframework/tests/validators.py | 2 | ||||
-rw-r--r-- | djangorestframework/views.py | 14 |
7 files changed, 119 insertions, 124 deletions
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index d1c83c17..4f88bde4 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -48,23 +48,18 @@ class RequestMixin(object): parsers = () - def _get_method(self): + @property + def method(self): """ - Returns the HTTP method for the current view. + Returns the HTTP method. """ 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): + @property + def content_type(self): """ Returns the content type header. """ @@ -73,11 +68,32 @@ class RequestMixin(object): return self._content_type - def _set_content_type(self, content_type): + @property + def DATA(self): """ - Set the content type header. + Returns the request data. """ - self._content_type = content_type + if not hasattr(self, '_data'): + self._load_data_and_files() + return self._data + + + @property + def FILES(self): + """ + Returns the request files. + """ + if not hasattr(self, '_files'): + self._load_data_and_files() + return self._files + + + def _load_data_and_files(self): + """ + Parse the request content into self.DATA and self.FILES. + """ + stream = self._get_stream() + (self._data, self._files) = self._parse(stream, self.content_type) def _get_stream(self): @@ -134,27 +150,6 @@ class RequestMixin(object): return self._stream - def _set_stream(self, stream): - """ - Set the stream representing the request body. - """ - self._stream = stream - - - def _load_data_and_files(self): - (self._data, self._files) = self._parse(self.stream, self.content_type) - - def _get_data(self): - if not hasattr(self, '_data'): - self._load_data_and_files() - return self._data - - def _get_files(self): - if not hasattr(self, '_files'): - self._load_data_and_files() - return self._files - - # TODO: Modify this so that it happens implictly, rather than being called explicitly # ie accessing any of .DATA, .FILES, .content_type, .method will force # form overloading. @@ -164,7 +159,10 @@ class RequestMixin(object): 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 is_form_media_type(self.content_type): + + # We only need to use form overloading on form POST requests + content_type = self.request.META.get('HTTP_CONTENT_TYPE', self.request.META.get('CONTENT_TYPE', '')) + if not self._USE_FORM_OVERLOADING or self.request.method != 'POST' or not not is_form_media_type(content_type): return # Temporarily switch to using the form parsers, then parse the content @@ -175,7 +173,7 @@ class RequestMixin(object): # Method overloading - change the method and remove the param from the content if self._METHOD_PARAM in content: - self.method = content[self._METHOD_PARAM].upper() + self._method = content[self._METHOD_PARAM].upper() del self._data[self._METHOD_PARAM] # Content overloading - rewind the stream and modify the content type @@ -207,28 +205,21 @@ class RequestMixin(object): @property - def parsed_media_types(self): + def _parsed_media_types(self): """ - Return an list of all the media types that this view can parse. + Return a list of all the media types that this view can parse. """ return [parser.media_type for parser in self.parsers] @property - def default_parser(self): + def _default_parser(self): """ - Return the view's most preferred parser. - (This has no behavioral effect, but is may be used by documenting renderers) + Return the view's default parser. """ return self.parsers[0] - method = property(_get_method, _set_method) - content_type = property(_get_content_type, _set_content_type) - stream = property(_get_stream, _set_stream) - DATA = property(_get_data) - FILES = property(_get_files) - ########## ResponseMixin ########## @@ -240,8 +231,9 @@ class ResponseMixin(object): Also supports overriding the content type by specifying an _accept= parameter in the URL. Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead. """ - ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params - REWRITE_IE_ACCEPT_HEADER = True + + _ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params + _IGNORE_IE_ACCEPT_HEADER = True renderers = () @@ -256,7 +248,7 @@ class ResponseMixin(object): try: renderer = self._determine_renderer(self.request) except ErrorResponse, exc: - renderer = self.default_renderer + renderer = self._default_renderer response = exc.response # Serialize the response content @@ -287,10 +279,10 @@ class ResponseMixin(object): See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html """ - if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None): + if self._ACCEPT_QUERY_PARAM and request.GET.get(self._ACCEPT_QUERY_PARAM, None): # Use _accept parameter override - accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)] - elif (self.REWRITE_IE_ACCEPT_HEADER and + accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)] + elif (self._IGNORE_IE_ACCEPT_HEADER and request.META.has_key('HTTP_USER_AGENT') and MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])): accept_list = ['text/html', '*/*'] @@ -299,7 +291,7 @@ class ResponseMixin(object): accept_list = request.META["HTTP_ACCEPT"].split(',') else: # No accept header specified - return self.default_renderer + return self._default_renderer # Parse the accept header into a dict of {qvalue: set of media types} # We ignore mietype parameters @@ -340,25 +332,24 @@ class ResponseMixin(object): # Return default if '*/*' in accept_set: - return self.default_renderer + return self._default_renderer raise ErrorResponse(status.HTTP_406_NOT_ACCEPTABLE, {'detail': 'Could not satisfy the client\'s Accept header', - 'available_types': self.rendered_media_types}) + 'available_types': self._rendered_media_types}) @property - def rendered_media_types(self): + def _rendered_media_types(self): """ - Return an list of all the media types that this resource can render. + Return an list of all the media types that this view can render. """ return [renderer.media_type for renderer in self.renderers] @property - def default_renderer(self): + def _default_renderer(self): """ - Return the resource's most preferred renderer. - (This renderer is used if the client does not send and Accept: header, or sends Accept: */*) + Return the view's default renderer. """ return self.renderers[0] @@ -367,8 +358,7 @@ class ResponseMixin(object): class AuthMixin(object): """ - Simple mixin class to provide authentication and permission checking, - by adding a set of authentication and permission classes on a ``View``. + Simple mixin class to add authentication and permission checking to a ``View`` class. """ authentication = () permissions = () @@ -404,16 +394,16 @@ class AuthMixin(object): ########## Resource Mixin ########## -class ResourceMixin(object): +class ResourceMixin(object): @property def CONTENT(self): if not hasattr(self, '_content'): - self._content = self._get_content(self.DATA, self.FILES) + self._content = self._get_content() return self._content - def _get_content(self, data, files): + def _get_content(self): resource = self.resource(self) - return resource.validate(data, files) + return resource.validate(self.DATA, self.FILES) def get_bound_form(self, content=None): resource = self.resource(self) diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 0aa30f70..e8763f34 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -52,7 +52,7 @@ class BaseRenderer(object): should render the output. EG: 'application/json; indent=4' - By default render simply returns the ouput as-is. + By default render simply returns the output as-is. Override this method to provide for other behavior. """ if obj is None: @@ -61,6 +61,41 @@ class BaseRenderer(object): return str(obj) +class JSONRenderer(BaseRenderer): + """ + Renderer which serializes to JSON + """ + media_type = 'application/json' + + def render(self, obj=None, media_type=None): + if obj is None: + return '' + + # If the media type looks like 'application/json; indent=4', then + # pretty print the result. + indent = get_media_type_params(media_type).get('indent', None) + sort_keys = False + try: + indent = max(min(int(indent), 8), 0) + sort_keys = True + except (ValueError, TypeError): + indent = None + + return json.dumps(obj, indent=indent, sort_keys=sort_keys) + + +class XMLRenderer(BaseRenderer): + """ + Renderer which serializes to XML. + """ + media_type = 'application/xml' + + def render(self, obj=None, media_type=None): + if obj is None: + return '' + return dict2xml(obj) + + class TemplateRenderer(BaseRenderer): """ A Base class provided for convenience. @@ -161,8 +196,8 @@ class DocumentingTemplateRenderer(BaseRenderer): Add the fields dynamically.""" super(GenericContentForm, self).__init__() - contenttype_choices = [(media_type, media_type) for media_type in view.parsed_media_types] - initial_contenttype = view.default_parser.media_type + contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types] + initial_contenttype = view._default_parser.media_type self.fields[view._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', choices=contenttype_choices, @@ -204,16 +239,19 @@ class DocumentingTemplateRenderer(BaseRenderer): template = loader.get_template(self.template) context = RequestContext(self.view.request, { 'content': content, - 'resource': self.view, # TODO: rename to view + 'view': self.view, 'request': self.view.request, # TODO: remove 'response': self.view.response, 'description': description, 'name': name, 'markeddown': markeddown, 'breadcrumblist': breadcrumb_list, + 'available_media_types': self.view._rendered_media_types, 'form': form_instance, 'login_url': login_url, 'logout_url': logout_url, + 'ACCEPT_PARAM': self.view._ACCEPT_QUERY_PARAM, + 'METHOD_PARAM': self.view._METHOD_PARAM, 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX }) @@ -228,39 +266,6 @@ class DocumentingTemplateRenderer(BaseRenderer): return ret -class JSONRenderer(BaseRenderer): - """ - Renderer which serializes to JSON - """ - media_type = 'application/json' - - def render(self, obj=None, media_type=None): - if obj is None: - return '' - - indent = get_media_type_params(media_type).get('indent', None) - if indent is not None: - try: - indent = int(indent) - except ValueError: - indent = None - - sort_keys = indent and True or False - return json.dumps(obj, indent=indent, sort_keys=sort_keys) - - -class XMLRenderer(BaseRenderer): - """ - Renderer which serializes to XML. - """ - media_type = 'application/xml' - - def render(self, obj=None, media_type=None): - if obj is None: - return '' - return dict2xml(obj) - - class DocumentingHTMLRenderer(DocumentingTemplateRenderer): """ Renderer which provides a browsable HTML interface for an API. diff --git a/djangorestframework/templates/renderer.html b/djangorestframework/templates/renderer.html index e213ecfa..3010d712 100644 --- a/djangorestframework/templates/renderer.html +++ b/djangorestframework/templates/renderer.html @@ -42,14 +42,14 @@ {% endfor %} {{ content|urlize_quoted_links }}</pre>{% endautoescape %}</div> - {% if 'GET' in resource.allowed_methods %} + {% if 'GET' in view.allowed_methods %} <form> <fieldset class='module aligned'> <h2>GET {{ name }}</h2> <div class='submit-row' style='margin: 0; border: 0'> <a href='{{ request.path }}' rel="nofollow" style='float: left'>GET</a> - {% for media_type in resource.rendered_media_types %} - {% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %} + {% for media_type in available_media_types %} + {% with ACCEPT_PARAM|add:"="|add:media_type as param %} [<a href='{{ request.path|add_query_param:param }}' rel="nofollow">{{ media_type }}</a>] {% endwith %} {% endfor %} @@ -63,8 +63,8 @@ *** (We could display only the POST form if method tunneling is disabled, but I think *** *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %} - {% if resource.METHOD_PARAM and form %} - {% if 'POST' in resource.allowed_methods %} + {% if METHOD_PARAM and form %} + {% if 'POST' in view.allowed_methods %} <form action="{{ request.path }}" method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}> <fieldset class='module aligned'> <h2>POST {{ name }}</h2> @@ -85,11 +85,11 @@ </form> {% endif %} - {% if 'PUT' in resource.allowed_methods %} + {% if 'PUT' in view.allowed_methods %} <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" /> + <input type="hidden" name="{{ METHOD_PARAM }}" value="PUT" /> {% csrf_token %} {{ form.non_field_errors }} {% for field in form %} @@ -107,12 +107,12 @@ </form> {% endif %} - {% if 'DELETE' in resource.allowed_methods %} + {% if 'DELETE' in view.allowed_methods %} <form action="{{ request.path }}" method="post"> <fieldset class='module aligned'> <h2>DELETE {{ name }}</h2> {% csrf_token %} - <input type="hidden" name="{{ resource.METHOD_PARAM }}" value="DELETE" /> + <input type="hidden" name="{{ METHOD_PARAM }}" value="DELETE" /> <div class='submit-row' style='margin: 0; border: 0'> <input type="submit" value="DELETE" class="default" /> </div> diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index c5a3f69e..293a7284 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -40,9 +40,9 @@ class UserAgentMungingTest(TestCase): self.assertEqual(resp['Content-Type'], 'text/html') def test_dont_rewrite_msie_accept_header(self): - """Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure + """Turn off _IGNORE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure that we get a JSON response if we set a */* accept header.""" - view = self.MockView.as_view(REWRITE_IE_ACCEPT_HEADER=False) + view = self.MockView.as_view(_IGNORE_IE_ACCEPT_HEADER=False) for user_agent in (MSIE_9_USER_AGENT, MSIE_8_USER_AGENT, diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py index fc82fd83..afa59b4e 100644 --- a/djangorestframework/tests/files.py +++ b/djangorestframework/tests/files.py @@ -2,7 +2,7 @@ from django.test import TestCase from django import forms from djangorestframework.compat import RequestFactory from djangorestframework.views import BaseView -from djangorestframework.resource import FormResource +from djangorestframework.resources import FormResource import StringIO class UploadFilesTests(TestCase): diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py index 52a675d2..fb09c5ba 100644 --- a/djangorestframework/tests/validators.py +++ b/djangorestframework/tests/validators.py @@ -5,7 +5,7 @@ from djangorestframework.compat import RequestFactory from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator from djangorestframework.response import ErrorResponse from djangorestframework.views import BaseView -from djangorestframework.resource import Resource +from djangorestframework.resources import Resource class TestValidatorMixinInterfaces(TestCase): diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 3abf101c..2a23c49a 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt from djangorestframework.compat import View from djangorestframework.response import Response, ErrorResponse from djangorestframework.mixins import * -from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status +from djangorestframework import resources, renderers, parsers, authentication, permissions, status __all__ = ( @@ -22,7 +22,7 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View): Performs request deserialization, response serialization, authentication and input validation.""" # Use the base resource by default - resource = resource.Resource + resource = resources.Resource # List of renderers the resource can serialize the response with, ordered by preference. renderers = ( renderers.JSONRenderer, @@ -36,9 +36,6 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View): parsers.FormParser, parsers.MultiPartParser ) - # List of validators to validate, cleanup and normalize the request content - validators = ( validators.FormValidator, ) - # List of all authenticating methods to attempt. authentication = ( authentication.UserLoggedInAuthenticaton, authentication.BasicAuthenticaton ) @@ -54,6 +51,9 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View): @property def allowed_methods(self): + """ + Return the list of allowed HTTP methods, uppercased. + """ return [method.upper() for method in self.http_method_names if hasattr(self, method)] def http_method_not_allowed(self, request, *args, **kwargs): @@ -61,7 +61,7 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View): Return an HTTP 405 error if an operation is called which does not have a handler method. """ raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, - {'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) + {'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) # Note: session based authentication is explicitly CSRF validated, @@ -127,7 +127,7 @@ class BaseView(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, View): class ModelView(BaseView): """A RESTful view that maps to a model in the database.""" - validators = (validators.ModelFormValidator,) + resource = resources.ModelResource class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): """A view which provides default operations for read/update/delete against a model instance.""" |