diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js
index 68a395cc53..cef216e9ba 100644
--- a/rd_ui/app/scripts/controllers/query_view.js
+++ b/rd_ui/app/scripts/controllers/query_view.js
@@ -15,6 +15,7 @@
maxAge = -1;
}
+ $scope.showLog = false;
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
}
@@ -57,6 +58,7 @@
// in view mode, latest dataset is always visible
// source mode changes this behavior
$scope.showDataset = true;
+ $scope.showLog = false;
$scope.lockButton = function(lock) {
$scope.queryExecuting = lock;
@@ -110,21 +112,21 @@
$scope.queryResult.cancelExecution();
Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id);
};
-
+
$scope.archiveQuery = function(options, data) {
if (data) {
data.id = $scope.query.id;
} else {
data = $scope.query;
}
-
+
$scope.isDirty = false;
-
+
options = _.extend({}, {
successMessage: 'Query archived',
errorMessage: 'Query could not be archived'
}, options);
-
+
return Query.delete({id: data.id}, function() {
$scope.query.is_archived = true;
$scope.query.schedule = null;
@@ -197,6 +199,10 @@
if (status === 'done' || status === 'failed') {
$scope.lockButton(false);
}
+
+ if ($scope.queryResult.getLog() != null) {
+ $scope.showLog = true;
+ }
});
$scope.openScheduleForm = function() {
diff --git a/rd_ui/app/scripts/services/resources.js b/rd_ui/app/scripts/services/resources.js
index 5ff4206027..da3bb31284 100644
--- a/rd_ui/app/scripts/services/resources.js
+++ b/rd_ui/app/scripts/services/resources.js
@@ -95,6 +95,14 @@
return this.job.error;
}
+ QueryResult.prototype.getLog = function() {
+ if (!this.query_result.data || !this.query_result.data.log || this.query_result.data.log.length == 0) {
+ return null;
+ }
+
+ return this.query_result.data.log;
+ }
+
QueryResult.prototype.getUpdatedAt = function () {
return this.query_result.retrieved_at || this.job.updated_at * 1000.0 || this.updatedAt;
}
diff --git a/rd_ui/app/styles/redash.css b/rd_ui/app/styles/redash.css
index 3771f8469b..c2fda05aa2 100644
--- a/rd_ui/app/styles/redash.css
+++ b/rd_ui/app/styles/redash.css
@@ -339,3 +339,7 @@ use this class when you need to keep the original display value
display: none !important;
}
}
+
+.log-container {
+ margin-bottom: 50px;
+}
diff --git a/rd_ui/app/views/query.html b/rd_ui/app/views/query.html
index 823cfa0375..0fde4626a9 100644
--- a/rd_ui/app/views/query.html
+++ b/rd_ui/app/views/query.html
@@ -192,6 +192,16 @@
diff --git a/redash/query_runner/python.py b/redash/query_runner/python.py
index e65f604d02..5891d1ca5b 100644
--- a/redash/query_runner/python.py
+++ b/redash/query_runner/python.py
@@ -1,6 +1,8 @@
import sys
+import datetime
import json
import logging
+import weakref
from redash.query_runner import *
from redash import models
@@ -12,96 +14,23 @@
from RestrictedPython import compile_restricted
from RestrictedPython.Guards import safe_builtins
-ALLOWED_MODULES = {}
+class CustomPrint(object):
+ """ CustomPrint redirect "print" calls to be sent as "log" on the result object """
+ def __init__(self, python_runner):
+ self._python_runner = python_runner
+ def write(self, text):
+ if self._python_runner()._enable_print_log:
+ if text and text.strip():
+ log_line = "[{0}] {1}".format(datetime.datetime.utcnow().isoformat(), text)
+ self._python_runner()._result["log"].append(log_line)
-def custom_write(obj):
- """
- Custom hooks which controls the way objects/lists/tuples/dicts behave in
- RestrictedPython
- """
- return obj
-
-
-def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
- if name in ALLOWED_MODULES:
- m = None
- if ALLOWED_MODULES[name] is None:
- m = importlib.import_module(name)
- ALLOWED_MODULES[name] = m
- else:
- m = ALLOWED_MODULES[name]
-
- return m
-
- raise Exception("'{0}' is not configured as a supported import module".format(name))
-
-def custom_get_item(obj, key):
- return obj[key]
-
-def custom_get_iter(obj):
- return iter(obj)
-
-def get_query_result(query_id):
- try:
- query = models.Query.get_by_id(query_id)
- except models.Query.DoesNotExist:
- raise Exception("Query id %s does not exist." % query_id)
-
- if query.latest_query_data is None:
- raise Exception("Query does not have results yet.")
-
- if query.latest_query_data.data is None:
- raise Exception("Query does not have results yet.")
-
- return json.loads(query.latest_query_data.data)
-
-
-def execute_query(data_source_name_or_id, query):
- try:
- if type(data_source_name_or_id) == int:
- data_source = models.DataSource.get_by_id(data_source_name_or_id)
- else:
- data_source = models.DataSource.get(models.DataSource.name==data_source_name_or_id)
- except models.DataSource.DoesNotExist:
- raise Exception("Wrong data source name/id: %s." % data_source_name_or_id)
-
- query_runner = get_query_runner(data_source.type, data_source.options)
-
- data, error = query_runner.run_query(query)
- if error is not None:
- raise Exception(error)
-
- # TODO: allow avoiding the json.dumps/loads in same process
- return json.loads(data)
-
-
-def add_result_column(result, column_name, friendly_name, column_type):
- """ Helper function to add columns inside a Python script running in re:dash in an easier way """
- if column_type not in SUPPORTED_COLUMN_TYPES:
- raise Exception("'{0}' is not a supported column type".format(column_type))
-
- if not "columns" in result:
- result["columns"] = []
-
- result["columns"].append({
- "name" : column_name,
- "friendly_name" : friendly_name,
- "type" : column_type
- })
-
-
-def add_result_row(result, values):
- if not "rows" in result:
- result["rows"] = []
-
- result["rows"].append(values)
+ def __call__(self):
+ return self
class Python(BaseQueryRunner):
- """
- This is very, very unsafe. Use at your own risk with people you really trust.
- """
+
@classmethod
def configuration_schema(cls):
return {
@@ -129,9 +58,97 @@ def __init__(self, configuration_json):
self.syntax = "python"
+ self._allowed_modules = {}
+ self._result = { "rows" : [], "columns" : [], "log" : [] }
+ self._enable_print_log = True
+
if self.configuration.get("allowedImportModules", None):
for item in self.configuration["allowedImportModules"].split(","):
- ALLOWED_MODULES[item] = None
+ self._allowed_modules[item] = None
+
+ def custom_import(self, name, globals=None, locals=None, fromlist=(), level=0):
+ if name in self._allowed_modules:
+ m = None
+ if self._allowed_modules[name] is None:
+ m = importlib.import_module(name)
+ self._allowed_modules[name] = m
+ else:
+ m = self._allowed_modules[name]
+
+ return m
+
+ raise Exception("'{0}' is not configured as a supported import module".format(name))
+
+ def custom_write(self, obj):
+ """
+ Custom hooks which controls the way objects/lists/tuples/dicts behave in
+ RestrictedPython
+ """
+ return obj
+
+ def custom_get_item(self, obj, key):
+ return obj[key]
+
+ def custom_get_iter(self, obj):
+ return iter(obj)
+
+ def disable_print_log(self):
+ self._enable_print_log = False
+
+ def enable_print_log(self):
+ self._enable_print_log = True
+
+ def add_result_column(self, result, column_name, friendly_name, column_type):
+ """ Helper function to add columns inside a Python script running in re:dash in an easier way """
+ if column_type not in SUPPORTED_COLUMN_TYPES:
+ raise Exception("'{0}' is not a supported column type".format(column_type))
+
+ if not "columns" in result:
+ result["columns"] = []
+
+ result["columns"].append({
+ "name" : column_name,
+ "friendly_name" : friendly_name,
+ "type" : column_type
+ })
+
+ def add_result_row(self, result, values):
+ if not "rows" in result:
+ result["rows"] = []
+
+ result["rows"].append(values)
+
+ def execute_query(self, data_source_name_or_id, query):
+ try:
+ if type(data_source_name_or_id) == int:
+ data_source = models.DataSource.get_by_id(data_source_name_or_id)
+ else:
+ data_source = models.DataSource.get(models.DataSource.name==data_source_name_or_id)
+ except models.DataSource.DoesNotExist:
+ raise Exception("Wrong data source name/id: %s." % data_source_name_or_id)
+
+ query_runner = get_query_runner(data_source.type, data_source.options)
+
+ data, error = query_runner.run_query(query)
+ if error is not None:
+ raise Exception(error)
+
+ # TODO: allow avoiding the json.dumps/loads in same process
+ return json.loads(data)
+
+ def get_query_result(self, query_id):
+ try:
+ query = models.Query.get_by_id(query_id)
+ except models.Query.DoesNotExist:
+ raise Exception("Query id %s does not exist." % query_id)
+
+ if query.latest_query_data is None:
+ raise Exception("Query does not have results yet.")
+
+ if query.latest_query_data.data is None:
+ raise Exception("Query does not have results yet.")
+
+ return json.loads(query.latest_query_data.data)
def run_query(self, query):
try:
@@ -139,22 +156,25 @@ def run_query(self, query):
code = compile_restricted(query, '', 'exec')
- safe_builtins["_write_"] = custom_write
- safe_builtins["__import__"] = custom_import
+ safe_builtins["_write_"] = self.custom_write
+ safe_builtins["__import__"] = self.custom_import
safe_builtins["_getattr_"] = getattr
safe_builtins["getattr"] = getattr
safe_builtins["_setattr_"] = setattr
safe_builtins["setattr"] = setattr
- safe_builtins["_getitem_"] = custom_get_item
- safe_builtins["_getiter_"] = custom_get_iter
+ safe_builtins["_getitem_"] = self.custom_get_item
+ safe_builtins["_getiter_"] = self.custom_get_iter
+ safe_builtins["_print_"] = CustomPrint(weakref.ref(self))
- script_locals = { "result" : { "rows" : [], "columns" : [] } }
+ script_locals = { "result" : self._result }
restricted_globals = dict(__builtins__=safe_builtins)
- restricted_globals["get_query_result"] = get_query_result
- restricted_globals["execute_query"] = execute_query
- restricted_globals["add_result_column"] = add_result_column
- restricted_globals["add_result_row"] = add_result_row
+ restricted_globals["get_query_result"] = self.get_query_result
+ restricted_globals["execute_query"] = self.execute_query
+ restricted_globals["add_result_column"] = self.add_result_column
+ restricted_globals["add_result_row"] = self.add_result_row
+ restricted_globals["disable_print_log"] = self.disable_print_log
+ restricted_globals["enable_print_log"] = self.enable_print_log
restricted_globals["TYPE_DATETIME"] = TYPE_DATETIME
restricted_globals["TYPE_BOOLEAN"] = TYPE_BOOLEAN
@@ -169,15 +189,13 @@ def run_query(self, query):
exec(code) in restricted_globals, script_locals
- if script_locals['result'] is None:
- raise Exception("result wasn't set to value.")
-
- json_data = json.dumps(script_locals['result'])
+ json_data = json.dumps(self._result)
except KeyboardInterrupt:
error = "Query cancelled by user."
json_data = None
except Exception as e:
- raise sys.exc_info()[1], None, sys.exc_info()[2]
+ error = str(e)
+ json_data = None
return json_data, error