46 Commits
0.7 ... 0.9.2

Author SHA1 Message Date
Matt Good
bebe884615 Update for 0.9.2 release 2014-12-05 15:43:46 -08:00
Matt Good
10c03880c7 Safely decode non-ascii SQL queries for display
SQL queries containing non-ascii byte strings would cause errors, both with and
without Pygments highlighting.

This updates the non-Pygments case to handle a simple decoding to ensure the
value is ascii-safe. It also removes passing an explicit "utf-8" encoding to
Pygments, since this causes errors when the bytes are not utf-8. When the
encoding is omitted, Pygments will default to "guess" the encoding by trying
utf-8 and falling back to latin-1.

Fixes #55
2014-12-04 14:06:48 -08:00
Matt Good
3fcdfc8f83 Use platform-sensitive filename normalization
The os.path.commonprefix() function only does basic string prefix checking, and
isn't aware of case-insensitive file names, or path separators. Instead, this
switches the filename formatting to use os.path.relpath() for normalizing paths
relative to sys.path.

Fixes #67
2014-12-02 18:15:59 -08:00
Matt Good
914553ddf5 Consolidate redundant code for SQL select/explain
The code for displaying the SQL select results and explain plan were nearly
identical, so this consolidates it to one function and template. It also fixes
correctly displaying the timing results as milliseconds.
2014-11-24 15:04:50 -08:00
Matt Good
b4fe0b954c Fix misspelled CSS classes 2014-11-24 14:46:41 -08:00
Matt Good
7557ee6794 Ensure SQL queries are HTML-escaped
The SQL queries were displayed with the `safe` filter which allowed properly
including the Pygments-highlighted HTML, but if Pygments wasn't installed this
allowed the raw SQL to be included without escaping. This change removes the
`safe` filter and instead wraps the Pygments HTML with the `Markup` class. This
allows proper auto-escaping in the template.

Fixes #70
2014-11-24 14:36:56 -08:00
Matt Good
382c5e7da9 Remove old commented code
Looks like some old code from the original Django toolbar was left in the
template.
2014-11-24 14:25:32 -08:00
Matt Good
6286dadc27 Version update for 0.9.1 release 2014-11-24 13:25:17 -08:00
Matt Good
82295aa4aa printable filter should replace non-ascii bytes
In Python 2, when repr() returns bytes, replace any non-ascii bytes
with the unicode ? character to ensure that the result is printable.

Fixes #66
2014-02-02 14:46:41 -08:00
Matt Good
70488fc14a Fix Py3 support for bytes SQL queries
Fixes #64
2014-01-14 17:56:47 -08:00
Matt Good
4901d68b01 Fix version number in docs 2014-01-03 15:14:49 -08:00
Matt Good
d671a849bb Bump version for new release 2014-01-03 15:07:48 -08:00
Matt Good
ffbdf0f563 Add SQLAlchemy to test app 2013-12-31 17:55:19 -05:00
Matt Good
b08fe477b6 Merge pull request #63 from defuz/pep8
PEP8 formatting
2013-12-02 16:20:27 -08:00
Ivan Ivaschenko
903ed85f00 remove coding:utf-8 and add some style fixes 2013-11-29 00:17:15 +02:00
Ivan Ivaschenko
5710314252 pep8 formatting + coding: utf8 2013-11-28 18:31:29 +02:00
Matt Good
277462d822 Fix build status image
RST example copied from Travis was a bit broken
2013-11-06 09:59:50 -08:00
Matt Good
26691d49cd Add Travis build statis to README 2013-11-06 09:58:49 -08:00
Matt Good
bd37db9dc4 Fix SQLAlchemy URL registration
The init_app() restructuring in 356e6c removed "load_panels()", but need
to add it back so panels like SQLAlchemy can register their URLs on the
Blueprint before it's activated.
2013-11-06 09:33:00 -08:00
Matt Good
091c4fa70c Compatibility fixes for Python 3
Fixes #54 (at least the basic stuff, this is still a bit experimental)
2013-11-06 09:06:05 -08:00
Matt Good
d5987410d1 Add basic sanity test 2013-11-06 09:00:05 -08:00
Matt Good
6d8249160d Rename "settings" panel to "config" 2013-11-01 14:04:10 -07:00
Matt Good
1e046d57f4 Remove some unneeded code from SettingsVars panel 2013-11-01 12:42:38 -07:00
Matt Good
9e3546b416 Merge commit 'refs/pull/51/head' of github.com:mgood/flask-debugtoolbar
Conflicts:
	flask_debugtoolbar/toolbar.py
2013-11-01 11:41:08 -07:00
Matt Good
26034475f2 Load panels with werkzeug's "import_string"
Also add back caching of the loaded panel classes.
This prevents repeated warnings if there was an import error loading the
class the first time.
2013-07-30 17:56:54 -07:00
Matt Good
356e6c8268 Restructure to support init_app
Fixes #38
2013-07-30 17:37:34 -07:00
Matt Good
5870fce178 Can check blueprint name instead of URL prefix 2013-07-30 09:25:02 -07:00
Alexey Diyan
855c7ab377 Port SettingsVarsDebugPanel from Django DebugToolbar 2013-05-10 19:17:32 +03:00
Matt Good
115e874a39 Reformat templates and static files with 2 space indentation
Fixes #45
2013-03-16 16:21:39 -07:00
Matt Good
b7c39113e7 Template editor should use Jinja loader's encoding
The template loader wouldn't handle non-ASCII template encodings.
Updated it to use the encoding parameter of the Jinja loader when
reading and writing the template files.

Fixes #46
2013-03-08 09:00:10 -08:00
Matt Good
05288daf17 Docs Makefile needs to ensure git submodules are initialized 2013-02-21 13:43:27 -08:00
Matt Good
79421df5cd Use Flask theme for the documentation 2013-02-21 13:37:12 -08:00
Matt Good
8d5d37fa0b Merge branch 'release/0.8.0' 2013-02-21 12:26:23 -08:00
Matt Good
1029b9131b Prepare 0.8.0 release 2013-02-21 12:25:45 -08:00
Matt Good
3bea63dc8a Fix werkzeug request logging with the log panel
Werkzeug will disable its default logging setup if another log handler
is already configured.  At some point the initialization order changed
and the logging panel's handler is getting added first now, so
werkzeug's request log will not be printed to the console by default. By
explicitly calling werkzeug's logger we now make sure it's initialized
before the logging panel's handler.

Fixes #33
2013-02-21 10:53:16 -08:00
Matt Good
856eb52a95 Nice error message for un-repr-able objects
The "printable" filter can throw an exception when trying to display a
user object whose __repr__ implementation returns unicode, which repr()
does not allow. Catch it and display a more informative message
instead.

See #31 and #39
2013-02-20 20:46:31 -08:00
Matt Good
50017f13a7 Switch to itsdangerous for SQL query signing
Fixing #31 where the query signing would break when the SECRET_KEY
contained non-ascii values. In the process switch to itsdangerous
instead of rolling our own signatures.
2013-02-20 20:02:00 -08:00
Matt Good
561889738f Merge pull request #41 from crosspop/ajaxfix
Intercept redirect only if it’s not XHR (Ajax)
2013-02-19 17:01:11 -08:00
Matt Good
b97e58c1f2 Merge pull request #42 from bollwyvl/share-jquery
adding reference to noConflict'ed jQuery
2013-02-19 11:10:48 -08:00
Nicholas Bollweg
0f923475c8 adding reference to noConflict'ed jQuery 2013-02-13 12:29:45 -05:00
Hong Minhee
43df69ab24 Intercept redirect only if it's not XHR (Ajax)
Intercept redirect can't really help debugging at all
if requests are XHR (Ajax), because browsers just don't render
their response by default.
2013-01-17 05:11:26 +09:00
Matt Good
3e4cd1ecfa Merge pull request #35 from rconradharris/patch-1
Profiler: Use update_wrapper on partial

If the profiler panel is applied before the request-vars panel, the wrapper added by the profiler causes an error since it's missing a `__module__` attribute.
2012-09-25 10:43:19 -07:00
Matt Good
cfa5984d3e Convert SQLAlchemy query duration to ms to match the label
The SQLAlchemy panel labels the query duration column "(ms)" but the
times from Flask-SQLAlchemy are given in seconds.

Fixes #36
2012-09-25 09:45:52 -07:00
Rick Harris
c9c0228a62 Profiler: Use update_wrapper on partial
The `update_wrapper` call is needed so that the new partial
func inherits the `__module__` of the original function.

The `__module__` is needed because the request-vars panel uses
`func.__module__` in building the qualified name.
2012-09-04 19:45:51 -05:00
Matt Good
d87e7fc347 Fix version number in docs 2012-05-19 18:22:18 -07:00
Matt Good
80eb747816 Release 0.7.1 to fix the in-place loading of the template editor 2012-05-18 18:20:28 -07:00
43 changed files with 1296 additions and 1034 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
build/
dist/
docs/_build
.tox

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/mitsuhiko/flask-sphinx-themes.git

9
.travis.yml Normal file
View File

@@ -0,0 +1,9 @@
language: python
python: "2.7"
env:
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
install:
- pip install tox --use-mirrors
script: tox

View File

@@ -1,6 +1,65 @@
Changes
=======
0.9.2 (2014-12-05)
------------------
Fixes:
- HTML escape SQL queries when syntax highlighting is not available
- Use case-insensitive comparison to normalize filenames on Windows
- Fix exception when SQL query contained non-ASCII characters
0.9.1 (2014-11-24)
------------------
Fixes:
- Fix SQL queries with byte strings on Python 3
- Fix displaying values whose `repr()` contains unprintable characters
0.9.0 (2014-01-03)
------------------
Enhancements:
- Python 3 compatibility (#54, thanks justinmayer and jmagnusson)
- Support .init_app() (#38)
- New "Config" panel displaying Flask config values (#51, thanks Alexey Diyan)
- Better PEP8-style formatting (#63, thanks Ivan Ivaschenko)
Fixes:
- Fix template editor with non-ASCII templates (#46)
0.8 (2013-02-21)
----------------
Enhancements:
- Use `itsdangerous <http://pythonhosted.org/itsdangerous/>`_ to sign SQL queries
- Expose the jQuery object as ``fldt.$`` so extensions can use the toolbar's
copy of jQuery (#42)
Fixes:
- Don't intercept redirects on XHR requests (#41)
- Fix SQL query time display as milliseconds (#36)
- Fix ``functools.partial`` error (#35)
- Fix werkzeug request logging with logging panel (#33)
- Fix SQL panel unicode encoding error (#31)
0.7.1 (2012-05-18)
------------------
Fixes:
- loading template editor in-place over current page
0.7 (2012-05-18)
----------------
@@ -12,7 +71,7 @@ Enhancements:
0.6.3.1 (2012-04-16)
------------------
--------------------
New release to add missing changelog for 0.6.3

View File

@@ -4,6 +4,9 @@ Flask Debug-toolbar
This is a port of the excellent `django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_
for Flask applications.
.. image:: https://travis-ci.org/mgood/flask-debugtoolbar.png?branch=master
:target: https://travis-ci.org/mgood/flask-debugtoolbar
Installation
------------

View File

@@ -41,7 +41,7 @@ help:
clean:
-rm -rf $(BUILDDIR)/*
html:
html: _themes
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
@@ -151,3 +151,7 @@ doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
_themes:
git clone git://github.com/Pylons/pylons_sphinx_theme.git _themes
cd ..; git submodule update --init; cd docs

1
docs/_themes Submodule

Submodule docs/_themes added at 0269f3d188

View File

@@ -48,9 +48,9 @@ copyright = u'2012, Matt Good'
# built documents.
#
# The short X.Y version.
version = '0.6'
version = '0.9'
# The full version, including alpha/beta/rc tags.
release = '0.6.1'
release = '0.9.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -91,15 +91,19 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = 'flask_small'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
html_theme_options = {
'github_fork': 'mgood/flask-debugtoolbar',
'index_logo': None,
}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".

View File

@@ -3,8 +3,8 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Flask-DebugToolbar's documentation!
==============================================
Flask-DebugToolbar
==================
This is a port of the excellent `django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_
for Flask applications.

View File

@@ -22,16 +22,19 @@ db = SQLAlchemy(app)
toolbar = DebugToolbarExtension(app)
class ExampleModel(db.Model):
__tablename__ = 'examples'
value = db.Column(db.String(100), primary_key=True)
@app.route('/')
def index():
app.logger.info("Hello there")
ExampleModel.query.get(1)
return render_template('index.html')
@app.route('/redirect')
def redirect_example():
@@ -44,4 +47,3 @@ if __name__ == "__main__":
manager = Manager(app)
manager.run()

View File

@@ -1,37 +1,38 @@
import os
from flask import current_app, request, g
from flask import Blueprint, current_app, request, g, send_from_directory
from flask.globals import _request_ctx_stack
from flask import send_from_directory
from jinja2 import Environment, PackageLoader
from werkzeug.exceptions import HTTPException
from werkzeug.urls import url_quote_plus
from flask_debugtoolbar.compat import iteritems
from flask_debugtoolbar.toolbar import DebugToolbar
from flask import Blueprint
from flask_debugtoolbar.utils import decode_text
module = Blueprint('debugtoolbar', __name__)
def replace_insensitive(string, target, replacement):
"""Similar to string.replace() but is case insensitive
Code borrowed from: http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
Code borrowed from:
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
"""
no_case = string.lower()
index = no_case.rfind(target.lower())
if index >= 0:
return string[:index] + replacement + string[index + len(target):]
else: # no results so return the original string
else: # no results so return the original string
return string
def _printable(value):
if isinstance(value, unicode):
return value.encode('unicode_escape')
elif isinstance(value, str):
return value.encode('string_escape')
else:
return repr(value)
try:
return decode_text(repr(value))
except Exception as e:
return '<repr(%s) raised %s: %s>' % (
object.__repr__(value), type(e).__name__, e)
class DebugToolbarExtension(object):
@@ -40,29 +41,9 @@ class DebugToolbarExtension(object):
_redirect_codes = [301, 302, 303, 304]
def __init__(self, app):
def __init__(self, app=None):
self.app = app
self.debug_toolbars = {}
self.hosts = ()
if not app.config.get('DEBUG_TB_ENABLED', app.debug):
return
if not app.config.get('SECRET_KEY'):
raise RuntimeError(
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
"var to be set")
DebugToolbar.load_panels(app)
self.hosts = app.config.get('DEBUG_TB_HOSTS', ())
self.app.before_request(self.process_request)
self.app.after_request(self.process_response)
self.app.teardown_request(self.teardown_request)
# Monkey-patch the Flask.dispatch_request method
app.dispatch_request = self.dispatch_request
# Configure jinja for the internal templates and add url rules
# for static data
@@ -73,11 +54,53 @@ class DebugToolbarExtension(object):
self.jinja_env.filters['urlencode'] = url_quote_plus
self.jinja_env.filters['printable'] = _printable
if app is not None:
self.init_app(app)
def init_app(self, app):
for k, v in iteritems(self._default_config(app)):
app.config.setdefault(k, v)
if not app.config['DEBUG_TB_ENABLED']:
return
if not app.config.get('SECRET_KEY'):
raise RuntimeError(
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
"var to be set")
DebugToolbar.load_panels(app)
app.before_request(self.process_request)
app.after_request(self.process_response)
app.teardown_request(self.teardown_request)
# Monkey-patch the Flask.dispatch_request method
app.dispatch_request = self.dispatch_request
app.add_url_rule('/_debug_toolbar/static/<path:filename>',
'_debug_toolbar.static', self.send_static_file)
'_debug_toolbar.static', self.send_static_file)
app.register_blueprint(module, url_prefix='/_debug_toolbar/views')
def _default_config(self, app):
return {
'DEBUG_TB_ENABLED': app.debug,
'DEBUG_TB_HOSTS': (),
'DEBUG_TB_INTERCEPT_REDIRECTS': True,
'DEBUG_TB_PANELS': (
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
'flask_debugtoolbar.panels.timer.TimerDebugPanel',
'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel',
'flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel',
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
'flask_debugtoolbar.panels.logger.LoggingPanel',
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
),
}
def dispatch_request(self):
"""Modified version of Flask.dispatch_request to call process_view."""
req = _request_ctx_stack.top.request
@@ -102,10 +125,11 @@ class DebugToolbarExtension(object):
def _show_toolbar(self):
"""Return a boolean to indicate if we need to show the toolbar."""
if request.path.startswith('/_debug_toolbar/'):
if request.blueprint == 'debugtoolbar':
return False
if self.hosts and request.remote_addr not in self.hosts:
hosts = current_app.config['DEBUG_TB_HOSTS']
if hosts and request.remote_addr not in hosts:
return False
return True
@@ -145,8 +169,9 @@ class DebugToolbarExtension(object):
# Intercept http redirect codes and display an html page with a
# link to the target.
if self.debug_toolbars[real_request].config['DEBUG_TB_INTERCEPT_REDIRECTS']:
if response.status_code in self._redirect_codes:
if current_app.config['DEBUG_TB_INTERCEPT_REDIRECTS']:
if (response.status_code in self._redirect_codes and
not real_request.is_xhr):
redirect_to = response.location
redirect_code = response.status_code
if redirect_to:
@@ -161,8 +186,8 @@ class DebugToolbarExtension(object):
# If the http response code is 200 then we process to add the
# toolbar to the returned html response.
if (response.status_code == 200
and response.headers['content-type'].startswith('text/html')):
if (response.status_code == 200 and
response.headers['content-type'].startswith('text/html')):
for panel in self.debug_toolbars[real_request].panels:
panel.process_response(real_request, response)

View File

@@ -0,0 +1,9 @@
import sys
PY2 = sys.version_info[0] == 2
if PY2:
iteritems = lambda d: d.iteritems()
else:
iteritems = lambda d: iter(d.items())

View File

@@ -1,16 +1,16 @@
"""Base DebugPanel class"""
class DebugPanel(object):
"""
Base class for debug panels.
"""
# name = Base
has_content = False # If content returns something, set to true in subclass
has_content = False # If content returns something, set to true in subclass
# If the client is able to activate/de-activate the panel
user_enable = False
# We'll maintain a local context instance so we can expose our template
# context variables to panels which need them:
context = {}
@@ -57,5 +57,3 @@ class DebugPanel(object):
def process_response(self, request, response):
pass

View File

@@ -0,0 +1,29 @@
from flask import current_app
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class ConfigVarsDebugPanel(DebugPanel):
"""
A panel to display all variables from Flask configuration
"""
name = 'ConfigVars'
has_content = True
def nav_title(self):
return _('Config')
def title(self):
return _('Config')
def url(self):
return ''
def content(self):
context = self.context.copy()
context.update({
'config': current_app.config,
})
return self.render('panels/config_vars.html', context)

View File

@@ -12,13 +12,14 @@ from flask_debugtoolbar.utils import format_fname
_ = lambda x: x
class ThreadTrackingHandler(logging.Handler):
def __init__(self):
if threading is None:
raise NotImplementedError("threading module is not available, \
the logging panel cannot be used without it")
logging.Handler.__init__(self)
self.records = {} # a dictionary that maps threads to log records
self.records = {} # a dictionary that maps threads to log records
def emit(self, record):
self.get_records().append(record)
@@ -46,18 +47,27 @@ _init_lock = threading.Lock()
def _init_once():
# Initialize the logging handler once, but after werkzeug has set up its
# default logger. Otherwise, if this sets up the logging first, werkzeug
# will not create a default logger, so the development server's output will
# not get printed.
global handler
if handler is not None:
return
with _init_lock:
global handler
if handler is not None:
return
handler = ThreadTrackingHandler()
logging.root.addHandler(handler)
return
with _init_lock:
if handler is not None:
return
# Call werkzeug's internal logging to make sure it gets configured
# before we add our handler. Otherwise werkzeug will see our handler
# and not configure console logging for the request log.
# Werkzeug's default log level is INFO so this message probably won't be
# seen.
try:
from werkzeug._internal import _log
except ImportError:
pass
else:
_log('debug', 'Initializing Flask-DebugToolbar log handler')
handler = ThreadTrackingHandler()
logging.root.addHandler(handler)
class LoggingPanel(DebugPanel):
@@ -78,7 +88,8 @@ class LoggingPanel(DebugPanel):
def nav_subtitle(self):
# FIXME l10n: use ngettext
return "%s message%s" % (len(handler.get_records()), (len(handler.get_records()) == 1) and '' or 's')
return "%s message%s" % \
(len(handler.get_records()), (len(handler.get_records()) == 1) and '' or 's')
def title(self):
return _('Log Messages')
@@ -102,5 +113,3 @@ class LoggingPanel(DebugPanel):
context.update({'records': records})
return self.render('panels/logger.html', context)

View File

@@ -37,7 +37,9 @@ class ProfilerDebugPanel(DebugPanel):
def process_view(self, request, view_func, view_kwargs):
if self.is_active:
return functools.partial(self.profiler.runcall, view_func)
func = functools.partial(self.profiler.runcall, view_func)
functools.update_wrapper(func, view_func)
return func
def process_response(self, request, response):
if not self.is_active:
@@ -116,6 +118,3 @@ class ProfilerDebugPanel(DebugPanel):
'function_calls': self.function_calls,
}
return self.render('panels/profiler.html', context)

View File

@@ -4,6 +4,7 @@ from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class RequestVarsDebugPanel(DebugPanel):
"""
A panel to display request variables (POST/GET, session, cookies).
@@ -37,11 +38,11 @@ class RequestVarsDebugPanel(DebugPanel):
'get': [(k, self.request.args.getlist(k)) for k in self.request.args],
'post': [(k, self.request.form.getlist(k)) for k in self.request.form],
'cookies': [(k, self.request.cookies.get(k)) for k in self.request.cookies],
'view_func': '%s.%s' % (self.view_func.__module__, self.view_func.__name__) if self.view_func else '[unknown]',
'view_func': ('%s.%s' % (self.view_func.__module__, self.view_func.__name__)
if self.view_func else '[unknown]'),
'view_args': self.view_args,
'view_kwargs': self.view_kwargs or {},
'session': self.session.items(),
})
return self.render('panels/request_vars.html', context)

View File

@@ -1,5 +1,3 @@
import hashlib
try:
from flask.ext.sqlalchemy import get_debug_queries, SQLAlchemy
except ImportError:
@@ -9,25 +7,58 @@ else:
sqlalchemy_available = True
from flask import request, current_app, abort, json_available, g
from flask.helpers import json
from flask_debugtoolbar import module
from flask_debugtoolbar.panels import DebugPanel
from flask_debugtoolbar.utils import format_fname, format_sql
import itsdangerous
_ = lambda x: x
def query_signer():
return itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY'],
salt='fdt-sql-query')
def is_select(statement):
prefix = b'select' if isinstance(statement, bytes) else 'select'
return statement.lower().strip().startswith(prefix)
def dump_query(statement, params):
if not params or not is_select(statement):
return None
try:
return query_signer().dumps([statement, params])
except TypeError:
return None
def load_query(data):
try:
statement, params = query_signer().loads(request.args['query'])
except (itsdangerous.BadSignature, TypeError):
abort(406)
# Make sure it is a select statement
if not is_select(statement):
abort(406)
return statement, params
class SQLAlchemyDebugPanel(DebugPanel):
"""
Panel that displays the time a response took in milliseconds.
"""
name = 'SQLAlchemy'
@property
def has_content(self):
if not json_available or not sqlalchemy_available:
return True # will display an error message
return True # will display an error message
return bool(get_debug_queries())
def process_request(self, request):
@@ -66,87 +97,35 @@ class SQLAlchemyDebugPanel(DebugPanel):
queries = get_debug_queries()
data = []
for query in queries:
is_select = query.statement.strip().lower().startswith('select')
_params = ''
try:
_params = json.dumps(query.parameters)
except TypeError:
pass # object not JSON serializable
hash = hashlib.sha1(
current_app.config['SECRET_KEY'] +
query.statement + _params).hexdigest()
data.append({
'duration': query.duration,
'sql': format_sql(query.statement, query.parameters),
'raw_sql': query.statement,
'hash': hash,
'params': _params,
'is_select': is_select,
'signed_query': dump_query(query.statement, query.parameters),
'context_long': query.context,
'context': format_fname(query.context)
})
return self.render('panels/sqlalchemy.html', { 'queries': data})
return self.render('panels/sqlalchemy.html', {'queries': data})
# Panel views
@module.route('/sqlalchemy/sql_select', methods=['GET', 'POST'])
def sql_select():
statement = request.args['sql']
params = request.args['params']
# Validate hash
hash = hashlib.sha1(
current_app.config['SECRET_KEY'] + statement + params).hexdigest()
if hash != request.args['hash']:
return abort(406)
# Make sure it is a select statement
if not statement.lower().strip().startswith('select'):
return abort(406)
params = json.loads(params)
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'],
defaults=dict(explain=True))
def sql_select(explain=False):
statement, params = load_query(request.args['query'])
engine = SQLAlchemy().get_engine(current_app)
if explain:
if engine.driver == 'pysqlite':
statement = 'EXPLAIN QUERY PLAN\n%s' % statement
else:
statement = 'EXPLAIN\n%s' % statement
result = engine.execute(statement, params)
return g.debug_toolbar.render('panels/sqlalchemy_select.html', {
'result': result.fetchall(),
'headers': result.keys(),
'sql': format_sql(statement, params),
'duration': float(request.args['duration']),
})
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'])
def sql_explain():
statement = request.args['sql']
params = request.args['params']
# Validate hash
hash = hashlib.sha1(
current_app.config['SECRET_KEY'] + statement + params).hexdigest()
if hash != request.args['hash']:
return abort(406)
# Make sure it is a select statement
if not statement.lower().strip().startswith('select'):
return abort(406)
params = json.loads(params)
engine = SQLAlchemy().get_engine(current_app)
if engine.driver == 'pysqlite':
query = 'EXPLAIN QUERY PLAN %s' % statement
else:
query = 'EXPLAIN %s' % statement
result = engine.execute(query, params)
return g.debug_toolbar.render('panels/sqlalchemy_explain.html', {
'result': result.fetchall(),
'headers': result.keys(),
'sql': format_sql(statement, params),
'duration': float(request.args['duration']),
})
})

View File

@@ -5,7 +5,10 @@ import traceback
import uuid
from jinja2.exceptions import TemplateSyntaxError
from flask import template_rendered, request, g, render_template_string, Response, current_app, abort, url_for
from flask import (
template_rendered, request, g, render_template_string,
Response, current_app, abort, url_for
)
from flask_debugtoolbar import module
from flask_debugtoolbar.panels import DebugPanel
@@ -77,6 +80,16 @@ def require_enabled():
abort(403)
def _get_source(template):
with open(template.filename, 'rb') as fp:
source = fp.read()
return source.decode(_template_encoding())
def _template_encoding():
return getattr(current_app.jinja_loader, 'encoding', 'utf-8')
@module.route('/template/<key>')
def template_editor(key):
require_enabled()
@@ -87,8 +100,7 @@ def template_editor(key):
'static_path': url_for('_debug_toolbar.static', filename=''),
'request': request,
'templates': [
{'name': t.name,
'source': open(t.filename).read()}
{'name': t.name, 'source': _get_source(t)}
for t in templates
]
})
@@ -98,8 +110,8 @@ def template_editor(key):
def save_template(key):
require_enabled()
template = TemplateDebugPanel.get_cache_for_key(key)[0]['template']
content = request.form['content']
with open(template.filename, 'w') as fp:
content = request.form['content'].encode(_template_encoding())
with open(template.filename, 'wb') as fp:
fp.write(content)
return 'ok'

View File

@@ -1,18 +1,19 @@
try:
import resource
except ImportError:
pass # Will fail on Win32 systems
pass # Will fail on Win32 systems
import time
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class TimerDebugPanel(DebugPanel):
"""
Panel that displays the time a response took in milliseconds.
"""
name = 'Timer'
try: # if resource module not available, don't show content panel
try: # if resource module not available, don't show content panel
resource
except NameError:
has_content = False
@@ -91,4 +92,3 @@ class TimerDebugPanel(DebugPanel):
})
return self.render('panels/timer.html', context)

View File

@@ -3,6 +3,7 @@ from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class VersionDebugPanel(DebugPanel):
"""
Panel that displays the Flask version.
@@ -24,5 +25,3 @@ class VersionDebugPanel(DebugPanel):
def content(self):
return None

View File

@@ -1,16 +1,16 @@
/* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */
#flDebug, #flDebug * {
margin:0;
padding:0;
border:0;
outline:0;
font-size:12px;
line-height:1.5em;
color:#000;
vertical-align:baseline;
background: none;
font-family: inherit;
text-align:left;
margin:0;
padding:0;
border:0;
outline:0;
font-size:12px;
line-height:1.5em;
color:#000;
vertical-align:baseline;
background: none;
font-family: inherit;
text-align:left;
}
#flDebug { font-family: sans-serif; color: #000; background: #fff; }
@@ -20,376 +20,320 @@
}
#flDebug #flDebugToolbar {
background:#111;
width:200px;
z-index:100000000;
position:fixed;
top:0;
bottom:0;
right:0;
opacity:0.9;
background:#111;
width:200px;
z-index:100000000;
position:fixed;
top:0;
bottom:0;
right:0;
opacity:0.9;
}
#flDebug #flDebugToolbar small {
color:#999;
color:#999;
}
#flDebug #flDebugToolbar ul {
margin:0;
padding:0;
list-style:none;
margin:0;
padding:0;
list-style:none;
}
#flDebug #flDebugToolbar li {
border-bottom:1px solid #222;
color:#fff;
display:block;
font-weight:bold;
float:none;
margin:0;
padding:0;
position:relative;
width:auto;
border-bottom:1px solid #222;
color:#fff;
display:block;
font-weight:bold;
float:none;
margin:0;
padding:0;
position:relative;
width:auto;
}
#flDebug #flDebugToolbar li>a,
#flDebug #flDebugToolbar li>div.contentless {
font-weight:normal;
font-style:normal;
text-decoration:none;
display:block;
font-size:16px;
padding:10px 10px 5px 25px;
color:#fff;
font-weight:normal;
font-style:normal;
text-decoration:none;
display:block;
font-size:16px;
padding:10px 10px 5px 25px;
color:#fff;
}
#flDebug #flDebugToolbar li a:hover {
color:#111;
background-color:#ffc;
color:#111;
background-color:#ffc;
}
#flDebug #flDebugToolbar li.active {
background-image:url(../img/indicator.png);
background-repeat:no-repeat;
background-position:left center;
background-color:#333;
padding-left:10px;
background-image:url(../img/indicator.png);
background-repeat:no-repeat;
background-position:left center;
background-color:#333;
padding-left:10px;
}
#flDebug #flDebugToolbar li.active a:hover {
color:#b36a60;
background-color:transparent;
color:#b36a60;
background-color:transparent;
}
#flDebug #flDebugToolbar li small {
font-size:12px;
color:#999;
font-style:normal;
text-decoration:none;
font-variant:small-caps;
font-size:12px;
color:#999;
font-style:normal;
text-decoration:none;
font-variant:small-caps;
}
#flDebug #flDebugToolbar li .switch {
font-size: 10px;
position: absolute;
display: block;
color: white;
height: 16px;
width: 16px;
cursor: pointer;
top: 15px;
right: 2px;
font-size: 10px;
position: absolute;
display: block;
color: white;
height: 16px;
width: 16px;
cursor: pointer;
top: 15px;
right: 2px;
}
#flDebug #flDebugToolbar li .switch.active {
background-image: url(../img/tick.png);
background-image: url(../img/tick.png);
}
#flDebug #flDebugToolbar li .switch.inactive {
background-image: url(../img/tick-red.png);
background-image: url(../img/tick-red.png);
}
#flDebug #flDebugToolbarHandle {
position:fixed;
background:#fff;
border:1px solid #111;
top:30px;
right:0;
z-index:100000000;
opacity:0.75;
position:fixed;
background:#fff;
border:1px solid #111;
top:30px;
right:0;
z-index:100000000;
opacity:0.75;
}
#flDebug a#flShowToolBarButton {
display:block;
height:75px;
width:30px;
border-right:none;
border-bottom:4px solid #fff;
border-top:4px solid #fff;
border-left:4px solid #fff;
color:#fff;
font-size:10px;
font-weight:bold;
text-decoration:none;
text-align:center;
text-indent:-999999px;
background:#000 url(../img/djdt_vertical.png) no-repeat left center;
opacity:0.5;
display:block;
height:75px;
width:30px;
border-right:none;
border-bottom:4px solid #fff;
border-top:4px solid #fff;
border-left:4px solid #fff;
color:#fff;
font-size:10px;
font-weight:bold;
text-decoration:none;
text-align:center;
text-indent:-999999px;
background:#000 url(../img/djdt_vertical.png) no-repeat left center;
opacity:0.5;
}
#flDebug a#flShowToolBarButton:hover {
background-color:#111;
padding-right:6px;
border-top-color:#FFE761;
border-left-color:#FFE761;
border-bottom-color:#FFE761;
opacity:1.0;
background-color:#111;
padding-right:6px;
border-top-color:#FFE761;
border-left-color:#FFE761;
border-bottom-color:#FFE761;
opacity:1.0;
}
#flDebug code {
display:block;
white-space:pre;
overflow:auto;
display:block;
white-space:pre;
overflow:auto;
}
#flDebug tr.flDebugOdd {
background-color:#f5f5f5;
background-color:#f5f5f5;
}
#flDebug .panelContent {
display:none;
position:fixed;
margin:0;
top:0;
right:200px;
bottom:0;
left:0px;
background-color:#eee;
color:#666;
z-index:100000000;
display:none;
position:fixed;
margin:0;
top:0;
right:200px;
bottom:0;
left:0px;
background-color:#eee;
color:#666;
z-index:100000000;
}
#flDebug .panelContent > div {
border-bottom:1px solid #ddd;
border-bottom:1px solid #ddd;
}
#flDebug .flDebugPanelTitle {
position:absolute;
background-color:#ffc;
color:#666;
padding-left:20px;
top:0;
right:0;
left:0;
height:50px;
position:absolute;
background-color:#ffc;
color:#666;
padding-left:20px;
top:0;
right:0;
left:0;
height:50px;
}
#flDebug .flDebugPanelTitle code {
display:inline;
font-size:inherit;
display:inline;
font-size:inherit;
}
#flDebug .flDebugPanelContent {
position:absolute;
top:50px;
right:0;
bottom:0;
left:0;
height:auto;
padding:0 0 0 20px;
position:absolute;
top:50px;
right:0;
bottom:0;
left:0;
height:auto;
padding:0 0 0 20px;
}
#flDebug .flDebugPanelContent .scroll {
height:100%;
overflow:auto;
display:block;
padding:0 10px 0 0;
height:100%;
overflow:auto;
display:block;
padding:0 10px 0 0;
}
#flDebug h3 {
font-size:24px;
font-weight:normal;
line-height:50px;
font-size:24px;
font-weight:normal;
line-height:50px;
}
#flDebug h4 {
font-size:20px;
font-weight:bold;
margin-top:0.8em;
font-size:20px;
font-weight:bold;
margin-top:0.8em;
}
#flDebug .panelContent table {
border:1px solid #ccc;
border-collapse:collapse;
width:100%;
background-color:#fff;
display:block;
margin-top:0.8em;
overflow: auto;
border:1px solid #ccc;
border-collapse:collapse;
width:100%;
background-color:#fff;
display:block;
margin-top:0.8em;
overflow: auto;
}
#flDebug .panelContent tbody td,
#flDebug .panelContent tbody th {
vertical-align:top;
padding:2px 3px;
vertical-align:top;
padding:2px 3px;
}
#flDebug .panelContent thead th {
padding:1px 6px 1px 3px;
text-align:left;
font-weight:bold;
font-size:14px;
padding:1px 6px 1px 3px;
text-align:left;
font-weight:bold;
font-size:14px;
}
#flDebug .panelContent tbody th {
width:12em;
text-align:right;
color:#666;
padding-right:.5em;
width:12em;
text-align:right;
color:#666;
padding-right:.5em;
}
#flDebug .flTemplateHideContextDiv {
background-color:#fff;
background-color:#fff;
}
/*
#flDebug .panelContent p a:hover, #flDebug .panelContent dd a:hover {
color:#111;
background-color:#ffc;
}
#flDebug .panelContent p {
padding:0 5px;
}
#flDebug .panelContent p, #flDebug .panelContent table, #flDebug .panelContent ol, #flDebug .panelContent ul, #flDebug .panelContent dl {
margin:5px 0 15px;
background-color:#fff;
}
#flDebug .panelContent table {
clear:both;
border:0;
padding:0;
margin:0;
border-collapse:collapse;
border-spacing:0;
}
#flDebug .panelContent table a {
color:#000;
padding:2px 4px;
}
#flDebug .panelContent table a:hover {
background-color:#ffc;
}
#flDebug .panelContent table th {
background-color:#333;
font-weight:bold;
color:#fff;
padding:3px 7px 3px;
text-align:left;
cursor:pointer;
}
#flDebug .panelContent table td {
padding:5px 10px;
font-size:14px;
background:#fff;
color:#000;
vertical-align:top;
border:0;
}
#flDebug .panelContent table tr.flDebugOdd td {
background:#eee;
}
*/
#flDebug .panelContent .flDebugClose {
text-indent:-9999999px;
display:block;
position:absolute;
top:4px;
right:15px;
height:40px;
width:40px;
background:url(../img/close.png) no-repeat center center;
text-indent:-9999999px;
display:block;
position:absolute;
top:4px;
right:15px;
height:40px;
width:40px;
background:url(../img/close.png) no-repeat center center;
}
#flDebug .panelContent .flDebugClose:hover {
background-image:url(../img/close_hover.png);
background-image:url(../img/close_hover.png);
}
#flDebug .panelContent .flDebugClose.flDebugBack {
background-image:url(../img/back.png);
background-image:url(../img/back.png);
}
#flDebug .panelContent .flDebugClose.flDebugBack:hover {
background-image:url(../img/back_hover.png);
background-image:url(../img/back_hover.png);
}
#flDebug .panelContent dt, #flDebug .panelContent dd {
display:block;
display:block;
}
#flDebug .panelContent dt {
margin-top:0.75em;
margin-top:0.75em;
}
#flDebug .panelContent dd {
margin-left:10px;
margin-left:10px;
}
#flDebug a.toggleTemplate {
padding:4px;
background-color:#bbb;
-moz-border-radius:3px;
-webkit-border-radius:3px;
padding:4px;
background-color:#bbb;
-moz-border-radius:3px;
-webkit-border-radius:3px;
}
#flDebug a.toggleTemplate:hover {
padding:4px;
background-color:#444;
color:#ffe761;
-moz-border-radius:3px;
-webkit-border-radius:3px;
padding:4px;
background-color:#444;
color:#ffe761;
-moz-border-radius:3px;
-webkit-border-radius:3px;
}
#flDebug a.flTemplateShowContext, #flDebug a.flTemplateShowContext span.toggleArrow {
color:#999;
color:#999;
}
#flDebug a.flTemplateShowContext:hover, #flDebug a.flTemplateShowContext:hover span.toggleArrow {
color:#000;
cursor:pointer;
color:#000;
cursor:pointer;
}
#flDebug .flDebugSqlWrap {
position:relative;
position:relative;
}
#flDebug .flDebugSql {
z-index:100000002;
z-index:100000002;
}
#flDebug .flSQLHideStacktraceDiv tbody th {
text-align: left;
}
#flDebug .flSqlExplain td {
white-space: pre;
text-align: left;
}
#flDebug span.flDebugLineChart {
background-color:#777;
height:3px;
position:absolute;
bottom:0;
top:0;
left:0;
display:block;
z-index:1000000001;
background-color:#777;
height:3px;
position:absolute;
bottom:0;
top:0;
left:0;
display:block;
z-index:1000000001;
}
#flDebug span.flDebugLineChartWarning {
background-color:#900;
background-color:#900;
}
#flDebug .highlight { color:#000; }
@@ -414,24 +358,24 @@
/* tablesorted */
#flDebug table.tablesorter {
width: 100%;
width: 100%;
}
#flDebug table.tablesorter thead th, table.tablesorter tfoot th {
padding-right: 20px;
padding-right: 20px;
}
#flDebug table.tablesorter thead th {
background: url(../img/bg.gif) center right no-repeat;
cursor: pointer;
background: url(../img/bg.gif) center right no-repeat;
cursor: pointer;
}
#flDebug table.tablesorter tbody tr.odd td {
background-color: #F0F0F6;
background-color: #F0F0F6;
}
#flDebug table.tablesorter thead .headerSortUp {
background-image: url(../img/asc.gif);
background-image: url(../img/asc.gif);
}
#flDebug table.tablesorter thead .headerSortDown {
background-image: url(../img/desc.gif);
background-image: url(../img/desc.gif);
}
#flDebug table.tablesorter thead .headerSortDown, #flDebug table.tablesorter thead .headerSortUp {
background-color: #8dbdd8;
background-color: #8dbdd8;
}

View File

@@ -1,184 +1,192 @@
(function($) {
$.cookie = function(name, value, options) { if (typeof value != 'undefined') { options = options || {}; if (value === null) { value = ''; options.expires = -1; } var expires = ''; if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { var date; if (typeof options.expires == 'number') { date = new Date(); date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); } else { date = options.expires; } expires = '; expires=' + date.toUTCString(); } var path = options.path ? '; path=' + (options.path) : ''; var domain = options.domain ? '; domain=' + (options.domain) : ''; var secure = options.secure ? '; secure' : ''; document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); } else { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = $.trim(cookies[i]); if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } };
$('head').append('<link rel="stylesheet" href="'+DEBUG_TOOLBAR_STATIC_PATH+'css/toolbar.css?'+ Math.random() +'" type="text/css" />');
var COOKIE_NAME = 'fldt';
var COOKIE_NAME_ACTIVE = COOKIE_NAME +'_active';
var fldt = {
init: function() {
$('#flDebug').show();
var current = null;
$('#flDebugPanelList li a').click(function() {
if (!this.className) {
return false;
}
current = $('#flDebug #' + this.className + '-content');
if (current.is(':visible')) {
$(document).trigger('close.flDebug');
$(this).parent().removeClass('active');
} else {
$('.panelContent').hide(); // Hide any that are already open
current.show();
$('#flDebugToolbar li').removeClass('active');
$(this).parent().addClass('active');
}
return false;
});
$('#flDebugPanelList li .switch').click(function() {
var $panel = $(this).parent();
var $this = $(this);
var dom_id = $panel.attr('id');
// Turn cookie content into an array of active panels
var active_str = $.cookie(COOKIE_NAME_ACTIVE);
var active = (active_str) ? active_str.split(';') : [];
active = $.grep(active, function(n,i) { return n != dom_id; });
if ($this.hasClass('active')) {
$this.removeClass('active');
$this.addClass('inactive');
}
else {
active.push(dom_id);
$this.removeClass('inactive');
$this.addClass('active');
}
if (active.length > 0) {
$.cookie(COOKIE_NAME_ACTIVE, active.join(';'), {
path: '/', expires: 10
});
}
else {
$.cookie(COOKIE_NAME_ACTIVE, null, {
path: '/', expires: -1
});
}
});
$('#flDebug a.flDebugClose').click(function() {
$(document).trigger('close.flDebug');
$('#flDebugToolbar li').removeClass('active');
return false;
});
$('#flDebug a.remoteCall').click(function() {
$('#flDebugWindow').load(this.href, {}, function() {
$('#flDebugWindow a.flDebugBack').click(function() {
$(this).parent().parent().hide();
return false;
});
});
$('#flDebugWindow').show();
return false;
});
$('#flDebugTemplatePanel a.flTemplateShowContext').click(function() {
fldt.toggle_arrow($(this).children('.toggleArrow'))
fldt.toggle_content($(this).parent().next());
return false;
});
$('#flDebugSQLPanel a.flSQLShowStacktrace').click(function() {
fldt.toggle_content($('.flSQLHideStacktraceDiv', $(this).parents('tr')));
return false;
});
$('#flHideToolBarButton').click(function() {
fldt.hide_toolbar(true);
return false;
});
$('#flShowToolBarButton').click(function() {
fldt.show_toolbar();
return false;
});
$(document).bind('close.flDebug', function() {
// If a sub-panel is open, close that
if ($('#flDebugWindow').is(':visible')) {
$('#flDebugWindow').hide();
return;
}
// If a panel is open, close that
if ($('.panelContent').is(':visible')) {
$('.panelContent').hide();
return;
}
// Otherwise, just minimize the toolbar
if ($('#flDebugToolbar').is(':visible')) {
fldt.hide_toolbar(true);
return;
}
});
if ($.cookie(COOKIE_NAME)) {
fldt.hide_toolbar(false);
} else {
fldt.show_toolbar(false);
}
$('#flDebug table.tablesorter')
.tablesorter()
.bind('sortStart', function() {
$(this).find('tbody tr')
.removeClass('flDebugEven')
.removeClass('flDebugOdd');
})
.bind('sortEnd', function() {
$(this).find('tbody tr').each(function(idx, elem) {
var even = idx % 2 == 0;
$(elem)
.toggleClass('flDebugEven', even)
.toggleClass('flDebugOdd', !even);
});
});
},
toggle_content: function(elem) {
if (elem.is(':visible')) {
elem.hide();
} else {
elem.show();
}
},
close: function() {
$(document).trigger('close.flDebug');
return false;
},
hide_toolbar: function(setCookie) {
// close any sub panels
$('#flDebugWindow').hide();
// close all panels
$('.panelContent').hide();
$('#flDebugToolbar li').removeClass('active');
// finally close toolbar
$('#flDebugToolbar').hide('fast');
$('#flDebugToolbarHandle').show();
// Unbind keydown
$(document).unbind('keydown.flDebug');
if (setCookie) {
$.cookie(COOKIE_NAME, 'hide', {
path: '/',
expires: 10
});
}
},
show_toolbar: function(animate) {
// Set up keybindings
$(document).bind('keydown.flDebug', function(e) {
if (e.keyCode == 27) {
fldt.close();
}
});
$('#flDebugToolbarHandle').hide();
if (animate) {
$('#flDebugToolbar').show('fast');
} else {
$('#flDebugToolbar').show();
}
$.cookie(COOKIE_NAME, null, {
path: '/',
expires: -1
});
},
toggle_arrow: function(elem) {
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
elem.html(elem.html() == uarr ? darr : uarr);
$.cookie = function(name, value, options) { if (typeof value != 'undefined') { options = options || {}; if (value === null) { value = ''; options.expires = -1; } var expires = ''; if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { var date; if (typeof options.expires == 'number') { date = new Date(); date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); } else { date = options.expires; } expires = '; expires=' + date.toUTCString(); } var path = options.path ? '; path=' + (options.path) : ''; var domain = options.domain ? '; domain=' + (options.domain) : ''; var secure = options.secure ? '; secure' : ''; document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); } else { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = $.trim(cookies[i]); if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } };
$('head').append('<link rel="stylesheet" href="'+DEBUG_TOOLBAR_STATIC_PATH+'css/toolbar.css?'+ Math.random() +'" type="text/css" />');
var COOKIE_NAME = 'fldt';
var COOKIE_NAME_ACTIVE = COOKIE_NAME +'_active';
var fldt = {
init: function() {
$('#flDebug').show();
var current = null;
$('#flDebugPanelList li a').click(function() {
if (!this.className) {
return false;
}
};
$(document).ready(function() {
fldt.init();
});
current = $('#flDebug #' + this.className + '-content');
if (current.is(':visible')) {
$(document).trigger('close.flDebug');
$(this).parent().removeClass('active');
} else {
$('.panelContent').hide(); // Hide any that are already open
current.show();
$('#flDebugToolbar li').removeClass('active');
$(this).parent().addClass('active');
}
return false;
});
$('#flDebugPanelList li .switch').click(function() {
var $panel = $(this).parent();
var $this = $(this);
var dom_id = $panel.attr('id');
// Turn cookie content into an array of active panels
var active_str = $.cookie(COOKIE_NAME_ACTIVE);
var active = (active_str) ? active_str.split(';') : [];
active = $.grep(active, function(n,i) { return n != dom_id; });
if ($this.hasClass('active')) {
$this.removeClass('active');
$this.addClass('inactive');
} else {
active.push(dom_id);
$this.removeClass('inactive');
$this.addClass('active');
}
if (active.length > 0) {
$.cookie(COOKIE_NAME_ACTIVE, active.join(';'), {
path: '/', expires: 10
});
} else {
$.cookie(COOKIE_NAME_ACTIVE, null, {
path: '/', expires: -1
});
}
});
$('#flDebug a.flDebugClose').click(function() {
$(document).trigger('close.flDebug');
$('#flDebugToolbar li').removeClass('active');
return false;
});
$('#flDebug a.remoteCall').click(function() {
$('#flDebugWindow').load(this.href, {}, function() {
$('#flDebugWindow a.flDebugBack').click(function() {
$(this).parent().parent().hide();
return false;
});
});
$('#flDebugWindow').show();
return false;
});
$('#flDebugTemplatePanel a.flTemplateShowContext').click(function() {
fldt.toggle_arrow($(this).children('.toggleArrow'))
fldt.toggle_content($(this).parent().next());
return false;
});
$('#flDebugSQLPanel a.flSQLShowStacktrace').click(function() {
fldt.toggle_content($('.flSQLHideStacktraceDiv', $(this).parents('tr')));
return false;
});
$('#flHideToolBarButton').click(function() {
fldt.hide_toolbar(true);
return false;
});
$('#flShowToolBarButton').click(function() {
fldt.show_toolbar();
return false;
});
$(document).bind('close.flDebug', function() {
// If a sub-panel is open, close that
if ($('#flDebugWindow').is(':visible')) {
$('#flDebugWindow').hide();
return;
}
// If a panel is open, close that
if ($('.panelContent').is(':visible')) {
$('.panelContent').hide();
return;
}
// Otherwise, just minimize the toolbar
if ($('#flDebugToolbar').is(':visible')) {
fldt.hide_toolbar(true);
return;
}
});
if ($.cookie(COOKIE_NAME)) {
fldt.hide_toolbar(false);
} else {
fldt.show_toolbar(false);
}
$('#flDebug table.tablesorter')
.tablesorter()
.bind('sortStart', function() {
$(this).find('tbody tr')
.removeClass('flDebugEven')
.removeClass('flDebugOdd');
})
.bind('sortEnd', function() {
$(this).find('tbody tr').each(function(idx, elem) {
var even = idx % 2 == 0;
$(elem)
.toggleClass('flDebugEven', even)
.toggleClass('flDebugOdd', !even);
});
});
},
toggle_content: function(elem) {
if (elem.is(':visible')) {
elem.hide();
} else {
elem.show();
}
},
close: function() {
$(document).trigger('close.flDebug');
return false;
},
hide_toolbar: function(setCookie) {
// close any sub panels
$('#flDebugWindow').hide();
// close all panels
$('.panelContent').hide();
$('#flDebugToolbar li').removeClass('active');
// finally close toolbar
$('#flDebugToolbar').hide('fast');
$('#flDebugToolbarHandle').show();
// Unbind keydown
$(document).unbind('keydown.flDebug');
if (setCookie) {
$.cookie(COOKIE_NAME, 'hide', {
path: '/',
expires: 10
});
}
},
show_toolbar: function(animate) {
// Set up keybindings
$(document).bind('keydown.flDebug', function(e) {
if (e.keyCode == 27) {
fldt.close();
}
});
$('#flDebugToolbarHandle').hide();
if (animate) {
$('#flDebugToolbar').show('fast');
} else {
$('#flDebugToolbar').show();
}
$.cookie(COOKIE_NAME, null, {
path: '/',
expires: -1
});
},
toggle_arrow: function(elem) {
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
elem.html(elem.html() == uarr ? darr : uarr);
},
load_href: function(href) {
$.get(href, function(data, status, xhr) {
document.open();
document.write(xhr.responseText);
document.close();
});
return false;
},
$: $
};
$(document).ready(function() {
fldt.init();
});
window.fldt = fldt;
})(jQuery.noConflict(true));

View File

@@ -1,56 +1,57 @@
<div id="flDebug" style="display:none;">
<script type="text/javascript">var DEBUG_TOOLBAR_STATIC_PATH = '{{ static_path }}'</script>
<script type="text/javascript" src="{{ static_path }}js/jquery.js"></script>
<script type="text/javascript" src="{{ static_path }}js/jquery.tablesorter.js"></script>
<script type="text/javascript" src="{{ static_path }}js/toolbar.js"></script>
<script type="text/javascript">var DEBUG_TOOLBAR_STATIC_PATH = '{{ static_path }}'</script>
<script type="text/javascript" src="{{ static_path }}js/jquery.js"></script>
<script type="text/javascript" src="{{ static_path }}js/jquery.tablesorter.js"></script>
<script type="text/javascript" src="{{ static_path }}js/toolbar.js"></script>
<div style="display: none;" id="flDebugToolbar">
<ol id="flDebugPanelList">
{% if panels %}
<li><a id="flHideToolBarButton" href="#" title="Hide Toolbar">Hide &raquo;</a></li>
{% else %}
<li id="flDebugButton">DEBUG</li>
{% endif %}
{% for panel in panels %}
<li id="{{ panel.dom_id() }}">
{% if panel.has_content %}
<a href="{{ panel.url()|default("#") }}" title="{{ panel.title() }}" class="{{ panel.dom_id() }}">
{% else %}
<div class="contentless">
{% endif %}
<div style="display: none;" id="flDebugToolbar">
<ol id="flDebugPanelList">
{% if panels %}
<li><a id="flHideToolBarButton" href="#" title="Hide Toolbar">Hide &raquo;</a></li>
{% else %}
<li id="flDebugButton">DEBUG</li>
{% endif %}
{{ panel.nav_title() }}
{% if panel.nav_subtitle() %}<br /><small>{{ panel.nav_subtitle() }}</small>{% endif %}
{% for panel in panels %}
<li id="{{ panel.dom_id() }}">
{% if panel.has_content %}
<a href="{{ panel.url()|default("#") }}" title="{{ panel.title() }}" class="{{ panel.dom_id() }}">
{% else %}
<div class="contentless">
{% endif %}
{% if panel.has_content %}
</a>
{% else %}
</div>
{% endif %}
{% if panel.user_activate %}
<span class="switch {{ 'active' if panel.is_active else 'inactive' }}" title="Enable or disable the panel"></span>
{% endif %}
</li>
{% endfor %}
</ol>
{{ panel.nav_title() }}
{% if panel.nav_subtitle() %}<br /><small>{{ panel.nav_subtitle() }}</small>{% endif %}
{% if panel.has_content %}
</a>
{% else %}
</div>
{% endif %}
{% if panel.user_activate %}
<span class="switch {{ 'active' if panel.is_active else 'inactive' }}" title="Enable or disable the panel"></span>
{% endif %}
</li>
{% endfor %}
</ol>
</div>
<div style="display:none;" id="flDebugToolbarHandle">
<a title="Show Toolbar" id="flShowToolBarButton" href="#">&laquo;</a>
</div>
{% for panel in panels %}
{% if panel.has_content %}
<div id="{{ panel.dom_id() }}-content" class="panelContent">
<div class="flDebugPanelTitle">
<a href="" class="flDebugClose">Close</a>
<h3>{{ panel.title()|safe }}</h3>
</div>
<div class="flDebugPanelContent">
<div class="scroll">
{{ panel.content()|safe }}
</div>
</div>
</div>
<div style="display:none;" id="flDebugToolbarHandle">
<a title="Show Toolbar" id="flShowToolBarButton" href="#">&laquo;</a>
</div>
{% for panel in panels %}
{% if panel.has_content %}
<div id="{{ panel.dom_id() }}-content" class="panelContent">
<div class="flDebugPanelTitle">
<a href="" class="flDebugClose">Close</a>
<h3>{{ panel.title()|safe }}</h3>
</div>
<div class="flDebugPanelContent">
<div class="scroll">
{{ panel.content()|safe }}
</div>
</div>
</div>
{% endif %}
{% endfor %}
<div id="flDebugWindow" class="panelContent"></div>
{% endif %}
{% endfor %}
<div id="flDebugWindow" class="panelContent"></div>
</div>

View File

@@ -0,0 +1,16 @@
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in config|dictsort %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key }}</td>
<td><code>{{ value|printable }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -1,16 +1,16 @@
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in headers.iteritems() %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|escape }}</td>
<td>{{ value|escape }}</td>
</tr>
{% endfor %}
</tbody>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in headers|dictsort %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|escape }}</td>
<td>{{ value|escape }}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -1,26 +1,26 @@
{% if records %}
<table>
<thead>
<tr>
<th>Level</th>
<th>Time</th>
<th>Message</th>
<th>Location</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ record.level }}</td>
<td>{{ record.time }}</td>
<td>{{ record.message }}</td>
<td title="{{ record.file_long }}:{{ record.line }}">{{ record.file }}:{{ record.line }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<thead>
<tr>
<th>Level</th>
<th>Time</th>
<th>Message</th>
<th>Location</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ record.level }}</td>
<td>{{ record.time }}</td>
<td>{{ record.message }}</td>
<td title="{{ record.file_long }}:{{ record.line }}">{{ record.file }}:{{ record.line }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No messages logged.</p>
<p>No messages logged.</p>
{% endif %}

View File

@@ -1,25 +1,25 @@
<table id="debug_toolbar_profiler_table" class="tablesorter">
<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="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ row.ncalls }}</td>
<td>{{ row.tottime }}</td>
<td>{{ '%.4f'|format(row.percall) }}</td>
<td>{{ row.cumtime }}</td>
<td>{{ '%.4f'|format(row.percall_cum) }}</td>
<td title="{{ row.filename_long }}">{{ row.filename|escape }}</td>
</tr>
{% endfor %}
</tbody>
<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="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ row.ncalls }}</td>
<td>{{ row.tottime }}</td>
<td>{{ '%.4f'|format(row.percall) }}</td>
<td>{{ row.cumtime }}</td>
<td>{{ '%.4f'|format(row.percall_cum) }}</td>
<td title="{{ row.filename_long }}">{{ row.filename|escape }}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -1,104 +1,104 @@
<h4>View information</h4>
<table>
<thead>
<tr>
<th>View Function</th>
<th>args</th>
<th>kwargs</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ view_func }}</td>
<td>{{ view_args|default("None") }}</td>
<td>
{% if view_kwargs.items() %}
{% for k, v in view_kwargs.items() %}
{{ k }}={{ v }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
None
{% endif %}
</td>
</tr>
</tbody>
<thead>
<tr>
<th>View Function</th>
<th>args</th>
<th>kwargs</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ view_func }}</td>
<td>{{ view_args|default("None") }}</td>
<td>
{% if view_kwargs.items() %}
{% for k, v in view_kwargs.items() %}
{{ k }}={{ v }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
None
{% endif %}
</td>
</tr>
</tbody>
</table>
{% macro show_map(map) %}
<table>
<colgroup>
<col style="width:20%"/>
<col/>
</colgroup>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in map %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|printable }}</td>
<td>{{ value|printable }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<colgroup>
<col style="width:20%"/>
<col/>
</colgroup>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in map %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|printable }}</td>
<td>{{ value|printable }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
<h4>COOKIES Variables</h4>
{% if cookies %}
{{ show_map(cookies) }}
{{ show_map(cookies) }}
{% else %}
<p>No COOKIE data</p>
<p>No COOKIE data</p>
{% endif %}
<h4>SESSION Variables</h4>
{% if session %}
{{ show_map(session) }}
{{ show_map(session) }}
{% else %}
<p>No SESSION data</p>
<p>No SESSION data</p>
{% endif %}
{% macro show_multi_map(map) %}
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in map %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|printable }}</td>
<td>
{%- set sep = joiner() -%}
{%- for v in value -%}
{{ sep() }}{{ v|printable }}
{%- endfor -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in map %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|printable }}</td>
<td>
{%- set sep = joiner() -%}
{%- for v in value -%}
{{ sep() }}{{ v|printable }}
{%- endfor -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
<h4>GET Variables</h4>
{% if get %}
{{ show_multi_map(get) }}
{{ show_multi_map(get) }}
{% else %}
<p>No GET data</p>
<p>No GET data</p>
{% endif %}
<h4>POST Variables</h4>
{% if post %}
{{ show_multi_map(post) }}
{{ show_multi_map(post) }}
{% else %}
<p>No POST data</p>
<p>No POST data</p>
{% endif %}

View File

@@ -1,68 +1,31 @@
<table>
<thead>
<tr>
<th>&nbsp;(ms)</th>
<th>Action</th>
<th>Context</th>
<th>Query</th>
</tr>
</thead>
<tbody>
{% for query in queries %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ '%.4f'|format(query.duration) }}</td>
<td>
{% if query.params %}
{% if query.is_select %}
<a class="remoteCall" href="/_debug_toolbar/views/sqlalchemy/sql_select?sql={{ query.raw_sql|urlencode }}&amp;params={{ query.params|urlencode }}&amp;duration={{ query.duration|urlencode }}&amp;hash={{ query.hash }}">SELECT</a><br />
<a class="remoteCall" href="/_debug_toolbar/views/sqlalchemy/sql_explain?sql={{ query.raw_sql|urlencode }}&amp;params={{ query.params|urlencode }}&amp;duration={{ query.duration|urlencode }}&amp;hash={{ query.hash }}">EXPLAIN</a><br />
{% if is_mysql %}
<a class="remoteCall" href="/__debug__/sql_profile/?sql={{ query.raw_sql|urlencode }}&amp;params={{ query.params|urlencode }}&amp;duration={{ query.duration|urlencode }}&amp;hash={{ query.hash }}">PROFILE</a><br />
{% endif %}
{% endif %}
{% endif %}
</td>
<td title="{{ query.context_long }}">
<thead>
<tr>
<th>&nbsp;(ms)</th>
<th>Action</th>
<th>Context</th>
<th>Query</th>
</tr>
</thead>
<tbody>
{% for query in queries %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ '%.4f'|format(query.duration * 1000) }}</td>
<td>
{% if query.signed_query %}
<a class="remoteCall" href="/_debug_toolbar/views/sqlalchemy/sql_select?query={{ query.signed_query }}&amp;duration={{ query.duration|urlencode }}">SELECT</a><br />
<a class="remoteCall" href="/_debug_toolbar/views/sqlalchemy/sql_explain?query={{ query.signed_query }}&amp;duration={{ query.duration|urlencode }}">EXPLAIN</a><br />
{% endif %}
</td>
<td title="{{ query.context_long }}">
{{ query.context }}
</td>
<td class="syntax">
<div class="flDebugSqlWrap">
<div class="flDebugSql">{{ query.sql|safe }}</div>
{#
{% if query.stacktrace %}
<div class="djSQLHideStacktraceDiv" style="display:none;">
<table>
<tr>
<th>{% trans "Line" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "File" %}</th>
</tr>
{% for file, line, method in query.stacktrace %}
<tr>
<td>{{ line }}</td>
<td><code>{{ method|escape }}</code></td>
<td><code>{{ file|escape }}</code></td>
</tr>
{% endfor %}
</table>
{% if query.template_info %}
<table>
{% for line in query.template_info.context %}
<tr>
<td>{{ line.num }}</td>
<td><code style="font-family: monospace;{% if line.highlight %}background-color: lightgrey{% endif %}">{{ line.content }}</code></td>
</tr>
{% endfor %}
</table>
<p><strong>{{ query.template_info.name|default:"(unknown)" }}</strong></p>
{% endif %}
</div>
{% endif %}
<span class="djDebugLineChart{% if query.is_slow %} djDebugLineChartWarning{% endif %}" style="width:{{ query.width_ratio }}%; left:{{ query.start_offset }}%;"></span>
#}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</td>
<td class="syntax">
<div class="flDebugSqlWrap">
<div class="flDebugSql">{{ query.sql }}</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -1,33 +0,0 @@
<div class="flDebugPanelTitle">
<a class="flDebugClose flDebugBack" href="">Back</a>
<h3>SQL Explained</h3>
</div>
<div class="flDebugPanelContent">
<div class="scroll">
<dl>
<dt>Executed SQL</dt>
<dd>{{ sql|safe }}</dd>
<dt>Time</dt>
<dd>{{ '%.4f'|format(duration) }} ms</dd>
</dl>
<table class="flSqlExplain">
<thead>
<tr>
{% for h in headers %}
<th>{{ h|upper }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in result %}
<tr class="{{ loop.cycle('fjDebugOdd', 'fjDebugEven') }}">
{% for column in row %}
<td>{{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@@ -1,37 +1,36 @@
<div class="flDebugPanelTitle">
<a class="flDebugClose flDebugBack" href="">Back</a>
<h3>SQL Explained</h3>
<a class="flDebugClose flDebugBack" href="">Back</a>
<h3>SQL Details</h3>
</div>
<div class="flDebugPanelContent">
<div class="scroll">
<dl>
<dt>Executed SQL</dt>
<dd>{{ sql|safe }}</dd>
<dt>Time</dt>
<dd>{{ '%.4f'|format(duration) }} ms</dd>
</dl>
{% if result %}
<table class="flSqlSelect">
<thead>
<tr>
{% for h in headers %}
<th>{{ h|upper }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in result %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
{% for column in row %}
<td>{{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Empty set</p>
{% endif %}
</div>
<div class="scroll">
<dl>
<dt>Executed SQL</dt>
<dd>{{ sql }}</dd>
<dt>Original query duration</dt>
<dd>{{ '%.4f'|format(duration * 1000) }} ms</dd>
</dl>
{% if result %}
<table class="flSqlSelect">
<thead>
<tr>
{% for h in headers %}
<th>{{ h|upper }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in result %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
{% for column in row %}
<td>{{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Empty set</p>
{% endif %}
</div>
</div>

View File

@@ -1,36 +1,26 @@
{% if templates %}
{% if editable %}
<script>
function loadHref() {
$.get(this.href, function(data, status, xhr) {
document.open();
document.write(xhr.responseText);
document.close();
});
return false;
}
</script>
<a href="/_debug_toolbar/views/template/{{ key }}" onclick="return loadHref.apply(this);">Edit templates</a>
{% endif %}
{% for template in templates %}
<h4>{{ template.template.name }}</h4>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for k, v in template.context|dictsort %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ k }}</td>
<td>{{ v|printable }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% if editable %}
<a href="/_debug_toolbar/views/template/{{ key }}" onclick="return fldt.load_href(this.href);">Edit templates</a>
{% endif %}
{% for template in templates %}
<h4>{{ template.template.name }}</h4>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for k, v in template.context|dictsort %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ k }}</td>
<td>{{ v|printable }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% else %}
<p>No template rendered</p>
<p>No template rendered</p>
{% endif %}

View File

@@ -1,21 +1,21 @@
<table>
<colgroup>
<col style="width:20%"/>
<col/>
</colgroup>
<thead>
<tr>
<th>Resource</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in rows %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|escape }}</td>
<td>{{ value|escape }}</td>
</tr>
{% endfor %}
</tbody>
<colgroup>
<col style="width:20%"/>
<col/>
</colgroup>
<thead>
<tr>
<th>Resource</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in rows %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ key|escape }}</td>
<td>{{ value|escape }}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -1,15 +1,15 @@
<html>
<head>
<title>Redirect intercepted</title>
</head>
<body>
<h1>Redirect ({{ redirect_code }})</h1>
<p>Location: <a href="{{ redirect_to }}">{{ redirect_to }}</a></p>
<p class="notice">
The Flask Debug Toolbar has intercepted a redirect to the above URL for
debug viewing purposes. You can click the above link to continue with the
redirect as normal. If you'd like to disable this feature, you can set the
config variable <code>DEBUG_TB_INTERCEPT_REDIRECTS</code> to <code>False</code>.
</p>
</body>
<head>
<title>Redirect intercepted</title>
</head>
<body>
<h1>Redirect ({{ redirect_code }})</h1>
<p>Location: <a href="{{ redirect_to }}">{{ redirect_to }}</a></p>
<p class="notice">
The Flask Debug Toolbar has intercepted a redirect to the above URL for
debug viewing purposes. You can click the above link to continue with the
redirect as normal. If you'd like to disable this feature, you can set the
config variable <code>DEBUG_TB_INTERCEPT_REDIRECTS</code> to <code>False</code>.
</p>
</body>
</html>

View File

@@ -1,27 +1,15 @@
import urllib
try:
from urllib.parse import unquote
except ImportError:
from urllib import unquote
from flask import url_for, current_app
from werkzeug.utils import import_string
class DebugToolbar(object):
# default config settings
config = {
'DEBUG_TB_INTERCEPT_REDIRECTS': True,
'DEBUG_TB_PANELS': (
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
'flask_debugtoolbar.panels.timer.TimerDebugPanel',
'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel',
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
'flask_debugtoolbar.panels.logger.LoggingPanel',
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
)
}
panel_classes = []
_cached_panel_classes = {}
def __init__(self, request, jinja_env):
self.jinja_env = jinja_env
@@ -34,36 +22,19 @@ class DebugToolbar(object):
self.create_panels()
@classmethod
def load_panels(cls, app):
cls.config.update(app.config)
for panel_path in cls.config['DEBUG_TB_PANELS']:
dot = panel_path.rindex('.')
panel_module, panel_classname = panel_path[:dot], panel_path[dot+1:]
try:
mod = __import__(panel_module, {}, {}, [''])
except ImportError, e:
app.logger.warning('Disabled %s due to ImportError: %s', panel_classname, e)
continue
panel_class = getattr(mod, panel_classname)
cls.panel_classes.append(panel_class)
def create_panels(self):
"""
Populate debug panels
"""
activated = self.request.cookies.get('fldt_active', '')
activated = urllib.unquote(activated).split(';')
activated = unquote(activated).split(';')
for panel_class in self.panel_classes:
panel_instance = panel_class(
context=self.template_context,
jinja_env=self.jinja_env)
for panel_class in self._iter_panels(current_app):
panel_instance = panel_class(jinja_env=self.jinja_env, context=self.template_context)
if panel_instance.dom_id() in activated:
panel_instance.is_active = True
self.panels.append(panel_instance)
def render_toolbar(self):
@@ -73,4 +44,33 @@ class DebugToolbar(object):
template = self.jinja_env.get_template('base.html')
return template.render(**context)
@classmethod
def load_panels(cls, app):
for panel_class in cls._iter_panels(app):
# just loop to make sure they've been loaded
pass
@classmethod
def _iter_panels(cls, app):
for panel_path in app.config['DEBUG_TB_PANELS']:
panel_class = cls._import_panel(app, panel_path)
if panel_class is not None:
yield panel_class
@classmethod
def _import_panel(cls, app, path):
cache = cls._cached_panel_classes
try:
return cache[path]
except KeyError:
pass
try:
panel_class = import_string(path)
except ImportError as e:
app.logger.warning('Disabled %s due to ImportError: %s', path, e)
panel_class = None
cache[path] = panel_class
return panel_class

View File

@@ -1,3 +1,4 @@
import itertools
import os.path
import sys
@@ -12,45 +13,65 @@ except ImportError:
HAVE_PYGMENTS = False
from flask import current_app
from flask import current_app, Markup
def format_fname(value):
# If the value is not an absolute path, the it is a builtin or
# a relative file (thus a project file).
# If the value has a builtin prefix, return it unchanged
if value.startswith(('{', '<')):
return value
value = os.path.normpath(value)
# If the file is absolute, try normalizing it relative to the project root
# to handle it as a project file
if os.path.isabs(value):
value = _shortest_relative_path(
value, [current_app.root_path], os.path)
# If the value is a relative path, it is a project file
if not os.path.isabs(value):
if value.startswith(('{', '<')):
return value
if value.startswith('.' + os.path.sep):
return value
return '.' + os.path.sep + value
return os.path.join('.', value)
# If the file is absolute and within the project root handle it as
# a project file
if value.startswith(current_app.root_path):
return "." + value[len(current_app.root_path):]
# Otherwise, normalize other paths relative to sys.path
return '<%s>' % _shortest_relative_path(value, sys.path, os.path)
# Loop through sys.path to find the longest match and return
# the relative path from there.
paths = sys.path
prefix = None
prefix_len = 0
for path in sys.path:
new_prefix = os.path.commonprefix([path, value])
if len(new_prefix) > prefix_len:
prefix = new_prefix
prefix_len = len(prefix)
if not prefix.endswith(os.path.sep):
prefix_len -= 1
path = value[prefix_len:]
return '<%s>' % path
def _shortest_relative_path(value, paths, path_module):
relpaths = _relative_paths(value, paths, path_module)
return min(itertools.chain(relpaths, [value]), key=len)
def _relative_paths(value, paths, path_module):
for path in paths:
try:
relval = path_module.relpath(value, path)
except ValueError:
# on Windows, relpath throws a ValueError for
# paths with different drives
continue
if not relval.startswith(path_module.pardir):
yield relval
def decode_text(value):
"""
Decode a text-like value for display.
Unicode values are returned unchanged. Byte strings will be decoded
with a text-safe replacement for unrecognized characters.
"""
if isinstance(value, bytes):
return value.decode('ascii', 'replace')
else:
return value
def format_sql(query, args):
if not HAVE_PYGMENTS:
return query
return decode_text(query)
return highlight(
return Markup(highlight(
query,
SqlLexer(encoding='utf-8'),
HtmlFormatter(encoding='utf-8', noclasses=True, style=PYGMENT_STYLE))
SqlLexer(),
HtmlFormatter(noclasses=True, style=PYGMENT_STYLE)))

View File

@@ -14,8 +14,8 @@ except:
setup(
name='Flask-DebugToolbar',
version='0.7',
url='http://github.com/mgood/flask-debugtoolbar',
version='0.9.2',
url='http://flask-debugtoolbar.rtfd.org/',
license='BSD',
author='Michael van Tellingen',
author_email='michaelvantellingen@gmail.com',
@@ -32,6 +32,8 @@ setup(
install_requires=[
'Flask>=0.8',
'Blinker',
'itsdangerous',
'werkzeug',
],
classifiers=[
'Development Status :: 4 - Beta',

30
test/basic_app.py Normal file
View File

@@ -0,0 +1,30 @@
from flask import Flask, render_template
from flask_debugtoolbar import DebugToolbarExtension
from flask_sqlalchemy import SQLAlchemy
app = Flask('basic_app')
app.debug = True
app.config['SECRET_KEY'] = 'abc123'
# make sure these are printable in the config panel
app.config['BYTES_VALUE'] = b'\x00'
app.config['UNICODE_VALUE'] = u'\uffff'
toolbar = DebugToolbarExtension(app)
db = SQLAlchemy(app)
class Foo(db.Model):
__tablename__ = 'foo'
id = db.Column(db.Integer, primary_key=True)
@app.route('/')
def index():
db.create_all()
Foo.query.filter_by(id=1).all()
return render_template('basic_app.html')
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,4 @@
<!doctype html>
<html>
<body>Hello world</body>
</html>

41
test/test_toolbar.py Normal file
View File

@@ -0,0 +1,41 @@
import sys
import pytest
from flask_debugtoolbar import _printable
def load_app(name):
app = __import__(name).app
app.config['TESTING'] = True
return app.test_client()
def test_basic_app():
app = load_app('basic_app')
index = app.get('/')
assert index.status_code == 200
assert b'<div id="flDebug"' in index.data
@pytest.mark.skipif(sys.version_info >= (3,),
reason='test only applies to Python 2')
def test_printable_unicode():
class UnicodeRepr(object):
def __repr__(self):
return u'\uffff'
printable = _printable(UnicodeRepr())
assert "raised UnicodeEncodeError: 'ascii' codec" in printable
@pytest.mark.skipif(sys.version_info >= (3,),
reason='test only applies to Python 2')
def test_printable_non_ascii():
class NonAsciiRepr(object):
def __repr__(self):
return 'a\xffb'
printable = u'%s' % _printable(NonAsciiRepr())
# should replace \xff with the unicode ? character
assert printable == u'a\ufffdb'

120
test/test_utils.py Normal file
View File

@@ -0,0 +1,120 @@
import pytest
import posixpath
import ntpath
from flask import Markup
from flask_debugtoolbar.utils import (_relative_paths, _shortest_relative_path,
format_sql, decode_text, HAVE_PYGMENTS)
@pytest.mark.parametrize('value,paths,expected,path_module', [
# should yield relative path to the parent directory
('/foo/bar', ['/foo'], ['bar'], posixpath),
('c:\\foo\\bar', ['c:\\foo'], ['bar'], ntpath),
# should not yield result if no path is a parent directory
('/foo/bar', ['/baz'], [], posixpath),
('c:\\foo\\bar', ['c:\\baz'], [], ntpath),
# should only yield relative paths for parent directories
('/foo/bar', ['/foo', '/baz'], ['bar'], posixpath),
('c:\\foo\\bar', ['c:\\foo', 'c:\\baz'], ['bar'], ntpath),
# should yield all results when multiple parents match
('/foo/bar/baz', ['/foo', '/foo/bar'], ['bar/baz', 'baz'], posixpath),
('c:\\foo\\bar\\baz', ['c:\\foo', 'c:\\foo\\bar'], ['bar\\baz', 'baz'], ntpath),
# should ignore case differences on windows
('c:\\Foo\\bar', ['c:\\foo'], ['bar'], ntpath),
# should preserve original case
('/Foo/Bar', ['/Foo'], ['Bar'], posixpath),
('c:\\Foo\\Bar', ['c:\\foo'], ['Bar'], ntpath),
])
def test_relative_paths(value, paths, expected, path_module):
assert list(_relative_paths(value, paths, path_module)) == expected
@pytest.mark.parametrize('value,paths,expected,path_module', [
# should yield relative path to the parent directory
('/foo/bar', ['/foo'], 'bar', posixpath),
('c:\\foo\\bar', ['c:\\foo'], 'bar', ntpath),
# should return the original value if no path is a parent directory
('/foo/bar', ['/baz'], '/foo/bar', posixpath),
('c:\\foo\\bar', ['c:\\baz'], 'c:\\foo\\bar', ntpath),
# should yield shortest result when multiple parents match
('/foo/bar/baz', ['/foo', '/foo/bar'], 'baz', posixpath),
('c:\\foo\\bar\\baz', ['c:\\foo', 'c:\\foo\\bar'], 'baz', ntpath),
])
def test_shortest_relative_path(value, paths, expected, path_module):
assert _shortest_relative_path(value, paths, path_module) == expected
def test_decode_text_unicode():
value = u'\uffff'
decoded = decode_text(value)
assert decoded == value
def test_decode_text_ascii():
value = 'abc'
assert decode_text(value.encode('ascii')) == value
def test_decode_text_non_ascii():
value = b'abc \xff xyz'
assert isinstance(value, bytes)
decoded = decode_text(value)
assert not isinstance(decoded, bytes)
assert decoded.startswith('abc')
assert decoded.endswith('xyz')
@pytest.fixture()
def no_pygments(monkeypatch):
monkeypatch.setattr('flask_debugtoolbar.utils.HAVE_PYGMENTS', False)
def test_format_sql_no_pygments(no_pygments):
sql = 'select 1'
assert format_sql(sql, {}) == sql
def test_format_sql_no_pygments_non_ascii(no_pygments):
sql = b"select '\xff'"
formatted = format_sql(sql, {})
assert formatted.startswith(u"select '")
def test_format_sql_no_pygments_escape_html(no_pygments):
sql = 'select x < 1'
formatted = format_sql(sql, {})
assert not isinstance(formatted, Markup)
assert Markup('%s') % formatted == 'select x &lt; 1'
@pytest.mark.skipif(not HAVE_PYGMENTS, reason='test requires the "Pygments" library')
def test_format_sql_pygments():
sql = 'select 1'
html = format_sql(sql, {})
assert isinstance(html, Markup)
assert html.startswith('<div')
assert 'select' in html
assert '1' in html
@pytest.mark.skipif(not HAVE_PYGMENTS, reason='test requires the "Pygments" library')
def test_format_sql_pygments_non_ascii():
sql = b"select 'abc \xff xyz'"
html = format_sql(sql, {})
assert isinstance(html, Markup)
assert html.startswith('<div')
assert 'select' in html
assert 'abc' in html
assert 'xyz' in html

10
tox.ini Normal file
View File

@@ -0,0 +1,10 @@
[tox]
envlist = py26,py27,py34
[testenv]
deps =
pytest
Flask-SQLAlchemy
Pygments
commands =
py.test