Merge pull request #23 from mistercrunch/where
Custom WHERE clause for tables (not druid) + error handling refactor
This commit is contained in:
commit
53fe171466
|
|
@ -1,4 +1,5 @@
|
|||
from wtforms import Field, Form, SelectMultipleField, SelectField, TextField
|
||||
from copy import copy
|
||||
|
||||
|
||||
class OmgWtForm(Form):
|
||||
|
|
@ -24,7 +25,8 @@ class OmgWtForm(Form):
|
|||
return ""
|
||||
|
||||
|
||||
def form_factory(datasource, viz, form_args=None):
|
||||
def form_factory(viz):
|
||||
datasource = viz.datasource
|
||||
from panoramix.viz import viz_types
|
||||
row_limits = [10, 50, 100, 500, 1000, 5000, 10000]
|
||||
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),
|
||||
'y': SelectField('Y Axis', 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()}
|
||||
select2 = [
|
||||
|
|
@ -70,7 +73,7 @@ def form_factory(datasource, viz, form_args=None):
|
|||
field_css_classes[field] += ['select2']
|
||||
|
||||
class QueryForm(OmgWtForm):
|
||||
field_order = viz.form_fields
|
||||
field_order = copy(viz.form_fields)
|
||||
css_classes = field_css_classes
|
||||
|
||||
for i in range(10):
|
||||
|
|
@ -85,4 +88,9 @@ def form_factory(datasource, viz, form_args=None):
|
|||
ff = [ff]
|
||||
for s in ff:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from sqlalchemy import (
|
|||
from sqlalchemy import Table as sqlaTable
|
||||
from sqlalchemy import create_engine, MetaData, desc, select, and_
|
||||
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 collections import namedtuple
|
||||
|
|
@ -99,7 +99,9 @@ class Table(Model, Queryable, AuditMixin):
|
|||
limit_spec=None,
|
||||
filter=None,
|
||||
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
|
||||
using the sqlalchemy expression API (new approach which supports
|
||||
|
|
@ -192,7 +194,8 @@ class Table(Model, Queryable, AuditMixin):
|
|||
limit_spec=None,
|
||||
filter=None,
|
||||
is_timeseries=True,
|
||||
timeseries_limit=15, row_limit=None):
|
||||
timeseries_limit=15, row_limit=None,
|
||||
extras=None):
|
||||
|
||||
qry_start_dttm = datetime.now()
|
||||
timestamp = literal_column(
|
||||
|
|
@ -236,6 +239,8 @@ class Table(Model, Queryable, AuditMixin):
|
|||
if op == 'not in':
|
||||
cond = ~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.order_by(desc(main_metric_expr))
|
||||
qry = qry.limit(row_limit)
|
||||
|
|
@ -530,7 +535,8 @@ class Datasource(Model, AuditMixin, Queryable):
|
|||
filter=None,
|
||||
is_timeseries=True,
|
||||
timeseries_limit=None,
|
||||
row_limit=None):
|
||||
row_limit=None,
|
||||
extras=None):
|
||||
qry_start_dttm = datetime.now()
|
||||
|
||||
# add tzinfo to native datetime with config
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "panoramix/base.html" %}
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
{% set datasource = viz.datasource %}
|
||||
<style>
|
||||
.select2-container-multi .select2-choices {
|
||||
height: 70px;
|
||||
|
|
@ -97,7 +98,7 @@ form input.form-control {
|
|||
<hr/>
|
||||
{% block viz %}
|
||||
{% if error_msg %}
|
||||
<span class="alert alert-danger">{{ error_msg }}</span>
|
||||
<div class="alert alert-danger">{{ error_msg }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class Panoramix(BaseView):
|
|||
json.dumps(obj.get_query(), indent=4),
|
||||
status=200,
|
||||
mimetype="application/json")
|
||||
return obj.render()
|
||||
return obj.check_and_render()
|
||||
|
||||
@has_access
|
||||
@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:
|
||||
return obj.render_no_data()
|
||||
|
||||
return obj.render()
|
||||
return obj.check_and_render()
|
||||
|
||||
@has_access
|
||||
@expose("/refresh_datasources/")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class BaseViz(object):
|
|||
self.error_msg = str(e)
|
||||
|
||||
def form_class(self):
|
||||
return form_factory(self.datasource, self, request.args)
|
||||
return form_factory(self)
|
||||
|
||||
def query_filters(self):
|
||||
args = self.form_data
|
||||
|
|
@ -86,6 +86,12 @@ class BaseViz(object):
|
|||
if from_dttm >= to_dttm:
|
||||
flash("The date range doesn't seem right.", "danger")
|
||||
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 = {
|
||||
'granularity': granularity,
|
||||
'from_dttm': from_dttm,
|
||||
|
|
@ -96,6 +102,7 @@ class BaseViz(object):
|
|||
'row_limit': row_limit,
|
||||
'filter': self.query_filters(),
|
||||
'timeseries_limit': limit,
|
||||
'extras': extras,
|
||||
}
|
||||
return d
|
||||
|
||||
|
|
@ -103,6 +110,12 @@ class BaseViz(object):
|
|||
self.template = "panoramix/no_data.html"
|
||||
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):
|
||||
form = self.form_class(self.form_data)
|
||||
return self.view.render_template(
|
||||
|
|
@ -123,9 +136,6 @@ class TableViz(BaseViz):
|
|||
return d
|
||||
|
||||
def render(self):
|
||||
if self.error_msg:
|
||||
return super(TableViz, self).render(error_msg=self.error_msg)
|
||||
|
||||
df = self.df
|
||||
if df is None or df.empty:
|
||||
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)
|
||||
return super(TableViz, self).render(df=df)
|
||||
|
||||
def form_class(self):
|
||||
return form_factory(self.datasource, self, request.args)
|
||||
|
||||
|
||||
class HighchartsViz(BaseViz):
|
||||
verbose_name = "Base Highcharts Viz"
|
||||
|
|
@ -159,9 +166,6 @@ class BubbleViz(HighchartsViz):
|
|||
'viz_type', 'since', 'until',
|
||||
'series', 'entity', 'x', 'y', 'size', 'limit']
|
||||
|
||||
def form_class(self):
|
||||
return form_factory(self.datasource, self, request.args)
|
||||
|
||||
def query_obj(self):
|
||||
d = super(BubbleViz, self).query_obj()
|
||||
d['granularity'] = 'all'
|
||||
|
|
@ -184,17 +188,14 @@ class BubbleViz(HighchartsViz):
|
|||
return d
|
||||
|
||||
def render(self):
|
||||
if not self.error_msg:
|
||||
df = self.df.fillna(0)
|
||||
df['x'] = df[[self.x_metric]]
|
||||
df['y'] = df[[self.y_metric]]
|
||||
df['z'] = df[[self.z_metric]]
|
||||
df['name'] = df[[self.entity]]
|
||||
df['group'] = df[[self.series]]
|
||||
chart = HighchartBubble(df)
|
||||
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
||||
else:
|
||||
return super(BubbleViz, self).render(error_msg=self.error_msg)
|
||||
df = self.df.fillna(0)
|
||||
df['x'] = df[[self.x_metric]]
|
||||
df['y'] = df[[self.y_metric]]
|
||||
df['z'] = df[[self.z_metric]]
|
||||
df['name'] = df[[self.entity]]
|
||||
df['group'] = df[[self.series]]
|
||||
chart = HighchartBubble(df)
|
||||
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
||||
|
||||
|
||||
class TimeSeriesViz(HighchartsViz):
|
||||
|
|
@ -243,9 +244,6 @@ class TimeSeriesViz(HighchartsViz):
|
|||
**CHART_ARGS)
|
||||
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):
|
||||
"""
|
||||
Doing a 2 phase query where we limit the number of series.
|
||||
|
|
@ -283,6 +281,7 @@ class TimeSeriesStackedBarViz(TimeSeriesViz):
|
|||
class DistributionBarViz(HighchartsViz):
|
||||
verbose_name = "Distribution - Bar Chart"
|
||||
chart_type = "column"
|
||||
form_fields = BaseViz.form_fields + ['limit']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(DistributionBarViz, self).query_obj()
|
||||
|
|
@ -305,6 +304,7 @@ class DistributionBarViz(HighchartsViz):
|
|||
class DistributionPieViz(HighchartsViz):
|
||||
verbose_name = "Distribution - Pie Chart"
|
||||
chart_type = "pie"
|
||||
form_fields = BaseViz.form_fields + ['limit']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(DistributionPieViz, self).query_obj()
|
||||
|
|
|
|||
Loading…
Reference in New Issue