mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
Merge pull request #23 from mistercrunch/where
Custom WHERE clause for tables (not druid) + error handling refactor
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from wtforms import Field, Form, SelectMultipleField, SelectField, TextField
|
from wtforms import Field, Form, SelectMultipleField, SelectField, TextField
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
|
||||||
class OmgWtForm(Form):
|
class OmgWtForm(Form):
|
||||||
@@ -24,7 +25,8 @@ class OmgWtForm(Form):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def form_factory(datasource, viz, form_args=None):
|
def form_factory(viz):
|
||||||
|
datasource = viz.datasource
|
||||||
from panoramix.viz import viz_types
|
from panoramix.viz import viz_types
|
||||||
row_limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
row_limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
||||||
series_limits = [0, 5, 10, 25, 50, 100, 500]
|
series_limits = [0, 5, 10, 25, 50, 100, 500]
|
||||||
@@ -57,6 +59,7 @@ def form_factory(datasource, viz, form_args=None):
|
|||||||
'x': SelectField('X Axis', choices=datasource.metrics_combo),
|
'x': SelectField('X Axis', choices=datasource.metrics_combo),
|
||||||
'y': SelectField('Y Axis', choices=datasource.metrics_combo),
|
'y': SelectField('Y Axis', choices=datasource.metrics_combo),
|
||||||
'size': SelectField('Bubble Size', choices=datasource.metrics_combo),
|
'size': SelectField('Bubble Size', choices=datasource.metrics_combo),
|
||||||
|
'where': TextField('Custom WHERE clause'),
|
||||||
}
|
}
|
||||||
field_css_classes = {k: ['form-control'] for k in px_form_fields.keys()}
|
field_css_classes = {k: ['form-control'] for k in px_form_fields.keys()}
|
||||||
select2 = [
|
select2 = [
|
||||||
@@ -70,7 +73,7 @@ def form_factory(datasource, viz, form_args=None):
|
|||||||
field_css_classes[field] += ['select2']
|
field_css_classes[field] += ['select2']
|
||||||
|
|
||||||
class QueryForm(OmgWtForm):
|
class QueryForm(OmgWtForm):
|
||||||
field_order = viz.form_fields
|
field_order = copy(viz.form_fields)
|
||||||
css_classes = field_css_classes
|
css_classes = field_css_classes
|
||||||
|
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
@@ -85,4 +88,9 @@ def form_factory(datasource, viz, form_args=None):
|
|||||||
ff = [ff]
|
ff = [ff]
|
||||||
for s in ff:
|
for s in ff:
|
||||||
setattr(QueryForm, s, px_form_fields[s])
|
setattr(QueryForm, s, px_form_fields[s])
|
||||||
|
|
||||||
|
# datasource type specific form elements
|
||||||
|
if datasource.__class__.__name__ == 'Table':
|
||||||
|
QueryForm.field_order += ['where']
|
||||||
|
setattr(QueryForm, 'where', px_form_fields['where'])
|
||||||
return QueryForm
|
return QueryForm
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy import (
|
|||||||
from sqlalchemy import Table as sqlaTable
|
from sqlalchemy import Table as sqlaTable
|
||||||
from sqlalchemy import create_engine, MetaData, desc, select, and_
|
from sqlalchemy import create_engine, MetaData, desc, select, and_
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import table, literal_column
|
from sqlalchemy.sql import table, literal_column, text
|
||||||
|
|
||||||
from copy import deepcopy, copy
|
from copy import deepcopy, copy
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@@ -99,7 +99,9 @@ class Table(Model, Queryable, AuditMixin):
|
|||||||
limit_spec=None,
|
limit_spec=None,
|
||||||
filter=None,
|
filter=None,
|
||||||
is_timeseries=True,
|
is_timeseries=True,
|
||||||
timeseries_limit=15, row_limit=None):
|
timeseries_limit=15,
|
||||||
|
row_limit=None,
|
||||||
|
extras=None):
|
||||||
"""
|
"""
|
||||||
Unused, legacy way of querying by building a SQL string without
|
Unused, legacy way of querying by building a SQL string without
|
||||||
using the sqlalchemy expression API (new approach which supports
|
using the sqlalchemy expression API (new approach which supports
|
||||||
@@ -192,7 +194,8 @@ class Table(Model, Queryable, AuditMixin):
|
|||||||
limit_spec=None,
|
limit_spec=None,
|
||||||
filter=None,
|
filter=None,
|
||||||
is_timeseries=True,
|
is_timeseries=True,
|
||||||
timeseries_limit=15, row_limit=None):
|
timeseries_limit=15, row_limit=None,
|
||||||
|
extras=None):
|
||||||
|
|
||||||
qry_start_dttm = datetime.now()
|
qry_start_dttm = datetime.now()
|
||||||
timestamp = literal_column(
|
timestamp = literal_column(
|
||||||
@@ -236,6 +239,8 @@ class Table(Model, Queryable, AuditMixin):
|
|||||||
if op == 'not in':
|
if op == 'not in':
|
||||||
cond = ~cond
|
cond = ~cond
|
||||||
where_clause_and.append(cond)
|
where_clause_and.append(cond)
|
||||||
|
if extras and 'where' in extras:
|
||||||
|
where_clause_and += [text(extras['where'])]
|
||||||
qry = qry.where(and_(*where_clause_and))
|
qry = qry.where(and_(*where_clause_and))
|
||||||
qry = qry.order_by(desc(main_metric_expr))
|
qry = qry.order_by(desc(main_metric_expr))
|
||||||
qry = qry.limit(row_limit)
|
qry = qry.limit(row_limit)
|
||||||
@@ -530,7 +535,8 @@ class Datasource(Model, AuditMixin, Queryable):
|
|||||||
filter=None,
|
filter=None,
|
||||||
is_timeseries=True,
|
is_timeseries=True,
|
||||||
timeseries_limit=None,
|
timeseries_limit=None,
|
||||||
row_limit=None):
|
row_limit=None,
|
||||||
|
extras=None):
|
||||||
qry_start_dttm = datetime.now()
|
qry_start_dttm = datetime.now()
|
||||||
|
|
||||||
# add tzinfo to native datetime with config
|
# add tzinfo to native datetime with config
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "panoramix/base.html" %}
|
{% extends "panoramix/base.html" %}
|
||||||
{% block head_css %}
|
{% block head_css %}
|
||||||
{{super()}}
|
{{super()}}
|
||||||
|
{% set datasource = viz.datasource %}
|
||||||
<style>
|
<style>
|
||||||
.select2-container-multi .select2-choices {
|
.select2-container-multi .select2-choices {
|
||||||
height: 70px;
|
height: 70px;
|
||||||
@@ -97,7 +98,7 @@ form input.form-control {
|
|||||||
<hr/>
|
<hr/>
|
||||||
{% block viz %}
|
{% block viz %}
|
||||||
{% if error_msg %}
|
{% if error_msg %}
|
||||||
<span class="alert alert-danger">{{ error_msg }}</span>
|
<div class="alert alert-danger">{{ error_msg }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class Panoramix(BaseView):
|
|||||||
json.dumps(obj.get_query(), indent=4),
|
json.dumps(obj.get_query(), indent=4),
|
||||||
status=200,
|
status=200,
|
||||||
mimetype="application/json")
|
mimetype="application/json")
|
||||||
return obj.render()
|
return obj.check_and_render()
|
||||||
|
|
||||||
@has_access
|
@has_access
|
||||||
@expose("/datasource/<datasource_name>/")
|
@expose("/datasource/<datasource_name>/")
|
||||||
@@ -224,7 +224,7 @@ class Panoramix(BaseView):
|
|||||||
if not hasattr(obj, 'df') or obj.df is None or obj.df.empty:
|
if not hasattr(obj, 'df') or obj.df is None or obj.df.empty:
|
||||||
return obj.render_no_data()
|
return obj.render_no_data()
|
||||||
|
|
||||||
return obj.render()
|
return obj.check_and_render()
|
||||||
|
|
||||||
@has_access
|
@has_access
|
||||||
@expose("/refresh_datasources/")
|
@expose("/refresh_datasources/")
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class BaseViz(object):
|
|||||||
self.error_msg = str(e)
|
self.error_msg = str(e)
|
||||||
|
|
||||||
def form_class(self):
|
def form_class(self):
|
||||||
return form_factory(self.datasource, self, request.args)
|
return form_factory(self)
|
||||||
|
|
||||||
def query_filters(self):
|
def query_filters(self):
|
||||||
args = self.form_data
|
args = self.form_data
|
||||||
@@ -86,6 +86,12 @@ class BaseViz(object):
|
|||||||
if from_dttm >= to_dttm:
|
if from_dttm >= to_dttm:
|
||||||
flash("The date range doesn't seem right.", "danger")
|
flash("The date range doesn't seem right.", "danger")
|
||||||
from_dttm = to_dttm # Making them identicial to not raise
|
from_dttm = to_dttm # Making them identicial to not raise
|
||||||
|
|
||||||
|
# extras are used to query elements specific to a datasource type
|
||||||
|
# for instance the extra where clause that applies only to Tables
|
||||||
|
extras = {
|
||||||
|
'where': args.get("where")
|
||||||
|
}
|
||||||
d = {
|
d = {
|
||||||
'granularity': granularity,
|
'granularity': granularity,
|
||||||
'from_dttm': from_dttm,
|
'from_dttm': from_dttm,
|
||||||
@@ -96,6 +102,7 @@ class BaseViz(object):
|
|||||||
'row_limit': row_limit,
|
'row_limit': row_limit,
|
||||||
'filter': self.query_filters(),
|
'filter': self.query_filters(),
|
||||||
'timeseries_limit': limit,
|
'timeseries_limit': limit,
|
||||||
|
'extras': extras,
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@@ -103,6 +110,12 @@ class BaseViz(object):
|
|||||||
self.template = "panoramix/no_data.html"
|
self.template = "panoramix/no_data.html"
|
||||||
return BaseViz.render(self)
|
return BaseViz.render(self)
|
||||||
|
|
||||||
|
def check_and_render(self, *args, **kwards):
|
||||||
|
if self.error_msg:
|
||||||
|
return BaseViz.render(self, error_msg=self.error_msg)
|
||||||
|
else:
|
||||||
|
return self.render(*args, **kwards)
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
def render(self, *args, **kwargs):
|
||||||
form = self.form_class(self.form_data)
|
form = self.form_class(self.form_data)
|
||||||
return self.view.render_template(
|
return self.view.render_template(
|
||||||
@@ -123,9 +136,6 @@ class TableViz(BaseViz):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
if self.error_msg:
|
|
||||||
return super(TableViz, self).render(error_msg=self.error_msg)
|
|
||||||
|
|
||||||
df = self.df
|
df = self.df
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return super(TableViz, self).render(error_msg="No data.")
|
return super(TableViz, self).render(error_msg="No data.")
|
||||||
@@ -138,9 +148,6 @@ class TableViz(BaseViz):
|
|||||||
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
||||||
return super(TableViz, self).render(df=df)
|
return super(TableViz, self).render(df=df)
|
||||||
|
|
||||||
def form_class(self):
|
|
||||||
return form_factory(self.datasource, self, request.args)
|
|
||||||
|
|
||||||
|
|
||||||
class HighchartsViz(BaseViz):
|
class HighchartsViz(BaseViz):
|
||||||
verbose_name = "Base Highcharts Viz"
|
verbose_name = "Base Highcharts Viz"
|
||||||
@@ -159,9 +166,6 @@ class BubbleViz(HighchartsViz):
|
|||||||
'viz_type', 'since', 'until',
|
'viz_type', 'since', 'until',
|
||||||
'series', 'entity', 'x', 'y', 'size', 'limit']
|
'series', 'entity', 'x', 'y', 'size', 'limit']
|
||||||
|
|
||||||
def form_class(self):
|
|
||||||
return form_factory(self.datasource, self, request.args)
|
|
||||||
|
|
||||||
def query_obj(self):
|
def query_obj(self):
|
||||||
d = super(BubbleViz, self).query_obj()
|
d = super(BubbleViz, self).query_obj()
|
||||||
d['granularity'] = 'all'
|
d['granularity'] = 'all'
|
||||||
@@ -184,17 +188,14 @@ class BubbleViz(HighchartsViz):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
if not self.error_msg:
|
df = self.df.fillna(0)
|
||||||
df = self.df.fillna(0)
|
df['x'] = df[[self.x_metric]]
|
||||||
df['x'] = df[[self.x_metric]]
|
df['y'] = df[[self.y_metric]]
|
||||||
df['y'] = df[[self.y_metric]]
|
df['z'] = df[[self.z_metric]]
|
||||||
df['z'] = df[[self.z_metric]]
|
df['name'] = df[[self.entity]]
|
||||||
df['name'] = df[[self.entity]]
|
df['group'] = df[[self.series]]
|
||||||
df['group'] = df[[self.series]]
|
chart = HighchartBubble(df)
|
||||||
chart = HighchartBubble(df)
|
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
||||||
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
|
||||||
else:
|
|
||||||
return super(BubbleViz, self).render(error_msg=self.error_msg)
|
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesViz(HighchartsViz):
|
class TimeSeriesViz(HighchartsViz):
|
||||||
@@ -243,9 +244,6 @@ class TimeSeriesViz(HighchartsViz):
|
|||||||
**CHART_ARGS)
|
**CHART_ARGS)
|
||||||
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
|
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
|
||||||
|
|
||||||
def form_class(self):
|
|
||||||
return form_factory(self.datasource, self, request.args)
|
|
||||||
|
|
||||||
def bake_query(self):
|
def bake_query(self):
|
||||||
"""
|
"""
|
||||||
Doing a 2 phase query where we limit the number of series.
|
Doing a 2 phase query where we limit the number of series.
|
||||||
@@ -283,6 +281,7 @@ class TimeSeriesStackedBarViz(TimeSeriesViz):
|
|||||||
class DistributionBarViz(HighchartsViz):
|
class DistributionBarViz(HighchartsViz):
|
||||||
verbose_name = "Distribution - Bar Chart"
|
verbose_name = "Distribution - Bar Chart"
|
||||||
chart_type = "column"
|
chart_type = "column"
|
||||||
|
form_fields = BaseViz.form_fields + ['limit']
|
||||||
|
|
||||||
def query_obj(self):
|
def query_obj(self):
|
||||||
d = super(DistributionBarViz, self).query_obj()
|
d = super(DistributionBarViz, self).query_obj()
|
||||||
@@ -305,6 +304,7 @@ class DistributionBarViz(HighchartsViz):
|
|||||||
class DistributionPieViz(HighchartsViz):
|
class DistributionPieViz(HighchartsViz):
|
||||||
verbose_name = "Distribution - Pie Chart"
|
verbose_name = "Distribution - Pie Chart"
|
||||||
chart_type = "pie"
|
chart_type = "pie"
|
||||||
|
form_fields = BaseViz.form_fields + ['limit']
|
||||||
|
|
||||||
def query_obj(self):
|
def query_obj(self):
|
||||||
d = super(DistributionPieViz, self).query_obj()
|
d = super(DistributionPieViz, self).query_obj()
|
||||||
|
|||||||
Reference in New Issue
Block a user