yet more API cleanup

This commit is contained in:
Tom Christie 2011-05-12 15:11:14 +01:00
parent 15f9e7c566
commit b5b231a874
7 changed files with 119 additions and 124 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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>

View File

@ -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,

View File

@ -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):

View File

@ -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):

View File

@ -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."""