diff options
| -rw-r--r-- | README.rst | 6 | ||||
| -rw-r--r-- | debug_toolbar/forms.py | 6 | ||||
| -rw-r--r-- | debug_toolbar/panels/logger.py | 9 | ||||
| -rw-r--r-- | debug_toolbar/panels/template.py | 3 | ||||
| -rw-r--r-- | debug_toolbar/templates/debug_toolbar/base.html | 6 | ||||
| -rw-r--r-- | debug_toolbar/templates/debug_toolbar/panels/sql.html | 6 | ||||
| -rw-r--r-- | debug_toolbar/utils/__init__.py | 6 | ||||
| -rw-r--r-- | debug_toolbar/utils/sql.py | 2 | ||||
| -rw-r--r-- | example/django-debug-toolbar.png | bin | 0 -> 96122 bytes | |||
| -rw-r--r-- | tests/templates/basic.html | 7 | ||||
| -rw-r--r-- | tests/tests.py | 101 | ||||
| -rw-r--r-- | tests/urls.py | 2 | ||||
| -rw-r--r-- | tests/views.py | 23 |
13 files changed, 149 insertions, 28 deletions
@@ -10,6 +10,12 @@ The Django Debug Toolbar is a configurable set of panels that display various debug information about the current request/response and when clicked, display more details about the panel's content. +Here's a screenshot: + +.. image:: https://raw.github.com/django-debug-toolbar/django-debug-toolbar/master/example/django-debug-toolbar.png + :width: 908 + :height: 557 + Currently, the following panels have been written and are working: - Django version diff --git a/debug_toolbar/forms.py b/debug_toolbar/forms.py index 60d339c..cc05f30 100644 --- a/debug_toolbar/forms.py +++ b/debug_toolbar/forms.py @@ -6,7 +6,7 @@ import hashlib from django import forms from django.conf import settings from django.db import connections -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_text from django.utils.functional import cached_property from django.core.exceptions import ValidationError @@ -76,8 +76,8 @@ class SQLSelectForm(forms.Form): return reformat_sql(self.cursor.db.ops.last_executed_query(self.cursor, sql, params)) def make_hash(self, data): - params = settings.SECRET_KEY + data['sql'] + data['params'] - return hashlib.sha1(smart_bytes(params)).hexdigest() + params = force_text(settings.SECRET_KEY) + data['sql'] + data['params'] + return hashlib.sha1(params.encode('utf-8')).hexdigest() @property def connection(self): diff --git a/debug_toolbar/panels/logger.py b/debug_toolbar/panels/logger.py index c6c1bd1..2c749ce 100644 --- a/debug_toolbar/panels/logger.py +++ b/debug_toolbar/panels/logger.py @@ -9,6 +9,8 @@ except ImportError: from django.utils.translation import ungettext, ugettext_lazy as _ from debug_toolbar.panels import DebugPanel +MESSAGE_IF_STRING_REPRESENTATION_INVALID = '[Could not get log message]' + class LogCollector(object): def __init__(self): @@ -49,8 +51,13 @@ class ThreadTrackingHandler(logging.Handler): self.collector = collector def emit(self, record): + try: + message = record.getMessage() + except Exception: + message = MESSAGE_IF_STRING_REPRESENTATION_INVALID + record = { - 'message': record.getMessage(), + 'message': message, 'time': datetime.datetime.fromtimestamp(record.created), 'level': record.levelname, 'file': record.pathname, diff --git a/debug_toolbar/panels/template.py b/debug_toolbar/panels/template.py index 8ecf5d6..f2f813c 100644 --- a/debug_toolbar/panels/template.py +++ b/debug_toolbar/panels/template.py @@ -7,6 +7,7 @@ from django import http from django.conf import settings from django.template.context import get_standard_processors from django.test.signals import template_rendered +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.db.models.query import QuerySet, RawQuerySet from debug_toolbar.panels import DebugPanel @@ -92,7 +93,7 @@ class TemplateDebugPanel(DebugPanel): context_list.append(pformat(temp_layer)) except UnicodeEncodeError: pass - kwargs['context'] = context_list + kwargs['context'] = [force_text(item) for item in context_list] self.templates.append(kwargs) def nav_title(self): diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index a790bd1..7b42c84 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -2,13 +2,13 @@ <style type="text/css"> @media print { #djDebug {display:none;}} </style> -<link rel="stylesheet" href="{{ STATIC_URL }}debug_toolbar/css/toolbar.min.css" type="text/css"> +<link rel="stylesheet" href="{{ STATIC_URL }}debug_toolbar/css/toolbar.min.css" type="text/css" /> <script type="text/javascript" src="{{ STATIC_URL }}debug_toolbar/js/toolbar.min.js"></script> <div id="djDebug" style="display:none;" dir="ltr"> <div style="display:none;" id="djDebugToolbar"> <ul id="djDebugPanelList"> {% if panels %} - <li><a id="djHideToolBarButton" href="#" title="{% trans "Hide Toolbar" %}">{% trans "Hide" %} »</a></li> + <li><a id="djHideToolBarButton" href="#" title="{% trans "Hide Toolbar" %}">{% trans "Hide" %} »</a></li> {% else %} <li id="djDebugButton">DEBUG</li> {% endif %} @@ -33,7 +33,7 @@ </ul> </div> <div style="display:none;" id="djDebugToolbarHandle"> - <a title="{% trans "Show Toolbar" %}" id="djShowToolBarButton" href="#">«</a> + <a title="{% trans "Show Toolbar" %}" id="djShowToolBarButton" href="#">«</a> </div> {% for panel in panels %} {% if panel.has_content %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql.html b/debug_toolbar/templates/debug_toolbar/panels/sql.html index 36201fc..064413c 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql.html @@ -4,7 +4,7 @@ <ul class="stats"> {% for alias, info in databases %} <li> - <strong class="label"><span style="background-color: rgb({{ info.rgb_color|join:", " }})" class="color"> </span> {{ alias }}</strong> + <strong class="label"><span style="background-color: rgb({{ info.rgb_color|join:", " }})" class="color"> </span> {{ alias }}</strong> <span class="info">{{ info.time_spent|floatformat:"2" }} ms ({% blocktrans count info.num_queries as num %}{{ num }} query{% plural %}{{ num }} queries{% endblocktrans %})</span> </li> {% endfor %} @@ -15,7 +15,7 @@ <table> <thead> <tr> - <th class="color"> </th> + <th class="color"> </th> <th class="query" colspan="2">{% trans 'Query' %}</th> <th class="timeline">{% trans 'Timeline' %}</th> <th class="time">{% trans 'Time (ms)' %}</th> @@ -25,7 +25,7 @@ <tbody> {% for query in queries %} <tr class="djDebugHoverable {% cycle 'djDebugOdd' 'djDebugEven' %}{% if query.is_slow %} djDebugRowWarning{% endif %}{% if query.starts_trans %} djDebugStartTransaction{% endif %}{% if query.ends_trans %} djDebugEndTransaction{% endif %}{% if query.in_trans %} djDebugInTransaction{% endif %}" id="sqlMain_{{ forloop.counter }}"> - <td class="color"><span style="background-color: rgb({{ query.rgb_color|join:", " }});"> </span></td> + <td class="color"><span style="background-color: rgb({{ query.rgb_color|join:", " }});"> </span></td> <td class="toggle"> <a class="djToggleSwitch" data-toggle-name="sqlMain" data-toggle-id="{{ forloop.counter }}" data-toggle-open="+" data-toggle-close="-" href="javascript:void(0)">+</a> </td> diff --git a/debug_toolbar/utils/__init__.py b/debug_toolbar/utils/__init__.py index 297c901..75f3024 100644 --- a/debug_toolbar/utils/__init__.py +++ b/debug_toolbar/utils/__init__.py @@ -7,6 +7,7 @@ import sys from django.conf import settings from django.views.debug import linebreak_iter +from django.utils.encoding import force_text from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils import six @@ -39,10 +40,7 @@ def tidy_stacktrace(stack): continue if socketserver_path in s_path: continue - if not text: - text = '' - else: - text = (''.join(text)).strip() + text = (''.join(force_text(t) for t in text)).strip() if text else '' trace.append((path, line_no, func_name, text)) return trace diff --git a/debug_toolbar/utils/sql.py b/debug_toolbar/utils/sql.py index 4e0d70d..2810699 100644 --- a/debug_toolbar/utils/sql.py +++ b/debug_toolbar/utils/sql.py @@ -29,5 +29,5 @@ def reformat_sql(sql): def swap_fields(sql): - return re.sub('SELECT</strong> (.*?) <strong>FROM', 'SELECT</strong> <a class="djDebugUncollapsed djDebugToggle" href="#">•••</a> ' + + return re.sub('SELECT</strong> (.*?) <strong>FROM', 'SELECT</strong> <a class="djDebugUncollapsed djDebugToggle" href="#">•••</a> ' + '<a class="djDebugCollapsed djDebugToggle" href="#">\g<1></a> <strong>FROM', sql) diff --git a/example/django-debug-toolbar.png b/example/django-debug-toolbar.png Binary files differnew file mode 100644 index 0000000..bd097c2 --- /dev/null +++ b/example/django-debug-toolbar.png diff --git a/tests/templates/basic.html b/tests/templates/basic.html new file mode 100644 index 0000000..a0ce0d4 --- /dev/null +++ b/tests/templates/basic.html @@ -0,0 +1,7 @@ +<html> + <head> + <title>{{ title }}</title> + </head> + <body> + </body> +</html> diff --git a/tests/tests.py b/tests/tests.py index 6883428..86622fc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals +import logging import threading +from xml.etree import ElementTree as ET import django from django.conf import settings @@ -10,11 +12,14 @@ from django.contrib.auth.models import User from django.db import connection from django.http import HttpResponse from django.test import TestCase, RequestFactory +from django.test.utils import override_settings from django.template import Template, Context from django.utils import six from django.utils import unittest from debug_toolbar.middleware import DebugToolbarMiddleware +from debug_toolbar.panels.logger import (LoggingPanel, + MESSAGE_IF_STRING_REPRESENTATION_INVALID) from debug_toolbar.panels.sql import SQLDebugPanel from debug_toolbar.panels.request_vars import RequestVarsDebugPanel from debug_toolbar.panels.template import TemplateDebugPanel @@ -26,6 +31,7 @@ rf = RequestFactory() class BaseTestCase(TestCase): + def setUp(self): request = rf.get('/') response = HttpResponse() @@ -40,6 +46,7 @@ class BaseTestCase(TestCase): class DebugToolbarTestCase(BaseTestCase): + urls = 'tests.urls' def test_middleware(self): @@ -163,21 +170,39 @@ class DebugToolbarTestCase(BaseTestCase): self.assertEqual(stats['view_kwargs'], 'None') self.assertEqual(stats['view_func'], '<no view>') + +@override_settings(DEBUG=True, INTERNAL_IPS=['127.0.0.1']) +class DebugToolbarIntegrationTestCase(TestCase): + + urls = 'tests.urls' + + @override_settings(DEFAULT_CHARSET='iso-8859-1') def test_non_utf8_charset(self): - with self.settings(DEBUG=True, - DEFAULT_CHARSET='iso-8859-1', - INTERNAL_IPS=['127.0.0.1']): + response = self.client.get('/regular/ASCII/') + self.assertContains(response, 'ASCII') # template + self.assertContains(response, 'djDebug') # toolbar + + response = self.client.get('/regular/LÀTÍN/') + self.assertContains(response, 'LÀTÍN') # template + self.assertContains(response, 'djDebug') # toolbar + + def test_non_ascii_session(self): + response = self.client.get('/set_session/') + self.assertContains(response, 'où') + if not six.PY3: + self.assertContains(response, 'là') - response = self.client.get('/regular/ASCII/') - self.assertContains(response, 'ASCII') # template - self.assertContains(response, 'djDebug') # toolbar + def test_object_with_non_ascii_repr_in_context(self): + response = self.client.get('/non_ascii_context/') + self.assertContains(response, 'nôt åscíì') - response = self.client.get('/regular/LÀTÍN/') - self.assertContains(response, 'LÀTÍN') # template - self.assertContains(response, 'djDebug') # toolbar + def test_xml_validation(self): + response = self.client.get('/regular/XML/') + ET.fromstring(response.content) # shouldn't raise ParseError class DebugToolbarNameFromObjectTest(BaseTestCase): + def test_func(self): def x(): return 1 @@ -196,6 +221,7 @@ class DebugToolbarNameFromObjectTest(BaseTestCase): class SQLPanelTestCase(BaseTestCase): + def test_recording(self): panel = self.toolbar.get_panel(SQLDebugPanel) self.assertEqual(len(panel._queries), 0) @@ -213,6 +239,18 @@ class SQLPanelTestCase(BaseTestCase): # ensure the stacktrace is populated self.assertTrue(len(query[1]['stacktrace']) > 0) + def test_non_ascii_query(self): + panel = self.toolbar.get_panel(SQLDebugPanel) + self.assertEqual(len(panel._queries), 0) + + # non-ASCII query + list(User.objects.extra(where=["username = 'café'"])) + self.assertEqual(len(panel._queries), 1) + + # non-ASCII parameters + list(User.objects.filter(username='café')) + self.assertEqual(len(panel._queries), 2) + @unittest.skipUnless(connection.vendor=='postgresql', 'Test valid only on PostgreSQL') def test_erroneous_query(self): @@ -246,6 +284,7 @@ class SQLPanelTestCase(BaseTestCase): class TemplatePanelTestCase(BaseTestCase): + def test_queryset_hook(self): template_panel = self.toolbar.get_panel(TemplateDebugPanel) sql_panel = self.toolbar.get_panel(SQLDebugPanel) @@ -263,3 +302,47 @@ class TemplatePanelTestCase(BaseTestCase): ctx = template_panel.templates[0]['context'][base_ctx_idx] self.assertIn('<<queryset of auth.User>>', ctx) self.assertIn('<<triggers database query>>', ctx) + + +class LoggingPanelTestCase(BaseTestCase): + def test_happy_case(self): + logger = logging.getLogger(__name__) + logger.info('Nothing to see here, move along!') + + logging_panel = self.toolbar.get_panel(LoggingPanel) + logging_panel.process_response(None, None) + records = logging_panel.get_stats()['records'] + + self.assertEqual(1, len(records)) + self.assertEqual('Nothing to see here, move along!', + records[0]['message']) + + def test_formatting(self): + logger = logging.getLogger(__name__) + logger.info('There are %d %s', 5, 'apples') + + logging_panel = self.toolbar.get_panel(LoggingPanel) + logging_panel.process_response(None, None) + records = logging_panel.get_stats()['records'] + + self.assertEqual(1, len(records)) + self.assertEqual('There are 5 apples', + records[0]['message']) + + def test_failing_formatting(self): + class BadClass(object): + def __str__(self): + raise Exception('Please not stringify me!') + + logger = logging.getLogger(__name__) + + # should not raise exception, but fail silently + logger.debug('This class is misbehaving: %s', BadClass()) + + logging_panel = self.toolbar.get_panel(LoggingPanel) + logging_panel.process_response(None, None) + records = logging_panel.get_stats()['records'] + + self.assertEqual(1, len(records)) + self.assertEqual(MESSAGE_IF_STRING_REPRESENTATION_INVALID, + records[0]['message']) diff --git a/tests/urls.py b/tests/urls.py index 4fb51c9..d9ecb8c 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -13,9 +13,11 @@ from django.contrib import admin admin.autodiscover() urlpatterns = patterns('tests.views', + url(r'^set_session/$', 'set_session'), url(r'^resolving1/(.+)/(.+)/$', 'resolving_view', name='positional-resolving'), url(r'^resolving2/(?P<arg1>.+)/(?P<arg2>.+)/$', 'resolving_view'), url(r'^resolving3/(.+)/$', 'resolving_view', { 'arg2' : 'default' }), url(r'^regular/(?P<title>.*)/$', 'regular_view'), + url(r'^non_ascii_context/$', 'non_ascii_context'), url(r'^execute_sql/$', 'execute_sql'), ) diff --git a/tests/views.py b/tests/views.py index f756023..5bafc97 100644 --- a/tests/views.py +++ b/tests/views.py @@ -1,7 +1,11 @@ +# coding: utf-8 + from __future__ import unicode_literals from django.contrib.auth.models import User from django.http import HttpResponse +from django.shortcuts import render +from django.utils import six def execute_sql(request): @@ -9,11 +13,24 @@ def execute_sql(request): return HttpResponse() -def regular_view(request, title='Test'): - content = '<html><head><title>%s</title><body></body></html>' % title - return HttpResponse(content) +def non_ascii_context(request): + class NonAsciiRepr(object): + def __repr__(self): + return 'nôt åscíì' if six.PY3 else 'nôt åscíì'.encode('utf-8') + return render(request, 'basic.html', {'title': NonAsciiRepr()}) + + +def regular_view(request, title): + return render(request, 'basic.html', {'title': title}) def resolving_view(request, arg1, arg2): # see test_url_resolving in tests.py return HttpResponse() + + +def set_session(request): + request.session['où'] = 'où' + if not six.PY3: + request.session['là'.encode('utf-8')] = 'là'.encode('utf-8') + return render(request, 'basic.html') |
