Fixed #32002 -- Added headers parameter to HttpResponse and subclasses.
This commit is contained in:
parent
2e7cc95499
commit
dcb69043d0
|
@ -97,8 +97,18 @@ class HttpResponseBase:
|
||||||
|
|
||||||
status_code = 200
|
status_code = 200
|
||||||
|
|
||||||
def __init__(self, content_type=None, status=None, reason=None, charset=None):
|
def __init__(self, content_type=None, status=None, reason=None, charset=None, headers=None):
|
||||||
self.headers = ResponseHeaders({})
|
self.headers = ResponseHeaders(headers or {})
|
||||||
|
self._charset = charset
|
||||||
|
if content_type and 'Content-Type' in self.headers:
|
||||||
|
raise ValueError(
|
||||||
|
"'headers' must not contain 'Content-Type' when the "
|
||||||
|
"'content_type' parameter is provided."
|
||||||
|
)
|
||||||
|
if 'Content-Type' not in self.headers:
|
||||||
|
if content_type is None:
|
||||||
|
content_type = 'text/html; charset=%s' % self.charset
|
||||||
|
self.headers['Content-Type'] = content_type
|
||||||
self._resource_closers = []
|
self._resource_closers = []
|
||||||
# This parameter is set by the handler. It's necessary to preserve the
|
# This parameter is set by the handler. It's necessary to preserve the
|
||||||
# historical behavior of request_finished.
|
# historical behavior of request_finished.
|
||||||
|
@ -114,10 +124,6 @@ class HttpResponseBase:
|
||||||
if not 100 <= self.status_code <= 599:
|
if not 100 <= self.status_code <= 599:
|
||||||
raise ValueError('HTTP status code must be an integer from 100 to 599.')
|
raise ValueError('HTTP status code must be an integer from 100 to 599.')
|
||||||
self._reason_phrase = reason
|
self._reason_phrase = reason
|
||||||
self._charset = charset
|
|
||||||
if content_type is None:
|
|
||||||
content_type = 'text/html; charset=%s' % self.charset
|
|
||||||
self['Content-Type'] = content_type
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reason_phrase(self):
|
def reason_phrase(self):
|
||||||
|
|
|
@ -11,7 +11,7 @@ class SimpleTemplateResponse(HttpResponse):
|
||||||
rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
|
rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
|
||||||
|
|
||||||
def __init__(self, template, context=None, content_type=None, status=None,
|
def __init__(self, template, context=None, content_type=None, status=None,
|
||||||
charset=None, using=None):
|
charset=None, using=None, headers=None):
|
||||||
# It would seem obvious to call these next two members 'template' and
|
# It would seem obvious to call these next two members 'template' and
|
||||||
# 'context', but those names are reserved as part of the test Client
|
# 'context', but those names are reserved as part of the test Client
|
||||||
# API. To avoid the name collision, we use different names.
|
# API. To avoid the name collision, we use different names.
|
||||||
|
@ -33,7 +33,7 @@ class SimpleTemplateResponse(HttpResponse):
|
||||||
# content argument doesn't make sense here because it will be replaced
|
# content argument doesn't make sense here because it will be replaced
|
||||||
# with rendered template so we always pass empty string in order to
|
# with rendered template so we always pass empty string in order to
|
||||||
# prevent errors and provide shorter signature.
|
# prevent errors and provide shorter signature.
|
||||||
super().__init__('', content_type, status, charset=charset)
|
super().__init__('', content_type, status, charset=charset, headers=headers)
|
||||||
|
|
||||||
# _is_rendered tracks whether the template and context has been baked
|
# _is_rendered tracks whether the template and context has been baked
|
||||||
# into a final response.
|
# into a final response.
|
||||||
|
@ -139,6 +139,6 @@ class TemplateResponse(SimpleTemplateResponse):
|
||||||
rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']
|
rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']
|
||||||
|
|
||||||
def __init__(self, request, template, context=None, content_type=None,
|
def __init__(self, request, template, context=None, content_type=None,
|
||||||
status=None, charset=None, using=None):
|
status=None, charset=None, using=None, headers=None):
|
||||||
super().__init__(template, context, content_type, status, charset, using)
|
super().__init__(template, context, content_type, status, charset, using, headers=headers)
|
||||||
self._request = request
|
self._request = request
|
||||||
|
|
|
@ -20,8 +20,10 @@ Here's an example::
|
||||||
|
|
||||||
def some_view(request):
|
def some_view(request):
|
||||||
# Create the HttpResponse object with the appropriate CSV header.
|
# Create the HttpResponse object with the appropriate CSV header.
|
||||||
response = HttpResponse(content_type='text/csv')
|
response = HttpResponse(
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
|
content_type='text/csv',
|
||||||
|
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
|
||||||
|
)
|
||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
writer.writerow(['First row', 'Foo', 'Bar', 'Baz'])
|
writer.writerow(['First row', 'Foo', 'Bar', 'Baz'])
|
||||||
|
@ -86,10 +88,11 @@ the assembly and transmission of a large CSV file::
|
||||||
rows = (["Row {}".format(idx), str(idx)] for idx in range(65536))
|
rows = (["Row {}".format(idx), str(idx)] for idx in range(65536))
|
||||||
pseudo_buffer = Echo()
|
pseudo_buffer = Echo()
|
||||||
writer = csv.writer(pseudo_buffer)
|
writer = csv.writer(pseudo_buffer)
|
||||||
response = StreamingHttpResponse((writer.writerow(row) for row in rows),
|
return StreamingHttpResponse(
|
||||||
content_type="text/csv")
|
(writer.writerow(row) for row in rows),
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
|
content_type="text/csv",
|
||||||
return response
|
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
|
||||||
|
)
|
||||||
|
|
||||||
Using the template system
|
Using the template system
|
||||||
=========================
|
=========================
|
||||||
|
@ -108,8 +111,10 @@ Here's an example, which generates the same CSV file as above::
|
||||||
|
|
||||||
def some_view(request):
|
def some_view(request):
|
||||||
# Create the HttpResponse object with the appropriate CSV header.
|
# Create the HttpResponse object with the appropriate CSV header.
|
||||||
response = HttpResponse(content_type='text/csv')
|
response = HttpResponse(
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
|
content_type='text/csv'
|
||||||
|
headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
|
||||||
|
)
|
||||||
|
|
||||||
# The data is hard-coded here, but you could load it from a database or
|
# The data is hard-coded here, but you could load it from a database or
|
||||||
# some other source.
|
# some other source.
|
||||||
|
|
|
@ -724,6 +724,10 @@ by ``HttpResponse``.
|
||||||
When using this interface, unlike a dictionary, ``del`` doesn't raise
|
When using this interface, unlike a dictionary, ``del`` doesn't raise
|
||||||
``KeyError`` if the header field doesn't exist.
|
``KeyError`` if the header field doesn't exist.
|
||||||
|
|
||||||
|
You can also set headers on instantiation::
|
||||||
|
|
||||||
|
>>> response = HttpResponse(headers={'Age': 120})
|
||||||
|
|
||||||
For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended
|
For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended
|
||||||
to use the :func:`~django.utils.cache.patch_cache_control` and
|
to use the :func:`~django.utils.cache.patch_cache_control` and
|
||||||
:func:`~django.utils.cache.patch_vary_headers` methods from
|
:func:`~django.utils.cache.patch_vary_headers` methods from
|
||||||
|
@ -738,15 +742,19 @@ containing a newline character (CR or LF) will raise ``BadHeaderError``
|
||||||
|
|
||||||
The :attr:`HttpResponse.headers` interface was added.
|
The :attr:`HttpResponse.headers` interface was added.
|
||||||
|
|
||||||
|
The ability to set headers on instantiation was added.
|
||||||
|
|
||||||
Telling the browser to treat the response as a file attachment
|
Telling the browser to treat the response as a file attachment
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
To tell the browser to treat the response as a file attachment, use the
|
To tell the browser to treat the response as a file attachment, set the
|
||||||
``content_type`` argument and set the ``Content-Disposition`` header. For example,
|
``Content-Type`` and ``Content-Disposition`` headers. For example, this is how
|
||||||
this is how you might return a Microsoft Excel spreadsheet::
|
you might return a Microsoft Excel spreadsheet::
|
||||||
|
|
||||||
>>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel')
|
>>> response = HttpResponse(my_data, headers={
|
||||||
>>> response.headers['Content-Disposition'] = 'attachment; filename="foo.xls"'
|
... 'Content-Type': 'application/vnd.ms-excel',
|
||||||
|
... 'Content-Disposition': 'attachment; filename="foo.xls"',
|
||||||
|
... })
|
||||||
|
|
||||||
There's nothing Django-specific about the ``Content-Disposition`` header, but
|
There's nothing Django-specific about the ``Content-Disposition`` header, but
|
||||||
it's easy to forget the syntax, so we've included it here.
|
it's easy to forget the syntax, so we've included it here.
|
||||||
|
@ -802,10 +810,10 @@ Attributes
|
||||||
Methods
|
Methods
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. method:: HttpResponse.__init__(content=b'', content_type=None, status=200, reason=None, charset=None)
|
.. method:: HttpResponse.__init__(content=b'', content_type=None, status=200, reason=None, charset=None, headers=None)
|
||||||
|
|
||||||
Instantiates an ``HttpResponse`` object with the given page content and
|
Instantiates an ``HttpResponse`` object with the given page content,
|
||||||
content type.
|
content type, and headers.
|
||||||
|
|
||||||
``content`` is most commonly an iterator, bytestring, :class:`memoryview`,
|
``content`` is most commonly an iterator, bytestring, :class:`memoryview`,
|
||||||
or string. Other types will be converted to a bytestring by encoding their
|
or string. Other types will be converted to a bytestring by encoding their
|
||||||
|
@ -829,6 +837,12 @@ Methods
|
||||||
given it will be extracted from ``content_type``, and if that
|
given it will be extracted from ``content_type``, and if that
|
||||||
is unsuccessful, the :setting:`DEFAULT_CHARSET` setting will be used.
|
is unsuccessful, the :setting:`DEFAULT_CHARSET` setting will be used.
|
||||||
|
|
||||||
|
``headers`` is a :class:`dict` of HTTP headers for the response.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``headers`` parameter was added.
|
||||||
|
|
||||||
.. method:: HttpResponse.__setitem__(header, value)
|
.. method:: HttpResponse.__setitem__(header, value)
|
||||||
|
|
||||||
Sets the given header name to the given value. Both ``header`` and
|
Sets the given header name to the given value. Both ``header`` and
|
||||||
|
|
|
@ -57,7 +57,7 @@ Attributes
|
||||||
Methods
|
Methods
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None, charset=None, using=None)
|
.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None, charset=None, using=None, headers=None)
|
||||||
|
|
||||||
Instantiates a :class:`~django.template.response.SimpleTemplateResponse`
|
Instantiates a :class:`~django.template.response.SimpleTemplateResponse`
|
||||||
object with the given template, context, content type, HTTP status, and
|
object with the given template, context, content type, HTTP status, and
|
||||||
|
@ -90,6 +90,13 @@ Methods
|
||||||
The :setting:`NAME <TEMPLATES-NAME>` of a template engine to use for
|
The :setting:`NAME <TEMPLATES-NAME>` of a template engine to use for
|
||||||
loading the template.
|
loading the template.
|
||||||
|
|
||||||
|
``headers``
|
||||||
|
A :class:`dict` of HTTP headers to add to the response.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``headers`` parameter was added.
|
||||||
|
|
||||||
.. method:: SimpleTemplateResponse.resolve_context(context)
|
.. method:: SimpleTemplateResponse.resolve_context(context)
|
||||||
|
|
||||||
Preprocesses context data that will be used for rendering a template.
|
Preprocesses context data that will be used for rendering a template.
|
||||||
|
@ -149,7 +156,7 @@ Methods
|
||||||
Methods
|
Methods
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, charset=None, using=None)
|
.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, charset=None, using=None, headers=None)
|
||||||
|
|
||||||
Instantiates a :class:`~django.template.response.TemplateResponse` object
|
Instantiates a :class:`~django.template.response.TemplateResponse` object
|
||||||
with the given request, template, context, content type, HTTP status, and
|
with the given request, template, context, content type, HTTP status, and
|
||||||
|
@ -185,6 +192,13 @@ Methods
|
||||||
The :setting:`NAME <TEMPLATES-NAME>` of a template engine to use for
|
The :setting:`NAME <TEMPLATES-NAME>` of a template engine to use for
|
||||||
loading the template.
|
loading the template.
|
||||||
|
|
||||||
|
``headers``
|
||||||
|
A :class:`dict` of HTTP headers to add to the response.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``headers`` parameter was added.
|
||||||
|
|
||||||
The rendering process
|
The rendering process
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
|
|
@ -332,6 +332,11 @@ Requests and Responses
|
||||||
Both interfaces will continue to be supported. See
|
Both interfaces will continue to be supported. See
|
||||||
:ref:`setting-header-fields` for details.
|
:ref:`setting-header-fields` for details.
|
||||||
|
|
||||||
|
* The new ``headers`` parameter of :class:`~django.http.HttpResponse`,
|
||||||
|
:class:`~django.template.response.SimpleTemplateResponse`, and
|
||||||
|
:class:`~django.template.response.TemplateResponse` allows setting response
|
||||||
|
:attr:`~django.http.HttpResponse.headers` on instantiation.
|
||||||
|
|
||||||
Security
|
Security
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -117,9 +117,10 @@ And the view::
|
||||||
|
|
||||||
def head(self, *args, **kwargs):
|
def head(self, *args, **kwargs):
|
||||||
last_book = self.get_queryset().latest('publication_date')
|
last_book = self.get_queryset().latest('publication_date')
|
||||||
response = HttpResponse()
|
response = HttpResponse(
|
||||||
# RFC 1123 date format
|
# RFC 1123 date format.
|
||||||
response.headers['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
headers={'Last-Modified': last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')},
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
If the view is accessed from a ``GET`` request, an object list is returned in
|
If the view is accessed from a ``GET`` request, an object list is returned in
|
||||||
|
|
|
@ -286,7 +286,7 @@ class QueryDictTests(SimpleTestCase):
|
||||||
QueryDict.fromkeys(0)
|
QueryDict.fromkeys(0)
|
||||||
|
|
||||||
|
|
||||||
class HttpResponseTests(unittest.TestCase):
|
class HttpResponseTests(SimpleTestCase):
|
||||||
|
|
||||||
def test_headers_type(self):
|
def test_headers_type(self):
|
||||||
r = HttpResponse()
|
r = HttpResponse()
|
||||||
|
@ -470,10 +470,31 @@ class HttpResponseTests(unittest.TestCase):
|
||||||
# del doesn't raise a KeyError on nonexistent headers.
|
# del doesn't raise a KeyError on nonexistent headers.
|
||||||
del r.headers['X-Foo']
|
del r.headers['X-Foo']
|
||||||
|
|
||||||
|
def test_instantiate_with_headers(self):
|
||||||
|
r = HttpResponse('hello', headers={'X-Foo': 'foo'})
|
||||||
|
self.assertEqual(r.headers['X-Foo'], 'foo')
|
||||||
|
self.assertEqual(r.headers['x-foo'], 'foo')
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_content_type(self):
|
||||||
r = HttpResponse('hello', content_type='application/json')
|
r = HttpResponse('hello', content_type='application/json')
|
||||||
self.assertEqual(r.headers['Content-Type'], 'application/json')
|
self.assertEqual(r.headers['Content-Type'], 'application/json')
|
||||||
|
|
||||||
|
def test_content_type_headers(self):
|
||||||
|
r = HttpResponse('hello', headers={'Content-Type': 'application/json'})
|
||||||
|
self.assertEqual(r.headers['Content-Type'], 'application/json')
|
||||||
|
|
||||||
|
def test_content_type_mutually_exclusive(self):
|
||||||
|
msg = (
|
||||||
|
"'headers' must not contain 'Content-Type' when the "
|
||||||
|
"'content_type' parameter is provided."
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
HttpResponse(
|
||||||
|
'hello',
|
||||||
|
content_type='application/json',
|
||||||
|
headers={'Content-Type': 'text/csv'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HttpResponseSubclassesTests(SimpleTestCase):
|
class HttpResponseSubclassesTests(SimpleTestCase):
|
||||||
def test_redirect(self):
|
def test_redirect(self):
|
||||||
|
|
|
@ -216,6 +216,14 @@ class SimpleTemplateResponseTest(SimpleTestCase):
|
||||||
|
|
||||||
self.assertEqual(unpickled_response.cookies['key'].value, 'value')
|
self.assertEqual(unpickled_response.cookies['key'].value, 'value')
|
||||||
|
|
||||||
|
def test_headers(self):
|
||||||
|
response = SimpleTemplateResponse(
|
||||||
|
'first/test.html',
|
||||||
|
{'value': 123, 'fn': datetime.now},
|
||||||
|
headers={'X-Foo': 'foo'},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.headers['X-Foo'], 'foo')
|
||||||
|
|
||||||
|
|
||||||
@override_settings(TEMPLATES=[{
|
@override_settings(TEMPLATES=[{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
@ -319,6 +327,15 @@ class TemplateResponseTest(SimpleTestCase):
|
||||||
unpickled_response = pickle.loads(pickled_response)
|
unpickled_response = pickle.loads(pickled_response)
|
||||||
pickle.dumps(unpickled_response)
|
pickle.dumps(unpickled_response)
|
||||||
|
|
||||||
|
def test_headers(self):
|
||||||
|
response = TemplateResponse(
|
||||||
|
self.factory.get('/'),
|
||||||
|
'first/test.html',
|
||||||
|
{'value': 123, 'fn': datetime.now},
|
||||||
|
headers={'X-Foo': 'foo'},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.headers['X-Foo'], 'foo')
|
||||||
|
|
||||||
|
|
||||||
@modify_settings(MIDDLEWARE={'append': ['template_tests.test_response.custom_urlconf_middleware']})
|
@modify_settings(MIDDLEWARE={'append': ['template_tests.test_response.custom_urlconf_middleware']})
|
||||||
@override_settings(ROOT_URLCONF='template_tests.urls')
|
@override_settings(ROOT_URLCONF='template_tests.urls')
|
||||||
|
|
Loading…
Reference in New Issue