Repository: jazzband/django-debug-toolbar

Issue:
Intercept redirects will cause ContentNotRenderedError with django-debug-toolbar 1.9 
I tried newest django-debug-toolbar, 1.9.
And when I enabled Intercept Redirects and accessed a view returns 302, It returned `ContentNotRenderedError`:

```
ContentNotRenderedError at /admin/

The response content must be rendered before it can be accessed.

Request Method: 	GET
Request URL: 	http://127.0.0.1:8000/admin/
Django Version: 	1.11.7
Exception Type: 	ContentNotRenderedError
Exception Value: 	

The response content must be rendered before it can be accessed.

Exception Location: 	/tmp/venv/lib/python3.6/site-packages/django/template/response.py in content, line 129
Python Executable: 	/tmp/venv/bin/python
Python Version: 	3.6.3
Python Path: 	

['/tmp/myproj',
 '/usr/lib/python36.zip',
 '/usr/lib/python3.6',
 '/usr/lib/python3.6/lib-dynload',
 '/tmp/venv/lib/python3.6/site-packages']

Server time: 	Wed, 15 Nov 2017 11:17:34 +0000

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/admin/

Django Version: 1.11.7
Python Version: 3.6.3
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'debug_toolbar']
Installed Middleware:
['debug_toolbar.middleware.DebugToolbarMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback:

File "/tmp/venv/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
  41.             response = get_response(request)

File "/tmp/venv/lib/python3.6/site-packages/django/utils/deprecation.py" in __call__
  142.             response = self.process_response(request, response)

File "/tmp/venv/lib/python3.6/site-packages/debug_toolbar/middleware.py" in process_response
  125.         content = force_text(response.content, encoding=response.charset)

File "/tmp/venv/lib/python3.6/site-packages/django/template/response.py" in content
  129.                 'The response content must be rendered before it can be accessed.'

Exception Type: ContentNotRenderedError at /admin/
Exception Value: The response content must be rendered before it can be accessed.
```

## Reason

This error caused by `content = force_text(response.content, encoding=response.charset)`, accessing `response.content`.

In this case the `response` was returned by `RedirectsPanel`.
https://github.com/jazzband/django-debug-toolbar/blob/master/debug_toolbar/panels/redirects.py#L9

The panel returns `SimpleTemplateResponse` object. this class will raise `ContentNotRenderedError` when `response.content` was accessed before calling `response.render()`.
like `content = force_text(response.content, ...)`.

This error wasn't on django-debug-toolbar 1.8.
And I think this change caused it
https://github.com/jazzband/django-debug-toolbar/commit/c369384945cfc5a4e65fa5f153e231a7ec06b11b

## Fix idea

Simply, we can avoid this error by adding `response.render()` in `RedirectsPanel`,
folloing here https://github.com/jazzband/django-debug-toolbar/blob/master/debug_toolbar/panels/redirects.py#L27

like this.

```python
                # Using SimpleTemplateResponse avoids running global context processors.
                response = SimpleTemplateResponse('debug_toolbar/redirect.html', context)
                response.cookies = cookies
                response.render()
```

Or any other nice plan? (or this issue was not correct?)
I don't know well about codes of django-debug-toolbar, so I didn't create PR yet.
Please ask and tell me anything.



Discussion:
I don't have the time right now to think about the best way to fix this, but maybe @jdufresne has an idea? If not I'll get back to this in the next few days.

This is a regression introduced by 64ef4a3bbb4d9d5d38933ebb8b3bdea38ccee4dd, and unfortunately there was no test to detect this :-/
> Simply, we can avoid this error by adding response.render() in RedirectsPanel

I believe this would still be a problem for `DebugToolbarMiddleware` for handling any other `SimpleTemplateResponse`/`TemplateResponse`. These types of responses could be returned by any view or middleware, not just django-debug-toolbar. It looks like `DebugToolbarMiddleware` never had support for delayed rendering. I'll add this with tests. I _think_ the correct approach is to render the content just before manipulating it:

```py
        # If the response supports deferred rendering and hasn't been rendered
        # yet, then ensure that it does get rendered before proceeding further.
        if hasattr(response, 'render') and callable(response.render):
            response.render()
        # Insert the toolbar in the response.
        content = force_text(response.content, encoding=response.charset)
```

Resolving patch:
diff --git a/debug_toolbar/panels/redirects.py b/debug_toolbar/panels/redirects.py
--- a/debug_toolbar/panels/redirects.py
+++ b/debug_toolbar/panels/redirects.py
@@ -25,4 +25,5 @@ def process_response(self, request, response):
                 # Using SimpleTemplateResponse avoids running global context processors.
                 response = SimpleTemplateResponse('debug_toolbar/redirect.html', context)
                 response.cookies = cookies
+                response.render()
         return response


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -227,6 +227,17 @@ def test_data_store_id_not_rendered_when_none(self):
         self.assertIn(b'id="djDebug"', response.content)
         self.assertNotIn(b'data-store-id', response.content)
 
+    def test_view_returns_template_response(self):
+        response = self.client.get('/template_response/basic/')
+        self.assertEqual(response.status_code, 200)
+
+    @override_settings(DEBUG_TOOLBAR_CONFIG={'DISABLE_PANELS': set()})
+    def test_incercept_redirects(self):
+        response = self.client.get('/redirect/')
+        self.assertEqual(response.status_code, 200)
+        # Link to LOCATION header.
+        self.assertIn(b'href="/regular/redirect/"', response.content)
+
 
 @unittest.skipIf(webdriver is None, "selenium isn't installed")
 @unittest.skipUnless('DJANGO_SELENIUM_TESTS' in os.environ, "selenium tests not requested")
diff --git a/tests/urls.py b/tests/urls.py
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -14,10 +14,12 @@
     url(r'^resolving2/(?P<arg1>.+)/(?P<arg2>.+)/$', views.resolving_view),
     url(r'^resolving3/(.+)/$', views.resolving_view, {'arg2': 'default'}),
     url(r'^regular/(?P<title>.*)/$', views.regular_view),
+    url(r'^template_response/(?P<title>.*)/$', views.template_response_view),
     url(r'^regular_jinja/(?P<title>.*)/$', views.regular_jinjia_view),
     url(r'^non_ascii_request/$', views.regular_view, {'title': NonAsciiRepr()}),
     url(r'^new_user/$', views.new_user),
     url(r'^execute_sql/$', views.execute_sql),
     url(r'^cached_view/$', views.cached_view),
+    url(r'^redirect/$', views.redirect_view),
     url(r'^__debug__/', include(debug_toolbar.urls)),
 ]
diff --git a/tests/views.py b/tests/views.py
--- a/tests/views.py
+++ b/tests/views.py
@@ -3,7 +3,8 @@
 from __future__ import absolute_import, unicode_literals
 
 from django.contrib.auth.models import User
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.template.response import TemplateResponse
 from django.shortcuts import render
 from django.views.decorators.cache import cache_page
 
@@ -17,6 +18,10 @@ def regular_view(request, title):
     return render(request, 'basic.html', {'title': title})
 
 
+def template_response_view(request, title):
+    return TemplateResponse(request, 'basic.html', {'title': title})
+
+
 def new_user(request, username='joe'):
     User.objects.create_user(username=username)
     return render(request, 'basic.html', {'title': 'new user'})
@@ -39,3 +44,7 @@ def regular_jinjia_view(request, title):
 def listcomp_view(request):
     lst = [i for i in range(50000) if i % 2 == 0]
     return render(request, 'basic.html', {'title': 'List comprehension', 'lst': lst})
+
+
+def redirect_view(request):
+    return HttpResponseRedirect('/regular/redirect/')