Skip to content

Commit 14245b5

Browse files
committed
Merge pull request #447 from aaugustin/load-panels-contents-on-demand
Load the content of panels dynamically
2 parents 0b4fc3e + b61c85f commit 14245b5

File tree

16 files changed

+208
-113
lines changed

16 files changed

+208
-113
lines changed

debug_toolbar/panels/__init__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from django.template.defaultfilters import slugify
44
from django.template.loader import render_to_string
5-
from debug_toolbar.middleware import DebugToolbarMiddleware
65

76

87
class DebugPanel(object):
@@ -11,14 +10,15 @@ class DebugPanel(object):
1110
"""
1211
# name = 'Base'
1312
# template = 'debug_toolbar/panels/base.html'
14-
has_content = False # If content returns something, set to true in subclass
13+
has_content = False # If content returns something, set to True in subclass
1514

1615
# We'll maintain a local context instance so we can expose our template
1716
# context variables to panels which need them:
1817
context = {}
1918

2019
# Panel methods
21-
def __init__(self, context={}):
20+
def __init__(self, toolbar, context={}):
21+
self.toolbar = toolbar
2222
self.context.update(context)
2323
self.slug = slugify(self.name)
2424

@@ -44,16 +44,14 @@ def content(self):
4444
return render_to_string(self.template, context)
4545

4646
def record_stats(self, stats):
47-
toolbar = DebugToolbarMiddleware.get_current()
48-
panel_stats = toolbar.stats.get(self.slug)
47+
panel_stats = self.toolbar.stats.get(self.slug)
4948
if panel_stats:
5049
panel_stats.update(stats)
5150
else:
52-
toolbar.stats[self.slug] = stats
51+
self.toolbar.stats[self.slug] = stats
5352

5453
def get_stats(self):
55-
toolbar = DebugToolbarMiddleware.get_current()
56-
return toolbar.stats.get(self.slug, {})
54+
return self.toolbar.stats.get(self.slug, {})
5755

5856
# Standard middleware methods
5957

debug_toolbar/static/debug_toolbar/js/toolbar.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@
2828
$(this).parent().removeClass('active');
2929
} else {
3030
$('.panelContent').hide(); // Hide any that are already open
31+
var inner = current.find('.djDebugPanelContent .scroll').first();
32+
if ($(inner).empty()) {
33+
var ajax_data = {
34+
data: {
35+
toolbar_id: $('#djDebug').data('toolbar-id'),
36+
panel_id: this.className
37+
},
38+
type: 'GET',
39+
url: $('#djDebug').data('render-panel-url')
40+
};
41+
$.ajax(ajax_data).done(function(data){
42+
inner.html(data);
43+
}).fail(function(xhr){
44+
var message = '<div class="djDebugPanelTitle"><a class="djDebugClose djDebugBack" href="">Back</a><h3>'+xhr.status+': '+xhr.statusText+'</h3></div>';
45+
$('#djDebugWindow').html(message).show();
46+
});
47+
}
3148
current.show();
3249
$('#djDebugToolbar li').removeClass('active');
3350
$(this).parent().addClass('active');

debug_toolbar/templates/debug_toolbar/base.html

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
//]]></script>
99
<script src="{{ STATIC_URL }}debug_toolbar/js/jquery.cookie.js"></script>
1010
<script src="{{ STATIC_URL }}debug_toolbar/js/toolbar.js"></script>
11-
<div id="djDebug" style="display:none;" dir="ltr" {{ TOOLBAR_ROOT_TAG_ATTRS|safe }}>
11+
<div id="djDebug" style="display:none;" dir="ltr"
12+
data-toolbar-id="{{ toolbar_id }}" data-render-panel-url="/__debug__/render_panel/"
13+
{{ TOOLBAR_ROOT_TAG_ATTRS|safe }}>
1214
<div style="display:none;" id="djDebugToolbar">
1315
<ul id="djDebugPanelList">
1416
{% if panels %}
@@ -22,7 +24,7 @@
2224
{% if panel.has_content and panel.enabled %}
2325
<a href="{{ panel.url|default:"#" }}" title="{{ panel.title }}" class="{{ panel.dom_id }}">
2426
{% else %}
25-
<div class="contentless{% if panel.disabled %} disabled{% endif %}">
27+
<div class="contentless{% if panel.disabled %} disabled{% endif %}">
2628
{% endif %}
2729
{{ panel.nav_title }}
2830
{% if panel.enabled %}
@@ -33,7 +35,7 @@
3335
{% if panel.has_content and panel.enabled %}
3436
</a>
3537
{% else %}
36-
</div>
38+
</div>
3739
{% endif %}
3840
</li>
3941
{% endfor %}
@@ -50,9 +52,8 @@
5052
<h3>{{ panel.title|safe }}</h3>
5153
</div>
5254
<div class="djDebugPanelContent">
53-
<div class="scroll">
54-
{{ panel.content|safe }}
55-
</div>
55+
<div class="scroll">
56+
</div>
5657
</div>
5758
</div>
5859
{% endif %}

debug_toolbar/toolbar/loader.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ def load_panels(self):
4545
"""
4646
global panel_classes
4747
for panel_class in panel_classes:
48-
panel_instance = panel_class(context=self.template_context)
49-
48+
panel_instance = panel_class(self, context=self.template_context)
5049
self._panels[panel_class] = panel_instance
5150

5251
def render_toolbar(self):
@@ -56,8 +55,8 @@ def render_toolbar(self):
5655
context = self.template_context.copy()
5756
context.update({
5857
'panels': self.panels,
58+
'toolbar_id': save_toolbar(self),
5959
})
60-
6160
return render_to_string('debug_toolbar/base.html', context)
6261

6362

@@ -101,3 +100,23 @@ def load_panel_classes():
101100
'Toolbar Panel module "%s" does not define a "%s" class' %
102101
(panel_module, panel_classname))
103102
panel_classes.append(panel_class)
103+
104+
105+
toolbar_counter = 0
106+
toolbar_maxsize = 10 # keep data for the last 10 requests
107+
toolbar_results = SortedDict()
108+
109+
110+
def save_toolbar(toolbar):
111+
global toolbar_counter, toolbar_results
112+
toolbar_counter += 1
113+
toolbar_results[toolbar_counter] = toolbar
114+
for _ in range(len(toolbar_results) - toolbar_maxsize):
115+
# When we drop support for Python 2.6 and switch to
116+
# collections.OrderedDict, use popitem(last=False).
117+
del toolbar_results[toolbar_results.keyOrder[0]]
118+
return toolbar_counter
119+
120+
121+
def get_saved_toolbar(toolbar_id):
122+
return toolbar_results.get(toolbar_id)

debug_toolbar/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
_PREFIX = '__debug__'
1313

1414
urlpatterns = patterns('debug_toolbar.views', # noqa
15+
url(r'^%s/render_panel/$' % _PREFIX, 'render_panel', name='render_panel'),
1516
url(r'^%s/sql_select/$' % _PREFIX, 'sql_select', name='sql_select'),
1617
url(r'^%s/sql_explain/$' % _PREFIX, 'sql_explain', name='sql_explain'),
1718
url(r'^%s/sql_profile/$' % _PREFIX, 'sql_profile', name='sql_profile'),

debug_toolbar/views.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,30 @@
66

77
from __future__ import unicode_literals
88

9-
from django.http import HttpResponseBadRequest
9+
from django.http import HttpResponse, HttpResponseBadRequest
1010
from django.shortcuts import render
11+
from django.utils.html import escape
12+
from django.utils.translation import ugettext as _
1113
from django.views.decorators.csrf import csrf_exempt
1214

1315
from debug_toolbar.forms import SQLSelectForm
16+
from debug_toolbar.toolbar.loader import get_saved_toolbar
17+
18+
19+
def render_panel(request):
20+
"""Render the contents of a panel"""
21+
toolbar = get_saved_toolbar(int(request.GET['toolbar_id']))
22+
if toolbar is None:
23+
content = _("Data for this panel isn't available anymore. "
24+
"Please reload the page and retry.")
25+
content = "<p>%s</p>" % escape(content)
26+
else:
27+
panel_id = request.GET['panel_id']
28+
for panel in toolbar.panels:
29+
if panel.dom_id() == panel_id:
30+
content = panel.content()
31+
break
32+
return HttpResponse(content)
1433

1534

1635
@csrf_exempt

tests/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Refresh the debug toolbar's configuration when overriding settings.
22

33
from debug_toolbar.utils.settings import CONFIG, CONFIG_DEFAULTS
4+
from debug_toolbar.toolbar.loader import load_panel_classes, panel_classes # noqa
5+
46
from django.dispatch import receiver
57
from django.test.signals import setting_changed
68

@@ -10,3 +12,11 @@ def update_toolbar_config(**kwargs):
1012
if kwargs['setting'] == 'DEBUG_TOOLBAR_CONFIG':
1113
CONFIG.update(CONFIG_DEFAULTS)
1214
CONFIG.update(kwargs['value'] or {})
15+
16+
17+
@receiver(setting_changed)
18+
def update_toolbar_panels(**kwargs):
19+
if kwargs['setting'] == 'DEBUG_TOOLBAR_PANELS':
20+
global panel_classes
21+
panel_classes = [] # noqa
22+
load_panel_classes()

tests/panels/test_logger.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,26 @@
1010

1111
class LoggingPanelTestCase(BaseTestCase):
1212

13+
def setUp(self):
14+
super(LoggingPanelTestCase, self).setUp()
15+
self.panel = self.toolbar.get_panel(LoggingPanel)
16+
self.logger = logging.getLogger(__name__)
17+
1318
def test_happy_case(self):
14-
logger = logging.getLogger(__name__)
15-
logger.info('Nothing to see here, move along!')
19+
self.logger.info('Nothing to see here, move along!')
1620

17-
logging_panel = self.toolbar.get_panel(LoggingPanel)
18-
logging_panel.process_response(None, None)
19-
records = logging_panel.get_stats()['records']
21+
self.panel.process_response(self.request, self.response)
22+
records = self.panel.get_stats()['records']
2023

2124
self.assertEqual(1, len(records))
2225
self.assertEqual('Nothing to see here, move along!',
2326
records[0]['message'])
2427

2528
def test_formatting(self):
26-
logger = logging.getLogger(__name__)
27-
logger.info('There are %d %s', 5, 'apples')
29+
self.logger.info('There are %d %s', 5, 'apples')
2830

29-
logging_panel = self.toolbar.get_panel(LoggingPanel)
30-
logging_panel.process_response(None, None)
31-
records = logging_panel.get_stats()['records']
31+
self.panel.process_response(self.request, self.response)
32+
records = self.panel.get_stats()['records']
3233

3334
self.assertEqual(1, len(records))
3435
self.assertEqual('There are 5 apples',
@@ -39,14 +40,11 @@ class BadClass(object):
3940
def __str__(self):
4041
raise Exception('Please not stringify me!')
4142

42-
logger = logging.getLogger(__name__)
43-
4443
# should not raise exception, but fail silently
45-
logger.debug('This class is misbehaving: %s', BadClass())
44+
self.logger.debug('This class is misbehaving: %s', BadClass())
4645

47-
logging_panel = self.toolbar.get_panel(LoggingPanel)
48-
logging_panel.process_response(None, None)
49-
records = logging_panel.get_stats()['records']
46+
self.panel.process_response(self.request, self.response)
47+
records = self.panel.get_stats()['records']
5048

5149
self.assertEqual(1, len(records))
5250
self.assertEqual(MESSAGE_IF_STRING_REPRESENTATION_INVALID,

tests/panels/test_profiling.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import unicode_literals
2+
3+
from django.contrib.auth.models import User
4+
from django.db import IntegrityError, transaction
5+
from django.test import TestCase
6+
from django.test.utils import override_settings
7+
8+
9+
@override_settings(DEBUG=True,
10+
DEBUG_TOOLBAR_PANELS=['debug_toolbar.panels.profiling.ProfilingDebugPanel'])
11+
class ProfilingPanelIntegrationTestCase(TestCase):
12+
13+
urls = 'tests.urls'
14+
15+
def test_view_executed_once(self):
16+
17+
self.assertEqual(User.objects.count(), 0)
18+
19+
response = self.client.get('/new_user/')
20+
self.assertContains(response, 'Profiling')
21+
self.assertEqual(User.objects.count(), 1)
22+
23+
with self.assertRaises(IntegrityError):
24+
if hasattr(transaction, 'atomic'): # Django >= 1.6
25+
with transaction.atomic():
26+
response = self.client.get('/new_user/')
27+
else:
28+
response = self.client.get('/new_user/')
29+
self.assertEqual(User.objects.count(), 1)

tests/panels/test_request_vars.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding: utf-8
2+
3+
from __future__ import unicode_literals
4+
5+
from django.utils import six
6+
7+
from debug_toolbar.panels.request_vars import RequestVarsDebugPanel
8+
9+
from ..base import BaseTestCase
10+
11+
12+
class RequestVarsDebugPanelTestCase(BaseTestCase):
13+
14+
def setUp(self):
15+
super(RequestVarsDebugPanelTestCase, self).setUp()
16+
self.panel = self.toolbar.get_panel(RequestVarsDebugPanel)
17+
18+
def test_non_ascii_session(self):
19+
self.request.session = {'où': 'où'}
20+
if not six.PY3:
21+
self.request.session['là'.encode('utf-8')] = 'là'.encode('utf-8')
22+
self.panel.process_request(self.request)
23+
self.panel.process_response(self.request, self.response)
24+
content = self.panel.content()
25+
if six.PY3:
26+
self.assertIn('où', content)
27+
else:
28+
self.assertIn('o\\xf9', content)
29+
self.assertIn('l\\xc3\\xa0', content)
30+
31+
def test_object_with_non_ascii_repr_in_request_vars(self):
32+
self.request.path = '/non_ascii_request/'
33+
self.panel.process_request(self.request)
34+
self.panel.process_response(self.request, self.response)
35+
self.assertIn('nôt åscíì', self.panel.content())

0 commit comments

Comments
 (0)