Browse Source

Fixed #32002 -- Added headers parameter to HttpResponse and subclasses.

Tom Carrick 4 years ago
parent
commit
dcb69043d0

+ 12 - 6
django/http/response.py

@@ -97,8 +97,18 @@ class HttpResponseBase:
 
     status_code = 200
 
-    def __init__(self, content_type=None, status=None, reason=None, charset=None):
-        self.headers = ResponseHeaders({})
+    def __init__(self, content_type=None, status=None, reason=None, charset=None, headers=None):
+        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 = []
         # This parameter is set by the handler. It's necessary to preserve the
         # historical behavior of request_finished.
@@ -114,10 +124,6 @@ class HttpResponseBase:
             if not 100 <= self.status_code <= 599:
                 raise ValueError('HTTP status code must be an integer from 100 to 599.')
         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
     def reason_phrase(self):

+ 4 - 4
django/template/response.py

@@ -11,7 +11,7 @@ class SimpleTemplateResponse(HttpResponse):
     rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
 
     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
         # 'context', but those names are reserved as part of the test Client
         # 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
         # with rendered template so we always pass empty string in order to
         # 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
         # into a final response.
@@ -139,6 +139,6 @@ class TemplateResponse(SimpleTemplateResponse):
     rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request']
 
     def __init__(self, request, template, context=None, content_type=None,
-                 status=None, charset=None, using=None):
-        super().__init__(template, context, content_type, status, charset, using)
+                 status=None, charset=None, using=None, headers=None):
+        super().__init__(template, context, content_type, status, charset, using, headers=headers)
         self._request = request

+ 13 - 8
docs/howto/outputting-csv.txt

@@ -20,8 +20,10 @@ Here's an example::
 
     def some_view(request):
         # Create the HttpResponse object with the appropriate CSV header.
-        response = HttpResponse(content_type='text/csv')
-        response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
+        response = HttpResponse(
+            content_type='text/csv',
+            headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
+        )
 
         writer = csv.writer(response)
         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))
         pseudo_buffer = Echo()
         writer = csv.writer(pseudo_buffer)
-        response = StreamingHttpResponse((writer.writerow(row) for row in rows),
-                                         content_type="text/csv")
-        response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
-        return response
+        return StreamingHttpResponse(
+            (writer.writerow(row) for row in rows),
+            content_type="text/csv",
+            headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'},
+        )
 
 Using the template system
 =========================
@@ -108,8 +111,10 @@ Here's an example, which generates the same CSV file as above::
 
     def some_view(request):
         # Create the HttpResponse object with the appropriate CSV header.
-        response = HttpResponse(content_type='text/csv')
-        response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
+        response = HttpResponse(
+            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
         # some other source.

+ 22 - 8
docs/ref/request-response.txt

@@ -724,6 +724,10 @@ by ``HttpResponse``.
 When using this interface, unlike a dictionary, ``del`` doesn't raise
 ``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
 to use the :func:`~django.utils.cache.patch_cache_control` and
 :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 ability to set headers on instantiation was added.
+
 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
-``content_type`` argument and set the ``Content-Disposition`` header. For example,
-this is how you might return a Microsoft Excel spreadsheet::
+To tell the browser to treat the response as a file attachment, set the
+``Content-Type`` and ``Content-Disposition`` headers. For example, this is how
+you might return a Microsoft Excel spreadsheet::
 
-    >>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel')
-    >>> response.headers['Content-Disposition'] = 'attachment; filename="foo.xls"'
+    >>> response = HttpResponse(my_data, headers={
+    ...     'Content-Type': 'application/vnd.ms-excel',
+    ...     'Content-Disposition': 'attachment; filename="foo.xls"',
+    ... })
 
 There's nothing Django-specific about the ``Content-Disposition`` header, but
 it's easy to forget the syntax, so we've included it here.
@@ -802,10 +810,10 @@ Attributes
 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
-    content type.
+    Instantiates an ``HttpResponse`` object with the given page content,
+    content type, and headers.
 
     ``content`` is most commonly an iterator, bytestring, :class:`memoryview`,
     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
     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)
 
     Sets the given header name to the given value. Both ``header`` and

+ 16 - 2
docs/ref/template-response.txt

@@ -57,7 +57,7 @@ Attributes
 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`
     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
         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)
 
     Preprocesses context data that will be used for rendering a template.
@@ -149,7 +156,7 @@ 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
     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
         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
 =====================
 

+ 5 - 0
docs/releases/3.2.txt

@@ -332,6 +332,11 @@ Requests and Responses
   Both interfaces will continue to be supported. See
   :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
 ~~~~~~~~
 

+ 4 - 3
docs/topics/class-based-views/index.txt

@@ -117,9 +117,10 @@ And the view::
 
         def head(self, *args, **kwargs):
             last_book = self.get_queryset().latest('publication_date')
-            response = HttpResponse()
-            # RFC 1123 date format
-            response.headers['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
+            response = HttpResponse(
+                # RFC 1123 date format.
+                headers={'Last-Modified': last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')},
+            )
             return response
 
 If the view is accessed from a ``GET`` request, an object list is returned in

+ 22 - 1
tests/httpwrappers/tests.py

@@ -286,7 +286,7 @@ class QueryDictTests(SimpleTestCase):
             QueryDict.fromkeys(0)
 
 
-class HttpResponseTests(unittest.TestCase):
+class HttpResponseTests(SimpleTestCase):
 
     def test_headers_type(self):
         r = HttpResponse()
@@ -470,10 +470,31 @@ class HttpResponseTests(unittest.TestCase):
         # del doesn't raise a KeyError on nonexistent headers.
         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):
         r = HttpResponse('hello', 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):
     def test_redirect(self):

+ 17 - 0
tests/template_tests/test_response.py

@@ -216,6 +216,14 @@ class SimpleTemplateResponseTest(SimpleTestCase):
 
         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=[{
     'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -319,6 +327,15 @@ class TemplateResponseTest(SimpleTestCase):
         unpickled_response = pickle.loads(pickled_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']})
 @override_settings(ROOT_URLCONF='template_tests.urls')