Form factory refactor

This commit is contained in:
Maxime Beauchemin 2015-09-04 22:14:07 -07:00
parent 6dd81a3e95
commit 1283bc0788
6 changed files with 139 additions and 152 deletions

View File

@ -6,3 +6,5 @@
* Save / bookmark / url shortener * Save / bookmark / url shortener
* SQL: Find a way to manage granularity * SQL: Find a way to manage granularity
* Create ~/.panoramix/ to host DB and config, generate default config there * Create ~/.panoramix/ to host DB and config, generate default config there
* Add a per-datasource permission

90
panoramix/forms.py Normal file
View File

@ -0,0 +1,90 @@
from wtforms import Field, Form, SelectMultipleField, SelectField, TextField
from flask_appbuilder.fieldwidgets import Select2Widget, Select2ManyWidget
class OmgWtForm(Form):
field_order = tuple()
css_classes = dict()
@property
def fields(self):
fields = []
for field in self.field_order:
if hasattr(self, field):
obj = getattr(self, field)
if isinstance(obj, Field):
fields.append(getattr(self, field))
return fields
def get_field(self, fieldname):
return getattr(self, fieldname)
def field_css_classes(self, fieldname):
if fieldname in self.css_classes:
return " ".join(self.css_classes[fieldname])
return ""
def form_factory(datasource, viz, form_args=None):
from panoramix.viz import viz_types
row_limits = [10, 50, 100, 500, 1000, 5000, 10000]
series_limits = [0, 5, 10, 25, 50, 100, 500]
group_by_choices = [(s, s) for s in datasource.groupby_column_names]
# Pool of all the fields that can be used in Panoramix
px_form_fields = {
'viz_type': SelectField(
'Viz',
choices=[(k, v.verbose_name) for k, v in viz_types.items()]),
'metrics': SelectMultipleField(
'Metrics', choices=datasource.metrics_combo),
'groupby': SelectMultipleField(
'Group by',
choices=[(s, s) for s in datasource.groupby_column_names]),
'granularity': TextField('Time Granularity', default="one day"),
'since': TextField('Since', default="one day ago"),
'until': TextField('Until', default="now"),
'row_limit':
SelectField(
'Row limit', choices=[(s, s) for s in row_limits]),
'limit':
SelectField(
'Series limit', choices=[(s, s) for s in series_limits]),
'rolling_type': SelectField(
'Rolling',
choices=[(s, s) for s in ['mean', 'sum', 'std']]),
'rolling_periods': TextField('Periods',),
'series': SelectField('Series', choices=group_by_choices),
'entity': SelectField('Entity', choices=group_by_choices),
'x': SelectField('X Axis', choices=datasource.metrics_combo),
'y': SelectField('Y Axis', choices=datasource.metrics_combo),
'size': SelectField('Bubble Size', choices=datasource.metrics_combo),
}
field_css_classes = {k: ['form-control'] for k in px_form_fields.keys()}
select2 = [
'viz_type', 'metrics', 'groupby',
'row_limit', 'rolling_type', 'series',
'entity', 'x', 'y', 'size',]
field_css_classes['since'] += ['select2_free_since']
field_css_classes['until'] += ['select2_free_until']
field_css_classes['granularity'] += ['select2_free_granularity']
for field in select2:
field_css_classes[field] += ['select2']
class QueryForm(OmgWtForm):
field_order = viz.form_fields
css_classes = field_css_classes
for i in range(10):
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names]))
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
'Filter 1', choices=[(m, m) for m in ['in', 'not in']]))
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
for ff in viz.form_fields:
if isinstance(ff, basestring):
ff = [ff]
for s in ff:
setattr(QueryForm, s, px_form_fields[s])
return QueryForm

View File

@ -38,32 +38,27 @@ form input.form-control {
<hr> <hr>
<form id="query" method="GET" style="display: none;"> <form id="query" method="GET" style="display: none;">
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div> {% for fieldname in form.field_order %}
{% if 'metrics' not in viz.hidden_fields %} {% if not fieldname.__iter__ %}
<div>{{ form.metrics.label }}: {{ form.metrics(class_="form-control select2") }}</div> <div>
{% endif %} {% set field = form.get_field(fieldname)%}
{% if 'granularity' not in viz.hidden_fields %} {{ field.label }}:
<div>{{ form.granularity.label }} {{ field(class_=form.field_css_classes(field.name)) }}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" </div>
title="Supports natural language time as in '10 seconds', '1 day' or '1 week'" {% else %}
id="blah"></i> <div class="row">
{{ form.granularity(class_="form-control select2_free_granularity") }}</div> <div class="form-group">
{% endif %} {% for name in fieldname %}
<div class="row"> <div class="col-xs-{{ (12 / fieldname|length) | int }}">
<div class="form-group"> {% set field = form.get_field(name)%}
<div class="col-xs-6">{{ form.since.label }} {{ field.label }}:
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" {{ field(class_=form.field_css_classes(field.name)) }}
title="Supports natural language time as in '1 day ago', '28 days' or '3 years'" </div>
id="blah"></i> {% endfor %}
{{ form.since(class_="form-control select2_free_since") }}</div> </div>
<div class="col-xs-6">{{ form.until.label }} </div>
{{ form.until(class_="form-control select2_free_until") }}</div> {% endif %}
</div> {% endfor %}
</div>
{% if 'groupby' not in viz.hidden_fields %}
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
{% endif %}
{% block extra_fields %}{% endblock %}
<hr> <hr>
<h4>Filters</h4> <h4>Filters</h4>
<div id="flt0" style="display: none;"> <div id="flt0" style="display: none;">

View File

@ -4,36 +4,6 @@
<div id="chart"></div> <div id="chart"></div>
{% endblock %} {% endblock %}
{% block extra_fields %}
{% if form.compare %}
<div>{{ form.compare.label }}: {{ form.compare(class_="form-control") }}</div>
{% endif %}
{% if form.rolling_type %}
<div class="row">
<span class="col col-sm-5">{{ form.rolling_type.label }}: {{ form.rolling_type(class_="form-control select2") }}</span>
<span class="col col-sm-4">{{ form.rolling_periods.label }}: {{ form.rolling_periods(class_="form-control") }}</span>
</div>
{% endif %}
{% if form.limit %}
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div>
{% endif %}
{% if form.series %}
<div>{{ form.series.label }}: {{ form.series(class_="form-control select2") }}</div>
{% endif %}
{% if form.entity %}
<div>{{ form.entity.label }}: {{ form.entity(class_="form-control select2") }}</div>
{% endif %}
{% if form.size %}
<div>{{ form.size.label }}: {{ form.size(class_="form-control select2") }}</div>
{% endif %}
{% if form.x %}
<div>{{ form.x.label }}: {{ form.x(class_="form-control select2") }}</div>
{% endif %}
{% if form.y %}
<div>{{ form.y.label }}: {{ form.y(class_="form-control select2") }}</div>
{% endif %}
{% endblock %}
{% block tail %} {% block tail %}
{{ super() }} {{ super() }}
{% if viz.stockchart %} {% if viz.stockchart %}

View File

@ -33,10 +33,6 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_fields %}
<div>{{ form.row_limit.label }}: {{ form.row_limit(class_="form-control select2") }}</div>
{% endblock %}
{% block tail %} {% block tail %}
{{ super() }} {{ super() }}
<script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script> <script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script>

View File

@ -2,12 +2,13 @@ from datetime import datetime
from flask import flash, request from flask import flash, request
import pandas as pd import pandas as pd
from collections import OrderedDict from collections import OrderedDict
from panoramix import utils
from panoramix.highchart import Highchart, HighchartBubble
from wtforms import Form, SelectMultipleField, SelectField, TextField
import config import config
import logging import logging
import numpy as np
from panoramix import utils
from panoramix.highchart import Highchart, HighchartBubble
from panoramix.forms import form_factory
CHART_ARGS = { CHART_ARGS = {
'height': 700, 'height': 700,
@ -16,58 +17,14 @@ CHART_ARGS = {
} }
class OmgWtForm(Form):
field_order = (
'viz_type', 'granularity', 'since', 'group_by', 'limit')
def fields(self):
fields = []
for field in self.field_order:
if hasattr(self, field):
obj = getattr(self, field)
if isinstance(obj, Field):
fields.append(getattr(self, field))
return fields
def form_factory(datasource, form_args=None, extra_fields_dict=None):
extra_fields_dict = extra_fields_dict or {}
if form_args:
limit = form_args.get("limit")
try:
limit = int(limit)
if limit not in limits:
limits.append(limit)
limits = sorted(limits)
except:
pass
class QueryForm(OmgWtForm):
viz_type = SelectField(
'Viz',
choices=[(k, v.verbose_name) for k, v in viz_types.items()])
metrics = SelectMultipleField('Metrics', choices=datasource.metrics_combo)
groupby = SelectMultipleField(
'Group by', choices=[
(s, s) for s in datasource.groupby_column_names])
granularity = TextField('Time Granularity', default="one day")
since = TextField('Since', default="one day ago")
until = TextField('Until', default="now")
for i in range(10):
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names]))
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
'Filter 1', choices=[(m, m) for m in ['in', 'not in']]))
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
for k, v in extra_fields_dict.items():
setattr(QueryForm, k, v)
return QueryForm
class BaseViz(object): class BaseViz(object):
verbose_name = "Base Viz" verbose_name = "Base Viz"
template = "panoramix/datasource.html" template = "panoramix/datasource.html"
hidden_fields = [] hidden_fields = []
form_fields = [
'viz_type', 'metrics', 'groupby', 'granularity',
('since', 'until')]
def __init__(self, datasource, form_data, view): def __init__(self, datasource, form_data, view):
self.datasource = datasource self.datasource = datasource
self.form_class = self.form_class() self.form_class = self.form_class()
@ -90,7 +47,7 @@ class BaseViz(object):
def form_class(self): def form_class(self):
return form_factory(self.datasource, request.args) return form_factory(self.datasource, self, request.args)
def query_filters(self): def query_filters(self):
args = self.form_data args = self.form_data
@ -159,6 +116,7 @@ class BaseViz(object):
class TableViz(BaseViz): class TableViz(BaseViz):
verbose_name = "Table View" verbose_name = "Table View"
template = 'panoramix/viz_table.html' template = 'panoramix/viz_table.html'
form_fields = BaseViz.form_fields + ['row_limit']
def query_obj(self): def query_obj(self):
d = super(TableViz, self).query_obj() d = super(TableViz, self).query_obj()
@ -178,17 +136,11 @@ class TableViz(BaseViz):
if self.form_data.get("granularity") == "all" and 'timestamp' in df: if self.form_data.get("granularity") == "all" and 'timestamp' in df:
del df['timestamp'] del df['timestamp']
for m in self.metrics: for m in self.metrics:
import numpy as np
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): def form_class(self):
limits = [10, 50, 100, 500, 1000, 5000, 10000] return form_factory(self.datasource, self, request.args)
return form_factory(self.datasource, request.args,
extra_fields_dict={
'row_limit':
SelectField('Row limit', choices=[(s, s) for s in limits])
})
class HighchartsViz(BaseViz): class HighchartsViz(BaseViz):
@ -204,28 +156,12 @@ class BubbleViz(HighchartsViz):
verbose_name = "Bubble Chart" verbose_name = "Bubble Chart"
chart_type = 'bubble' chart_type = 'bubble'
hidden_fields = ['granularity', 'metrics', 'groupby'] hidden_fields = ['granularity', 'metrics', 'groupby']
form_fields = [
'viz_type', 'since', 'until',
'series', 'entity', 'x', 'y', 'size', 'limit']
def form_class(self): def form_class(self):
datasource = self.datasource return form_factory(self.datasource, self, request.args)
limits = [0, 5, 10, 25, 50, 100, 500]
return form_factory(self.datasource, request.args,
extra_fields_dict={
#'compare': TextField('Period Compare',),
'series': SelectField(
'Series', choices=[
(s, s) for s in datasource.groupby_column_names]),
'entity': SelectField(
'Entity', choices=[
(s, s) for s in datasource.groupby_column_names]),
'x': SelectField(
'X Axis', choices=datasource.metrics_combo),
'y': SelectField(
'Y Axis', choices=datasource.metrics_combo),
'size': SelectField(
'Bubble Size', choices=datasource.metrics_combo),
'limit': SelectField(
'Limit', choices=[(s, s) for s in limits]),
})
def query_obj(self): def query_obj(self):
d = super(BubbleViz, self).query_obj() d = super(BubbleViz, self).query_obj()
@ -264,12 +200,18 @@ class BubbleViz(HighchartsViz):
return super(BubbleViz, self).render(error_msg=self.error_msg) return super(BubbleViz, self).render(error_msg=self.error_msg)
class TimeSeriesViz(HighchartsViz): class TimeSeriesViz(HighchartsViz):
verbose_name = "Time Series - Line Chart" verbose_name = "Time Series - Line Chart"
chart_type = "spline" chart_type = "spline"
stockchart = True stockchart = True
sort_legend_y = True sort_legend_y = True
form_fields = [
'viz_type',
'granularity', ('since', 'until'),
'metrics',
'groupby', 'limit',
('rolling_type', 'rolling_periods'),
]
def render(self): def render(self):
if request.args.get("granularity") == "all": if request.args.get("granularity") == "all":
@ -285,7 +227,6 @@ class TimeSeriesViz(HighchartsViz):
values=metrics,) values=metrics,)
rolling_periods = request.args.get("rolling_periods") rolling_periods = request.args.get("rolling_periods")
limit = request.args.get("limit")
rolling_type = request.args.get("rolling_type") rolling_type = request.args.get("rolling_type")
if rolling_periods and rolling_type: if rolling_periods and rolling_type:
if rolling_type == 'mean': if rolling_type == 'mean':
@ -306,17 +247,7 @@ class TimeSeriesViz(HighchartsViz):
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd) return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
def form_class(self): def form_class(self):
limits = [0, 5, 10, 25, 50, 100, 500] return form_factory(self.datasource, self, request.args)
return form_factory(self.datasource, request.args,
extra_fields_dict={
#'compare': TextField('Period Compare',),
'rolling_type': SelectField(
'Rolling',
choices=[(s, s) for s in ['mean', 'sum', 'std']]),
'rolling_periods': TextField('Periods',),
'limit': SelectField(
'Series limit', choices=[(s, s) for s in limits])
})
def bake_query(self): def bake_query(self):
""" """
@ -324,14 +255,17 @@ class TimeSeriesViz(HighchartsViz):
""" """
return self.datasource.query(**self.query_obj()) return self.datasource.query(**self.query_obj())
class TimeSeriesCompareViz(TimeSeriesViz): class TimeSeriesCompareViz(TimeSeriesViz):
verbose_name = "Time Series - Percent Change" verbose_name = "Time Series - Percent Change"
compare = 'percent' compare = 'percent'
class TimeSeriesCompareValueViz(TimeSeriesViz): class TimeSeriesCompareValueViz(TimeSeriesViz):
verbose_name = "Time Series - Value Change" verbose_name = "Time Series - Value Change"
compare = 'value' compare = 'value'
class TimeSeriesAreaViz(TimeSeriesViz): class TimeSeriesAreaViz(TimeSeriesViz):
verbose_name = "Time Series - Stacked Area Chart" verbose_name = "Time Series - Stacked Area Chart"
stacked=True stacked=True