Commit 628c3568 by Anna

Merge remote-tracking branch 'upstream/master'

parents 60bb3b09 1d34bc0b
...@@ -105,7 +105,7 @@ Here's a brief example that demonstrates: ...@@ -105,7 +105,7 @@ Here's a brief example that demonstrates:
<html> <html>
<head> <head>
<script src="/static/rest_framework/js/coreapi-0.1.0.js"></script> <script src="/static/rest_framework/js/coreapi-0.1.0.js"></script>
<script src="/docs/schema.js' %}"></script> <script src="/docs/schema.js"></script>
<script> <script>
const coreapi = window.coreapi const coreapi = window.coreapi
const schema = window.schema const schema = window.schema
......
...@@ -38,6 +38,32 @@ You can determine your currently installed version using `pip freeze`: ...@@ -38,6 +38,32 @@ You can determine your currently installed version using `pip freeze`:
--- ---
## 3.6.x series
### 3.6.2
**Date**: [10th March 2017][3.6.2-milestone]
* Support for Safari & IE in API docs. ([#4959][gh4959], [#4961][gh4961])
* Add missing `mark_safe` in API docs template tags. ([#4952][gh4952], [#4953][gh4953])
* Add missing glyicon fonts. ([#4950][gh4950], [#4951][gh4951])
* Fix One-to-one fields in API docs. ([#4955][gh4955], [#4956][gh4956])
* Test clean ups. ([#4949][gh4949])
### 3.6.1
**Date**: [9th March 2017][3.6.1-milestone]
* Ensure `markdown` dependancy is optional. ([#4947][gh4947])
### 3.6.0
**Date**: [9th March 2017][3.6.0-milestone]
See the [release announcement][3.6-release].
---
## 3.5.x series ## 3.5.x series
### 3.5.4 ### 3.5.4
...@@ -625,6 +651,7 @@ For older release notes, [please see the version 2.x documentation][old-release- ...@@ -625,6 +651,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
[ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582 [ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582
[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3 [rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
[old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/version-2.4.x/docs/topics/release-notes.md [old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/version-2.4.x/docs/topics/release-notes.md
[3.6-release]: 3.6-announcement.md
[3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22 [3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22
[3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22
...@@ -658,6 +685,9 @@ For older release notes, [please see the version 2.x documentation][old-release- ...@@ -658,6 +685,9 @@ For older release notes, [please see the version 2.x documentation][old-release-
[3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22 [3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22
[3.5.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.3+Release%22 [3.5.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.3+Release%22
[3.5.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.4+Release%22 [3.5.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.4+Release%22
[3.6.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.6.0+Release%22
[3.6.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.6.1+Release%22
[3.6.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.6.2+Release%22
<!-- 3.0.1 --> <!-- 3.0.1 -->
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
...@@ -1254,3 +1284,17 @@ For older release notes, [please see the version 2.x documentation][old-release- ...@@ -1254,3 +1284,17 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh4634]: https://github.com/tomchristie/django-rest-framework/issues/4634 [gh4634]: https://github.com/tomchristie/django-rest-framework/issues/4634
[gh4669]: https://github.com/tomchristie/django-rest-framework/issues/4669 [gh4669]: https://github.com/tomchristie/django-rest-framework/issues/4669
[gh4712]: https://github.com/tomchristie/django-rest-framework/issues/4712 [gh4712]: https://github.com/tomchristie/django-rest-framework/issues/4712
<!-- 3.6.1 -->
[gh4947]: https://github.com/tomchristie/django-rest-framework/issues/4947
<!-- 3.6.2 -->
[gh4959]: https://github.com/tomchristie/django-rest-framework/issues/4959
[gh4961]: https://github.com/tomchristie/django-rest-framework/issues/4961
[gh4952]: https://github.com/tomchristie/django-rest-framework/issues/4952
[gh4953]: https://github.com/tomchristie/django-rest-framework/issues/4953
[gh4950]: https://github.com/tomchristie/django-rest-framework/issues/4950
[gh4951]: https://github.com/tomchristie/django-rest-framework/issues/4951
[gh4955]: https://github.com/tomchristie/django-rest-framework/issues/4955
[gh4956]: https://github.com/tomchristie/django-rest-framework/issues/4956
[gh4949]: https://github.com/tomchristie/django-rest-framework/issues/4949
...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __ ...@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
""" """
__title__ = 'Django REST framework' __title__ = 'Django REST framework'
__version__ = '3.6.1' __version__ = '3.6.2'
__author__ = 'Tom Christie' __author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause' __license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2017 Tom Christie' __copyright__ = 'Copyright 2011-2017 Tom Christie'
......
...@@ -531,7 +531,7 @@ class SchemaGenerator(object): ...@@ -531,7 +531,7 @@ class SchemaGenerator(object):
try: try:
model_field = model._meta.get_field(variable) model_field = model._meta.get_field(variable)
except: except:
pass model_field = None
if model_field is not None and model_field.verbose_name: if model_field is not None and model_field.verbose_name:
title = force_text(model_field.verbose_name) title = force_text(model_field.verbose_name)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -7,7 +7,7 @@ function normalizeHTTPHeader (str) { ...@@ -7,7 +7,7 @@ function normalizeHTTPHeader (str) {
.replace(/(Md5)/g, function ($1) { return 'MD5' }) .replace(/(Md5)/g, function ($1) { return 'MD5' })
} }
let responseDisplay = 'data' var responseDisplay = 'data'
const coreapi = window.coreapi const coreapi = window.coreapi
const schema = window.schema const schema = window.schema
...@@ -28,6 +28,39 @@ $('#language-control li').click(function (event) { ...@@ -28,6 +28,39 @@ $('#language-control li').click(function (event) {
codeBlocks.filter('[data-language="' + language +'"]').removeClass("hide") codeBlocks.filter('[data-language="' + language +'"]').removeClass("hide")
}) })
function formEntries (form) {
// Polyfill for new FormData(form).entries()
var formData = new FormData(form)
if (formData.entries !== undefined) {
return formData.entries()
}
var entries = []
for (var {name, type, value, files, checked, selectedOptions} of Array.from(form.elements)) {
if (!name) {
continue
}
if (type === 'file') {
for (var file of files) {
entries.push([name, file])
}
} else if (type === 'select-multiple' || type === 'select-one') {
for (var elm of Array.from(selectedOptions)) {
entries.push([name, elm.value])
}
} else if (type === 'checkbox') {
if (checked) {
entries.push([name, value])
}
} else {
entries.push([name, value])
}
}
return entries
}
// API Explorer // API Explorer
$('form.api-interaction').submit(function(event) { $('form.api-interaction').submit(function(event) {
event.preventDefault(); event.preventDefault();
...@@ -36,23 +69,23 @@ $('form.api-interaction').submit(function(event) { ...@@ -36,23 +69,23 @@ $('form.api-interaction').submit(function(event) {
const key = form.data("key"); const key = form.data("key");
var params = {}; var params = {};
const formData = new FormData(form.get()[0]); const entries = formEntries(form.get()[0]);
for (var [paramKey, paramValue] of formData.entries()) { for (var [paramKey, paramValue] of entries) {
var elem = form.find("[name=" + paramKey + "]") var elem = form.find("[name=" + paramKey + "]")
var dataType = elem.data('type') || 'string' var dataType = elem.data('type') || 'string'
if (dataType === 'integer' && paramValue) { if (dataType === 'integer' && paramValue) {
let value = parseInt(paramValue) var value = parseInt(paramValue)
if (!isNaN(value)) { if (!isNaN(value)) {
params[paramKey] = value params[paramKey] = value
} }
} else if (dataType === 'number' && paramValue) { } else if (dataType === 'number' && paramValue) {
let value = parseFloat(paramValue) var value = parseFloat(paramValue)
if (!isNaN(value)) { if (!isNaN(value)) {
params[paramKey] = value params[paramKey] = value
} }
} else if (dataType === 'boolean' && paramValue) { } else if (dataType === 'boolean' && paramValue) {
let value = { var value = {
'true': true, 'true': true,
'false': false 'false': false
}[paramValue.toLowerCase()] }[paramValue.toLowerCase()]
...@@ -86,7 +119,7 @@ $('form.api-interaction').submit(function(event) { ...@@ -86,7 +119,7 @@ $('form.api-interaction').submit(function(event) {
function requestCallback(request) { function requestCallback(request) {
// Fill in the "GET /foo/" display. // Fill in the "GET /foo/" display.
let parser = document.createElement('a'); var parser = document.createElement('a');
parser.href = request.url; parser.href = request.url;
const method = request.options.method const method = request.options.method
const path = parser.pathname + parser.hash + parser.search const path = parser.pathname + parser.hash + parser.search
...@@ -111,7 +144,7 @@ $('form.api-interaction').submit(function(event) { ...@@ -111,7 +144,7 @@ $('form.api-interaction').submit(function(event) {
// Fill in the Raw HTTP response display. // Fill in the Raw HTTP response display.
var panelText = 'HTTP/1.1 ' + response.status + ' ' + response.statusText + '\n'; var panelText = 'HTTP/1.1 ' + response.status + ' ' + response.statusText + '\n';
response.headers.forEach((header, key) => { response.headers.forEach(function(header, key) {
panelText += normalizeHTTPHeader(key) + ': ' + header + '\n' panelText += normalizeHTTPHeader(key) + ': ' + header + '\n'
}) })
if (responseText) { if (responseText) {
...@@ -121,7 +154,7 @@ $('form.api-interaction').submit(function(event) { ...@@ -121,7 +154,7 @@ $('form.api-interaction').submit(function(event) {
} }
// Instantiate a client to make the outgoing request. // Instantiate a client to make the outgoing request.
let options = { var options = {
requestCallback: requestCallback, requestCallback: requestCallback,
responseCallback: responseCallback, responseCallback: responseCallback,
} }
......
let codec = new window.coreapi.codecs.CoreJSONCodec() var codec = new window.coreapi.codecs.CoreJSONCodec()
let coreJSON = window.atob('{{ schema }}') var coreJSON = window.atob('{{ schema }}')
window.schema = codec.decode(coreJSON) window.schema = codec.decode(coreJSON)
...@@ -63,14 +63,14 @@ def form_for_link(link): ...@@ -63,14 +63,14 @@ def form_for_link(link):
if field.required if field.required
] ]
schema = coreschema.Object(properties=properties, required=required) schema = coreschema.Object(properties=properties, required=required)
return coreschema.render_to_form(schema) return mark_safe(coreschema.render_to_form(schema))
@register.simple_tag @register.simple_tag
def render_markdown(markdown_text): def render_markdown(markdown_text):
if not markdown: if not markdown:
return markdown_text return markdown_text
return markdown.markdown(markdown_text) return mark_safe(markdown.markdown(markdown_text))
@register.simple_tag @register.simple_tag
......
...@@ -69,7 +69,7 @@ class TestAPITestClient(TestCase): ...@@ -69,7 +69,7 @@ class TestAPITestClient(TestCase):
self.client.credentials(HTTP_AUTHORIZATION='example') self.client.credentials(HTTP_AUTHORIZATION='example')
for _ in range(0, 3): for _ in range(0, 3):
response = self.client.get('/view/') response = self.client.get('/view/')
self.assertEqual(response.data['auth'], 'example') assert response.data['auth'] == 'example'
def test_force_authenticate(self): def test_force_authenticate(self):
""" """
...@@ -78,7 +78,7 @@ class TestAPITestClient(TestCase): ...@@ -78,7 +78,7 @@ class TestAPITestClient(TestCase):
user = User.objects.create_user('example', 'example@example.com') user = User.objects.create_user('example', 'example@example.com')
self.client.force_authenticate(user) self.client.force_authenticate(user)
response = self.client.get('/view/') response = self.client.get('/view/')
self.assertEqual(response.data['user'], 'example') assert response.data['user'] == 'example'
def test_force_authenticate_with_sessions(self): def test_force_authenticate_with_sessions(self):
""" """
...@@ -89,16 +89,16 @@ class TestAPITestClient(TestCase): ...@@ -89,16 +89,16 @@ class TestAPITestClient(TestCase):
# First request does not yet have an active session # First request does not yet have an active session
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
self.assertEqual(response.data['active_session'], False) assert response.data['active_session'] is False
# Subsequent requests have an active session # Subsequent requests have an active session
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
self.assertEqual(response.data['active_session'], True) assert response.data['active_session'] is True
# Force authenticating as `None` should also logout the user session. # Force authenticating as `None` should also logout the user session.
self.client.force_authenticate(None) self.client.force_authenticate(None)
response = self.client.get('/session-view/') response = self.client.get('/session-view/')
self.assertEqual(response.data['active_session'], False) assert response.data['active_session'] is False
def test_csrf_exempt_by_default(self): def test_csrf_exempt_by_default(self):
""" """
...@@ -107,7 +107,7 @@ class TestAPITestClient(TestCase): ...@@ -107,7 +107,7 @@ class TestAPITestClient(TestCase):
User.objects.create_user('example', 'example@example.com', 'password') User.objects.create_user('example', 'example@example.com', 'password')
self.client.login(username='example', password='password') self.client.login(username='example', password='password')
response = self.client.post('/view/') response = self.client.post('/view/')
self.assertEqual(response.status_code, 200) assert response.status_code == 200
def test_explicitly_enforce_csrf_checks(self): def test_explicitly_enforce_csrf_checks(self):
""" """
...@@ -118,8 +118,8 @@ class TestAPITestClient(TestCase): ...@@ -118,8 +118,8 @@ class TestAPITestClient(TestCase):
client.login(username='example', password='password') client.login(username='example', password='password')
response = client.post('/view/') response = client.post('/view/')
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'} expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
self.assertEqual(response.status_code, 403) assert response.status_code == 403
self.assertEqual(response.data, expected) assert response.data == expected
def test_can_logout(self): def test_can_logout(self):
""" """
...@@ -127,10 +127,10 @@ class TestAPITestClient(TestCase): ...@@ -127,10 +127,10 @@ class TestAPITestClient(TestCase):
""" """
self.client.credentials(HTTP_AUTHORIZATION='example') self.client.credentials(HTTP_AUTHORIZATION='example')
response = self.client.get('/view/') response = self.client.get('/view/')
self.assertEqual(response.data['auth'], 'example') assert response.data['auth'] == 'example'
self.client.logout() self.client.logout()
response = self.client.get('/view/') response = self.client.get('/view/')
self.assertEqual(response.data['auth'], b'') assert response.data['auth'] == b''
def test_logout_resets_force_authenticate(self): def test_logout_resets_force_authenticate(self):
""" """
...@@ -139,50 +139,50 @@ class TestAPITestClient(TestCase): ...@@ -139,50 +139,50 @@ class TestAPITestClient(TestCase):
user = User.objects.create_user('example', 'example@example.com', 'password') user = User.objects.create_user('example', 'example@example.com', 'password')
self.client.force_authenticate(user) self.client.force_authenticate(user)
response = self.client.get('/view/') response = self.client.get('/view/')
self.assertEqual(response.data['user'], 'example') assert response.data['user'] == 'example'
self.client.logout() self.client.logout()
response = self.client.get('/view/') response = self.client.get('/view/')
self.assertEqual(response.data['user'], '') assert response.data['user'] == ''
def test_follow_redirect(self): def test_follow_redirect(self):
""" """
Follow redirect by setting follow argument. Follow redirect by setting follow argument.
""" """
response = self.client.get('/redirect-view/') response = self.client.get('/redirect-view/')
self.assertEqual(response.status_code, 302) assert response.status_code == 302
response = self.client.get('/redirect-view/', follow=True) response = self.client.get('/redirect-view/', follow=True)
self.assertIsNotNone(response.redirect_chain) assert response.redirect_chain is not None
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response = self.client.post('/redirect-view/') response = self.client.post('/redirect-view/')
self.assertEqual(response.status_code, 302) assert response.status_code == 302
response = self.client.post('/redirect-view/', follow=True) response = self.client.post('/redirect-view/', follow=True)
self.assertIsNotNone(response.redirect_chain) assert response.redirect_chain is not None
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response = self.client.put('/redirect-view/') response = self.client.put('/redirect-view/')
self.assertEqual(response.status_code, 302) assert response.status_code == 302
response = self.client.put('/redirect-view/', follow=True) response = self.client.put('/redirect-view/', follow=True)
self.assertIsNotNone(response.redirect_chain) assert response.redirect_chain is not None
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response = self.client.patch('/redirect-view/') response = self.client.patch('/redirect-view/')
self.assertEqual(response.status_code, 302) assert response.status_code == 302
response = self.client.patch('/redirect-view/', follow=True) response = self.client.patch('/redirect-view/', follow=True)
self.assertIsNotNone(response.redirect_chain) assert response.redirect_chain is not None
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response = self.client.delete('/redirect-view/') response = self.client.delete('/redirect-view/')
self.assertEqual(response.status_code, 302) assert response.status_code == 302
response = self.client.delete('/redirect-view/', follow=True) response = self.client.delete('/redirect-view/', follow=True)
self.assertIsNotNone(response.redirect_chain) assert response.redirect_chain is not None
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response = self.client.options('/redirect-view/') response = self.client.options('/redirect-view/')
self.assertEqual(response.status_code, 302) assert response.status_code == 302
response = self.client.options('/redirect-view/', follow=True) response = self.client.options('/redirect-view/', follow=True)
self.assertIsNotNone(response.redirect_chain) assert response.redirect_chain is not None
self.assertEqual(response.status_code, 200) assert response.status_code == 200
def test_invalid_multipart_data(self): def test_invalid_multipart_data(self):
""" """
...@@ -200,8 +200,8 @@ class TestAPITestClient(TestCase): ...@@ -200,8 +200,8 @@ class TestAPITestClient(TestCase):
data=None, data=None,
content_type='application/json' content_type='application/json'
) )
self.assertEqual(response.status_code, 200, response.content) assert response.status_code == 200
self.assertEqual(response.data, {"flag": True}) assert response.data == {"flag": True}
class TestAPIRequestFactory(TestCase): class TestAPIRequestFactory(TestCase):
...@@ -214,7 +214,7 @@ class TestAPIRequestFactory(TestCase): ...@@ -214,7 +214,7 @@ class TestAPIRequestFactory(TestCase):
request = factory.post('/view/') request = factory.post('/view/')
request.user = user request.user = user
response = view(request) response = view(request)
self.assertEqual(response.status_code, 200) assert response.status_code == 200
def test_explicitly_enforce_csrf_checks(self): def test_explicitly_enforce_csrf_checks(self):
""" """
...@@ -226,8 +226,8 @@ class TestAPIRequestFactory(TestCase): ...@@ -226,8 +226,8 @@ class TestAPIRequestFactory(TestCase):
request.user = user request.user = user
response = view(request) response = view(request)
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'} expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
self.assertEqual(response.status_code, 403) assert response.status_code == 403
self.assertEqual(response.data, expected) assert response.data == expected
def test_invalid_format(self): def test_invalid_format(self):
""" """
...@@ -249,7 +249,7 @@ class TestAPIRequestFactory(TestCase): ...@@ -249,7 +249,7 @@ class TestAPIRequestFactory(TestCase):
request = factory.get('/view') request = factory.get('/view')
force_authenticate(request, user=user) force_authenticate(request, user=user)
response = view(request) response = view(request)
self.assertEqual(response.data['user'], 'example') assert response.data['user'] == 'example'
def test_upload_file(self): def test_upload_file(self):
# This is a 1x1 black png # This is a 1x1 black png
...@@ -264,13 +264,13 @@ class TestAPIRequestFactory(TestCase): ...@@ -264,13 +264,13 @@ class TestAPIRequestFactory(TestCase):
""" """
factory = APIRequestFactory() factory = APIRequestFactory()
request = factory.get('/view/?demo=test') request = factory.get('/view/?demo=test')
self.assertEqual(dict(request.GET), {'demo': ['test']}) assert dict(request.GET) == {'demo': ['test']}
request = factory.get('/view/', {'demo': 'test'}) request = factory.get('/view/', {'demo': 'test'})
self.assertEqual(dict(request.GET), {'demo': ['test']}) assert dict(request.GET) == {'demo': ['test']}
def test_request_factory_url_arguments_with_unicode(self): def test_request_factory_url_arguments_with_unicode(self):
factory = APIRequestFactory() factory = APIRequestFactory()
request = factory.get('/view/?demo=testé') request = factory.get('/view/?demo=testé')
self.assertEqual(dict(request.GET), {'demo': ['testé']}) assert dict(request.GET) == {'demo': ['testé']}
request = factory.get('/view/', {'demo': 'testé'}) request = factory.get('/view/', {'demo': 'testé'})
self.assertEqual(dict(request.GET), {'demo': ['testé']}) assert dict(request.GET) == {'demo': ['testé']}
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