Last active
November 30, 2020 12:20
-
-
Save beti7/df179597aa034b8217f84282284cbaac to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#get inside container as root | |
sudo docker exec -u0 -ti redash_server_1 bash | |
# get inside redash/handlers/ folder where query_results.py script is | |
cd redash/handlers/ | |
# make a copy of the query_results.py script with another name | |
cp query_results.py query_results_original.py | |
#edit the script using nano | |
# update and upgrade apt if needed to install nano if missing | |
sudo apt-get update && sudo apt-get upgrade && apt-get install nano | |
nano query_results.py | |
# comment this code: (around line 65) | |
# return {'query_result': serialize_query_result(query_result, current_user.is_api_user())} | |
And then insert the code below right between the previously commented code and: | |
if query_result: | |
(be careful with spaces and tabs, it’s Python code!) | |
# this code should be inserted instead of the commented one: | |
serialize_query_result_var = serialize_query_result(query_result, current_user.is_api_user()) | |
serialize_query_result_var.pop("query") | |
return {'query_result': serialize_query_result_var} | |
Save and exit nano with CTRL+X, then press Y (Yes) and Enter | |
#exit from redash_server_1 redash/handlers/ | |
exit | |
# restart redash_server_1 and wait for few seconds | |
sudo docker restart redash_server_1 | |
# see if redash_server_1 is restarted | |
sudo docker ps | |
# (see column status, if the container is up few moments ago, you got it right) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import logging | |
import time | |
import unicodedata | |
from flask import make_response, request | |
from flask_login import current_user | |
from flask_restful import abort | |
from redash import models, settings | |
from redash.handlers.base import BaseResource, get_object_or_404, record_event | |
from redash.permissions import (has_access, not_view_only, require_access, | |
require_permission, view_only) | |
from redash.tasks import QueryTask | |
from redash.tasks.queries import enqueue_query | |
from redash.utils import (collect_parameters_from_request, gen_query_hash, json_dumps, utcnow, to_filename) | |
from redash.models.parameterized_query import (ParameterizedQuery, InvalidParameterError, | |
QueryDetachedFromDataSourceError, dropdown_values) | |
from redash.serializers import serialize_query_result, serialize_query_result_to_csv, serialize_query_result_to_xlsx | |
def error_response(message, http_status=400): | |
return {'job': {'status': 4, 'error': message}}, http_status | |
error_messages = { | |
'unsafe_when_shared': error_response('This query contains potentially unsafe parameters and cannot be executed on a shared dashboard or an embedded visualization.', 403), | |
'unsafe_on_view_only': error_response('This query contains potentially unsafe parameters and cannot be executed with read-only access to this data source.', 403), | |
'no_permission': error_response('You do not have permission to run queries with this data source.', 403), | |
'select_data_source': error_response('Please select data source to run this query.', 401) | |
} | |
def run_query(query, parameters, data_source, query_id, max_age=0): | |
if data_source.paused: | |
if data_source.pause_reason: | |
message = '{} is paused ({}). Please try later.'.format(data_source.name, data_source.pause_reason) | |
else: | |
message = '{} is paused. Please try later.'.format(data_source.name) | |
return error_response(message) | |
try: | |
query.apply(parameters) | |
except (InvalidParameterError, QueryDetachedFromDataSourceError) as e: | |
abort(400, message=e.message) | |
if query.missing_params: | |
return error_response(u'Missing parameter value for: {}'.format(u", ".join(query.missing_params))) | |
if max_age == 0: | |
query_result = None | |
else: | |
query_result = models.QueryResult.get_latest(data_source, query.text, max_age) | |
record_event(current_user.org, current_user, { | |
'action': 'execute_query', | |
'cache': 'hit' if query_result else 'miss', | |
'object_id': data_source.id, | |
'object_type': 'data_source', | |
'query': query.text, | |
'query_id': query_id, | |
'parameters': parameters | |
}) | |
if query_result: | |
serialize_query_result_var = serialize_query_result(query_result, current_user.is_api_user()) | |
serialize_query_result_var.pop("query") | |
return {'query_result': serialize_query_result_var} | |
# return {'query_result': serialize_query_result(query_result, current_user.is_api_user())} | |
else: | |
job = enqueue_query(query.text, data_source, current_user.id, current_user.is_api_user(), metadata={ | |
"Username": repr(current_user) if current_user.is_api_user() else current_user.email, | |
"Query ID": query_id | |
}) | |
return {'job': job.to_dict()} | |
def get_download_filename(query_result, query, filetype): | |
retrieved_at = query_result.retrieved_at.strftime("%Y_%m_%d") | |
if query: | |
filename = to_filename(query.name) if query.name != '' else str(query.id) | |
else: | |
filename = str(query_result.id) | |
return u"{}_{}.{}".format(filename, retrieved_at, filetype) | |
class QueryResultListResource(BaseResource): | |
@require_permission('execute_query') | |
def post(self): | |
""" | |
Execute a query (or retrieve recent results). | |
:qparam string query: The query text to execute | |
:qparam number query_id: The query object to update with the result (optional) | |
:qparam number max_age: If query results less than `max_age` seconds old are available, | |
return them, otherwise execute the query; if omitted or -1, returns | |
any cached result, or executes if not available. Set to zero to | |
always execute. | |
:qparam number data_source_id: ID of data source to query | |
:qparam object parameters: A set of parameter values to apply to the query. | |
""" | |
params = request.get_json(force=True) | |
query = params['query'] | |
max_age = params.get('max_age', -1) | |
# max_age might have the value of None, in which case calling int(None) will fail | |
if max_age is None: | |
max_age = -1 | |
max_age = int(max_age) | |
query_id = params.get('query_id', 'adhoc') | |
parameters = params.get('parameters', collect_parameters_from_request(request.args)) | |
parameterized_query = ParameterizedQuery(query, org=self.current_org) | |
data_source_id = params.get('data_source_id') | |
if data_source_id: | |
data_source = models.DataSource.get_by_id_and_org(params.get('data_source_id'), self.current_org) | |
else: | |
return error_messages['select_data_source'] | |
if not has_access(data_source, self.current_user, not_view_only): | |
return error_messages['no_permission'] | |
return run_query(parameterized_query, parameters, data_source, query_id, max_age) | |
ONE_YEAR = 60 * 60 * 24 * 365.25 | |
class QueryResultDropdownResource(BaseResource): | |
def get(self, query_id): | |
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) | |
require_access(query.data_source, current_user, view_only) | |
try: | |
return dropdown_values(query_id, self.current_org) | |
except QueryDetachedFromDataSourceError as e: | |
abort(400, message=e.message) | |
class QueryDropdownsResource(BaseResource): | |
def get(self, query_id, dropdown_query_id): | |
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) | |
require_access(query, current_user, view_only) | |
related_queries_ids = [p['queryId'] for p in query.parameters if p['type'] == 'query'] | |
if int(dropdown_query_id) not in related_queries_ids: | |
dropdown_query = get_object_or_404(models.Query.get_by_id_and_org, dropdown_query_id, self.current_org) | |
require_access(dropdown_query.data_source, current_user, view_only) | |
return dropdown_values(dropdown_query_id, self.current_org) | |
class QueryResultResource(BaseResource): | |
@staticmethod | |
def add_cors_headers(headers): | |
if 'Origin' in request.headers: | |
origin = request.headers['Origin'] | |
if set(['*', origin]) & settings.ACCESS_CONTROL_ALLOW_ORIGIN: | |
headers['Access-Control-Allow-Origin'] = origin | |
headers['Access-Control-Allow-Credentials'] = str(settings.ACCESS_CONTROL_ALLOW_CREDENTIALS).lower() | |
@require_permission('view_query') | |
def options(self, query_id=None, query_result_id=None, filetype='json'): | |
headers = {} | |
self.add_cors_headers(headers) | |
if settings.ACCESS_CONTROL_REQUEST_METHOD: | |
headers['Access-Control-Request-Method'] = settings.ACCESS_CONTROL_REQUEST_METHOD | |
if settings.ACCESS_CONTROL_ALLOW_HEADERS: | |
headers['Access-Control-Allow-Headers'] = settings.ACCESS_CONTROL_ALLOW_HEADERS | |
return make_response("", 200, headers) | |
@require_permission('view_query') | |
def post(self, query_id): | |
""" | |
Execute a saved query. | |
:param number query_id: The ID of the query whose results should be fetched. | |
:param object parameters: The parameter values to apply to the query. | |
:qparam number max_age: If query results less than `max_age` seconds old are available, | |
return them, otherwise execute the query; if omitted or -1, returns | |
any cached result, or executes if not available. Set to zero to | |
always execute. | |
""" | |
params = request.get_json(force=True, silent=True) or {} | |
parameter_values = params.get('parameters', {}) | |
max_age = params.get('max_age', -1) | |
# max_age might have the value of None, in which case calling int(None) will fail | |
if max_age is None: | |
max_age = -1 | |
max_age = int(max_age) | |
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) | |
allow_executing_with_view_only_permissions = query.parameterized.is_safe | |
if has_access(query, self.current_user, allow_executing_with_view_only_permissions): | |
return run_query(query.parameterized, parameter_values, query.data_source, query_id, max_age) | |
else: | |
if not query.parameterized.is_safe: | |
if current_user.is_api_user(): | |
return error_messages['unsafe_when_shared'] | |
else: | |
return error_messages['unsafe_on_view_only'] | |
else: | |
return error_messages['no_permission'] | |
@require_permission('view_query') | |
def get(self, query_id=None, query_result_id=None, filetype='json'): | |
""" | |
Retrieve query results. | |
:param number query_id: The ID of the query whose results should be fetched | |
:param number query_result_id: the ID of the query result to fetch | |
:param string filetype: Format to return. One of 'json', 'xlsx', or 'csv'. Defaults to 'json'. | |
:<json number id: Query result ID | |
:<json string query: Query that produced this result | |
:<json string query_hash: Hash code for query text | |
:<json object data: Query output | |
:<json number data_source_id: ID of data source that produced this result | |
:<json number runtime: Length of execution time in seconds | |
:<json string retrieved_at: Query retrieval date/time, in ISO format | |
""" | |
# TODO: | |
# This method handles two cases: retrieving result by id & retrieving result by query id. | |
# They need to be split, as they have different logic (for example, retrieving by query id | |
# should check for query parameters and shouldn't cache the result). | |
should_cache = query_result_id is not None | |
parameter_values = collect_parameters_from_request(request.args) | |
max_age = int(request.args.get('maxAge', 0)) | |
query_result = None | |
query = None | |
if query_result_id: | |
query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org) | |
if query_id is not None: | |
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) | |
if query_result is None and query is not None and query.latest_query_data_id is not None: | |
query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, | |
query.latest_query_data_id, | |
self.current_org) | |
if query is not None and query_result is not None and self.current_user.is_api_user(): | |
if query.query_hash != query_result.query_hash: | |
abort(404, message='No cached result found for this query.') | |
if query_result: | |
require_access(query_result.data_source, self.current_user, view_only) | |
if isinstance(self.current_user, models.ApiUser): | |
event = { | |
'user_id': None, | |
'org_id': self.current_org.id, | |
'action': 'api_get', | |
'api_key': self.current_user.name, | |
'file_type': filetype, | |
'user_agent': request.user_agent.string, | |
'ip': request.remote_addr | |
} | |
if query_id: | |
event['object_type'] = 'query' | |
event['object_id'] = query_id | |
else: | |
event['object_type'] = 'query_result' | |
event['object_id'] = query_result_id | |
self.record_event(event) | |
if filetype == 'json': | |
response = self.make_json_response(query_result) | |
elif filetype == 'xlsx': | |
response = self.make_excel_response(query_result) | |
else: | |
response = self.make_csv_response(query_result) | |
if len(settings.ACCESS_CONTROL_ALLOW_ORIGIN) > 0: | |
self.add_cors_headers(response.headers) | |
if should_cache: | |
response.headers.add_header('Cache-Control', 'private,max-age=%d' % ONE_YEAR) | |
filename = get_download_filename(query_result, query, filetype) | |
response.headers.add_header( | |
"Content-Disposition", | |
'attachment; filename="{}"'.format(filename.encode("utf-8")) | |
) | |
return response | |
else: | |
abort(404, message='No cached result found for this query.') | |
def make_json_response(self, query_result): | |
data = json_dumps({'query_result': query_result.to_dict()}) | |
headers = {'Content-Type': "application/json"} | |
return make_response(data, 200, headers) | |
@staticmethod | |
def make_csv_response(query_result): | |
headers = {'Content-Type': "text/csv; charset=UTF-8"} | |
return make_response(serialize_query_result_to_csv(query_result), 200, headers) | |
@staticmethod | |
def make_excel_response(query_result): | |
headers = {'Content-Type': "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"} | |
return make_response(serialize_query_result_to_xlsx(query_result), 200, headers) | |
class JobResource(BaseResource): | |
def get(self, job_id, query_id=None): | |
""" | |
Retrieve info about a running query job. | |
""" | |
job = QueryTask(job_id=job_id) | |
return {'job': job.to_dict()} | |
def delete(self, job_id): | |
""" | |
Cancel a query job in progress. | |
""" | |
job = QueryTask(job_id=job_id) | |
job.cancel() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Step by step guide for changing script redash/handlers/query_results.py inside docker
or just copy and replace this script query_results.py with the existing one, and then restart redash_server_1