mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-03-19 17:34:13 +03:00
yet more API cleanup
This commit is contained in:
parent
15f9e7c566
commit
b5b231a874
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in New Issue
Block a user