Added debug_toolbar app to project.
authorMarek Stępniowski <marek@stepniowski.com>
Tue, 9 Sep 2008 13:50:45 +0000 (15:50 +0200)
committerMarek Stępniowski <marek@stepniowski.com>
Tue, 9 Sep 2008 13:50:45 +0000 (15:50 +0200)
24 files changed:
apps/debug_toolbar/__init__.py [new file with mode: 0644]
apps/debug_toolbar/middleware.py [new file with mode: 0644]
apps/debug_toolbar/models.py [new file with mode: 0644]
apps/debug_toolbar/panels/__init__.py [new file with mode: 0644]
apps/debug_toolbar/panels/cache.py [new file with mode: 0644]
apps/debug_toolbar/panels/headers.py [new file with mode: 0644]
apps/debug_toolbar/panels/http_vars.py [new file with mode: 0644]
apps/debug_toolbar/panels/profiler.py [new file with mode: 0644]
apps/debug_toolbar/panels/sql.py [new file with mode: 0644]
apps/debug_toolbar/panels/templates.py [new file with mode: 0644]
apps/debug_toolbar/panels/timer.py [new file with mode: 0644]
apps/debug_toolbar/panels/version.py [new file with mode: 0644]
apps/debug_toolbar/settings.py [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/base.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/cache.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/headers.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/http_vars.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/profiler.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/sql.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html [new file with mode: 0644]
apps/debug_toolbar/templates/debug_toolbar/panels/templates.html [new file with mode: 0644]
apps/debug_toolbar/toolbar/__init__.py [new file with mode: 0644]
apps/debug_toolbar/toolbar/loader.py [new file with mode: 0644]
wolnelektury/settings.py

diff --git a/apps/debug_toolbar/__init__.py b/apps/debug_toolbar/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/debug_toolbar/middleware.py b/apps/debug_toolbar/middleware.py
new file mode 100644 (file)
index 0000000..2ef7f1b
--- /dev/null
@@ -0,0 +1,61 @@
+"""
+Debug Toolbar middleware
+"""
+import re
+from django.conf import settings
+from django.utils.encoding import smart_str
+from debug_toolbar.toolbar.loader import DebugToolbar
+
+_HTML_TYPES = ('text/html', 'application/xhtml+xml')
+_END_HEAD_RE = re.compile(r'</head>', re.IGNORECASE)
+_END_BODY_RE = re.compile(r'<body[^<]*>', re.IGNORECASE)
+
+class DebugToolbarMiddleware(object):
+    """
+    Middleware to set up Debug Toolbar on incoming request and render toolbar
+    on outgoing response.
+    """
+    def __init__(self):
+        self.debug_toolbar = None
+
+    def show_toolbar(self, request):
+        if not settings.DEBUG:
+            return False
+        if not request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS:
+            return False
+        return True
+
+    def process_request(self, request):
+        if self.show_toolbar(request):
+            self.debug_toolbar = DebugToolbar(request)
+            self.debug_toolbar.load_panels()
+            debug = request.GET.get('djDebug')
+            # kinda ugly, needs changes to the loader to optimize
+            for panel in self.debug_toolbar.panels:
+                response = panel.process_request(request)
+                if not response:
+                    if debug == panel.name:
+                        response = panel.process_ajax(request)
+                if response:
+                    response.skip_debug_response = True
+                    return response
+
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        if self.show_toolbar(request):
+            for panel in self.debug_toolbar.panels:
+                cb = panel.process_view(request, callback, callback_args, callback_kwargs)
+                if cb:
+                    callback = cb
+            return callback
+
+    def process_response(self, request, response):
+        if self.show_toolbar(request) and not getattr(response, 'skip_debug_response', False):
+            if response['Content-Type'].split(';')[0] in _HTML_TYPES and not request.is_ajax():
+                # Saving this here in case we ever need to inject into <head>
+                #response.content = _END_HEAD_RE.sub(smart_str(self.debug_toolbar.render_styles() + "%s" % match.group()), response.content)
+                for panel in self.debug_toolbar.panels:
+                    nr = panel.process_response(request, response)
+                    # Incase someone forgets `return response`
+                    if nr: response = nr
+                response.content = _END_BODY_RE.sub(smart_str('<body>' + self.debug_toolbar.render_toolbar()), response.content)
+        return response
diff --git a/apps/debug_toolbar/models.py b/apps/debug_toolbar/models.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/debug_toolbar/panels/__init__.py b/apps/debug_toolbar/panels/__init__.py
new file mode 100644 (file)
index 0000000..704ae1e
--- /dev/null
@@ -0,0 +1,33 @@
+"""Base DebugPanel class"""
+
+class DebugPanel(object):
+    """
+    Base class for debug panels.
+    """
+    # name = Base
+    
+    def __init__(self, request):
+        self.request = request
+
+    def process_request(self, request):
+        return None
+    
+    def process_response(self, request, response):
+        return response
+    
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        return None
+
+    def dom_id(self):
+        return 'djDebug%sPanel' % (self.name.replace(' ', ''))
+
+    def title(self):
+        raise NotImplementedError
+
+    def url(self):
+        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
diff --git a/apps/debug_toolbar/panels/cache.py b/apps/debug_toolbar/panels/cache.py
new file mode 100644 (file)
index 0000000..4fef6bf
--- /dev/null
@@ -0,0 +1,102 @@
+from debug_toolbar.panels import DebugPanel
+try: from cStringIO import StringIO
+except ImportError: import StringIO
+from django.core import cache
+from django.core.cache.backends.base import BaseCache
+import time
+import inspect
+import os.path
+from django.template.loader import render_to_string
+
+class CacheStatTracker(BaseCache):
+    """A small class used to track cache calls."""
+    def __init__(self, cache):
+        self.cache = cache
+        self.reset()
+
+    def reset(self):
+        self.calls = []
+        self.hits = 0
+        self.misses = 0
+        self.sets = 0
+        self.gets = 0
+        self.get_many = 0
+        self.deletes = 0
+        self.total_time = 0
+
+    def _get_func_info(self):
+        stack = inspect.stack()[2]
+        return (os.path.basename(stack[1]), stack[2], stack[3], stack[4])
+    
+    def get(self, key, default=None):
+        t = time.time()
+        value = self.cache.get(key, default)
+        this_time = time.time()-t
+        self.total_time += this_time*1000
+        if value is None:
+            self.misses += 1
+        else:
+            self.hits += 1
+        self.gets += 1
+        self.calls.append((this_time, 'get', (key,), self._get_func_info()))
+        return value
+
+    def set(self, key, value, timeout=None):
+        t = time.time()
+        self.cache.set(key, value, timeout)
+        this_time = time.time()-t
+        self.total_time += this_time*1000
+        self.sets += 1
+        self.calls.append((this_time, 'set', (key, value, timeout), self._get_func_info()))
+
+    def delete(self, key):
+        t = time.time()
+        self.instance.delete(key, value)
+        this_time = time.time()-t
+        self.total_time += this_time*1000
+        self.deletes += 1
+        self.calls.append((this_time, 'delete', (key,), self._get_func_info()))
+
+    def get_many(self, keys):
+        t = time.time()
+        results = self.cache.get_many(keys)
+        this_time = time.time()-t
+        self.total_time += this_time*1000
+        self.get_many += 1
+        for key, value in results.iteritems():
+            if value is None:
+                self.misses += 1
+            else:
+                self.hits += 1
+        self.calls.append((this_time, 'get_many', (keys,), self._get_func_info()))
+
+class CacheDebugPanel(DebugPanel):
+    """
+    Panel that displays the cache statistics.
+    """
+    name = 'Cache'
+
+    def __init__(self, request):
+        # This is hackish but to prevent threading issues
+        # is somewhat needed
+        if isinstance(cache.cache, CacheStatTracker):
+            cache.cache.reset()
+            self.cache = cache.cache
+        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
+
+    def url(self):
+        return ''
+
+    def content(self):
+        context = dict(
+            cache_calls = len(self.cache.calls),
+            cache_time = self.cache.total_time,
+            cache = self.cache,
+        )
+        return render_to_string('debug_toolbar/panels/cache.html', context)
\ No newline at end of file
diff --git a/apps/debug_toolbar/panels/headers.py b/apps/debug_toolbar/panels/headers.py
new file mode 100644 (file)
index 0000000..3912687
--- /dev/null
@@ -0,0 +1,42 @@
+from debug_toolbar.panels import DebugPanel
+from django.template.loader import render_to_string
+
+class HeaderDebugPanel(DebugPanel):
+    """
+    A panel to display HTTP headers.
+    """
+    name = 'Header'
+    # List of headers we want to display
+    header_filter = [
+        'CONTENT_TYPE',
+        'HTTP_ACCEPT',
+        'HTTP_ACCEPT_CHARSET',
+        'HTTP_ACCEPT_ENCODING',
+        'HTTP_ACCEPT_LANGUAGE',
+        'HTTP_CACHE_CONTROL',
+        'HTTP_CONNECTION',
+        'HTTP_HOST',
+        'HTTP_KEEP_ALIVE',
+        'HTTP_REFERER',
+        'HTTP_USER_AGENT',
+        'QUERY_STRING',
+        'REMOTE_ADDR',
+        'REMOTE_HOST',
+        'REQUEST_METHOD',
+        'SCRIPT_NAME',
+        'SERVER_NAME',
+        'SERVER_PORT',
+        'SERVER_PROTOCOL',
+        'SERVER_SOFTWARE',
+    ]
+    def title(self):
+        return 'HTTP Headers'
+
+    def url(self):
+        return ''
+
+    def content(self):
+        context = {
+            'headers': dict([(k, self.request.META[k]) for k in self.header_filter if k in self.request.META]),
+        }
+        return render_to_string('debug_toolbar/panels/headers.html', context)
\ No newline at end of file
diff --git a/apps/debug_toolbar/panels/http_vars.py b/apps/debug_toolbar/panels/http_vars.py
new file mode 100644 (file)
index 0000000..2bf1789
--- /dev/null
@@ -0,0 +1,24 @@
+from debug_toolbar.panels import DebugPanel
+from django.template.loader import render_to_string
+
+class HttpVarsDebugPanel(DebugPanel):
+    """
+    A panel to display HTTP variables (POST/GET).
+    """
+    name = 'HttpVars'
+    # List of headers we want to display
+
+    def title(self):
+        return 'HTTP Globals'
+
+    def url(self):
+        return ''
+
+    def content(self):
+        context = {
+            'get': [(k, self.request.GET.getlist(k)) for k in self.request.GET.iterkeys()],
+            'post': [(k, self.request.POST.getlist(k)) for k in self.request.POST.iterkeys()],
+            'session': [(k, self.request.session.get(k)) for k in self.request.session.iterkeys()],
+            'cookies': [(k, self.request.COOKIES.get(k)) for k in self.request.COOKIES.iterkeys()],
+        }
+        return render_to_string('debug_toolbar/panels/http_vars.html', context)
\ No newline at end of file
diff --git a/apps/debug_toolbar/panels/profiler.py b/apps/debug_toolbar/panels/profiler.py
new file mode 100644 (file)
index 0000000..1c05c08
--- /dev/null
@@ -0,0 +1,53 @@
+from debug_toolbar.panels import DebugPanel
+try: import cProfile as profile
+except ImportError: import profile
+import pstats
+from django.template.loader import render_to_string
+
+class ProfilerDebugPanel(DebugPanel):
+    """
+    Panel that displays the time a response took with cProfile output.
+    """
+    name = 'Profiler'
+
+    def __init__(self, request):
+        super(ProfilerDebugPanel, self).__init__(request)
+
+    def process_response(self, request, response):
+        stats = pstats.Stats(self.profiler)
+        function_calls = []
+        for func in stats.strip_dirs().sort_stats(1).fcn_list:
+            current = []
+            if stats.stats[func][0] != stats.stats[func][1]:
+                current.append('%d/%d' % (stats.stats[func][1], stats.stats[func][0]))
+            else:
+                current.append(stats.stats[func][1])
+            current.append(stats.stats[func][2]*1000)
+            current.append(stats.stats[func][2]*1000/stats.stats[func][1])
+            current.append(stats.stats[func][3]*1000)
+            current.append(stats.stats[func][3]*1000/stats.stats[func][0])
+            current.append(pstats.func_std_string(func))
+            function_calls.append(current)
+        self.stats = stats
+        self.function_calls = function_calls
+        return response
+
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        self.callback = callback
+        self.profiler = profile.Profile()
+        return self.profiler.runcall(callback, request, *callback_args, **callback_kwargs)
+        
+    def title(self):
+        return 'View: %.2fms' % (float(self.stats.total_tt)*1000,)
+
+    def url(self):
+        return ''
+
+    def content(self):
+        context = {
+            'callback': self.callback.__name__,
+            'module': self.callback.__module__,
+            'stats': self.stats,
+            'function_calls': self.function_calls,
+        }
+        return render_to_string('debug_toolbar/panels/profiler.html', context)
\ No newline at end of file
diff --git a/apps/debug_toolbar/panels/sql.py b/apps/debug_toolbar/panels/sql.py
new file mode 100644 (file)
index 0000000..977e315
--- /dev/null
@@ -0,0 +1,79 @@
+from debug_toolbar.panels import DebugPanel
+from django.db import connection
+from django.db.backends import util
+from django.template.loader import render_to_string
+from django.shortcuts import render_to_response
+from django.http import HttpResponse
+from django.utils import simplejson
+from time import time
+
+class DatabaseStatTracker(util.CursorDebugWrapper):
+    """Replacement for CursorDebugWrapper which stores additional information
+    in `connection.queries`."""
+    def execute(self, sql, params=()):
+        start = time()
+        try:
+            return self.cursor.execute(sql, params)
+        finally:
+            stop = time()
+            # 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': params,
+            })
+
+util.CursorDebugWrapper = DatabaseStatTracker
+
+class SQLDebugPanel(DebugPanel):
+    """
+    Panel that displays information about the SQL queries run while processing the request.
+    """
+    name = 'SQL'
+    
+    def reformat_sql(self, sql):
+        sql = sql.replace('`,`', '`, `')
+        sql = sql.replace('` FROM `', '` \n  FROM `')
+        sql = sql.replace('` WHERE ', '` \n  WHERE ')
+        sql = sql.replace('`) WHERE ', '`) \n  WHERE ')
+        sql = sql.replace(' ORDER BY ', ' \n  ORDER BY ')
+        return sql
+    
+    def process_ajax(self, request):
+        action = request.GET.get('op')
+        if action == 'explain':
+            # XXX: loop through each sql statement to output explain?
+            sql = request.GET.get('sql', '').split(';')[0]
+            if sql.lower().startswith('select'):
+                if 'params' in request.GET:
+                    params = simplejson.loads(request.GET['params'])
+                else:
+                    params = []
+                cursor = connection.cursor()
+                cursor.execute("EXPLAIN %s" % (sql,), params)
+                response = cursor.fetchall()
+                cursor.close()
+                context = {'explain': response, 'sql': self.reformat_sql(sql), 'params': params}
+                return render_to_response('debug_toolbar/panels/sql_explain.html', context)
+            else:
+                return HttpResponse('Invalid SQL', mimetype="text/plain", status_code=403)
+    
+    def process_request(self, request):
+        self.queries_offset = len(connection.queries)
+    
+    def process_response(self, request, response):
+        self.queries = connection.queries[self.queries_offset:]
+        self.total_time = sum(map(lambda q: float(q['time'])*1000, self.queries))
+        return response
+
+    def title(self):
+        return 'SQL: %.2fms (%d queries)' % (self.total_time, len(self.queries))
+
+    def url(self):
+        return ''
+
+    def content(self):
+        queries = [(q['time'], q['sql'], q['raw_sql'], simplejson.dumps(q['params'])) for q in sorted(self.queries, key=lambda x: x['time'])[::-1]]
+        context = {'queries': queries}
+        return render_to_string('debug_toolbar/panels/sql.html', context)
\ No newline at end of file
diff --git a/apps/debug_toolbar/panels/templates.py b/apps/debug_toolbar/panels/templates.py
new file mode 100644 (file)
index 0000000..ae773c6
--- /dev/null
@@ -0,0 +1,60 @@
+from debug_toolbar.panels import DebugPanel
+from django.conf import settings
+from django.dispatch import dispatcher
+from django.core.signals import request_started
+from django.test.signals import template_rendered
+from django.template.loader import render_to_string
+
+# Based on http://www.djangosnippets.org/snippets/766/
+from django.test.utils import instrumented_test_render
+from django.template import Template, Context
+if Template.render != instrumented_test_render:
+    Template.original_render = Template.render
+    Template.render = instrumented_test_render
+# MONSTER monkey-patch
+old_template_init = Template.__init__
+def new_template_init(self, template_string, origin=None, name='<Unknown Template>'):
+    old_template_init(self, template_string, origin, name)
+    self.origin = origin
+Template.__init__ = new_template_init
+
+class TemplatesDebugPanel(DebugPanel):
+    """
+    Panel that displays information about the SQL queries run while processing the request.
+    """
+    name = 'Templates'
+    
+    def __init__(self, *args, **kwargs):
+        super(TemplatesDebugPanel, self).__init__(*args, **kwargs)
+        self.templates_used = []
+        self.contexts_used = []
+        template_rendered.connect(self._storeRenderedTemplates)
+        
+    def _storeRenderedTemplates(self, **kwargs):
+        template = kwargs.pop('template')
+        if template:
+            self.templates_used.append(template)
+        context = kwargs.pop('context')
+        if context:
+            self.contexts_used.append(context)
+
+    def process_response(self, request, response):
+        self.templates = [
+            (t.name, t.origin and t.origin.name or 'No origin')
+            for t in self.templates_used
+        ]
+        return response
+
+    def title(self):
+        return 'Templates: %.2fms'
+
+    def url(self):
+        return ''
+
+    def content(self):
+        context = {
+            'templates': self.templates,
+            'template_dirs': settings.TEMPLATE_DIRS,
+        }
+        
+        return render_to_string('debug_toolbar/panels/templates.html', context)
\ No newline at end of file
diff --git a/apps/debug_toolbar/panels/timer.py b/apps/debug_toolbar/panels/timer.py
new file mode 100644 (file)
index 0000000..d520191
--- /dev/null
@@ -0,0 +1,21 @@
+import time
+from debug_toolbar.panels import DebugPanel
+
+class TimerDebugPanel(DebugPanel):
+    """
+    Panel that displays the time a response took.
+    """
+    name = 'Timer'
+
+    def __init__(self, request):
+        super(TimerDebugPanel, self).__init__(request)
+        self._start_time = time.time()
+
+    def title(self):
+        return 'Time: %0.2fms' % ((time.time() - self._start_time) * 1000)
+
+    def url(self):
+        return ''
+
+    def content(self):
+        return ''
diff --git a/apps/debug_toolbar/panels/version.py b/apps/debug_toolbar/panels/version.py
new file mode 100644 (file)
index 0000000..7ea6543
--- /dev/null
@@ -0,0 +1,17 @@
+import django
+from debug_toolbar.panels import DebugPanel
+
+class VersionDebugPanel(DebugPanel):
+    """
+    Panel that displays the Django version.
+    """
+    name = 'Version'
+    
+    def title(self):
+        return 'Version: %s' % (django.get_version())
+
+    def url(self):
+        return ''
+
+    def content(self):
+        return ''
diff --git a/apps/debug_toolbar/settings.py b/apps/debug_toolbar/settings.py
new file mode 100644 (file)
index 0000000..4e8e5ba
--- /dev/null
@@ -0,0 +1,11 @@
+from django.conf import settings
+
+DEBUG_TOOLBAR_PANELS = getattr(settings, 'DEBUG_TOOLBAR_PANELS', (
+    'debug_toolbar.panels.version.VersionDebugPanel',
+    'debug_toolbar.panels.profiler.ProfilerDebugPanel',
+    'debug_toolbar.panels.sql.SQLDebugPanel',
+    'debug_toolbar.panels.cache.CacheDebugPanel',
+    # 'debug_toolbar.panels.templates.TemplatesDebugPanel',
+    'debug_toolbar.panels.headers.HeaderDebugPanel',
+    'debug_toolbar.panels.http_vars.HttpVarsDebugPanel',
+))
diff --git a/apps/debug_toolbar/templates/debug_toolbar/base.html b/apps/debug_toolbar/templates/debug_toolbar/base.html
new file mode 100644 (file)
index 0000000..29ca16c
--- /dev/null
@@ -0,0 +1,191 @@
+<script type="text/javascript">
+var djDebugLastObj = null;
+function djDebugToggle(tag) {
+    if (djDebugLastObj) {
+        djDebugLastObj.className = '';
+        document.getElementById(djDebugLastObj.getAttribute('rel')).style.display = 'none';
+    }
+    djDebugWindowClose();
+    var obj = document.getElementById(tag.getAttribute('rel'));
+    if (!obj) return;
+    if (djDebugLastObj == tag) {
+        djDebugLastObj = null;
+        return;
+    }
+    if (obj.style.display != 'block') {
+        obj.style.display = 'block';
+        tag.className = 'active';
+        djDebugLastObj = tag;
+    }
+}
+function djDebugClose() {
+    if (!djDebugLastObj) return;
+    djDebugLastObj.className = '';
+    document.getElementById(djDebugLastObj.getAttribute('rel')).style.display = 'none';
+    djDebugLastObj = null;
+}
+function djDebugWindowHtml(text) {
+    var obj = document.getElementById('djDebugWindow');
+    var frame = obj.getElementsByTagName('div')[1];
+    frame.innerHTML = text;
+}
+function djDebugWindow(url) {
+    var obj = document.getElementById('djDebugWindow');
+    djDebugWindowHtml('Loading request...');
+    djDebugHttp.open('get', url, true);
+
+    djDebugHttp.onreadystatechange = function() {
+      if (djDebugHttp.readyState == 4) {
+        // XXX: why does status return 0?
+        if (djDebugHttp.status == 200 || djDebugHttp.status == 0) {
+          djDebugWindowHtml(djDebugHttp.responseText);
+        }
+        else {
+          djDebugWindowHtml('There was an error loading the document.');
+        }
+      }
+    }
+    djDebugHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+    try {
+      djDebugHttp.send(null);
+    }
+    catch (ex) {
+      djDebugWindowHtml('There was an error loading the document.');
+    }
+    obj.style.display = 'block';
+}
+function djDebugCreateXMLHttpRequest() {
+  var http;
+  if (window.XMLHttpRequest) { // Mozilla, Safari,...
+    http = new XMLHttpRequest();
+    if (http.overrideMimeType) {
+      // set type accordingly to anticipated content type
+      http.overrideMimeType('text/html');
+    }
+  }
+  else if (window.ActiveXObject) { // IE
+    try {
+      http = new ActiveXObject("Msxml2.XMLHTTP");
+    } catch (e) {
+      try {
+        http = new ActiveXObject("Microsoft.XMLHTTP");
+      } catch (e) {}
+    }
+  }
+  if (!http) {
+    alert('Cannot create XMLHTTP instance');
+    return false;
+  }
+  return http;
+}
+var djDebugHttp = djDebugCreateXMLHttpRequest();
+function djDebugWindowClose() {
+    var obj = document.getElementById('djDebugWindow');
+    obj.style.display = 'none';
+}
+function djDebugCloseAll() {
+    djDebugClose();
+    djDebugWindowClose();
+}
+function djDebugHide() {
+    var obj = document.getElementById('djDebug');
+    obj.style.display = 'none';
+}
+</script>
+<style type="text/css">
+#djDebug * { font: normal 12px Arial, sans-serif; margin: 0; padding: 0; float: none; position: static; border: 0;  }
+#djDebugToolbar {
+       height:22px;
+       background:#244035;
+       color:#90EF45;
+       text-transform:lowercase;
+       margin: 5px 0;
+       z-index:100000000;
+       border-top:1px solid #345447;
+       border-bottom:1px solid #10211a;
+}
+#djDebugToolbar ul { margin: 0; padding: 0 10px; }
+#djDebugToolbar li { display: inline; height: 22px; float: left; }
+#djDebugToolbar li span,
+#djDebugToolbar li a { color: #FFE761; text-decoration: none; display: inline; width: auto; position: relative; float: none; margin: 0; height: 12px; line-height: 22px; padding: 4px 10px; border-left: 1px solid #345447; border-right: 1px solid #10211a; padding-right: 10px; }
+#djDebugToolbar li a:hover, #djDebugToolbar li.active a { color: #fff; background-color: #2e4d41; }
+#djDebugToolbar #djDebugButton span { border-left: 0; color: #fff; font-weight: bold; }
+#djDebugToolbar #djDebugHide { float: right; }
+#djDebugToolbar #djDebugHide a { border: 0; font-size: 93%; }
+#djDebug .panelContent {
+       display:none;
+       position:absolute;
+       margin:0;
+       padding:10px;
+       top:43px;
+       width:auto;
+       left:10px;
+       right:10px;
+       bottom:10px;
+       background:#244035;
+       color:#fff;
+       z-index:1000000;
+       overflow:auto;
+       border:1px solid #345447;
+}
+#djDebug .panelContent * { color: #fff; }
+#djDebug .panelContent a:hover { color: #FFE761; }
+#djDebug .panelContent table { width: 100%; }
+#djDebug .panelContent p { padding: 5px; }
+#djDebug .panelContent p, #djDebug .panelContent table,
+#djDebug .panelContent ul, #djDebug .panelContent dl,
+#djDebug .panelContent .title { margin: 5px 0; }
+#djDebug .panelContent dt, #djDebug .panelContent dd { display: block; }
+#djDebug .panelContent dt { font-weight: bold; width: 100px; clear: left; float: left; }
+#djDebug .panelContent dd { margin-left: 20px; margin-bottom: 5px; margin-left: 100px; }
+#djDebug .panelContent th, #djDebug td { padding: 5px; }
+#djDebug .panelContent td { background:#244035; }
+#djDebug .panelContent th { font-weight: bold; text-align: left; background: transparent; color: #fff; }
+#djDebug .panelContent thead th { border-bottom: 1px solid #2e4d41; color: #FFE761; }
+#djDebug .panelContent .row2 td { background:#2e4d41; }
+#djDebugWindow { z-index: 20000000; }
+#djDebug .panelContent .title { font-weight: bold; font-size: 15px; color: #90EF45; }
+#djDebug .panelContent .title a { float: right; font-weight: bold; font-size: 10px; }
+#djDebug .panelContent .title .close { float: right; font-weight: bold; margin-left: 15px; }
+</style>
+
+<div id="djDebug">
+       <div id="djDebugToolbar">
+               <ul id="djDebugPanelList">
+                       <li id="djDebugButton"><span>Django Debug</span></li>
+                       {% for panel in panels %}
+                               <li rel="{{ panel.dom_id }}">
+                                       {% if panel.content %}
+                                               <a href="{{ panel.url|default:"#" }}" onclick="djDebugToggle(this.parentNode)" title="{{ panel.title }}" class="{{ panel.dom_id }}">{{ panel.title }}</a>
+                                       {% else %}
+                                               <span>{{ panel.title }}</span>
+                                       {% endif %}
+                               </li>
+                       {% endfor %}
+                       <li id="djDebugHide"><a href="javascript:djDebugHide();">Hide Debug Toolbar</a></li>
+               </ul>
+       </div>
+       {% for panel in panels %}
+               {% with panel.content as content %}
+                       {% if content %}
+                               <div id="{{ panel.dom_id }}" class="panelContent">
+                                       <div class="title">
+                                               <a href="javascript:djDebugCloseAll()" class="close">Close</a>
+                                       <!-- <h1>{{ panel.title }}</h1> -->
+                                       </div>
+                                       <div class="content">
+                                           {{ content|safe }}
+                                       </div>
+                               </div>
+                       {% endif %}
+               {% endwith %}
+       {% endfor %}
+       <div id="djDebugWindow" class="panelContent">
+           <div class="title">
+                       <a href="javascript:djDebugCloseAll()" class="close">Close</a>
+               <a href="javascript:djDebugWindowClose();" class="close">Back</a>
+           </div>
+           <div class="content">
+       </div>
+    </div>
+</div>
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/cache.html b/apps/debug_toolbar/templates/debug_toolbar/panels/cache.html
new file mode 100644 (file)
index 0000000..3d630b1
--- /dev/null
@@ -0,0 +1,60 @@
+<div class="title">
+    Cache Usage
+</div>
+<table>
+    <colgroup>
+        <col width="12%"/>
+        <col width="12%"/>
+        <col width="12%"/>
+        <col width="12%"/>
+        <col width="12%"/>
+        <col width="12%"/>
+        <col width="12%"/>
+        <col width="12%"/>
+    </colgroup>
+    <tr>
+        <th>Total Calls</th>
+        <td>{{ cache_calls }}</td>
+        <th>Total Time</th>
+        <td>{{ cache_time }}ms</td>
+        <th>Hits</th>
+        <td>{{ cache.hits }}</td>
+        <th>Misses</th>
+        <td>{{ cache.misses }}</td>
+    </tr>
+    <tr>
+        <th>gets</th>
+        <td>{{ cache.gets }}</td>
+        <th>sets</th>
+        <td>{{ cache.sets }}</td>
+        <th>deletes</th>
+        <td>{{ cache.deletes }}</td>
+        <th>get_many</th>
+        <td>{{ cache.get_many }}</td>
+    </tr>
+</table>
+{% if cache.calls %}
+<div class="title">
+    Breakdown
+</div>
+<table>
+    <thead>
+        <tr>
+            <th>Time&nbsp;(ms)</th>
+            <th>Type</th>
+            <th>Parameters</th>
+            <th>Function</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for query in cache.calls %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ query.0|floatformat:"4" }}</td>
+                <td>{{ query.1|escape }}</td>
+                <td>{{ query.2|escape }}</td>
+                <td>{{ query.3.0 }}:{{ query.3.1 }}({{ query.3.2|escape }}); {{ query.3.3.0|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% endif %}
\ No newline at end of file
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/headers.html b/apps/debug_toolbar/templates/debug_toolbar/panels/headers.html
new file mode 100644 (file)
index 0000000..fb253d6
--- /dev/null
@@ -0,0 +1,19 @@
+<div class="title">
+    Request Headers
+</div>
+<table>
+    <thead>
+        <tr>
+            <th>Key</th>
+            <th>Value</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for key, value in headers.iteritems %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ key|escape }}</td>
+                <td>{{ value|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
\ No newline at end of file
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/http_vars.html b/apps/debug_toolbar/templates/debug_toolbar/panels/http_vars.html
new file mode 100644 (file)
index 0000000..f7013dc
--- /dev/null
@@ -0,0 +1,100 @@
+<div class="title">request.GET</div>
+{% if get %}
+<table>
+    <colgroup>
+        <col style="width:20%"/>
+        <col/>
+    </colgroup>
+    <thead>
+        <tr>
+            <th>Key</th>
+            <th>Value</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for key, value in get %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ key|escape }}</td>
+                <td>{{ value|join:", "|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% else %}
+<p>No GET variables.</p>
+{% endif %}
+<div class="title">request.POST</div>
+{% if post %}
+<table>
+    <colgroup>
+        <col style="width:20%"/>
+        <col/>
+    </colgroup>
+    <thead>
+        <tr>
+            <th>Key</th>
+            <th>Value</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for key, value in post %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ key|escape }}</td>
+                <td>{{ value|join:", "|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% else %}
+<p>No POST variables.</p>
+{% endif %}
+<div class="title">request.session</div>
+{% if session %}
+<table>
+    <colgroup>
+        <col style="width:20%"/>
+        <col/>
+    </colgroup>
+    <thead>
+        <tr>
+            <th>Key</th>
+            <th>Value</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for key, value in session %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ key|escape }}</td>
+                <td>{{ value|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% else %}
+<p>No SESSION variables.</p>
+{% endif %}
+<div class="title">request.COOKIES</div>
+{% if cookies %}
+<table>
+    <colgroup>
+        <col style="width:20%"/>
+        <col/>
+    </colgroup>
+    <thead>
+        <tr>
+            <th>Key</th>
+            <th>Value</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for key, value in cookies %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ key|escape }}</td>
+                <td>{{ value|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% else %}
+<p>No COOKIES variables.</p>
+{% endif %}
\ No newline at end of file
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/profiler.html b/apps/debug_toolbar/templates/debug_toolbar/panels/profiler.html
new file mode 100644 (file)
index 0000000..41f6920
--- /dev/null
@@ -0,0 +1,28 @@
+<div class="title">
+    Profile Output
+</div>
+<p>`profile` module output for the view `{{ module|escape }}.{{ callback|escape }}`</p>
+<table>
+    <thead>
+        <tr>
+            <th>Calls</th>
+            <th>Total Time (ms)</th>
+            <th>Per Call (ms)</th>
+            <th>Cumulative Time (ms)</th>
+            <th>Per Call (ms)</th>
+            <th>Function</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for row in function_calls %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ row.0 }}</td>
+                <td>{{ row.1|floatformat:"4" }}</td>
+                <td>{{ row.2|floatformat:"4" }}</td>
+                <td>{{ row.3|floatformat:"4" }}</td>
+                <td>{{ row.4|floatformat:"4" }}</td>
+                <td>{{ row.5|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
\ No newline at end of file
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/sql.html b/apps/debug_toolbar/templates/debug_toolbar/panels/sql.html
new file mode 100644 (file)
index 0000000..28008e2
--- /dev/null
@@ -0,0 +1,19 @@
+<div class="title">
+    SQL Queries
+</div>
+<table>
+    <thead>
+        <tr>
+            <th>Time&nbsp;(ms)</th>
+            <th>Query</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for query in queries %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ query.0|floatformat:"4" }}</td>
+                <td><a href="javascript:djDebugWindow('?djDebug=SQL&amp;op=explain&amp;sql={{ query.2|urlencode }}&amp;params={{ query.3|urlencode }}');">{{ query.1|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
\ No newline at end of file
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html b/apps/debug_toolbar/templates/debug_toolbar/panels/sql_explain.html
new file mode 100644 (file)
index 0000000..31dd10d
--- /dev/null
@@ -0,0 +1,39 @@
+<div class="title">SQL Explain</div>
+<dl>
+    <dt>Query:</dt>
+    <dd style="white-space:pre-wrap;">{{ sql|escape|linebreaksbr }}</dd>
+    <dt>Parameters:</dt>
+    <dd>{{ params|join:", "|escape }}</dd>
+</dl>
+<table>
+    <thead>
+        <tr>
+            <th>ID</th>
+            <th>Select Type</th>
+            <th>Table</th>
+            <th>Type</th>
+            <th>Possible Keys</th>
+            <th>Key</th>
+            <th>Ken Length</th>
+            <th>Ref</th>
+            <th>Rows</th>
+            <th>Extra</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for row in explain %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ row.0|escape }}</td>
+                <td>{{ row.1|escape }}</td>
+                <td>{{ row.2|escape }}</td>
+                <td>{{ row.3|escape }}</td>
+                <td>{{ row.4|escape }}</td>
+                <td>{{ row.5|escape }}</td>
+                <td>{{ row.6|escape }}</td>
+                <td>{{ row.7|escape }}</td>
+                <td>{{ row.8|escape }}</td>
+                <td>{{ row.9|escape }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
\ No newline at end of file
diff --git a/apps/debug_toolbar/templates/debug_toolbar/panels/templates.html b/apps/debug_toolbar/templates/debug_toolbar/panels/templates.html
new file mode 100644 (file)
index 0000000..09dd3ef
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="title">
+    Templates
+</div>
+<table>
+    <thead>
+        <tr>
+            <th>Time&nbsp;(ms)</th>
+            <th>Template</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for template in templates %}
+            <tr class="{% cycle 'row1' 'row2' %}">
+                <td>{{ template }}</td>
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>
\ No newline at end of file
diff --git a/apps/debug_toolbar/toolbar/__init__.py b/apps/debug_toolbar/toolbar/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/debug_toolbar/toolbar/loader.py b/apps/debug_toolbar/toolbar/loader.py
new file mode 100644 (file)
index 0000000..4164324
--- /dev/null
@@ -0,0 +1,49 @@
+"""
+The main DebugToolbar class that loads and renders the Toolbar.
+"""
+from django.template.loader import render_to_string
+from debug_toolbar.settings import DEBUG_TOOLBAR_PANELS
+
+class DebugToolbar(object):
+
+    def __init__(self, request):
+        self.request = request
+        self.panels = []
+        self.panel_list = []
+        self.content_list = []
+    
+    def load_panels(self):
+        """
+        Populate debug panel lists from settings.DEBUG_TOOLBAR_PANELS.
+        """
+        from django.conf import settings
+        from django.core import exceptions
+
+        for panel_path in DEBUG_TOOLBAR_PANELS:
+            try:
+                dot = panel_path.rindex('.')
+            except ValueError:
+                raise exceptions.ImproperlyConfigured, '%s isn\'t a debug panel module' % panel_path
+            panel_module, panel_classname = panel_path[:dot], panel_path[dot+1:]
+            try:
+                mod = __import__(panel_module, {}, {}, [''])
+            except ImportError, e:
+                raise exceptions.ImproperlyConfigured, 'Error importing debug panel %s: "%s"' % (panel_module, e)
+            try:
+                panel_class = getattr(mod, panel_classname)
+            except AttributeError:
+                raise exceptions.ImproperlyConfigured, 'Toolbar Panel module "%s" does not define a "%s" class' % (panel_module, panel_classname)
+
+            try:
+                panel_instance = panel_class(self.request)
+            except:
+                raise
+                continue # Some problem loading panel
+
+            self.panels.append(panel_instance)
+
+    def render_toolbar(self):
+        """
+        Renders the overall Toolbar with panels inside.
+        """
+        return render_to_string('debug_toolbar/base.html', {'panels': self.panels})
index c5b8d67..2948693 100644 (file)
@@ -68,17 +68,23 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     'django.core.context_processors.request',
 )
 
     'django.core.context_processors.request',
 )
 
-MIDDLEWARE_CLASSES = [
+MIDDLEWARE_CLASSES = (
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.middleware.doc.XViewMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.middleware.doc.XViewMiddleware',
+    'debug_toolbar.middleware.DebugToolbarMiddleware',
     'pagination.middleware.PaginationMiddleware',
     'pagination.middleware.PaginationMiddleware',
-]
+)
+
+DEBUG_TOOLBAR_PANELS = (
+    'debug_toolbar.panels.version.VersionDebugPanel',
+    'debug_toolbar.panels.sql.SQLDebugPanel',
+    'debug_toolbar.panels.timer.TimerDebugPanel',
+    'debug_toolbar.panels.headers.HeaderDebugPanel',
+)
 
 
-# If DEBUG is enabled add query log to bottom of every template
-if DEBUG:
-    MIDDLEWARE_CLASSES.append('middleware.ProfileMiddleware')
+INTERNAL_IPS = ('127.0.0.1', )
 
 ROOT_URLCONF = 'urls'
 
 
 ROOT_URLCONF = 'urls'
 
@@ -118,6 +124,7 @@ INSTALLED_APPS = (
     'pagination',
     'chunks',
     'compress',
     'pagination',
     'chunks',
     'compress',
+    'debug_toolbar',
     'catalogue',
 )
 
     'catalogue',
 )