Repository: jazzband/django-debug-toolbar

Issue:
Conflict with django_jinja 
We upgraded from django-debug-toolbar 1.4 to 1.5 and ended up with an error that seems to break compatibility with django_jinja.

We received the same error as this Stack Overflow question:
http://stackoverflow.com/questions/38569760/django-debug-toolbar-template-object-has-no-attribute-engine

Line 175 in `panels.templates.panel.py` raises:

``` python
Exception Value: 'Template' object has no attribute 'engine'
```

If you look at the asker's written solution, you'll notice that they patched line 175 by checking for the `engine` attribute on `self.templates[0]['template']` or to alternately look for a `backend` attribute (which exists and fulfills the same function in Jinja2).

This seems to have it's origin in the way that template dirs are grabbed, by pulling the engine info from an individual template:

``` python
        # Fetch context_processors/template_dirs from any template
        if self.templates:
            context_processors = self.templates[0]['context_processors']
            template_dirs = self.templates[0]['template'].engine.dirs
        else:
            context_processors = None
            template_dirs = []
```

In the following linked commit, you can see that debug_toolbar used to have a `get_template_dirs()` function that used `django.template.engine.Engine.get_default()` to get an engine and `engine.dirs` to pull in the template dirs. I haven't tested based on this commit, but I suspect that removing the call to `django.template.engine.Engine.get_default()` and pulling template dirs from an individual template caused the breaking change:
https://github.com/jazzband/django-debug-toolbar/commit/6390279a5eba1dc47fc12627204484fda67d3825#diff-f6bb86ee9199645f22f9b23d002ed707L24

The following seems to work and I would assume has a lower chance of breaking compatibility with other template engines.

``` python
from django.template.engine import Engine
engine = Engine.get_default()
engine.dirs # returns [] in my case
```

We encountered this using:

```
Django==1.10 (also tried with 1.9.9)
django_jinja==2.2.0
Jinja2==2.8
django-debug-toolbar==1.5
```



Discussion:
I get this too, you can hack it fixed by supplying your own panel class:

```
# debug.py
from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel

class TemplatesPanel(BaseTemplatesPanel):
    def generate_stats(self, *args):
        template = self.templates[0]['template']
        if not hasattr(template, 'engine') and hasattr(template, 'backend'):
            template.engine = template.backend
        return super().generate_stats(*args)
```

```
# settings.py
DEBUG_TOOLBAR_PANELS = [
    'debug_toolbar.panels.versions.VersionsPanel',
    'debug_toolbar.panels.timer.TimerPanel',
    'debug_toolbar.panels.settings.SettingsPanel',
    'debug_toolbar.panels.headers.HeadersPanel',
    'debug_toolbar.panels.request.RequestPanel',
    'debug_toolbar.panels.sql.SQLPanel',
    'debug_toolbar.panels.staticfiles.StaticFilesPanel',
    'myapp.debug.TemplatesPanel', # original broken by django-jinja, remove this whole block later
    'debug_toolbar.panels.cache.CachePanel',
    'debug_toolbar.panels.signals.SignalsPanel',
    'debug_toolbar.panels.logging.LoggingPanel',
    'debug_toolbar.panels.redirects.RedirectsPanel',
]
```

Hi guys,

Are there plans to resolve this?

There are no specific plans for the development of the debug toolbar. This will be fixed when someone writes a patch and someone else merges it.

PS: would you mind picking a better word that "guys" -- a synonym for "men" -- in order not to exclude half of humanity from the discussion? Thanks!


Resolving patch:
diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py
--- a/debug_toolbar/panels/templates/panel.py
+++ b/debug_toolbar/panels/templates/panel.py
@@ -172,7 +172,10 @@ def generate_stats(self, request, response):
         # Fetch context_processors/template_dirs from any template
         if self.templates:
             context_processors = self.templates[0]['context_processors']
-            template_dirs = self.templates[0]['template'].engine.dirs
+            template = self.templates[0]['template']
+            # django templates have the 'engine' attribute, while jinja templates use 'backend'
+            engine_backend = getattr(template, 'engine', None) or getattr(template, 'backend')
+            template_dirs = engine_backend.dirs
         else:
             context_processors = None
             template_dirs = []


Test patch:
diff --git a/tests/panels/test_redirects.py b/tests/panels/test_redirects.py
--- a/tests/panels/test_redirects.py
+++ b/tests/panels/test_redirects.py
@@ -38,7 +38,7 @@ def test_redirect(self):
 
     def test_redirect_with_broken_context_processor(self):
         TEMPLATES = copy.deepcopy(settings.TEMPLATES)
-        TEMPLATES[0]['OPTIONS']['context_processors'] = ['tests.context_processors.broken']
+        TEMPLATES[1]['OPTIONS']['context_processors'] = ['tests.context_processors.broken']
 
         with self.settings(TEMPLATES=TEMPLATES):
             redirect = HttpResponse(status=302)
diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py
--- a/tests/panels/test_template.py
+++ b/tests/panels/test_template.py
@@ -4,6 +4,7 @@
 
 from django.contrib.auth.models import User
 from django.template import Context, RequestContext, Template
+from django.test import TestCase, override_settings
 
 from ..base import BaseTestCase
 from ..models import NonAsciiRepr
@@ -83,5 +84,15 @@ def test_disabled(self):
             self.assertFalse(self.panel.enabled)
 
 
+@override_settings(DEBUG=True,
+                   DEBUG_TOOLBAR_PANELS=['debug_toolbar.panels.templates.TemplatesPanel'])
+class JinjaTemplateTestCase(TestCase):
+    def test_django_jinja2(self):
+        r = self.client.get('/regular_jinja/foobar/')
+        self.assertContains(r, 'Test for foobar (Jinja)')
+        self.assertContains(r, '<h3>Templates (1 rendered)</h3>')
+        self.assertContains(r, '<small>jinja2/basic.jinja</small>')
+
+
 def context_processor(request):
     return {'content': 'set by processor'}
diff --git a/tests/settings.py b/tests/settings.py
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -24,6 +24,7 @@
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'debug_toolbar',
+    'django_jinja',
     'tests',
 ]
 
@@ -43,6 +44,12 @@
 ROOT_URLCONF = 'tests.urls'
 
 TEMPLATES = [
+    {
+        'NAME': 'jinja2',
+        'BACKEND': 'django_jinja.backend.Jinja2',
+        'APP_DIRS': True,
+        'DIRS': [os.path.join(BASE_DIR, 'tests', 'templates', 'jinja2')],
+    },
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
         'APP_DIRS': True,
diff --git a/tests/templates/jinja2/basic.jinja b/tests/templates/jinja2/basic.jinja
new file mode 100644
--- /dev/null
+++ b/tests/templates/jinja2/basic.jinja
@@ -0,0 +1,2 @@
+{% extends 'base.html' %}
+{% block content %}Test for {{ title }} (Jinja){% endblock %}
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -152,6 +152,20 @@ def test_basic(self):
         self.assertIn("Name", table.text)
         self.assertIn("Version", table.text)
 
+    @override_settings(DEBUG_TOOLBAR_CONFIG={
+        'DISABLE_PANELS':
+            {'debug_toolbar.panels.redirects.RedirectsPanel'}
+    })
+    def test_basic_jinja(self):
+        self.selenium.get(self.live_server_url + '/regular_jinja/basic')
+        template_panel = self.selenium.find_element_by_id('TemplatesPanel')
+
+        # Click to show the template panel
+        self.selenium.find_element_by_class_name('TemplatesPanel').click()
+
+        self.assertIn('Templates (1 rendered)', template_panel.text)
+        self.assertIn('jinja2/basic.jinja', template_panel.text)
+
     @override_settings(DEBUG_TOOLBAR_CONFIG={'RESULTS_CACHE_SIZE': 0})
     def test_expired_store(self):
         self.selenium.get(self.live_server_url + '/regular/basic/')
diff --git a/tests/urls.py b/tests/urls.py
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -14,6 +14,7 @@
     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'^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),
diff --git a/tests/views.py b/tests/views.py
--- a/tests/views.py
+++ b/tests/views.py
@@ -30,3 +30,7 @@ def resolving_view(request, arg1, arg2):
 @cache_page(60)
 def cached_view(request):
     return HttpResponse()
+
+
+def regular_jinjia_view(request, title):
+    return render(request, 'jinja2/basic.jinja', {'title': title})