diff options
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | README.rst | 2 | ||||
| -rw-r--r-- | debug_toolbar/urls.py | 5 | ||||
| -rw-r--r-- | debug_toolbar/utils/__init__.py | 10 | ||||
| -rw-r--r-- | debug_toolbar/utils/tracking/db.py | 47 | ||||
| -rw-r--r-- | debug_toolbar/views.py | 26 | ||||
| -rw-r--r-- | example/urls.py | 10 | ||||
| -rw-r--r-- | runtests.py | 5 | ||||
| -rw-r--r-- | test_pgsql.py | 28 | ||||
| -rw-r--r-- | tests/tests.py | 16 | ||||
| -rw-r--r-- | tests/urls.py | 7 |
11 files changed, 130 insertions, 30 deletions
@@ -1,6 +1,10 @@ # Make file to compress and join all JS files all: compress_js compress_css +test: + pip install Django + python runtests.py + compress_js: java -jar ~/bin/yuicompressor.jar debug_toolbar/static/debug_toolbar/js/jquery.js > debug_toolbar/static/debug_toolbar/js/toolbar.min.js java -jar ~/bin/yuicompressor.jar debug_toolbar/static/debug_toolbar/js/toolbar.js >> debug_toolbar/static/debug_toolbar/js/toolbar.min.js @@ -120,7 +120,7 @@ The debug toolbar has two settings that can be set in ``settings.py``: If not set or set to None, the debug_toolbar middleware will use its built-in show_toolbar method for determining whether the toolbar should show or not. The default checks are that DEBUG must be - set to True or the IP of the request must be in INTERNAL_IPS. You can + set to True and the IP of the request must be in INTERNAL_IPS. You can provide your own method for displaying the toolbar which contains your custom logic. This method should return True or False. diff --git a/debug_toolbar/urls.py b/debug_toolbar/urls.py index 1446d99..0053ae7 100644 --- a/debug_toolbar/urls.py +++ b/debug_toolbar/urls.py @@ -4,7 +4,10 @@ URLpatterns for the debug toolbar. These should not be loaded explicitly; the debug toolbar middleware will patch this into the urlconf for the request. """ -from django.conf.urls.defaults import * +try: + from django.conf.urls import patterns, url +except ImportError: # django < 1.4 + from django.conf.urls.defaults import patterns, url _PREFIX = '__debug__' diff --git a/debug_toolbar/utils/__init__.py b/debug_toolbar/utils/__init__.py index bfb485c..2d2ff10 100644 --- a/debug_toolbar/utils/__init__.py +++ b/debug_toolbar/utils/__init__.py @@ -56,9 +56,15 @@ def render_stacktrace(trace): stacktrace = [] for frame in trace: params = map(escape, frame[0].rsplit(os.path.sep, 1) + list(frame[1:])) + params_dict = dict((unicode(idx), v) for idx, v in enumerate(params)) try: - stacktrace.append(u'<span class="path">{0}/</span><span class="file">{1}</span> in <span class="func">{3}</span>(<span class="lineno">{2}</span>)\n <span class="code">{4}</span>'.format(*params)) - except IndexError: + stacktrace.append(u'<span class="path">%(0)s/</span>' + u'<span class="file">%(1)s</span>' + u' in <span class="func">%(3)s</span>' + u'(<span class="lineno">%(2)s</span>)\n' + u' <span class="code">%(4)s</span>' + % params_dict) + except KeyError: # This frame doesn't have the expected format, so skip it and move on to the next one continue return mark_safe('\n'.join(stacktrace)) diff --git a/debug_toolbar/utils/tracking/db.py b/debug_toolbar/utils/tracking/db.py index 4d87090..0ff3359 100644 --- a/debug_toolbar/utils/tracking/db.py +++ b/debug_toolbar/utils/tracking/db.py @@ -5,13 +5,22 @@ from threading import local from django.conf import settings from django.template import Node -from django.utils import simplejson from django.utils.encoding import force_unicode, smart_str -from django.utils.hashcompat import sha_constructor from debug_toolbar.utils import ms_from_timedelta, tidy_stacktrace, \ get_template_info, get_stack from debug_toolbar.utils.compat.db import connections + +try: + import json +except ImportError: # python < 2.6 + from django.utils import simplejson as json + +try: + from hashlib import sha1 +except ImportError: # python < 2.5 + from django.utils.hashcompat import sha_constructor as sha1 + # TODO:This should be set in the toolbar loader as a default and panels should # get a copy of the toolbar object with access to its config dictionary SQL_WARNING_THRESHOLD = getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}) \ @@ -82,12 +91,25 @@ class NormalCursorWrapper(object): for key, value in params.iteritems()) return map(self._quote_expr, params) + def _decode(self, param): + try: + return force_unicode(param, strings_only=True) + except UnicodeDecodeError: + return '(encoded string)' + def execute(self, sql, params=()): - __traceback_hide__ = True start = datetime.now() try: return self.cursor.execute(sql, params) finally: + # FIXME: Sometimes connections which are not in the connections + # dict are used (for example in test database destroying). + # The code below (at least get_transaction_id(alias) needs to have + # the connection in the connections dict. It would be good to + # not have this requirement at all, but for now lets just skip + # these connections. + if self.db.alias not in connections: + return stop = datetime.now() duration = ms_from_timedelta(stop - start) enable_stacktraces = getattr(settings, @@ -98,10 +120,8 @@ class NormalCursorWrapper(object): stacktrace = [] _params = '' try: - _params = simplejson.dumps( - [force_unicode(x, strings_only=True) for x in params] - ) - except TypeError: + _params = json.dumps(map(self._decode, params)) + except Exception: pass # object not JSON serializable template_info = None @@ -119,7 +139,7 @@ class NormalCursorWrapper(object): del cur_frame alias = getattr(self.db, 'alias', 'default') - conn = connections[alias].connection + conn = self.db.connection # HACK: avoid imports if conn: engine = conn.__class__.__module__.split('.', 1)[0] @@ -134,7 +154,7 @@ class NormalCursorWrapper(object): 'duration': duration, 'raw_sql': sql, 'params': _params, - 'hash': sha_constructor(settings.SECRET_KEY \ + 'hash': sha1(settings.SECRET_KEY \ + smart_str(sql) \ + _params).hexdigest(), 'stacktrace': stacktrace, @@ -146,10 +166,17 @@ class NormalCursorWrapper(object): } if engine == 'psycopg2': + # If an erroneous query was ran on the connection, it might + # be in a state where checking isolation_level raises an + # exception. + try: + iso_level = conn.isolation_level + except conn.InternalError: + iso_level = 'unknown' params.update({ 'trans_id': self.logger.get_transaction_id(alias), 'trans_status': conn.get_transaction_status(), - 'iso_level': conn.isolation_level, + 'iso_level': iso_level, 'encoding': conn.encoding, }) diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py index be917ff..4b4ebc9 100644 --- a/debug_toolbar/views.py +++ b/debug_toolbar/views.py @@ -9,11 +9,19 @@ import django.views.static from django.conf import settings from django.http import HttpResponseBadRequest from django.shortcuts import render_to_response -from django.utils import simplejson -from django.utils.hashcompat import sha_constructor from debug_toolbar.utils.compat.db import connections +try: + import json +except ImportError: # python < 2.6 + from django.utils import simplejson as json + +try: + from hashlib import sha1 +except ImportError: # python < 2.5 + from django.utils.hashcompat import sha_constructor as sha1 + class InvalidSQLError(Exception): def __init__(self, value): @@ -37,11 +45,11 @@ def sql_select(request): sql = request.GET.get('sql', '') params = request.GET.get('params', '') alias = request.GET.get('alias', 'default') - hash = sha_constructor(settings.SECRET_KEY + sql + params).hexdigest() + hash = sha1(settings.SECRET_KEY + sql + params).hexdigest() if hash != request.GET.get('hash', ''): return HttpResponseBadRequest('Tamper alert') # SQL Tampering alert if sql.lower().strip().startswith('select'): - params = simplejson.loads(params) + params = json.loads(params) cursor = connections[alias].cursor() cursor.execute(sql, params) headers = [d[0] for d in cursor.description] @@ -72,11 +80,11 @@ def sql_explain(request): sql = request.GET.get('sql', '') params = request.GET.get('params', '') alias = request.GET.get('alias', 'default') - hash = sha_constructor(settings.SECRET_KEY + sql + params).hexdigest() + hash = sha1(settings.SECRET_KEY + sql + params).hexdigest() if hash != request.GET.get('hash', ''): return HttpResponseBadRequest('Tamper alert') # SQL Tampering alert if sql.lower().strip().startswith('select'): - params = simplejson.loads(params) + params = json.loads(params) cursor = connections[alias].cursor() conn = connections[alias].connection @@ -87,6 +95,8 @@ def sql_explain(request): # EXPLAIN QUERY PLAN dumps a more human-readable summary # See http://www.sqlite.org/lang_explain.html for details cursor.execute("EXPLAIN QUERY PLAN %s" % (sql,), params) + elif engine == "psycopg2": + cursor.execute("EXPLAIN ANALYZE %s" % (sql,), params) else: cursor.execute("EXPLAIN %s" % (sql,), params) @@ -118,11 +128,11 @@ def sql_profile(request): sql = request.GET.get('sql', '') params = request.GET.get('params', '') alias = request.GET.get('alias', 'default') - hash = sha_constructor(settings.SECRET_KEY + sql + params).hexdigest() + hash = sha1(settings.SECRET_KEY + sql + params).hexdigest() if hash != request.GET.get('hash', ''): return HttpResponseBadRequest('Tamper alert') # SQL Tampering alert if sql.lower().strip().startswith('select'): - params = simplejson.loads(params) + params = json.loads(params) cursor = connections[alias].cursor() result = None headers = None diff --git a/example/urls.py b/example/urls.py index 25acc31..d8bed27 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,15 +1,15 @@ from django.conf import settings from django.conf.urls.defaults import * from django.contrib import admin -from django.views.generic.simple import direct_to_template +from django.views.generic import TemplateView admin.autodiscover() urlpatterns = patterns('', - (r'^$', direct_to_template, {'template': 'index.html'}), - (r'^jquery/index/$', direct_to_template, {'template': 'jquery/index.html'}), - (r'^mootools/index/$', direct_to_template, {'template': 'mootools/index.html'}), - (r'^prototype/index/$', direct_to_template, {'template': 'prototype/index.html'}), + (r'^$', TemplateView.as_view(template_name='index.html')), + (r'^jquery/index/$', TemplateView.as_view(template_name='jquery/index.html')), + (r'^mootools/index/$', TemplateView.as_view(template_name='mootools/index.html')), + (r'^prototype/index/$', TemplateView.as_view(template_name='prototype/index.html')), (r'^admin/', include(admin.site.urls)), ) diff --git a/runtests.py b/runtests.py index 3432f8b..ed49824 100644 --- a/runtests.py +++ b/runtests.py @@ -1,11 +1,14 @@ #!/usr/bin/env python import sys +import os from os.path import dirname, abspath from optparse import OptionParser from django.conf import settings, global_settings -if not settings.configured: +# For convenience configure settings if they are not pre-configured or if we +# haven't been provided settings to use by environment variable. +if not settings.configured and not os.environ.get('DJANGO_SETTINGS_MODULE'): settings.configure( DATABASES={ 'default': { diff --git a/test_pgsql.py b/test_pgsql.py new file mode 100644 index 0000000..28c0178 --- /dev/null +++ b/test_pgsql.py @@ -0,0 +1,28 @@ +from django.conf import global_settings +DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + # Edit the below settings before use... + 'USER': '', + 'NAME': '', + 'HOST': '', + 'PASSWORD': '', + } +} +INSTALLED_APPS=[ + 'django.contrib.auth', + 'django.contrib.admin', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + + 'debug_toolbar', + + 'tests', +] +MIDDLEWARE_CLASSES=global_settings.MIDDLEWARE_CLASSES + ( + 'debug_toolbar.middleware.DebugToolbarMiddleware', +) +ROOT_URLCONF='' +DEBUG=False +SITE_ID=1 diff --git a/tests/tests.py b/tests/tests.py index ea2938d..f76f1ab 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,10 +1,13 @@ +from __future__ import with_statement import thread from django.conf import settings 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.template import Template, Context +from django.utils import unittest from debug_toolbar.middleware import DebugToolbarMiddleware from debug_toolbar.panels.sql import SQLDebugPanel @@ -214,6 +217,19 @@ class SQLPanelTestCase(BaseTestCase): # ensure the stacktrace is populated self.assertTrue(len(query[1]['stacktrace']) > 0) + @unittest.skipUnless(connection.vendor=='postgresql', + 'Test valid only on PostgreSQL') + def test_erroneous_query(self): + """ + Test that an error in the query isn't swallowed by the middleware. + """ + from django.db import connection + from django.db.utils import DatabaseError + try: + connection.cursor().execute("erroneous query") + except DatabaseError as e: + self.assertTrue('erroneous query' in str(e)) + def test_disable_stacktraces(self): panel = self.toolbar.get_panel(SQLDebugPanel) self.assertEquals(len(panel._queries), 0) diff --git a/tests/urls.py b/tests/urls.py index a556703..778f417 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -4,14 +4,17 @@ URLpatterns for the debug toolbar. These should not be loaded explicitly; the debug toolbar middleware will patch this into the urlconf for the request. """ -from django.conf.urls.defaults import * from django.contrib import admin +try: + from django.conf.urls import patterns, url +except ImportError: # django < 1.4 + from django.conf.urls.defaults import patterns, url admin.autodiscover() urlpatterns = patterns('', # This pattern should be last to ensure tests still work - url(r'^resolving1/(.+)/(.+)/$', 'tests.views.resolving_view', name = 'positional-resolving'), + url(r'^resolving1/(.+)/(.+)/$', 'tests.views.resolving_view', name='positional-resolving'), url(r'^resolving2/(?P<arg1>.+)/(?P<arg2>.+)/$', 'tests.views.resolving_view'), url(r'^resolving3/(.+)/$', 'tests.views.resolving_view', { 'arg2' : 'default' }), url(r'^execute_sql/$', 'tests.views.execute_sql'), |
