diff --git a/setup.py b/setup.py index a24ff19473765..2da3f3254bb2d 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,6 @@ def get_git_sha(): 'sqlparse==0.1.19', 'thrift>=0.9.3', 'thrift-sasl>=0.2.1', - 'werkzeug==0.11.15', ], extras_require={ 'cors': ['Flask-Cors>=2.0.0'], diff --git a/superset/assets/javascripts/explorev2/components/controls/Filter.jsx b/superset/assets/javascripts/explorev2/components/controls/Filter.jsx index 9e783fc6214d1..64e19c33ef512 100644 --- a/superset/assets/javascripts/explorev2/components/controls/Filter.jsx +++ b/superset/assets/javascripts/explorev2/components/controls/Filter.jsx @@ -4,8 +4,22 @@ import Select from 'react-select'; import { Button, Row, Col } from 'react-bootstrap'; import SelectControl from './SelectControl'; -const arrayFilterOps = ['in', 'not in']; -const strFilterOps = ['==', '!=', '>', '<', '>=', '<=', 'regex']; +const operatorsArr = [ + { val: 'in', type: 'array', useSelect: true, multi: true }, + { val: 'not in', type: 'array', useSelect: true, multi: true }, + { val: '==', type: 'string', useSelect: true, multi: false }, + { val: '!=', type: 'string', useSelect: true, multi: false }, + { val: '>=', type: 'string' }, + { val: '<=', type: 'string' }, + { val: '>', type: 'string' }, + { val: '<', type: 'string' }, + { val: 'regex', type: 'string', datasourceTypes: ['druid'] }, + { val: 'LIKE', type: 'string', datasourceTypes: ['table'] }, +]; +const operators = {}; +operatorsArr.forEach(op => { + operators[op.val] = op; +}); const propTypes = { choices: PropTypes.array, @@ -13,11 +27,9 @@ const propTypes = { removeFilter: PropTypes.func, filter: PropTypes.object.isRequired, datasource: PropTypes.object, - having: PropTypes.bool, }; const defaultProps = { - having: false, changeFilter: () => {}, removeFilter: () => {}, choices: [], @@ -27,102 +39,91 @@ const defaultProps = { export default class Filter extends React.Component { constructor(props) { super(props); - const filterOps = props.datasource.type === 'table' ? - ['in', 'not in'] : ['==', '!=', 'in', 'not in', 'regex']; - this.opChoices = this.props.having ? ['==', '!=', '>', '<', '>=', '<='] - : filterOps; + this.state = { + valuesLoading: false, + }; + } + componentDidMount() { + this.fetchFilterValues(this.props.filter.col); } fetchFilterValues(col) { - if (!this.props.datasource) { - return; - } const datasource = this.props.datasource; - let choices = []; - if (col) { + if (col && this.props.datasource && this.props.datasource.filter_select) { + this.setState({ valuesLoading: true }); $.ajax({ type: 'GET', url: `/superset/filter/${datasource.type}/${datasource.id}/${col}/`, success: (data) => { - choices = Object.keys(data).map((k) => - ([`'${data[k]}'`, `'${data[k]}'`])); - this.props.changeFilter('choices', choices); + this.props.changeFilter('choices', data); + this.setState({ valuesLoading: false }); }, }); } } - switchFilterValue(prevFilter, nextOp) { - const prevOp = prevFilter.op; - let newVal = null; - if (arrayFilterOps.indexOf(prevOp) !== -1 - && strFilterOps.indexOf(nextOp) !== -1) { + switchFilterValue(prevOp, nextOp) { + if (operators[prevOp].type !== operators[nextOp].type) { + const val = this.props.filter.value; + let newVal; // switch from array to string - newVal = this.props.filter.val.length > 0 ? this.props.filter.val[0] : ''; - } - if (strFilterOps.indexOf(prevOp) !== -1 - && arrayFilterOps.indexOf(nextOp) !== -1) { - // switch from string to array - newVal = this.props.filter.val === '' ? [] : [this.props.filter.val]; - } - return newVal; - } - changeFilter(control, event) { - let value = event; - if (event && event.target) { - value = event.target.value; - } - if (event && event.value) { - value = event.value; - } - if (control === 'op') { - const newVal = this.switchFilterValue(this.props.filter, value); - if (newVal) { - this.props.changeFilter(['op', 'val'], [value, newVal]); - } else { - this.props.changeFilter(control, value); + if (operators[nextOp].type === 'string' && val && val.length > 0) { + newVal = val[0]; + } else if (operators[nextOp].type === 'string' && val) { + newVal = [val]; } - } else { - this.props.changeFilter(control, value); - } - if (control === 'col' && value !== null && this.props.datasource.filter_select) { - this.fetchFilterValues(value); + this.props.changeFilter('val', newVal); } } + changeText(event) { + this.props.changeFilter('val', event.target.value); + } + changeSelect(value) { + this.props.changeFilter('val', value); + } + changeColumn(event) { + this.props.changeFilter('col', event.value); + this.fetchFilterValues(event.value); + } + changeOp(event) { + this.switchFilterValue(this.props.filter.op, event.value); + this.props.changeFilter('op', event.value); + } removeFilter(filter) { this.props.removeFilter(filter); } renderFilterFormControl(filter) { - const datasource = this.props.datasource; - if (datasource && datasource.filter_select) { - if (!filter.choices) { - this.fetchFilterValues(filter.col); - } - } - // switching filter value between array/string when needed - if (strFilterOps.indexOf(filter.op) !== -1) { - // druid having filter or regex/==/!= filters + const operator = operators[filter.op]; + if (operator.useSelect) { return ( - ); } return ( - ); } render() { + const datasource = this.props.datasource; const filter = this.props.filter; + const opsChoices = operatorsArr + .filter(o => !o.datasourceTypes || o.datasourceTypes.indexOf(datasource.type) >= 0) + .map(o => ({ value: o.val, label: o.val })); + const colChoices = datasource ? + datasource.filterable_cols.map(c => ({ value: c[0], label: c[1] })) : + null; return (
@@ -130,9 +131,10 @@ export default class Filter extends React.Component { ({ value: o, label: o }))} + options={opsChoices} + clearable={false} value={filter.op} - onChange={this.changeFilter.bind(this, 'op')} + onChange={this.changeOp.bind(this)} /> diff --git a/superset/assets/javascripts/explorev2/components/controls/FilterControl.jsx b/superset/assets/javascripts/explorev2/components/controls/FilterControl.jsx index cac520a2c7b21..9c6c6e773b99f 100644 --- a/superset/assets/javascripts/explorev2/components/controls/FilterControl.jsx +++ b/superset/assets/javascripts/explorev2/components/controls/FilterControl.jsx @@ -4,14 +4,12 @@ import Filter from './Filter'; const propTypes = { name: PropTypes.string, - choices: PropTypes.array, onChange: PropTypes.func, value: PropTypes.array, datasource: PropTypes.object, }; const defaultProps = { - choices: [], onChange: () => {}, value: [], }; @@ -19,8 +17,11 @@ const defaultProps = { export default class FilterControl extends React.Component { addFilter() { const newFilters = Object.assign([], this.props.value); + const col = this.props.datasource && this.props.datasource.filterable_cols.length > 0 ? + this.props.datasource.filterable_cols[0][0] : + null; newFilters.push({ - col: null, + col, op: 'in', val: this.props.datasource.filter_select ? [] : '', }); @@ -43,22 +44,17 @@ export default class FilterControl extends React.Component { this.props.onChange(this.props.value.filter((f, i) => i !== index)); } render() { - const filters = []; - this.props.value.forEach((filter, i) => { - const filterBox = ( -
- -
- ); - filters.push(filterBox); - }); + const filters = this.props.value.map((filter, i) => ( +
+ +
+ )); return (
{filters} diff --git a/superset/assets/javascripts/explorev2/components/controls/SelectControl.jsx b/superset/assets/javascripts/explorev2/components/controls/SelectControl.jsx index cb46720fc1e60..e16f9d6f36cda 100644 --- a/superset/assets/javascripts/explorev2/components/controls/SelectControl.jsx +++ b/superset/assets/javascripts/explorev2/components/controls/SelectControl.jsx @@ -47,30 +47,39 @@ export default class SelectControl extends React.PureComponent { this.props.onChange(optionValue); } getOptions(props) { - const options = props.choices.map((c) => { - const label = c.length > 1 ? c[1] : c[0]; - const newOptions = { - value: c[0], - label, - }; - if (c[2]) newOptions.imgSrc = c[2]; - return newOptions; + // Accepts different formats of input + const options = props.choices.map(c => { + let option; + if (Array.isArray(c)) { + const label = c.length > 1 ? c[1] : c[0]; + option = { + value: c[0], + label, + }; + if (c[2]) option.imgSrc = c[2]; + } else if (Object.is(c)) { + option = c; + } else { + option = { + value: c, + label: c, + }; + } + return option; }); if (props.freeForm) { // For FreeFormSelect, insert value into options if not exist - const values = props.choices.map((c) => c[0]); + const values = options.map(c => c.value); if (props.value) { - if (typeof props.value === 'object') { - props.value.forEach((v) => { - if (values.indexOf(v) === -1) { - options.push({ value: v, label: v }); - } - }); - } else { - if (values.indexOf(props.value) === -1) { - options.push({ value: props.value, label: props.value }); - } + let valuesToAdd = props.value; + if (!Array.isArray(valuesToAdd)) { + valuesToAdd = [valuesToAdd]; } + valuesToAdd.forEach(v => { + if (values.indexOf(v) < 0) { + options.push({ value: v, label: v }); + } + }); } } return options; diff --git a/superset/assets/javascripts/explorev2/stores/controls.jsx b/superset/assets/javascripts/explorev2/stores/controls.jsx index db5ca17847a6b..f5267662d8e7c 100644 --- a/superset/assets/javascripts/explorev2/stores/controls.jsx +++ b/superset/assets/javascripts/explorev2/stores/controls.jsx @@ -1177,7 +1177,6 @@ export const controls = { default: [], description: '', mapStateToProps: (state) => ({ - choices: (state.datasource) ? state.datasource.filterable_cols : [], datasource: state.datasource, }), }, diff --git a/superset/assets/spec/javascripts/explorev2/components/FilterControl_spec.js b/superset/assets/spec/javascripts/explorev2/components/FilterControl_spec.js index 71d6a4082dec9..63b3f35aebead 100644 --- a/superset/assets/spec/javascripts/explorev2/components/FilterControl_spec.js +++ b/superset/assets/spec/javascripts/explorev2/components/FilterControl_spec.js @@ -17,6 +17,7 @@ const defaultProps = { id: 1, type: 'table', filter_select: false, + filterable_cols: ['country_name'], }, }; diff --git a/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js b/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js index b21efe2ed2a71..121032bd4d81c 100644 --- a/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js +++ b/superset/assets/spec/javascripts/explorev2/components/Filter_spec.js @@ -10,11 +10,8 @@ import Filter from '../../../../javascripts/explorev2/components/controls/Filter import SelectControl from '../../../../javascripts/explorev2/components/controls/SelectControl'; const defaultProps = { - choices: ['country_name'], changeFilter: sinon.spy(), - removeFilter: () => { - // noop - }, + removeFilter: () => {}, filter: { col: null, op: 'in', @@ -22,8 +19,9 @@ const defaultProps = { }, datasource: { id: 1, - type: 'table', + type: 'qtable', filter_select: false, + filterable_cols: ['col1', 'col2'], }, }; @@ -44,7 +42,7 @@ describe('Filter', () => { expect(wrapper.find(Select)).to.have.lengthOf(2); expect(wrapper.find(Button)).to.have.lengthOf(1); expect(wrapper.find(SelectControl)).to.have.lengthOf(1); - expect(wrapper.find('#select-op').prop('options')).to.have.lengthOf(2); + expect(wrapper.find('#select-op').prop('options')).to.have.lengthOf(8); }); it('renders five op choices for table datasource', () => { @@ -53,16 +51,17 @@ describe('Filter', () => { id: 1, type: 'druid', filter_select: false, + filterable_cols: ['country_name'], }; const druidWrapper = shallow(); - expect(druidWrapper.find('#select-op').prop('options')).to.have.lengthOf(5); + expect(druidWrapper.find('#select-op').prop('options')).to.have.lengthOf(9); }); it('renders six op choices for having filter', () => { const props = defaultProps; props.having = true; const havingWrapper = shallow(); - expect(havingWrapper.find('#select-op').prop('options')).to.have.lengthOf(6); + expect(havingWrapper.find('#select-op').prop('options')).to.have.lengthOf(9); }); it('calls changeFilter when select is changed', () => { diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py index 33683508f52bb..6bb1c52a8d95d 100644 --- a/superset/connectors/druid/models.py +++ b/superset/connectors/druid/models.py @@ -320,6 +320,7 @@ class DruidDatasource(Model, BaseDatasource): is_hidden = Column(Boolean, default=False) filter_select_enabled = Column(Boolean, default=False) description = Column(Text) + fetch_values_from = Column(String(100)) default_endpoint = Column(Text) user_id = Column(Integer, ForeignKey('ab_user.id')) owner = relationship( @@ -690,18 +691,18 @@ def granularity(period_name, timezone=None, origin=None): def values_for_column(self, column_name, - from_dttm, - to_dttm, - limit=500): + limit=10000): """Retrieve some values for the given column""" # TODO: Use Lexicographic TopNMetricSpec once supported by PyDruid - from_dttm = from_dttm.replace(tzinfo=DRUID_TZ) - to_dttm = to_dttm.replace(tzinfo=DRUID_TZ) + if self.fetch_values_from: + from_dttm = utils.parse_human_datetime(self.fetch_values_from) + else: + from_dttm = datetime(1970, 1, 1) qry = dict( datasource=self.datasource_name, granularity="all", - intervals=from_dttm.isoformat() + '/' + to_dttm.isoformat(), + intervals=from_dttm.isoformat() + '/' + datetime.now().isoformat(), aggregations=dict(count=count("count")), dimension=column_name, metric="count", @@ -712,10 +713,7 @@ def values_for_column(self, client.topn(**qry) df = client.export_pandas() - if df is None or df.size == 0: - raise Exception(_("No data was returned.")) - - return df + return [row[0] for row in df.to_records(index=False)] def get_query_str( # noqa / druid self, client, qry_start_dttm, @@ -1009,6 +1007,14 @@ def get_filters(self, raw_filters): # noqa cond = ~cond elif op == 'regex': cond = Filter(type="regex", pattern=eq, dimension=col) + elif op == '>=': + cond = Dimension(col) >= eq + elif op == '<=': + cond = Dimension(col) <= eq + elif op == '>': + cond = Dimension(col) > eq + elif op == '<': + cond = Dimension(col) < eq if filters: filters = Filter(type="and", fields=[ cond, diff --git a/superset/connectors/druid/views.py b/superset/connectors/druid/views.py index d86e380295269..1c242f48d9b88 100644 --- a/superset/connectors/druid/views.py +++ b/superset/connectors/druid/views.py @@ -146,7 +146,8 @@ class DruidDatasourceModelView(SupersetModelView, DeleteMixin): # noqa related_views = [DruidColumnInlineView, DruidMetricInlineView] edit_columns = [ 'datasource_name', 'cluster', 'description', 'owner', - 'is_featured', 'is_hidden', 'filter_select_enabled', + 'is_featured', 'is_hidden', + 'filter_select_enabled', 'fetch_values_from', 'default_endpoint', 'offset', 'cache_timeout'] add_columns = edit_columns show_columns = add_columns + ['perm'] @@ -157,6 +158,9 @@ class DruidDatasourceModelView(SupersetModelView, DeleteMixin): # noqa 'description': Markup( "Supports markdown"), + 'fetch_values_from': _( + "Time expression to use as a predicate when retrieving " + "distinct values to populate the filter component"), } base_filters = [['id', DatasourceFilter, lambda: []]] label_columns = { diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 0ed21fcd7efb4..7d213c1425245 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -172,6 +172,7 @@ class SqlaTable(Model, BaseDatasource): database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False) is_featured = Column(Boolean, default=False) filter_select_enabled = Column(Boolean, default=False) + fetch_values_predicate = Column(String(1000)) user_id = Column(Integer, ForeignKey('ab_user.id')) owner = relationship('User', backref='tables', foreign_keys=[user_id]) database = relationship( @@ -285,33 +286,25 @@ def get_col(self, col_name): if col_name == col.column_name: return col - def values_for_column(self, - column_name, - from_dttm, - to_dttm, - limit=500): + def values_for_column(self, column_name, limit=10000): """Runs query against sqla to retrieve some sample values for the given column. """ - granularity = self.main_dttm_col - cols = {col.column_name: col for col in self.columns} target_col = cols[column_name] tbl = table(self.table_name) - qry = sa.select([target_col.sqla_col]) - qry = qry.select_from(tbl) - qry = qry.distinct(column_name) - qry = qry.limit(limit) + qry = ( + select([target_col.sqla_col]) + .select_from(tbl) + .distinct(column_name) + ) + if limit: + qry = qry.limit(limit) - if granularity: - dttm_col = cols[granularity] - timestamp = dttm_col.sqla_col.label('timestamp') - time_filter = [ - timestamp >= text(dttm_col.dttm_sql_literal(from_dttm)), - timestamp <= text(dttm_col.dttm_sql_literal(to_dttm)), - ] - qry = qry.where(and_(*time_filter)) + if self.fetch_values_predicate: + tp = self.get_template_processor() + qry = qry.where(tp.process_template(self.fetch_values_predicate)) engine = self.database.get_sqla_engine() sql = "{}".format( @@ -319,10 +312,15 @@ def values_for_column(self, engine, compile_kwargs={"literal_binds": True}, ), ) - return pd.read_sql_query( + df = pd.read_sql_query( sql=sql, con=engine ) + return [row[0] for row in df.to_records(index=False)] + + def get_template_processor(self, **kwargs): + return get_template_processor( + table=self, database=self.database, **kwargs) def get_query_str( # sqla self, engine, qry_start_dttm, @@ -340,6 +338,7 @@ def get_query_str( # sqla extras=None, columns=None): """Querying any sqla table from this common interface""" + template_kwargs = { 'from_dttm': from_dttm, 'groupby': groupby, @@ -347,8 +346,7 @@ def get_query_str( # sqla 'row_limit': row_limit, 'to_dttm': to_dttm, } - template_processor = get_template_processor( - table=self, database=self.database, **template_kwargs) + template_processor = self.get_template_processor(**template_kwargs) # For backward compatibility if granularity not in self.dttm_cols: @@ -452,14 +450,30 @@ def visit_column(element, compiler, **kw): op = flt['op'] eq = flt['val'] col_obj = cols.get(col) - if col_obj and op in ('in', 'not in'): - values = [types.strip("'").strip('"') for types in eq] - if col_obj.is_num: - values = [utils.js_string_to_num(s) for s in values] - cond = col_obj.sqla_col.in_(values) - if op == 'not in': - cond = ~cond - where_clause_and.append(cond) + if col_obj: + if op in ('in', 'not in'): + values = [types.strip("'").strip('"') for types in eq] + if col_obj.is_num: + values = [utils.js_string_to_num(s) for s in values] + cond = col_obj.sqla_col.in_(values) + if op == 'not in': + cond = ~cond + where_clause_and.append(cond) + elif op == '==': + where_clause_and.append(col_obj.sqla_col == eq) + elif op == '!=': + where_clause_and.append(col_obj.sqla_col != eq) + elif op == '>': + where_clause_and.append(col_obj.sqla_col > eq) + elif op == '<': + where_clause_and.append(col_obj.sqla_col < eq) + elif op == '>=': + where_clause_and.append(col_obj.sqla_col >= eq) + elif op == '<=': + where_clause_and.append(col_obj.sqla_col <= eq) + elif op == 'LIKE': + where_clause_and.append( + col_obj.sqla_col.like(eq.replace('%', '%%'))) if extras: where = extras.get('where') if where: diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index f5dccc76ce200..c1e4002559db9 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -130,7 +130,7 @@ class TableModelView(SupersetModelView, DeleteMixin): # noqa add_columns = ['database', 'schema', 'table_name'] edit_columns = [ 'table_name', 'sql', 'is_featured', 'filter_select_enabled', - 'database', 'schema', + 'fetch_values_predicate', 'database', 'schema', 'description', 'owner', 'main_dttm_col', 'default_endpoint', 'offset', 'cache_timeout'] show_columns = edit_columns + ['perm'] @@ -150,6 +150,11 @@ class TableModelView(SupersetModelView, DeleteMixin): # noqa "This fields acts a Superset view, meaning that Superset will " "run a query against this string as a subquery." ), + 'fetch_values_predicate': _( + "Predicate applied when fetching distinct value to " + "populate the filter control component. Supports " + "jinja template syntax." + ), } base_filters = [['id', DatasourceFilter, lambda: []]] label_columns = { diff --git a/superset/migrations/versions/732f1c06bcbf_add_fetch_values_predicate.py b/superset/migrations/versions/732f1c06bcbf_add_fetch_values_predicate.py new file mode 100644 index 0000000000000..2d7ce54107a83 --- /dev/null +++ b/superset/migrations/versions/732f1c06bcbf_add_fetch_values_predicate.py @@ -0,0 +1,23 @@ +"""add fetch values predicate + +Revision ID: 732f1c06bcbf +Revises: d6db5a5cdb5d +Create Date: 2017-03-03 09:15:56.800930 + +""" + +# revision identifiers, used by Alembic. +revision = '732f1c06bcbf' +down_revision = 'd6db5a5cdb5d' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('datasources', sa.Column('fetch_values_from', sa.String(length=100), nullable=True)) + op.add_column('tables', sa.Column('fetch_values_predicate', sa.String(length=1000), nullable=True)) + + +def downgrade(): + op.drop_column('tables', 'fetch_values_predicate') + op.drop_column('datasources', 'fetch_values_from') diff --git a/superset/migrations/versions/ea033256294a_.py b/superset/migrations/versions/ea033256294a_.py new file mode 100644 index 0000000000000..8a5bbd21c32d4 --- /dev/null +++ b/superset/migrations/versions/ea033256294a_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: ea033256294a +Revises: ('732f1c06bcbf', 'b318dfe5fb6c') +Create Date: 2017-03-16 14:55:59.431283 + +""" + +# revision identifiers, used by Alembic. +revision = 'ea033256294a' +down_revision = ('732f1c06bcbf', 'b318dfe5fb6c') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/views/core.py b/superset/views/core.py index a6c71a8276c20..9e9b5dbe3b15d 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1128,33 +1128,17 @@ def filter(self, datasource_type, datasource_id, column): :return: """ # TODO: Cache endpoint by user, datasource and column - error_redirect = '/slicemodelview/list/' datasource_class = ConnectorRegistry.sources[datasource_type] - datasource = db.session.query( datasource_class).filter_by(id=datasource_id).first() if not datasource: - flash(DATASOURCE_MISSING_ERR, "alert") return json_error_response(DATASOURCE_MISSING_ERR) if not self.datasource_access(datasource): - flash(get_datasource_access_error_msg(datasource.name), "danger") return json_error_response(DATASOURCE_ACCESS_ERR) - viz_type = request.args.get("viz_type") - if not viz_type and datasource.default_endpoint: - return redirect(datasource.default_endpoint) - if not viz_type: - viz_type = "table" - try: - obj = viz.viz_types[viz_type]( - datasource, - form_data=request.args, - slice_=None) - except Exception as e: - flash(str(e), "danger") - return redirect(error_redirect) - return json_success(obj.get_values_for_column(column)) + payload = json.dumps(datasource.values_for_column(column)) + return json_success(payload) def save_or_overwrite_slice( self, args, slc, slice_add_perm, slice_overwrite_perm, diff --git a/superset/viz.py b/superset/viz.py index 02fc0791ad6b6..092fcd576f89a 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -45,7 +45,6 @@ class BaseViz(object): is_timeseries = False def __init__(self, datasource, form_data, slice_=None): - self.orig_form_data = form_data if not datasource: raise Exception("Viz is missing a datasource") self.datasource = datasource @@ -63,34 +62,6 @@ def __init__(self, datasource, form_data, slice_=None): self.status = None self.error_message = None - def get_filter_url(self): - """Returns the URL to retrieve column values used in the filter""" - data = self.orig_form_data.copy() - # Remove unchecked checkboxes because HTML is weird like that - ordered_data = MultiDict() - for key in sorted(data.keys()): - # if MultiDict is initialized with MD({key:[emptyarray]}), - # key is included in d.keys() but accessing it throws - try: - if data[key] is False: - del data[key] - continue - except IndexError: - pass - - if isinstance(data, (MultiDict, ImmutableMultiDict)): - v = data.getlist(key) - else: - v = data.get(key) - if not isinstance(v, list): - v = [v] - for item in v: - ordered_data.add(key, item) - href = Href( - '/superset/filter/{self.datasource.type}/' - '{self.datasource.id}/'.format(**locals())) - return href(ordered_data) - def get_df(self, query_obj=None): """Returns a pandas dataframe based on the query object""" if not query_obj: @@ -277,7 +248,6 @@ def get_payload(self, force=False): 'cache_timeout': cache_timeout, 'data': data, 'error': self.error_message, - 'filter_endpoint': self.filter_endpoint, 'form_data': self.form_data, 'query': self.query, 'status': self.status, @@ -312,7 +282,6 @@ def data(self): """This is the data object serialized to the js layer""" content = { 'form_data': self.form_data, - 'filter_endpoint': self.filter_endpoint, 'token': self.token, 'viz_name': self.viz_type, 'filter_select_enabled': self.datasource.filter_select_enabled, @@ -324,40 +293,9 @@ def get_csv(self): include_index = not isinstance(df.index, pd.RangeIndex) return df.to_csv(index=include_index, encoding="utf-8") - def get_values_for_column(self, column): - """ - Retrieves values for a column to be used by the filter dropdown. - - :param column: column name - :return: JSON containing the some values for a column - """ - form_data = self.form_data - - since = form_data.get("since", "1 year ago") - from_dttm = utils.parse_human_datetime(since) - now = datetime.now() - if from_dttm > now: - from_dttm = now - (from_dttm - now) - until = form_data.get("until", "now") - to_dttm = utils.parse_human_datetime(until) - if from_dttm > to_dttm: - raise Exception("From date cannot be larger than to date") - - kwargs = dict( - column_name=column, - from_dttm=from_dttm, - to_dttm=to_dttm, - ) - df = self.datasource.values_for_column(**kwargs) - return df[column].to_json() - def get_data(self, df): return [] - @property - def filter_endpoint(self): - return self.get_filter_url() - @property def json_data(self): return json.dumps(self.data)