diff --git a/flaskext/debugtoolbar/__init__.py b/flaskext/debugtoolbar/__init__.py index 186fc82..dc70e25 100644 --- a/flaskext/debugtoolbar/__init__.py +++ b/flaskext/debugtoolbar/__init__.py @@ -1,12 +1,14 @@ import os -from flask import current_app, request, signals +from flask import current_app, request from flask.globals import _request_ctx_stack from flask.helpers import send_from_directory from jinja2 import Environment, PackageLoader from werkzeug.exceptions import HTTPException +from werkzeug.urls import url_quote_plus from flaskext.debugtoolbar.toolbar import DebugToolbar +from flaskext.debugtoolbar.views import module def replace_insensitive(string, target, replacement): """Similar to string.replace() but is case insensitive @@ -36,7 +38,6 @@ class DebugToolbarExtension(object): self.app.before_request(self.process_request) self.app.after_request(self.process_response) - # Monkey-patch the Flask.dispatch_request method app.dispatch_request = self.dispatch_request @@ -46,7 +47,9 @@ class DebugToolbarExtension(object): autoescape=True, extensions=['jinja2.ext.i18n'], loader=PackageLoader(__name__, 'templates')) + self.jinja_env.filters['urlencode'] = url_quote_plus + app.register_module(module, url_prefix='/_debug_toolbar/views') app.add_url_rule('/_debug_toolbar/static/', '_debug_toolbar.static', self.send_static_file) @@ -64,6 +67,7 @@ class DebugToolbarExtension(object): try: if req.routing_exception is not None: raise req.routing_exception + rule = req.url_rule # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically @@ -76,6 +80,9 @@ class DebugToolbarExtension(object): view_func = app.view_functions[rule.endpoint] view_func = self.process_view(app, view_func, req.view_args) + if req.path.startswith('/_debug_toolbar/views'): + req.view_args['render'] = self.render + return view_func(**req.view_args) except HTTPException, e: diff --git a/flaskext/debugtoolbar/panels/sqlalchemy.py b/flaskext/debugtoolbar/panels/sqlalchemy.py index 20a9c52..011e0c8 100644 --- a/flaskext/debugtoolbar/panels/sqlalchemy.py +++ b/flaskext/debugtoolbar/panels/sqlalchemy.py @@ -1,9 +1,13 @@ +import hashlib + try: from flaskext.sqlalchemy import get_debug_queries except ImportError: get_debug_queries = None +import simplejson +from flask import current_app from flaskext.debugtoolbar.panels import DebugPanel from flaskext.debugtoolbar.utils import format_fname, format_sql @@ -44,9 +48,25 @@ class SQLAlchemyDebugPanel(DebugPanel): queries = get_debug_queries() data = [] for query in queries: + is_select = query.statement.strip().lower().startswith('select') + _params = '' + try: + _params = simplejson.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': self._format_sql(query.statement, query.parameters), + 'sql': format_sql(query.statement, query.parameters), + 'raw_sql': query.statement, + 'hash': hash, + 'params': _params, + 'is_select': is_select, 'context_long': query.context, 'context': format_fname(query.context) }) diff --git a/flaskext/debugtoolbar/templates/panels/sqlalchemy.html b/flaskext/debugtoolbar/templates/panels/sqlalchemy.html index 49df5e8..c98fb7c 100644 --- a/flaskext/debugtoolbar/templates/panels/sqlalchemy.html +++ b/flaskext/debugtoolbar/templates/panels/sqlalchemy.html @@ -12,17 +12,15 @@ {{ '%.4f'|format(query.duration) }} - {# {% if query.params %} {% if query.is_select %} - SELECT
- EXPLAIN
+ SELECT
+ EXPLAIN
{% if is_mysql %} - PROFILE
+ PROFILE
{% endif %} {% endif %} {% endif %} - #} {{ query.context }} diff --git a/flaskext/debugtoolbar/templates/panels/sqlalchemy_explain.html b/flaskext/debugtoolbar/templates/panels/sqlalchemy_explain.html new file mode 100644 index 0000000..9d93b9c --- /dev/null +++ b/flaskext/debugtoolbar/templates/panels/sqlalchemy_explain.html @@ -0,0 +1,33 @@ +
+ Back +

SQL Explained

+
+
+
+
+
Executed SQL
+
{{ sql|safe }}
+
Time
+
{{ duration }} ms
+
+ + + + {% for h in headers %} + + {% endfor %} + + + + {% for row in result %} + + {% for column in row %} + + {% endfor %} + + {% endfor %} + +
{{ h|upper }}
{{ column }}
+
+
+ diff --git a/flaskext/debugtoolbar/views.py b/flaskext/debugtoolbar/views.py new file mode 100644 index 0000000..7e57640 --- /dev/null +++ b/flaskext/debugtoolbar/views.py @@ -0,0 +1,39 @@ +import hashlib + +import simplejson +from flask import Module, request, current_app, abort +from flaskext.sqlalchemy import SQLAlchemy +from flaskext.debugtoolbar.utils import format_sql + + +module = Module(__name__) + +@module.route('/sql_explain', methods=['GET', 'POST']) +def sql_explain(render): + 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 = simplejson.loads(params) + + db = SQLAlchemy(current_app) + if db.engine.driver == 'pysqlite': + query = 'EXPLAIN QUERY PLAN %s' % statement + else: + query = 'EXPLAIN %s' % statement + + result = db.engine.execute(query, params).fetchall() + return render('panels/sqlalchemy_explain.html', { + 'result': result, + 'sql': format_sql(statement, params), + 'duration': request.args['duration'], + })