From 1971bf653c57da4c0d21e7311e760c360953372b Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Sun, 11 Sep 2016 07:39:07 -0700 Subject: [PATCH] Numerous improvements to SQL Lab (#1088) * Improving the Visualize flow * Fixed the timer * CTAS * Expiclit engine handling * make tab full height, stretch for longer content (#1081) * Better error handling for queries * Hooked and fixed CSV export * Linting * Tying in the dttm in the viz flow * Indicator showing when going offline * Addressing comments, fixing the build * Fixing unit tests --- caravel/assets/javascripts/SqlLab/actions.js | 5 + .../SqlLab/components/LeftPane.jsx | 60 ----------- .../SqlLab/components/QueryAutoRefresh.jsx | 28 +++-- .../SqlLab/components/QueryTable.jsx | 2 +- .../SqlLab/components/ResultSet.jsx | 2 +- .../SqlLab/components/SouthPane.jsx | 101 ++++++++++++------ .../SqlLab/components/SqlEditor.jsx | 73 +++++++------ .../SqlLab/components/SqlEditorLeft.jsx | 9 +- .../SqlLab/components/SqlShrink.jsx | 3 +- .../SqlLab/components/TabbedSqlEditors.jsx | 1 - .../javascripts/SqlLab/components/Timer.jsx | 2 +- .../SqlLab/components/VisualizeModal.jsx | 75 +++++++++---- caravel/assets/javascripts/SqlLab/reducers.js | 26 ++++- caravel/assets/javascripts/modules/dates.js | 3 +- caravel/models.py | 24 +++-- caravel/sql_lab.py | 12 ++- caravel/views.py | 41 +++---- tests/base_tests.py | 17 +-- tests/core_tests.py | 19 ++-- 19 files changed, 298 insertions(+), 205 deletions(-) delete mode 100644 caravel/assets/javascripts/SqlLab/components/LeftPane.jsx diff --git a/caravel/assets/javascripts/SqlLab/actions.js b/caravel/assets/javascripts/SqlLab/actions.js index efb7f668db2ed..74889528e2eb9 100644 --- a/caravel/assets/javascripts/SqlLab/actions.js +++ b/caravel/assets/javascripts/SqlLab/actions.js @@ -23,6 +23,7 @@ export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR'; export const ADD_ALERT = 'ADD_ALERT'; export const REMOVE_ALERT = 'REMOVE_ALERT'; export const REFRESH_QUERIES = 'REFRESH_QUERIES'; +export const SET_NETWORK_STATUS = 'SET_NETWORK_STATUS'; export function resetState() { return { type: RESET_STATE }; @@ -36,6 +37,10 @@ export function addQueryEditor(queryEditor) { return { type: ADD_QUERY_EDITOR, queryEditor }; } +export function setNetworkStatus(networkOn) { + return { type: SET_NETWORK_STATUS, networkOn }; +} + export function addAlert(alert) { return { type: ADD_ALERT, alert }; } diff --git a/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx b/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx deleted file mode 100644 index e4ff0517575cf..0000000000000 --- a/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { Alert, Button } from 'react-bootstrap'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import * as Actions from '../actions'; -import QueryLink from './QueryLink'; - -const LeftPane = (props) => { - let queryElements; - if (props.workspaceQueries.length > 0) { - queryElements = props.workspaceQueries.map((q) => ); - } else { - queryElements = ( - - Use the save button on the SQL editor to save a query - into this section for future reference. - - ); - } - return ( -
-
-
-
- Saved Queries -
-
-
- {queryElements} -
-
-

- -
- ); -}; - -LeftPane.propTypes = { - workspaceQueries: React.PropTypes.array, - actions: React.PropTypes.object, -}; - -LeftPane.defaultProps = { - workspaceQueries: [], -}; - -function mapStateToProps(state) { - return { - workspaceQueries: state.workspaceQueries, - }; -} -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators(Actions, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(LeftPane); diff --git a/caravel/assets/javascripts/SqlLab/components/QueryAutoRefresh.jsx b/caravel/assets/javascripts/SqlLab/components/QueryAutoRefresh.jsx index e2ef5207a2c82..0a18c98ee8b9f 100644 --- a/caravel/assets/javascripts/SqlLab/components/QueryAutoRefresh.jsx +++ b/caravel/assets/javascripts/SqlLab/components/QueryAutoRefresh.jsx @@ -4,7 +4,8 @@ import { connect } from 'react-redux'; import * as Actions from '../actions'; const $ = require('jquery'); - +const QUERY_UPDATE_FREQ = 1000; +const QUERY_UPDATE_BUFFER_MS = 5000; class QueryAutoRefresh extends React.Component { componentWillMount() { @@ -15,7 +16,7 @@ class QueryAutoRefresh extends React.Component { } startTimer() { if (!(this.timer)) { - this.timer = setInterval(this.stopwatch.bind(this), 1000); + this.timer = setInterval(this.stopwatch.bind(this), QUERY_UPDATE_FREQ); } } stopTimer() { @@ -23,12 +24,20 @@ class QueryAutoRefresh extends React.Component { this.timer = null; } stopwatch() { - const url = '/caravel/queries/0'; + const url = '/caravel/queries/' + (this.props.queriesLastUpdate - QUERY_UPDATE_BUFFER_MS); // No updates in case of failure. - $.getJSON(url, (data, status) => { - if (status === 'success') { + $.getJSON(url, (data) => { + if (Object.keys(data).length > 0) { this.props.actions.refreshQueries(data); } + if (!this.props.networkOn) { + this.props.actions.setNetworkStatus(true); + } + }) + .fail(() => { + if (this.props.networkOn) { + this.props.actions.setNetworkStatus(false); + } }); } render() { @@ -37,13 +46,18 @@ class QueryAutoRefresh extends React.Component { } QueryAutoRefresh.propTypes = { actions: React.PropTypes.object, + queriesLastUpdate: React.PropTypes.integer, + networkOn: React.PropTypes.boolean, }; QueryAutoRefresh.defaultProps = { // queries: null, }; -function mapStateToProps() { - return {}; +function mapStateToProps(state) { + return { + queriesLastUpdate: state.queriesLastUpdate, + networkOn: state.networkOn, + }; } function mapDispatchToProps(dispatch) { diff --git a/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx b/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx index 576837a1f6801..032cb4b42f63c 100644 --- a/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx +++ b/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx @@ -42,7 +42,7 @@ class QueryTable extends React.Component { q.duration = fDuration(q.startDttm, q.endDttm); } q.started = moment.utc(q.startDttm).format('HH:mm:ss'); - const source = q.ctas ? q.executedSql : q.sql; + const source = (q.ctas) ? q.executedSql : q.sql; q.sql = ( ); diff --git a/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx b/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx index baa59fd79241f..f2f8e0b9e8fcd 100644 --- a/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx +++ b/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx @@ -43,7 +43,7 @@ class ResultSet extends React.Component { > Visualize - diff --git a/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx b/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx index 51bbec1effe90..3f728dc5503fd 100644 --- a/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx @@ -1,44 +1,85 @@ -import { Alert, Tab, Tabs } from 'react-bootstrap'; +import { Alert, Button, Tab, Tabs } from 'react-bootstrap'; import QueryHistory from './QueryHistory'; import ResultSet from './ResultSet'; import React from 'react'; -const SouthPane = function (props) { - let results =
; - if (props.latestQuery) { - if (props.latestQuery.state === 'running') { - results = ( - Loading.. - ); - } else if (props.latestQuery.state === 'failed') { - results = {props.latestQuery.msg}; - } else if (props.latestQuery.state === 'success') { - results = ; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import * as Actions from '../actions'; +import shortid from 'shortid'; + +class SouthPane extends React.Component { + popSelectStar() { + const qe = { + id: shortid.generate(), + title: this.props.latestQuery.tempTable, + autorun: false, + dbId: this.props.latestQuery.dbId, + sql: `SELECT * FROM ${this.props.latestQuery.tempTable}`, + }; + this.props.actions.addQueryEditor(qe); + } + render() { + let results =
; + const latestQuery = this.props.latestQuery; + if (latestQuery) { + if (['running', 'pending'].includes(latestQuery.state)) { + results = ( + Loading.. + ); + } else if (latestQuery.state === 'failed') { + results = {latestQuery.errorMessage}; + } else if (latestQuery.state === 'success' && latestQuery.ctas) { + results = ( +
+ + Table [{latestQuery.tempTable}] was created + +

+ + +

+
); + } else if (latestQuery.state === 'success') { + results = ; + } + } else { + results = Run a query to display results here; } - } else { - results = Run a query to display results here; + return ( +
+ + +
+ {results} +
+
+ + + +
+
+ ); } - return ( -
- - -
- {results} -
-
- - - -
-
- ); -}; +} SouthPane.propTypes = { latestQuery: React.PropTypes.object, + actions: React.PropTypes.object, }; SouthPane.defaultProps = { }; -export default SouthPane; +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(Actions, dispatch), + }; +} +export default connect(null, mapDispatchToProps)(SouthPane); diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx index ef676ce3b87b6..e2c094666d099 100644 --- a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx @@ -97,15 +97,19 @@ class SqlEditor extends React.Component { that.props.actions.querySuccess(query, results); } }, - error(err) { + error(err, textStatus, errorThrown) { let msg; try { msg = err.responseJSON.error; } catch (e) { - msg = (err.responseText) ? err.responseText : e; + if (err.responseText !== undefined) { + msg = err.responseText; + } } - if (typeof(msg) !== 'string') { - msg = JSON.stringify(msg); + if (textStatus === 'error' && errorThrown === '') { + msg = 'Could not connect to server'; + } else if (msg === null) { + msg = `[${textStatus}] ${errorThrown}`; } that.props.actions.queryFailed(query, msg); }, @@ -135,6 +139,15 @@ class SqlEditor extends React.Component { ctasChanged(event) { this.setState({ ctas: event.target.value }); } + + sqlEditorHeight() { + // quick hack to make the white bg of the tab stretch full height. + const tabNavHeight = 40; + const navBarHeight = 56; + const mysteryVerticalHeight = 50; + return window.innerHeight - tabNavHeight - navBarHeight - mysteryVerticalHeight; + } + render() { let runButtons = ( @@ -248,34 +261,30 @@ class SqlEditor extends React.Component {
); return ( -
-
-
- - - - - - - {editorBottomBar} -
- - -
-
-
+
+ + + + + + + {editorBottomBar} +
+ + +
); } diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditorLeft.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditorLeft.jsx index b3c7bdaf1efe7..d2441414a9ef3 100644 --- a/caravel/assets/javascripts/SqlLab/components/SqlEditorLeft.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditorLeft.jsx @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import * as Actions from '../actions'; import shortid from 'shortid'; import Select from 'react-select'; -import { Button } from 'react-bootstrap'; +import { Label, Button } from 'react-bootstrap'; import TableElement from './TableElement'; @@ -116,10 +116,15 @@ class SqlEditorTopToolbar extends React.Component { }); } render() { + let networkAlert = null; + if (!this.props.networkOn) { + networkAlert =

; + } const tables = this.props.tables.filter((t) => (t.queryEditorId === this.props.queryEditor.id)); const shouldShowReset = window.location.search === '?reset=1'; return (
+ {networkAlert}
@@ -124,7 +162,7 @@ class VisualizeModal extends React.Component { Datasource Name 0)} > Visualize diff --git a/caravel/assets/javascripts/SqlLab/reducers.js b/caravel/assets/javascripts/SqlLab/reducers.js index 7127415a50ad3..dcdeac9c47428 100644 --- a/caravel/assets/javascripts/SqlLab/reducers.js +++ b/caravel/assets/javascripts/SqlLab/reducers.js @@ -11,9 +11,9 @@ const defaultQueryEditor = { dbId: null, }; -// TODO(bkyryliuk): document the object schemas export const initialState = { alerts: [], + networkOn: true, queries: {}, databases: {}, queryEditors: [defaultQueryEditor], @@ -131,7 +131,7 @@ export const sqlLabReducer = function (state, action) { return alterInObject(state, 'queries', action.query, alts); }, [actions.QUERY_FAILED]() { - const alts = { state: 'failed', msg: action.msg, endDttm: now() }; + const alts = { state: 'failed', errorMessage: action.msg, endDttm: now() }; return alterInObject(state, 'queries', action.query, alts); }, [actions.SET_ACTIVE_QUERY_EDITOR]() { @@ -177,11 +177,27 @@ export const sqlLabReducer = function (state, action) { [actions.REMOVE_ALERT]() { return removeFromArr(state, 'alerts', action.alert); }, + [actions.SET_NETWORK_STATUS]() { + if (state.networkOn !== action.networkOn) { + return Object.assign({}, state, { networkOn: action.networkOn }); + } + return state; + }, [actions.REFRESH_QUERIES]() { - const newQueries = Object.assign({}, state.queries); + let newQueries = Object.assign({}, state.queries); // Fetch the updates to the queries present in the store. - for (const queryId in state.queries) { - newQueries[queryId] = Object.assign(newQueries[queryId], action.alteredQueries[queryId]); + let change = false; + for (const id in action.alteredQueries) { + const changedQuery = action.alteredQueries[id]; + if ( + !state.queries.hasOwnProperty(id) || + state.queries[id].changedOn !== changedQuery.changedOn) { + newQueries[id] = Object.assign({}, state.queries[id], changedQuery); + change = true; + } + } + if (!change) { + newQueries = state.queries; } const queriesLastUpdate = now(); return Object.assign({}, state, { queries: newQueries, queriesLastUpdate }); diff --git a/caravel/assets/javascripts/modules/dates.js b/caravel/assets/javascripts/modules/dates.js index f9781bf6195fc..9b7d3e1f4c164 100644 --- a/caravel/assets/javascripts/modules/dates.js +++ b/caravel/assets/javascripts/modules/dates.js @@ -77,12 +77,11 @@ export const timeFormatFactory = function (d3timeFormat) { export const fDuration = function (t1, t2, f = 'HH:mm:ss.SS') { const diffSec = t2 - t1; const duration = moment(new Date(diffSec)); - duration.millisecond((diffSec - Math.round(diffSec)) * 1000); return duration.utc().format(f); }; export const now = function () { // seconds from EPOCH as a float - return moment().utc().valueOf() / 1000.0; + return moment().utc().valueOf(); }; diff --git a/caravel/models.py b/caravel/models.py index 3b130017dfc49..c0752ff7442f2 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -7,6 +7,7 @@ import functools import json import logging +import re import textwrap from collections import namedtuple from copy import deepcopy, copy @@ -421,12 +422,16 @@ class Database(Model, AuditMixinNullable): def __repr__(self): return self.database_name + @property + def backend(self): + url = make_url(self.sqlalchemy_uri_decrypted) + return url.get_backend_name() + def get_sqla_engine(self, schema=None): extra = self.get_extra() - params = extra.get('engine_params', {}) url = make_url(self.sqlalchemy_uri_decrypted) - backend = url.get_backend_name() - if backend == 'presto' and schema: + params = extra.get('engine_params', {}) + if self.backend == 'presto' and schema: if '/' in url.database: url.database = url.database.split('/')[0] + '/' + schema else: @@ -1876,7 +1881,7 @@ class Query(Model): __tablename__ = 'query' id = Column(Integer, primary_key=True) - client_id = Column(String(11)) + client_id = Column(String(11), unique=True) database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False) @@ -1885,7 +1890,6 @@ class Query(Model): user_id = Column( Integer, ForeignKey('ab_user.id'), nullable=True) status = Column(String(16), default=QueryStatus.PENDING) - name = Column(String(256)) tab_name = Column(String(256)) sql_editor_id = Column(String(256)) schema = Column(String(256)) @@ -1910,7 +1914,7 @@ class Query(Model): start_time = Column(Numeric(precision=3)) end_time = Column(Numeric(precision=3)) changed_on = Column( - DateTime, default=datetime.now, onupdate=datetime.now, nullable=True) + DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=True) database = relationship( 'Database', foreign_keys=[database_id], backref='queries') @@ -1922,6 +1926,7 @@ class Query(Model): def to_dict(self): return { 'changedOn': self.changed_on, + 'changed_on': self.changed_on.isoformat(), 'dbId': self.database_id, 'endDttm': self.end_time, 'errorMessage': self.error_message, @@ -1941,3 +1946,10 @@ def to_dict(self): 'tempTable': self.tmp_table_name, 'userId': self.user_id, } + @property + def name(self): + ts = datetime.now().isoformat() + ts = ts.replace('-', '').replace(':', '').split('.')[0] + tab = self.tab_name.replace(' ', '_').lower() if self.tab_name else 'notab' + tab = re.sub(r'\W+', '', tab) + return "sqllab_{tab}_{ts}".format(**locals()) diff --git a/caravel/sql_lab.py b/caravel/sql_lab.py index e5613045f0a33..b1d64a9c09079 100644 --- a/caravel/sql_lab.py +++ b/caravel/sql_lab.py @@ -14,7 +14,7 @@ def is_query_select(sql): return sql.upper().startswith('SELECT') -def create_table_as(sql, table_name, override=False): +def create_table_as(sql, table_name, schema=None, override=False): """Reformats the query into the create table as query. Works only for the single select SQL statements, in all other cases @@ -29,10 +29,12 @@ def create_table_as(sql, table_name, override=False): # TODO(bkyryliuk): drop table if allowed, check the namespace and # the permissions. # TODO raise if multi-statement + if schema: + table_name = schema + '.' + table_name exec_sql = '' if is_query_select(sql): if override: - exec_sql = 'DROP TABLE IF EXISTS {};\n'.format(table_name) + exec_sql = 'DROP TABLE IF EXISTS {table_name};\n' exec_sql += "CREATE TABLE {table_name} AS \n{sql}" else: raise Exception("Could not generate CREATE TABLE statement") @@ -43,7 +45,6 @@ def create_table_as(sql, table_name, override=False): def get_sql_results(query_id, return_results=True): """Executes the sql query returns the results.""" db.session.commit() # HACK - q = db.session.query(models.Query).all() query = db.session.query(models.Query).filter_by(id=query_id).one() database = query.database executed_sql = query.sql.strip().strip(';') @@ -56,7 +57,8 @@ def get_sql_results(query_id, return_results=True): query.tmp_table_name = 'tmp_{}_table_{}'.format( query.user_id, start_dttm.strftime('%Y_%m_%d_%H_%M_%S')) - executed_sql = create_table_as(executed_sql, query.tmp_table_name) + executed_sql = create_table_as( + executed_sql, query.tmp_table_name, database.force_ctas_schema) query.select_as_cta_used = True elif query.limit: executed_sql = database.wrap_sql_limit(executed_sql, query.limit) @@ -77,7 +79,7 @@ def get_sql_results(query_id, return_results=True): cursor = result_proxy.cursor query.status = QueryStatus.RUNNING db.session.flush() - if hasattr(cursor, "poll"): + if database.backend == 'presto': polled = cursor.poll() # poll returns dict -- JSON status information or ``None`` # if the query is done diff --git a/caravel/views.py b/caravel/views.py index 081610fdcdb64..999c1082d1fed 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -26,7 +26,7 @@ from flask_appbuilder.models.sqla.filters import BaseFilter from sqlalchemy import create_engine -from werkzeug.datastructures import ImmutableMultiDict, MultiDict +from werkzeug.datastructures import ImmutableMultiDict from werkzeug.routing import BaseConverter from wtforms.validators import ValidationError @@ -1419,12 +1419,13 @@ def sync_druid_source(self): def sqllab_viz(self): data = json.loads(request.args.get('data')) table_name = data.get('datasourceName') + viz_type = data.get('chartType') table = db.session.query(models.SqlaTable).filter_by(table_name=table_name).first() if not table: table = models.SqlaTable( table_name=table_name, ) - table.database_id = data.get('databaseId') + table.database_id = data.get('dbId') table.sql = data.get('sql') db.session.add(table) cols = [] @@ -1435,6 +1436,7 @@ def sqllab_viz(self): column_name=column_name, filterable=is_dim, groupby=is_dim, + is_dttm=config.get('is_date', False), )) agg = config.get('agg') if agg: @@ -1442,14 +1444,16 @@ def sqllab_viz(self): metric_name="{agg}__{column_name}".format(**locals()), expression="{agg}({column_name})".format(**locals()), )) - metrics.append(models.SqlMetric( - metric_name="count".format(**locals()), - expression="count(*)".format(**locals()), - )) + if not metrics: + metrics.append(models.SqlMetric( + metric_name="count".format(**locals()), + expression="count(*)".format(**locals()), + )) table.columns = cols table.metrics = metrics db.session.commit() - return redirect('/caravel/explore/table/{table.id}/'.format(**locals())) + url = '/caravel/explore/table/{table.id}/?viz_type={viz_type}' + return redirect(url.format(**locals())) @has_access @expose("/sql//") @@ -1596,12 +1600,15 @@ def sql_json(self): mimetype="application/json") @has_access - @expose("/csv/") + @expose("/csv/") @log_this - def csv(self, query_id): + def csv(self, client_id): """Download the query results as csv.""" - s = db.session() - query = s.query(models.Query).filter_by(id=int(query_id)).first() + query = ( + db.session.query(models.Query) + .filter_by(client_id=client_id) + .one() + ) if not self.database_access(query.database): flash(get_database_access_error_msg(query.database.database_name)) @@ -1628,20 +1635,16 @@ def queries(self, last_updated_ms): mimetype="application/json") # Unix time, milliseconds. - last_updated_ms_int = int(last_updated_ms) if last_updated_ms else 0 - - # Local date time, DO NOT USE IT. - # last_updated_dt = datetime.fromtimestamp(int(last_updated_ms) / 1000) + last_updated_ms_int = int(float(last_updated_ms)) if last_updated_ms else 0 # UTC date time, same that is stored in the DB. - last_updated_dt = utils.EPOCH + timedelta( - seconds=last_updated_ms_int / 1000) + last_updated_dt = utils.EPOCH + timedelta(seconds=last_updated_ms_int / 1000) sql_queries = ( db.session.query(models.Query) .filter( - models.Query.user_id == g.user.get_id() or - models.Query.changed_on >= last_updated_dt + models.Query.user_id == g.user.get_id(), + models.Query.changed_on >= last_updated_dt, ) .all() ) diff --git a/tests/base_tests.py b/tests/base_tests.py index 437266faadb3c..4cae61bf12157 100644 --- a/tests/base_tests.py +++ b/tests/base_tests.py @@ -14,13 +14,6 @@ os.environ['CARAVEL_CONFIG'] = 'tests.caravel_test_config' -''' -app.config['TESTING'] = True -app.config['CSRF_ENABLED'] = False -app.config['SECRET_KEY'] = 'thisismyscretkey' -app.config['WTF_CSRF_ENABLED'] = False -app.config['PUBLIC_ROLE_LIKE_GAMMA'] = True -''' BASE_DIR = app.config.get("BASE_DIR") @@ -68,6 +61,16 @@ def get_query_by_sql(self, sql): session.close() return query + def get_latest_query(self, sql): + session = db.create_scoped_session() + query = ( + session.query(models.Query) + .order_by(models.Query.id.desc()) + .first() + ) + session.close() + return query + def logout(self): self.client.get('/logout/', follow_redirects=True) diff --git a/tests/core_tests.py b/tests/core_tests.py index 32724a731c42e..cefa2d852d57c 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -4,12 +4,12 @@ from __future__ import print_function from __future__ import unicode_literals -from datetime import datetime import csv import doctest import imp import json import io +import random import unittest @@ -367,16 +367,19 @@ def test_sql_json_has_access(self): assert len(data['data']) > 0 def test_csv_endpoint(self): - sql = "SELECT first_name, last_name FROM ab_user " \ - "where first_name='admin'" - self.run_sql(sql, 'admin') + sql = """ + SELECT first_name, last_name + FROM ab_user + WHERE first_name='admin' + """ + client_id = "{}".format(random.getrandbits(64))[:10] + self.run_sql(sql, 'admin', client_id) - query1_id = self.get_query_by_sql(sql).id self.login('admin') - resp = self.client.get('/caravel/csv/{}'.format(query1_id)) + resp = self.client.get('/caravel/csv/{}'.format(client_id)) data = csv.reader(io.StringIO(resp.data.decode('utf-8'))) - expected_data = csv.reader(io.StringIO( - "first_name,last_name\nadmin, user\n")) + expected_data = csv.reader( + io.StringIO("first_name,last_name\nadmin, user\n")) self.assertEqual(list(expected_data), list(data)) self.logout()