diff options
| -rw-r--r-- | README.rst | 19 | ||||
| -rw-r--r-- | debug_toolbar/media/toolbar.css | 158 | ||||
| -rw-r--r-- | debug_toolbar/media/toolbar.js | 2 | ||||
| -rw-r--r-- | debug_toolbar/middleware.py | 9 | ||||
| -rw-r--r-- | debug_toolbar/panels/__init__.py | 20 | ||||
| -rw-r--r-- | debug_toolbar/panels/cache.py | 3 | ||||
| -rw-r--r-- | debug_toolbar/panels/headers.py | 7 | ||||
| -rw-r--r-- | debug_toolbar/panels/request_vars.py | 3 | ||||
| -rw-r--r-- | debug_toolbar/panels/sql.py | 26 | ||||
| -rw-r--r-- | debug_toolbar/panels/template.py | 13 | ||||
| -rw-r--r-- | debug_toolbar/panels/timer.py | 10 | ||||
| -rw-r--r-- | debug_toolbar/templates/debug_toolbar/panels/cache.html | 4 | ||||
| -rw-r--r-- | debug_toolbar/templates/debug_toolbar/panels/sql.html | 10 | ||||
| -rw-r--r-- | debug_toolbar/templates/debug_toolbar/panels/templates.html | 22 | ||||
| -rw-r--r-- | debug_toolbar/toolbar/loader.py | 8 | ||||
| -rw-r--r-- | debug_toolbar/views.py | 13 | 
16 files changed, 263 insertions, 64 deletions
@@ -3,20 +3,18 @@ Django Debug Toolbar  ====================  The Django Debug Toolbar is a configurable set of panels that display various -debug information about the current request/response.  It is a small toolbar -that, when activated, situates itself in the top-right location of the browser -window.  When particular panels are clicked, more details about that panel's -content are displayed. +debug information about the current request/response and when clicked, display  +more details about the panel's content.  Currently, the following panels have been written and are working:  - Django version -- SQL queries including time to execute  - Request timer  - Common HTTP headers -- GET/POST variable display +- GET/POST/cookie/session variable display +- SQL queries including time to execute and links to EXPLAIN each query  - Cache stats -- Templates used and template directories +- Templates and context used, and their template paths  If you have ideas for other panels please let us know. @@ -71,3 +69,10 @@ TODO  - Panel idea: Show some commonly used settings from settings.py  - Panel idea: AJAX call to show cprofile data similar to the ?prof idea  - CSS Stylings +- Restructure panels to popular context that pushes up to the toolbar +- Consider moving panel to the bottom of the HTML document, which would allow us +  to use common reset.css for consistent toolbar styling without affecting +  anything else. +- Make the trigger whether to display the toolbar configurable with options such +  as if: DEBUG is true, IP is in INTERNAL_IPS, authenticated user is_superuser, +  etc. diff --git a/debug_toolbar/media/toolbar.css b/debug_toolbar/media/toolbar.css index 7d2ad82..43ac588 100644 --- a/debug_toolbar/media/toolbar.css +++ b/debug_toolbar/media/toolbar.css @@ -1,16 +1,142 @@ -#djDebugToolbar {height:30px; background:orange; color:#000; z-index:100000000; border:1px solid #06171D; border-width:1px 0;} -#djDebugToolbar ul {margin:0; padding:0 10px; list-style:none;} -#djDebugToolbar li {display:inline; width:auto; position:relative; float:none; margin:0 10px 0 0; padding:0; height:20px; line-height:30px; padding:8px 10px 8px 0; border-right: 1px solid #06171D;} -#djDebugToolbar #djDebugButton {color:red; font-weight:bold;} -#djDebug * {margin:0; padding:0; float:none; position:static; } -#djDebug .panelContent {display:none; position:absolute; margin:0; padding:5px; top:35px; width:auto; left:5px; right:5px; bottom:5px; background:white; color:black; border:1px solid black; z-index:1000000; overflow:auto;} -#djDebug .panelContent p {padding: 0 5px;} -#djDebug .panelContent p, #djDebug .panelContent table, #djDebug .panelContent ul, #djDebug .panelContent dl {margin:5px 0;} -#djDebug .close {float:right; font-weight:bold;} -#djDebug .panelContent dt, #djDebug .panelContent dd {display: block;} -#djDebug .panelContent dd {margin-left:10px;} -#djDebug th, #djDebug td {padding: 5px;} -#djDebug .row1 td {background:#fff;} -#djDebug .row2 td {background:#ddd;} -#djDebugWindow {z-index:2000000;} -#djDebugWindow .back {float:right; font-weight:bold;} +#djDebugToolbar { +    background: #326342; +    height: 30px; +    z-index: 100000000; +    border-bottom: 2px solid #234f32; +} + +#djDebugToolbar ul { +    margin: 0; +    padding: 0; +    list-style: none; +} + +#djDebugToolbar li { +    border-left: 1px solid #487858; +    color: #fff; +    display: inline; +    font-size: 11px; +    font-weight: bold; +    float: none; +    height: 20px; +    margin: 0; +    padding: 0; +    line-height: 30px; +    padding: 8px 10px 9px; +    position: relative; +    width: auto; +} + +#djDebugToolbar li:hover { +    background: #487858; +} + +#djDebugToolbar li:hover a { +    color: #fff; +} + +#djDebugToolbar li:last-child { +    border-right: 1px solid #487858; +} + +#djDebugToolbar #djDebugButton { +    color: #92ef3f; +} + +#djDebug * { +    color: #fff; +    text-shadow: 0 0 2px #333; +    float: none; +    margin: 0; +    padding: 0; +    position: static; +} + +#djDebug a { +    color: #f7c757; +} + +#djDebug a:hover { +    color: #fff; +} + +#djDebug .panelContent { +    background: #2a5738; +    border-bottom: 2px solid #234f32; +    border-top: 2px solid #487858; +    display: none; +    position: absolute; +    margin: 0; +    padding: 10px; +    top: 35px; +    width: auto; +    left: 0px; +    right: 0px; +    bottom: 5px; +    color: black; +    z-index: 1000000; +    overflow: auto; +} + +#djDebug h3 { +    border-bottom: 1px solid #40684c; +    color: #92ef3f; +    padding: 0 0 5px; +} + +#djDebug .panelContent p { +    padding: 0 5px; +} + +#djDebug .panelContent p, #djDebug .panelContent table, #djDebug .panelContent ol, #djDebug .panelContent ul, #djDebug .panelContent dl { +    margin: 5px 0 15px; +} + +#djDebug .panelContent table { +    width: 100%; +} + +#djDebug .close { +    float: right; +    font-weight: bold; +} + +#djDebug .panelContent dt, #djDebug .panelContent dd { +    display: block; +} + +#djDebug .panelContent dd { +    margin-left: 10px; +} + +#djDebug th { +    font-weight: bold; +    text-align: left; +    padding: 5px; +} + +#djDebug td { +    padding: 5px; +} + +#djDebug .row1 td { +    background: #40684c; +} + +#djDebug .row2 td { +    background: #356042; +} +#djDebug .syntax  { color: #ffffff } +#djDebug .syntax .err { color: #ffffff } /* Error */ +#djDebug .syntax .g { color: #ffffff } /* Generic */ +#djDebug .syntax .k { color: #F7C757; font-weight: bold } /* Keyword */ +#djDebug .syntax .o { color: #ffffff } /* Operator */ +#djDebug .syntax .n { color: #ffffff } /* Name */ +#djDebug .syntax .mi { color: #92ef3f; font-weight: bold } /* Literal.Number.Integer */ +#djDebug .syntax .l { color: #ffffff } /* Literal */ +#djDebug .syntax .x { color: #ffffff } /* Other */ +#djDebug .syntax .p { color: #ffffff } /* Punctuation */ +#djDebug .syntax .m { color: #92ef3f; font-weight: bold } /* Literal.Number */ +#djDebug .syntax .s { color: #0086d2 } /* Literal.String */ +#djDebug .syntax .w { color: #888888 } /* Text.Whitespace */ +#djDebug .syntax .il { color: #92ef3f; font-weight: bold } /* Literal.Number.Integer.Long */ diff --git a/debug_toolbar/media/toolbar.js b/debug_toolbar/media/toolbar.js index 19a6abf..455a2fc 100644 --- a/debug_toolbar/media/toolbar.js +++ b/debug_toolbar/media/toolbar.js @@ -1,3 +1,4 @@ +var _$ = window.$;  jQuery.noConflict();  jQuery(function($) {  	$.djDebug = function(data, klass) { @@ -62,3 +63,4 @@ jQuery(function($) {  jQuery(function() {  	jQuery.djDebug();  }); +$ = _$; diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py index c17c424..fefab18 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -54,13 +54,20 @@ class DebugToolbarMiddleware(object):          if self.show_toolbar(request):              self.debug_toolbar = DebugToolbar(request) -            self.debug_toolbar.load_panels() +            for panel in self.debug_toolbar.panels: +                panel.process_request(request)          return None +    def process_view(self, request, view_func, view_args, view_kwargs): +        for panel in self.debug_toolbar.panels: +            panel.process_view(request, view_func, view_args, view_kwargs) +      def process_response(self, request, response):          if response.status_code != 200:              return response +        for panel in self.debug_toolbar.panels: +            panel.process_response(request, response)          if self.show_toolbar(request):              if response['Content-Type'].split(';')[0] in _HTML_TYPES:                  script_loc = request.META.get('SCRIPT_NAME', '') diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index 29cec02..54b3318 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -6,9 +6,10 @@ class DebugPanel(object):      """      # name = Base      has_content = False # If content returns something, set to true in subclass -     -    def __init__(self, request): -        self.request = request + +    # Panel methods +    def __init__(self): +        pass      def dom_id(self):          return 'djDebug%sPanel' % (self.name.replace(' ', '')) @@ -20,6 +21,15 @@ class DebugPanel(object):          raise NotImplementedError      def content(self): -        # TODO: This is a bit flaky in that panel.content() returns a string  -        # that gets inserted into the toolbar HTML template.          raise NotImplementedError + +    # Standard middleware methods +    def process_request(self, request): +        pass + +    def process_view(self, request, view_func, view_args, view_kwargs): +        pass + +    def process_response(self, request, response): +        pass + diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py index 263b62b..613d4d9 100644 --- a/debug_toolbar/panels/cache.py +++ b/debug_toolbar/panels/cache.py @@ -78,7 +78,7 @@ class CacheDebugPanel(DebugPanel):      name = 'Cache'      has_content = True -    def __init__(self, request): +    def __init__(self):          # This is hackish but to prevent threading issues is somewhat needed          if isinstance(cache.cache, CacheStatTracker):              cache.cache.reset() @@ -86,7 +86,6 @@ class CacheDebugPanel(DebugPanel):          else:              self.cache = CacheStatTracker(cache.cache)              cache.cache = self.cache -        super(CacheDebugPanel, self).__init__(request)      def title(self):          return 'Cache: %.2fms' % self.cache.total_time diff --git a/debug_toolbar/panels/headers.py b/debug_toolbar/panels/headers.py index 3ab4a95..213198a 100644 --- a/debug_toolbar/panels/headers.py +++ b/debug_toolbar/panels/headers.py @@ -37,8 +37,13 @@ class HeaderDebugPanel(DebugPanel):      def url(self):          return '' +    def process_request(self, request): +        self.headers = dict( +            [(k, request.META[k]) for k in self.header_filter if k in request.META] +        ) +      def content(self):          context = { -            'headers': dict([(k, self.request.META[k]) for k in self.header_filter if k in self.request.META]), +            'headers': self.headers          }          return render_to_string('debug_toolbar/panels/headers.html', context)
\ No newline at end of file diff --git a/debug_toolbar/panels/request_vars.py b/debug_toolbar/panels/request_vars.py index a137393..88a7204 100644 --- a/debug_toolbar/panels/request_vars.py +++ b/debug_toolbar/panels/request_vars.py @@ -14,6 +14,9 @@ class RequestVarsDebugPanel(DebugPanel):      def url(self):          return '' +    def process_request(self, request): +        self.request = request +      def content(self):          context = {              'get': [(k, self.request.GET.getlist(k)) for k in self.request.GET.iterkeys()], diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py index fd08395..22d65a7 100644 --- a/debug_toolbar/panels/sql.py +++ b/debug_toolbar/panels/sql.py @@ -1,9 +1,11 @@  import time  from debug_toolbar.panels import DebugPanel +from django.conf import settings  from django.db import connection  from django.db.backends import util  from django.template.loader import render_to_string  from django.utils import simplejson +from django.utils.hashcompat import sha_constructor  class DatabaseStatTracker(util.CursorDebugWrapper):      """ @@ -16,32 +18,38 @@ class DatabaseStatTracker(util.CursorDebugWrapper):              return self.cursor.execute(sql, params)          finally:              stop = time.time() +            _params = None +            try: +                _params = simplejson.dumps(params) +            except TypeError: +                pass # object not JSON serializable              # We keep `sql` to maintain backwards compatibility              self.db.queries.append({                  'sql': self.db.ops.last_executed_query(self.cursor, sql, params),                  'time': stop - start,                  'raw_sql': sql, -                'params': simplejson.dumps(params), +                'params': _params, +                'hash': sha_constructor(settings.SECRET_KEY + sql + _params).hexdigest(),              })  util.CursorDebugWrapper = DatabaseStatTracker  class SQLDebugPanel(DebugPanel):      """ -    Panel that displays information about the SQL queries run while processing the request. +    Panel that displays information about the SQL queries run while processing +    the request.      """      name = 'SQL'      has_content = True -     -    def __init__(self, request): -        super(SQLDebugPanel, self).__init__(request) + +    def __init__(self):          self._offset = len(connection.queries)          self._sql_time = 0      def title(self):          self._sql_time = sum(map(lambda q: float(q['time']) * 1000, connection.queries)) -        return '%d SQL Quer%s (%.2fms)' % ( -            len(connection.queries), -            len(connection.queries) == 1 and 'y' or 'ies',  +        return '%d SQL %s (%.2fms)' % ( +            len(connection.queries),  +            (len(connection.queries) == 1) and 'query' or 'queries',              self._sql_time          ) @@ -72,7 +80,7 @@ def reformat_sql(sql):          from pygments import highlight          from pygments.lexers import SqlLexer          from pygments.formatters import HtmlFormatter -        sql = highlight(sql, SqlLexer(), HtmlFormatter(noclasses=True)) +        sql = highlight(sql, SqlLexer(), HtmlFormatter())      except ImportError:          pass      return sql diff --git a/debug_toolbar/panels/template.py b/debug_toolbar/panels/template.py index e08f220..022a41f 100644 --- a/debug_toolbar/panels/template.py +++ b/debug_toolbar/panels/template.py @@ -1,12 +1,12 @@ +from pprint import pformat  from django.conf import settings  from django.core.signals import request_started  from django.dispatch import Signal +from django.template.context import get_standard_processors  from django.template.loader import render_to_string  from django.test.signals import template_rendered  from debug_toolbar.panels import DebugPanel -from pprint import pformat -  # Code taken and adapted from Simon Willison and Django Snippets:  # http://www.djangosnippets.org/snippets/766/ @@ -32,8 +32,7 @@ class TemplateDebugPanel(DebugPanel):      name = 'Template'      has_content = True -    def __init__(self, request): -        super(TemplateDebugPanel, self).__init__(request) +    def __init__(self):          self.templates = []          template_rendered.connect(self._storeTemplateInfo) @@ -46,6 +45,11 @@ class TemplateDebugPanel(DebugPanel):      def url(self):          return '' +    def process_request(self, request): +        self.context_processors = dict( +            [("%s.%s" % (k.__module__, k.__name__), pformat(k(request))) for k in get_standard_processors()] +        ) +      def content(self):          template_context = []          for i, d in enumerate(self.templates): @@ -67,5 +71,6 @@ class TemplateDebugPanel(DebugPanel):          context = {              'templates': template_context,              'template_dirs': settings.TEMPLATE_DIRS, +            'context_processors': self.context_processors,          }          return render_to_string('debug_toolbar/panels/templates.html', context) diff --git a/debug_toolbar/panels/timer.py b/debug_toolbar/panels/timer.py index d520191..ea8ed4a 100644 --- a/debug_toolbar/panels/timer.py +++ b/debug_toolbar/panels/timer.py @@ -3,16 +3,18 @@ from debug_toolbar.panels import DebugPanel  class TimerDebugPanel(DebugPanel):      """ -    Panel that displays the time a response took. +    Panel that displays the time a response took in milliseconds.      """      name = 'Timer' -    def __init__(self, request): -        super(TimerDebugPanel, self).__init__(request) +    def process_request(self, request):          self._start_time = time.time() +    def process_response(self, request, response): +        self.total_time = (time.time() - self._start_time) * 1000 +      def title(self): -        return 'Time: %0.2fms' % ((time.time() - self._start_time) * 1000) +        return 'Time: %0.2fms' % (self.total_time)      def url(self):          return '' diff --git a/debug_toolbar/templates/debug_toolbar/panels/cache.html b/debug_toolbar/templates/debug_toolbar/panels/cache.html index 21ea173..3142783 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/cache.html +++ b/debug_toolbar/templates/debug_toolbar/panels/cache.html @@ -31,6 +31,7 @@  		<td>{{ cache.get_many }}</td>  	</tr>  </table> +{% if cache.calls %}  <h3>Breakdown</h3>  <table>  	<thead> @@ -51,4 +52,5 @@  			</tr>  		{% endfor %}  	</tbody> -</table>
\ No newline at end of file +</table> +{% endif %} diff --git a/debug_toolbar/templates/debug_toolbar/panels/sql.html b/debug_toolbar/templates/debug_toolbar/panels/sql.html index 4663047..e218f22 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/sql.html +++ b/debug_toolbar/templates/debug_toolbar/panels/sql.html @@ -11,9 +11,13 @@  		{% for query in queries %}  			<tr class="{% cycle 'row1' 'row2' %}">  				<td>{{ query.time|floatformat:"4" }}</td> -				<td><a class="remoteCall" href="/__debug__/sql_explain/?sql={{ query.raw_sql|urlencode }}¶ms={{ query.params|urlencode }}&time={{ query.time|floatformat:"4"|urlencode }}">EXPLAIN</a></td> -				<td>{{ query.sql|safe }}</td> +				<td> +				{% if query.params %} +					<a class="remoteCall" href="/__debug__/sql_explain/?sql={{ query.raw_sql|urlencode }}¶ms={{ query.params|urlencode }}&time={{ query.time|floatformat:"4"|urlencode }}&hash={{ query.hash }}">EXPLAIN</a> +				{% endif %} +				</td> +				<td class="syntax">{{ query.sql|safe }}</td>  			</tr>  		{% endfor %}  	</tbody> -</table>
\ No newline at end of file +</table> diff --git a/debug_toolbar/templates/debug_toolbar/panels/templates.html b/debug_toolbar/templates/debug_toolbar/panels/templates.html index 575d507..a92657b 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/templates.html +++ b/debug_toolbar/templates/debug_toolbar/panels/templates.html @@ -1,4 +1,4 @@ -<h3>Template path(s):</h3> +<h3>Template path{{ template_dirs|length|pluralize }}:</h3>  {% if template_dirs %}  	<ol>  	{% for template in template_dirs %} @@ -6,9 +6,9 @@  	{% endfor %}  	</ol>  {% else %} -	None +	<p>None</p>  {% endif %} -<h3>Templates Used</h3> +<h3>Template{{ templates|length|pluralize }}</h3>  {% if templates %}  <dl>  {% for template in templates %} @@ -21,5 +21,19 @@  {% endfor %}  </dl>  {% else %} -	None +	<p>None</p> +{% endif %} +<h3>Context processor{{ context_processors|length|pluralize }}</h3> +{% if context_processors %} +<dl> +{% for key, value in context_processors.iteritems %} +	<dt><strong>{{ key|escape }}</strong></dt> +	<dd> +		<div class="djTemplateShowContextDiv"><a class="djTemplateShowContext">Toggle Context</a></div> +		<div class="djTemplateHideContextDiv" style="display:none;"><pre>{{ value|escape }}</pre></div> +	</dd> +{% endfor %} +</dl> +{% else %} +	<p>None</p>  {% endif %} diff --git a/debug_toolbar/toolbar/loader.py b/debug_toolbar/toolbar/loader.py index f45cbda..0ac9096 100644 --- a/debug_toolbar/toolbar/loader.py +++ b/debug_toolbar/toolbar/loader.py @@ -8,8 +8,6 @@ class DebugToolbar(object):      def __init__(self, request):          self.request = request          self.panels = [] -        self.panel_list = [] -        self.content_list = []          # Override this tuple by copying to settings.py as `DEBUG_TOOLBAR_PANELS`          self.default_panels = (              'debug_toolbar.panels.version.VersionDebugPanel', @@ -20,7 +18,8 @@ class DebugToolbar(object):              'debug_toolbar.panels.cache.CacheDebugPanel',              'debug_toolbar.panels.template.TemplateDebugPanel',          ) -     +        self.load_panels() +      def load_panels(self):          """          Populate debug panels @@ -48,8 +47,9 @@ class DebugToolbar(object):                  raise exceptions.ImproperlyConfigured, 'Toolbar Panel module "%s" does not define a "%s" class' % (panel_module, panel_classname)              try: -                panel_instance = panel_class(self.request) +                panel_instance = panel_class()              except: +                print panel_class                  raise # Bubble up problem loading panel              self.panels.append(panel_instance) diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py index 97739e6..b67a70b 100644 --- a/debug_toolbar/views.py +++ b/debug_toolbar/views.py @@ -8,8 +8,10 @@ import os  import django.views.static  from django.conf import settings  from django.db import connection +from django.http import HttpResponse  from django.shortcuts import render_to_response  from django.utils import simplejson +from django.utils.hashcompat import sha_constructor  def debug_media(request, path):      root = getattr(settings, 'DEBUG_TOOLBAR_MEDIA_ROOT', None) @@ -21,16 +23,21 @@ def debug_media(request, path):  def sql_explain(request):      """      Returns the output of the SQL EXPLAIN on the given query. -     +      Expected GET variables: -        sql: urlencoded sql with position arguments +        sql: urlencoded sql with positional arguments          params: JSON encoded parameter values          time: time for SQL to execute passed in from toolbar just for redisplay +        hash: the hash of (secret + sql + params) for tamper checking      """      from debug_toolbar.panels.sql import reformat_sql      sql = request.GET.get('sql', '') +    params = request.GET.get('params', '') +    hash = sha_constructor(settings.SECRET_KEY + sql + params).hexdigest() +    if hash != request.GET.get('hash', ''): +        return HttpResponse('<h3>Tamper alert</h3>') # SQL Tampering alert      if sql.lower().startswith('select'): -        params = simplejson.loads(request.GET.get('params', '')) +        params = simplejson.loads(params)          cursor = connection.cursor()          cursor.execute("EXPLAIN %s" % (sql,), params)          headers = [d[0] for d in cursor.description]  | 
