Commit bae47b7f by Tom Christie

Merge pull request #3453 from tomchristie/remove-content-overriding

Remove content overriding
parents 3d7740fb ed677f01
...@@ -69,6 +69,16 @@ If using the `i18n_patterns` function provided by Django, as well as `format_suf ...@@ -69,6 +69,16 @@ If using the `i18n_patterns` function provided by Django, as well as `format_suf
--- ---
## Query parameter formats
An alternative to the format suffixes is to include the requested format in a query parameter. REST framework provides this option by default, and it is used in the browsable API to switch between differing available representations.
To select a representation using its short format, use the `format` query parameter. For example: `http://example.com/organizations/?format=csv`.
The name of this query parameter can be modified using the `URL_FORMAT_OVERRIDE` setting. Set the value to `None` to disable this behavior.
---
## Accept headers vs. format suffixes ## Accept headers vs. format suffixes
There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead. There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead.
......
...@@ -249,47 +249,23 @@ Default: ...@@ -249,47 +249,23 @@ Default:
--- ---
## Browser overrides ## Content type controls
*The following settings provide URL or form-based overrides of the default browser behavior.* #### URL_FORMAT_OVERRIDE
#### FORM_METHOD_OVERRIDE
The name of a form field that may be used to override the HTTP method of the form.
If the value of this setting is `None` then form method overloading will be disabled.
Default: `'_method'`
#### FORM_CONTENT_OVERRIDE
The name of a form field that may be used to override the content of the form payload. Must be used together with `FORM_CONTENTTYPE_OVERRIDE`.
If either setting is `None` then form content overloading will be disabled.
Default: `'_content'`
#### FORM_CONTENTTYPE_OVERRIDE
The name of a form field that may be used to override the content type of the form payload. Must be used together with `FORM_CONTENT_OVERRIDE`.
If either setting is `None` then form content overloading will be disabled.
Default: `'_content_type'`
#### URL_ACCEPT_OVERRIDE The name of a URL parameter that may be used to override the default content negotiation `Accept` header behavior, by using a `format=…` query parameter in the request URL.
The name of a URL parameter that may be used to override the HTTP `Accept` header. For example: `http://example.com/organizations/?format=csv`
If the value of this setting is `None` then URL accept overloading will be disabled. If the value of this setting is `None` then URL format overrides will be disabled.
Default: `'accept'` Default: `'format'`
#### URL_FORMAT_OVERRIDE #### FORMAT_SUFFIX_KWARG
The name of a URL parameter that may be used to override the default `Accept` header based content negotiation. The name of a parameter in the URL conf that may be used to provide a format suffix. This setting is applied when using `format_suffix_patterns` to include suffixed URL patterns.
If the value of this setting is `None` then URL format overloading will be disabled. For example: `http://example.com/organizations.csv/`
Default: `'format'` Default: `'format'`
...@@ -451,12 +427,6 @@ A string representing the key that should be used for the URL fields generated b ...@@ -451,12 +427,6 @@ A string representing the key that should be used for the URL fields generated b
Default: `'url'` Default: `'url'`
#### FORMAT_SUFFIX_KWARG
The name of a parameter in the URL conf that may be used to provide a format suffix.
Default: `'format'`
#### NUM_PROXIES #### NUM_PROXIES
An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes. An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes.
......
...@@ -4,67 +4,74 @@ ...@@ -4,67 +4,74 @@
> >
> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby. > — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby.
## Browser based PUT, DELETE, etc... In order to allow the browsable API to function, there are a couple of browser enhancements that REST framework needs to provide.
As of version 3.3.0 onwards these are enabled with javascript, using the [ajax-form][ajax-form] library.
REST framework supports browser-based `PUT`, `DELETE` and other methods, by ## Browser based PUT, DELETE, etc...
overloading `POST` requests using a hidden form field.
Note that this is the same strategy as is used in [Ruby on Rails][rails]. The [AJAX form library][ajax-form] supports browser-based `PUT`, `DELETE` and other methods on HTML forms.
For example, given the following form: After including the library, use the `data-method` attribute on the form, like so:
<form action="/news-items/5" method="POST"> <form action="/" data-method="PUT">
<input type="hidden" name="_method" value="DELETE"> <input name='foo'/>
...
</form> </form>
`request.method` would return `"DELETE"`. Note that prior to 3.3.0, this support was server-side rather than javascript based. The method overloading style (as used in [Ruby on Rails][rails]) is no longer supported due to subtle issues that it introduces in request parsing.
## HTTP header based method overriding ## Browser based submission of non-form content
REST framework also supports method overriding via the semi-standard `X-HTTP-Method-Override` header. This can be useful if you are working with non-form content such as JSON and are working with an older web server and/or hosting provider that doesn't recognise particular HTTP methods such as `PATCH`. For example [Amazon Web Services ELB][aws_elb]. Browser-based submission of content types such as JSON are supported by the [AJAX form library][ajax-form], using form fields with `data-override='content-type'` and `data-override='content'` attributes.
To use it, make a `POST` request, setting the `X-HTTP-Method-Override` header. For example:
For example, making a `PATCH` request via `POST` in jQuery: <form action="/">
<input data-override='content-type' value='application/json' type='hidden'/>
<textarea data-override='content'>{}</textarea>
<input type="submit"/>
</form>
$.ajax({ Note that prior to 3.3.0, this support was server-side rather than javascript based.
url: '/myresource/',
method: 'POST',
headers: {'X-HTTP-Method-Override': 'PATCH'},
...
});
## Browser based submission of non-form content ## URL based format suffixes
Browser-based submission of content types other than form are supported by REST framework can take `?format=json` style URL parameters, which can be a
using form fields named `_content` and `_content_type`: useful shortcut for determining which content type should be returned from
the view.
For example, given the following form: This behavior is controlled using the `URL_FORMAT_OVERRIDE` setting.
<form action="/news-items/5" method="PUT"> ## HTTP header based method overriding
<input type="hidden" name="_content_type" value="application/json">
<input name="_content" value="{'count': 1}">
</form>
`request.content_type` would return `"application/json"`, and Prior to version 3.3.0 the semi extension header `X-HTTP-Method-Override` was supported for overriding the request method. This behavior is no longer in core, but can be adding if needed using middleware.
`request.stream` would return `"{'count': 1}"`
## URL based accept headers For example:
REST framework can take `?accept=application/json` style URL parameters, METHOD_OVERRIDE_HEADER = 'HTTP_X_HTTP_METHOD_OVERRIDE'
which allow the `Accept` header to be overridden.
This can be useful for testing the API from a web browser, where you don't class MethodOverrideMiddleware(object):
have any control over what is sent in the `Accept` header. def process_view(self, request, callback, callback_args, callback_kwargs):
if request.method != 'POST':
return
if METHOD_OVERRIDE_HEADER not in request.META:
return
request.method = request.META[METHOD_OVERRIDE_HEADER]
## URL based format suffixes ## URL based accept headers
REST framework can take `?format=json` style URL parameters, which can be a Until version 3.3.0 REST framework included built-in support for `?accept=application/json` style URL parameters, which would allow the `Accept` header to be overridden.
useful shortcut for determining which content type should be returned from
the view. Since the introduction of the content negotiation API this behavior is no longer included in core, but may be added using a custom content negotiation class, if needed.
For example:
This is a more concise than using the `accept` override, but it also gives class AcceptQueryParamOverride()
you less control. (For example you can't specify any media type parameters) def get_accept_list(self, request):
header = request.META.get('HTTP_ACCEPT', '*/*')
header = request.query_params.get('_accept', header)
return [token.strip() for token in header.split(',')]
## Doesn't HTML5 support PUT and DELETE forms? ## Doesn't HTML5 support PUT and DELETE forms?
...@@ -74,7 +81,7 @@ was later [dropped from the spec][html5]. There remains ...@@ -74,7 +81,7 @@ was later [dropped from the spec][html5]. There remains
as well as how to support content types other than form-encoded data. as well as how to support content types other than form-encoded data.
[cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260 [cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
[ajax-form]: https://github.com/tomchristie/ajax-form
[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work [rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24 [html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
[put_delete]: http://amundsen.com/examples/put-delete-forms/ [put_delete]: http://amundsen.com/examples/put-delete-forms/
[aws_elb]: https://forums.aws.amazon.com/thread.jspa?messageID=400724
...@@ -92,9 +92,6 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -92,9 +92,6 @@ class DefaultContentNegotiation(BaseContentNegotiation):
""" """
Given the incoming request, return a tokenised list of media Given the incoming request, return a tokenised list of media
type strings. type strings.
Allows URL style accept override. eg. "?accept=application/json"
""" """
header = request.META.get('HTTP_ACCEPT', '*/*') header = request.META.get('HTTP_ACCEPT', '*/*')
header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')] return [token.strip() for token in header.split(',')]
...@@ -420,9 +420,6 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -420,9 +420,6 @@ class BrowsableAPIRenderer(BaseRenderer):
if method not in view.allowed_methods: if method not in view.allowed_methods:
return # Not a valid method return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
return # Cannot use form overloading
try: try:
view.check_permissions(request) view.check_permissions(request)
if obj is not None: if obj is not None:
...@@ -530,13 +527,6 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -530,13 +527,6 @@ class BrowsableAPIRenderer(BaseRenderer):
instance = None instance = None
with override_method(view, request, method) as request: with override_method(view, request, method) as request:
# If we're not using content overloading there's no point in
# supplying a generic form, as the view won't treat the form's
# value as the content of the request.
if not (api_settings.FORM_CONTENT_OVERRIDE and
api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None
# Check permissions # Check permissions
if not self.show_form_for_method(view, method, request, instance): if not self.show_form_for_method(view, method, request, instance):
return return
...@@ -564,26 +554,20 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -564,26 +554,20 @@ class BrowsableAPIRenderer(BaseRenderer):
# Generate a generic form that includes a content type field, # Generate a generic form that includes a content type field,
# and a content field. # and a content field.
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
content_field = api_settings.FORM_CONTENT_OVERRIDE
media_types = [parser.media_type for parser in view.parser_classes] media_types = [parser.media_type for parser in view.parser_classes]
choices = [(media_type, media_type) for media_type in media_types] choices = [(media_type, media_type) for media_type in media_types]
initial = media_types[0] initial = media_types[0]
# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form): class GenericContentForm(forms.Form):
def __init__(self): _content_type = forms.ChoiceField(
super(GenericContentForm, self).__init__()
self.fields[content_type_field] = forms.ChoiceField(
label='Media type', label='Media type',
choices=choices, choices=choices,
initial=initial initial=initial,
widget=forms.Select(attrs={'data-override': 'content-type'})
) )
self.fields[content_field] = forms.CharField( _content = forms.CharField(
label='Content', label='Content',
widget=forms.Textarea, widget=forms.Textarea(attrs={'data-override': 'content'}),
initial=content initial=content
) )
......
...@@ -86,7 +86,7 @@ def clone_request(request, method): ...@@ -86,7 +86,7 @@ def clone_request(request, method):
ret._full_data = request._full_data ret._full_data = request._full_data
ret._content_type = request._content_type ret._content_type = request._content_type
ret._stream = request._stream ret._stream = request._stream
ret._method = method ret.method = method
if hasattr(request, '_user'): if hasattr(request, '_user'):
ret._user = request._user ret._user = request._user
if hasattr(request, '_auth'): if hasattr(request, '_auth'):
...@@ -129,11 +129,6 @@ class Request(object): ...@@ -129,11 +129,6 @@ class Request(object):
- authentication_classes(list/tuple). The authentications used to try - authentication_classes(list/tuple). The authentications used to try
authenticating the request's user. authenticating the request's user.
""" """
_METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
def __init__(self, request, parsers=None, authenticators=None, def __init__(self, request, parsers=None, authenticators=None,
negotiator=None, parser_context=None): negotiator=None, parser_context=None):
self._request = request self._request = request
...@@ -144,7 +139,6 @@ class Request(object): ...@@ -144,7 +139,6 @@ class Request(object):
self._data = Empty self._data = Empty
self._files = Empty self._files = Empty
self._full_data = Empty self._full_data = Empty
self._method = Empty
self._content_type = Empty self._content_type = Empty
self._stream = Empty self._stream = Empty
...@@ -163,29 +157,9 @@ class Request(object): ...@@ -163,29 +157,9 @@ class Request(object):
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
@property @property
def method(self):
"""
Returns the HTTP method.
This allows the `method` to be overridden by using a hidden `form`
field on a form POST request.
"""
if not _hasattr(self, '_method'):
self._load_method_and_content_type()
return self._method
@property
def content_type(self): def content_type(self):
""" meta = self._request.META
Returns the content type header. return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`,
as it allows the content type to be overridden by using a hidden form
field on a form POST request.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
return self._content_type
@property @property
def stream(self): def stream(self):
...@@ -265,9 +239,6 @@ class Request(object): ...@@ -265,9 +239,6 @@ class Request(object):
""" """
Parses the request content into `self.data`. Parses the request content into `self.data`.
""" """
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not _hasattr(self, '_data'): if not _hasattr(self, '_data'):
self._data, self._files = self._parse() self._data, self._files = self._parse()
if self._files: if self._files:
...@@ -276,32 +247,14 @@ class Request(object): ...@@ -276,32 +247,14 @@ class Request(object):
else: else:
self._full_data = self._data self._full_data = self._data
def _load_method_and_content_type(self):
"""
Sets the method and content_type, and then check if they've
been overridden.
"""
self._content_type = self.META.get('HTTP_CONTENT_TYPE',
self.META.get('CONTENT_TYPE', ''))
self._perform_form_overloading()
if not _hasattr(self, '_method'):
self._method = self._request.method
# Allow X-HTTP-METHOD-OVERRIDE header
if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META:
self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper()
def _load_stream(self): def _load_stream(self):
""" """
Return the content body of the request, as a stream. Return the content body of the request, as a stream.
""" """
meta = self._request.META
try: try:
content_length = int( content_length = int(
self.META.get( meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
)
) )
except (ValueError, TypeError): except (ValueError, TypeError):
content_length = 0 content_length = 0
...@@ -313,50 +266,6 @@ class Request(object): ...@@ -313,50 +266,6 @@ class Request(object):
else: else:
self._stream = six.BytesIO(self.raw_post_data) self._stream = six.BytesIO(self.raw_post_data)
def _perform_form_overloading(self):
"""
If this is a form POST request, then we need to check if the method and
content/content_type have been overridden by setting them in hidden
form fields or not.
"""
USE_FORM_OVERLOADING = (
self._METHOD_PARAM or
(self._CONTENT_PARAM and self._CONTENTTYPE_PARAM)
)
# We only need to use form overloading on form POST requests.
if (
self._request.method != 'POST' or
not USE_FORM_OVERLOADING or
not is_form_media_type(self._content_type)
):
return
# At this point we're committed to parsing the request as form data.
self._data = self._request.POST
self._files = self._request.FILES
self._full_data = self._data.copy()
self._full_data.update(self._files)
# Method overloading - change the method and remove the param from the content.
if (
self._METHOD_PARAM and
self._METHOD_PARAM in self._data
):
self._method = self._data[self._METHOD_PARAM].upper()
# Content overloading - modify the content type, and force re-parse.
if (
self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and
self._CONTENTTYPE_PARAM in self._data
):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self): def _parse(self):
""" """
Parse the request content, returning a two-tuple of (data, files) Parse the request content, returning a two-tuple of (data, files)
......
...@@ -22,7 +22,6 @@ def preserve_builtin_query_params(url, request=None): ...@@ -22,7 +22,6 @@ def preserve_builtin_query_params(url, request=None):
overrides = [ overrides = [
api_settings.URL_FORMAT_OVERRIDE, api_settings.URL_FORMAT_OVERRIDE,
api_settings.URL_ACCEPT_OVERRIDE
] ]
for param in overrides: for param in overrides:
......
...@@ -91,13 +91,8 @@ DEFAULTS = { ...@@ -91,13 +91,8 @@ DEFAULTS = {
), ),
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart', 'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
# Browser enhancements # Hyperlink settings
'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': 'accept',
'URL_FORMAT_OVERRIDE': 'format', 'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format', 'FORMAT_SUFFIX_KWARG': 'format',
'URL_FIELD_NAME': 'url', 'URL_FIELD_NAME': 'url',
......
function replaceDocument(docString) {
var doc = document.open("text/html");
doc.write(docString);
doc.close();
}
function doAjaxSubmit(e) {
var form = $(this);
var btn = $(this.clk);
var method = btn.data('method') || form.data('method') || form.attr('method') || 'GET';
method = method.toUpperCase()
if (method === 'GET') {
// GET requests can always use standard form submits.
return;
}
var contentType =
form.find('input[data-override="content-type"]').val() ||
form.find('select[data-override="content-type"] option:selected').text();
if (method === 'POST' && !contentType) {
// POST requests can use standard form submits, unless we have
// overridden the content type.
return;
}
// At this point we need to make an AJAX form submission.
e.preventDefault();
var url = form.attr('action');
var data;
if (contentType) {
data = form.find('[data-override="content"]').val() || ''
} else {
contentType = form.attr('enctype') || form.attr('encoding')
if (contentType === 'multipart/form-data') {
if (!window.FormData) {
alert('Your browser does not support AJAX multipart form submissions');
return;
}
// Use the FormData API and allow the content type to be set automatically,
// so it includes the boundary string.
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
contentType = false;
data = new FormData(form[0]);
} else {
contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
data = form.serialize();
}
}
var ret = $.ajax({
url: url,
method: method,
data: data,
contentType: contentType,
processData: false,
headers: {'Accept': 'text/html; q=1.0, */*'},
});
ret.always(function(data, textStatus, jqXHR) {
if (textStatus != 'success') {
jqXHR = data;
}
var responseContentType = jqXHR.getResponseHeader("content-type") || "";
if (responseContentType.toLowerCase().indexOf('text/html') === 0) {
replaceDocument(jqXHR.responseText);
try {
// Modify the location and scroll to top, as if after page load.
history.replaceState({}, '', url);
scroll(0,0);
} catch(err) {
// History API not supported, so redirect.
window.location = url;
}
} else {
// Not HTML content. We can't open this directly, so redirect.
window.location = url;
}
});
return ret;
}
function captureSubmittingElement(e) {
var target = e.target;
var form = this;
form.clk = target;
}
$.fn.ajaxForm = function() {
var options = {}
return this
.unbind('submit.form-plugin click.form-plugin')
.bind('submit.form-plugin', options, doAjaxSubmit)
.bind('click.form-plugin', options, captureSubmittingElement);
};
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
function sameOrigin(url) {
// test that a given url is a same-origin URL
// url could be relative or scheme relative or absolute
var host = document.location.host; // host + port
var protocol = document.location.protocol;
var sr_origin = '//' + host;
var origin = protocol + sr_origin;
// Allow absolute or scheme relative URLs to same origin
return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
(url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
// or any other URL that isn't scheme relative or absolute i.e relative.
!(/^(\/\/|http:|https:).*/.test(url));
}
var csrftoken = getCookie('csrftoken');
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
// Send the token to same-origin, relative URLs only.
// Send the token only if the method warrants CSRF protection
// Using the CSRFToken value acquired earlier
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
...@@ -104,9 +104,7 @@ ...@@ -104,9 +104,7 @@
{% endif %} {% endif %}
{% if delete_form %} {% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST"> <form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
<button class="btn btn-danger"> <button class="btn btn-danger">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
</button> </button>
...@@ -180,7 +178,7 @@ ...@@ -180,7 +178,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Edit</h4> <h4 class="modal-title" id="myModalLabel">Edit</h4>
</div> </div>
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate> <form action="{{ request.get_full_path }}" data-method="PUT" enctype="multipart/form-data" class="form-horizontal" novalidate>
<div class="modal-body"> <div class="modal-body">
<fieldset> <fieldset>
{{ put_form }} {{ put_form }}
...@@ -188,7 +186,7 @@ ...@@ -188,7 +186,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</div> </div>
</form> </form>
</div> </div>
...@@ -204,7 +202,7 @@ ...@@ -204,7 +202,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">{{ error_title }}</h4> <h4 class="modal-title" id="myModalLabel">{{ error_title }}</h4>
</div> </div>
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate> <form action="{{ request.get_full_path }}" data-method="{{ request.method }}" enctype="multipart/form-data" class="form-horizontal" novalidate>
<div class="modal-body"> <div class="modal-body">
<fieldset> <fieldset>
{{ error_form }} {{ error_form }}
...@@ -212,7 +210,7 @@ ...@@ -212,7 +210,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="{{ request.method }}" type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</div> </div>
</form> </form>
</div> </div>
...@@ -221,10 +219,17 @@ ...@@ -221,10 +219,17 @@
{% endif %} {% endif %}
{% block script %} {% block script %}
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> <script src="{% static "rest_framework/js/jquery-1.11.3-min.js" %}"></script>
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
<script src="{% static "rest_framework/js/csrf.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script> <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% static "rest_framework/js/default.js" %}"></script> <script src="{% static "rest_framework/js/default.js" %}"></script>
<script>
$(document).ready(function() {
$('form').ajaxForm();
});
</script>
{% endblock %} {% endblock %}
</body> </body>
{% endblock %} {% endblock %}
......
...@@ -94,17 +94,13 @@ ...@@ -94,17 +94,13 @@
{% endif %} {% endif %}
{% if options_form %} {% if options_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST"> <form class="button-form" action="{{ request.get_full_path }}" data-method="OPTIONS">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
</form> </form>
{% endif %} {% endif %}
{% if delete_form %} {% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST"> <form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button> <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
</form> </form>
{% endif %} {% endif %}
...@@ -168,7 +164,7 @@ ...@@ -168,7 +164,7 @@
</div> </div>
{% endif %} {% endif %}
<div {% if post_form %}class="tab-pane"{% endif %} id="post-generic-content-form"> <div {% if raw_data_post_form %}class="tab-pane"{% endif %} id="post-generic-content-form">
{% with form=raw_data_post_form %} {% with form=raw_data_post_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset> <fieldset>
...@@ -200,11 +196,11 @@ ...@@ -200,11 +196,11 @@
<div class="well tab-content"> <div class="well tab-content">
{% if put_form %} {% if put_form %}
<div class="tab-pane" id="put-object-form"> <div class="tab-pane" id="put-object-form">
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate> <form action="{{ request.get_full_path }}" data-method="PUT" enctype="multipart/form-data" class="form-horizontal" novalidate>
<fieldset> <fieldset>
{{ put_form }} {{ put_form }}
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> <button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
</div> </div>
</fieldset> </fieldset>
</form> </form>
...@@ -213,15 +209,15 @@ ...@@ -213,15 +209,15 @@
<div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form"> <div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form">
{% with form=raw_data_put_or_patch_form %} {% with form=raw_data_put_or_patch_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> <form action="{{ request.get_full_path }}" data-method="PUT" class="form-horizontal">
<fieldset> <fieldset>
{% include "rest_framework/raw_data_form.html" %} {% include "rest_framework/raw_data_form.html" %}
<div class="form-actions"> <div class="form-actions">
{% if raw_data_put_form %} {% if raw_data_put_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> <button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
{% endif %} {% endif %}
{% if raw_data_patch_form %} {% if raw_data_patch_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button> <button data-method="PATCH" class="btn btn-primary js-tooltip" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
{% endif %} {% endif %}
</div> </div>
</fieldset> </fieldset>
...@@ -237,10 +233,17 @@ ...@@ -237,10 +233,17 @@
</div><!-- ./wrapper --> </div><!-- ./wrapper -->
{% block script %} {% block script %}
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> <script src="{% static "rest_framework/js/jquery-1.11.3.min.js" %}"></script>
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
<script src="{% static "rest_framework/js/csrf.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script> <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% static "rest_framework/js/default.js" %}"></script> <script src="{% static "rest_framework/js/default.js" %}"></script>
<script>
$(document).ready(function() {
$('form').ajaxForm();
});
</script>
{% endblock %} {% endblock %}
</body> </body>
{% endblock %} {% endblock %}
......
{% load rest_framework %} {% load rest_framework %}
{% csrf_token %}
{{ form.non_field_errors }} {{ form.non_field_errors }}
{% for field in form %} {% for field in form %}
<div class="form-group"> <div class="form-group">
......
...@@ -191,17 +191,6 @@ class RendererEndToEndTests(TestCase): ...@@ -191,17 +191,6 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS) self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
RendererB.media_type
)
resp = self.client.get('/' + param)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_unsatisfiable_accept_header_on_request_returns_406_status(self): def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
resp = self.client.get('/', HTTP_ACCEPT='foo/bar') resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
......
...@@ -3,27 +3,20 @@ Tests for content parsing, and form-overloaded content parsing. ...@@ -3,27 +3,20 @@ Tests for content parsing, and form-overloaded content parsing.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import json
from io import BytesIO
import django import django
import pytest import pytest
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework import status from rest_framework import status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import ( from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
BaseParser, FormParser, JSONParser, MultiPartParser from rest_framework.request import Request
)
from rest_framework.request import Empty, Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIClient, APIRequestFactory from rest_framework.test import APIClient, APIRequestFactory
from rest_framework.views import APIView from rest_framework.views import APIView
...@@ -43,36 +36,6 @@ class PlainTextParser(BaseParser): ...@@ -43,36 +36,6 @@ class PlainTextParser(BaseParser):
return stream.read() return stream.read()
class TestMethodOverloading(TestCase):
def test_method(self):
"""
Request methods should be same as underlying request.
"""
request = Request(factory.get('/'))
self.assertEqual(request.method, 'GET')
request = Request(factory.post('/'))
self.assertEqual(request.method, 'POST')
def test_overloaded_method(self):
"""
POST requests can be overloaded to another method by setting a
reserved form field
"""
request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'}))
self.assertEqual(request.method, 'DELETE')
def test_x_http_method_override_header(self):
"""
POST requests can also be overloaded to another method by setting
the X-HTTP-Method-Override header.
"""
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
class TestContentParsing(TestCase): class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self): def test_standard_behaviour_determines_no_content_GET(self):
""" """
...@@ -137,49 +100,6 @@ class TestContentParsing(TestCase): ...@@ -137,49 +100,6 @@ class TestContentParsing(TestCase):
request.parsers = (PlainTextParser(), ) request.parsers = (PlainTextParser(), )
self.assertEqual(request.data, content) self.assertEqual(request.data, content)
def test_overloaded_behaviour_allows_content_tunnelling(self):
"""
Ensure request.data returns content for overloaded POST request.
"""
json_data = {'foobar': 'qwerty'}
content = json.dumps(json_data)
content_type = 'application/json'
form_data = {
api_settings.FORM_CONTENT_OVERRIDE: content,
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
}
request = Request(factory.post('/', form_data))
request.parsers = (JSONParser(), )
self.assertEqual(request.data, json_data)
def test_form_POST_unicode(self):
"""
JSON POST via default web interface with unicode data
"""
# Note: environ and other variables here have simplified content compared to real Request
CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
environ = {
'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'CONTENT_LENGTH': len(CONTENT),
'wsgi.input': BytesIO(CONTENT),
}
wsgi_request = WSGIRequest(environ=environ)
wsgi_request._load_post_and_files()
parsers = (JSONParser(), FormParser(), MultiPartParser())
parser_context = {
'encoding': 'utf-8',
'kwargs': {},
'args': (),
}
request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
method = request.method
self.assertEqual(method, 'POST')
self.assertEqual(request._content_type, 'application/json')
self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
self.assertEqual(request._data, Empty)
self.assertEqual(request._files, Empty)
class MockView(APIView): class MockView(APIView):
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
......
...@@ -5,11 +5,11 @@ from django.test import TestCase ...@@ -5,11 +5,11 @@ from django.test import TestCase
from django.utils import six from django.utils import six
from rest_framework import generics, routers, serializers, status, viewsets from rest_framework import generics, routers, serializers, status, viewsets
from rest_framework.parsers import JSONParser
from rest_framework.renderers import ( from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, JSONRenderer BaseRenderer, BrowsableAPIRenderer, JSONRenderer
) )
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView from rest_framework.views import APIView
from tests.models import BasicModel from tests.models import BasicModel
...@@ -79,6 +79,14 @@ class MockViewSettingContentType(APIView): ...@@ -79,6 +79,14 @@ class MockViewSettingContentType(APIView):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview') return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
class JSONView(APIView):
parser_classes = (JSONParser,)
def post(self, request, **kwargs):
assert request.data
return Response(DUMMYCONTENT)
class HTMLView(APIView): class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, ) renderer_classes = (BrowsableAPIRenderer, )
...@@ -114,6 +122,7 @@ urlpatterns = [ ...@@ -114,6 +122,7 @@ urlpatterns = [
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^html$', HTMLView.as_view()), url(r'^html$', HTMLView.as_view()),
url(r'^json$', JSONView.as_view()),
url(r'^html1$', HTMLView1.as_view()), url(r'^html1$', HTMLView1.as_view()),
url(r'^html_new_model$', HTMLNewModelView.as_view()), url(r'^html_new_model$', HTMLNewModelView.as_view()),
url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)), url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)),
...@@ -166,17 +175,6 @@ class RendererIntegrationTests(TestCase): ...@@ -166,17 +175,6 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS) self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_accept_query(self):
"""The '_accept' query string should behave in the same way as the Accept header."""
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
RendererB.media_type
)
resp = self.client.get('/' + param)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
def test_specified_renderer_serializes_content_on_format_query(self): def test_specified_renderer_serializes_content_on_format_query(self):
"""If a 'format' query is specified, the renderer with the matching """If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response.""" format attribute should serialize the response."""
...@@ -203,6 +201,25 @@ class RendererIntegrationTests(TestCase): ...@@ -203,6 +201,25 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.status_code, DUMMYSTATUS) self.assertEqual(resp.status_code, DUMMYSTATUS)
class UnsupportedMediaTypeTests(TestCase):
urls = 'tests.test_response'
def test_should_allow_posting_json(self):
response = self.client.post('/json', data='{"test": 123}', content_type='application/json')
self.assertEqual(response.status_code, 200)
def test_should_not_allow_posting_xml(self):
response = self.client.post('/json', data='<test>123</test>', content_type='application/xml')
self.assertEqual(response.status_code, 415)
def test_should_not_allow_posting_a_form(self):
response = self.client.post('/json', data={'test': 123})
self.assertEqual(response.status_code, 415)
class Issue122Tests(TestCase): class Issue122Tests(TestCase):
""" """
Tests that covers #122. Tests that covers #122.
...@@ -270,16 +287,6 @@ class Issue807Tests(TestCase): ...@@ -270,16 +287,6 @@ class Issue807Tests(TestCase):
resp = self.client.get('/setbyview', **headers) resp = self.client.get('/setbyview', **headers)
self.assertEqual('setbyview', resp['Content-Type']) self.assertEqual('setbyview', resp['Content-Type'])
def test_viewset_label_help_text(self):
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
'text/html'
)
resp = self.client.get('/html_new_model_viewset/' + param)
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
# self.assertContains(resp, 'Text comes here')
# self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self): def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model') resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
......
...@@ -74,21 +74,6 @@ class ClassBasedViewIntegrationTests(TestCase): ...@@ -74,21 +74,6 @@ class ClassBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected) self.assertEqual(sanitise_json_error(response.data), expected)
def test_400_parse_error_tunneled_content(self):
content = 'f00bar'
content_type = 'application/json'
form_data = {
api_settings.FORM_CONTENT_OVERRIDE: content,
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
}
request = factory.post('/', form_data)
response = self.view(request)
expected = {
'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
class FunctionBasedViewIntegrationTests(TestCase): class FunctionBasedViewIntegrationTests(TestCase):
def setUp(self): def setUp(self):
...@@ -103,21 +88,6 @@ class FunctionBasedViewIntegrationTests(TestCase): ...@@ -103,21 +88,6 @@ class FunctionBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected) self.assertEqual(sanitise_json_error(response.data), expected)
def test_400_parse_error_tunneled_content(self):
content = 'f00bar'
content_type = 'application/json'
form_data = {
api_settings.FORM_CONTENT_OVERRIDE: content,
api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
}
request = factory.post('/', form_data)
response = self.view(request)
expected = {
'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
class TestCustomExceptionHandler(TestCase): class TestCustomExceptionHandler(TestCase):
def setUp(self): def setUp(self):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment