2016-03-16 23:25:41 -04:00
|
|
|
"""Contains the logic to create cohesive forms on the explore view"""
|
2016-04-07 11:39:08 -04:00
|
|
|
from __future__ import absolute_import
|
|
|
|
from __future__ import division
|
|
|
|
from __future__ import print_function
|
|
|
|
from __future__ import unicode_literals
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-04-08 02:01:40 -04:00
|
|
|
from collections import OrderedDict
|
|
|
|
from copy import copy
|
2016-06-24 01:43:52 -04:00
|
|
|
import json
|
2016-04-26 14:51:01 -04:00
|
|
|
import math
|
2016-04-08 02:01:40 -04:00
|
|
|
|
2016-06-27 23:10:40 -04:00
|
|
|
from flask_babel import lazy_gettext as _
|
2016-03-18 02:44:58 -04:00
|
|
|
from wtforms import (
|
|
|
|
Form, SelectMultipleField, SelectField, TextField, TextAreaField,
|
2016-04-26 14:51:01 -04:00
|
|
|
BooleanField, IntegerField, HiddenField, DecimalField)
|
2016-03-18 02:44:58 -04:00
|
|
|
from wtforms import validators, widgets
|
2016-04-08 02:01:40 -04:00
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
from caravel import app
|
2016-04-08 02:01:40 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
config = app.config
|
|
|
|
|
2016-05-10 12:22:32 -04:00
|
|
|
TIMESTAMP_CHOICES = [
|
|
|
|
('smart_date', 'Adaptative formating'),
|
|
|
|
("%m/%d/%Y", '"%m/%d/%Y" | 01/14/2019'),
|
|
|
|
("%Y-%m-%d", '"%Y-%m-%d" | 2019-01-14'),
|
|
|
|
("%Y-%m-%d %H:%M:%S",
|
|
|
|
'"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'),
|
|
|
|
("%H:%M:%S", '"%H:%M:%S" | 01:32:10'),
|
|
|
|
]
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
class BetterBooleanField(BooleanField):
|
|
|
|
|
2016-03-16 23:25:41 -04:00
|
|
|
"""Fixes the html checkbox to distinguish absent from unchecked
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
(which doesn't distinguish False from NULL/missing )
|
|
|
|
If value is unchecked, this hidden <input> fills in False value
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __call__(self, **kwargs):
|
|
|
|
html = super(BetterBooleanField, self).__call__(**kwargs)
|
|
|
|
html += u'<input type="hidden" name="{}" value="false">'.format(self.name)
|
|
|
|
return widgets.HTMLString(html)
|
|
|
|
|
|
|
|
|
|
|
|
class SelectMultipleSortableField(SelectMultipleField):
|
|
|
|
|
|
|
|
"""Works along with select2sortable to preserves the sort order"""
|
|
|
|
|
|
|
|
def iter_choices(self):
|
|
|
|
d = OrderedDict()
|
|
|
|
for value, label in self.choices:
|
|
|
|
selected = self.data is not None and self.coerce(value) in self.data
|
|
|
|
d[value] = (value, label, selected)
|
|
|
|
if self.data:
|
|
|
|
for value in self.data:
|
2016-06-02 15:39:21 -04:00
|
|
|
if value and value in d:
|
2016-03-18 02:44:58 -04:00
|
|
|
yield d.pop(value)
|
|
|
|
while d:
|
2016-03-29 11:40:18 -04:00
|
|
|
yield d.popitem(last=False)[1]
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
|
class FreeFormSelect(widgets.Select):
|
|
|
|
|
|
|
|
"""A WTF widget that allows for free form entry"""
|
|
|
|
|
|
|
|
def __call__(self, field, **kwargs):
|
|
|
|
kwargs.setdefault('id', field.id)
|
|
|
|
if self.multiple:
|
|
|
|
kwargs['multiple'] = True
|
|
|
|
html = ['<select %s>' % widgets.html_params(name=field.name, **kwargs)]
|
|
|
|
found = False
|
|
|
|
for val, label, selected in field.iter_choices():
|
|
|
|
html.append(self.render_option(val, label, selected))
|
|
|
|
if field.data and val == field.data:
|
|
|
|
found = True
|
|
|
|
if not found:
|
|
|
|
html.insert(1, self.render_option(field.data, field.data, True))
|
|
|
|
html.append('</select>')
|
|
|
|
return widgets.HTMLString(''.join(html))
|
|
|
|
|
|
|
|
|
|
|
|
class FreeFormSelectField(SelectField):
|
|
|
|
|
2016-03-16 23:25:41 -04:00
|
|
|
"""A WTF SelectField that allows for free form input"""
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
widget = FreeFormSelect()
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
def pre_validate(self, form):
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
class OmgWtForm(Form):
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
"""Caravelification of the WTForm Form object"""
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
fieldsets = {}
|
|
|
|
css_classes = dict()
|
|
|
|
|
|
|
|
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 ""
|
|
|
|
|
|
|
|
|
|
|
|
class FormFactory(object):
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
"""Used to create the forms in the explore view dynamically"""
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
series_limits = [0, 5, 10, 25, 50, 100, 500]
|
|
|
|
fieltype_class = {
|
|
|
|
SelectField: 'select2',
|
|
|
|
SelectMultipleField: 'select2',
|
|
|
|
FreeFormSelectField: 'select2_freeform',
|
|
|
|
SelectMultipleSortableField: 'select2Sortable',
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, viz):
|
|
|
|
self.viz = viz
|
2016-03-29 00:55:58 -04:00
|
|
|
from caravel.viz import viz_types
|
2016-03-18 02:44:58 -04:00
|
|
|
viz = self.viz
|
|
|
|
datasource = viz.datasource
|
2016-04-15 18:00:49 -04:00
|
|
|
if not datasource.metrics_combo:
|
|
|
|
raise Exception("Please define at least one metric for your table")
|
2016-03-18 02:44:58 -04:00
|
|
|
default_metric = datasource.metrics_combo[0][0]
|
2016-04-09 01:32:12 -04:00
|
|
|
|
|
|
|
gb_cols = datasource.groupby_column_names
|
|
|
|
default_groupby = gb_cols[0] if gb_cols else None
|
2016-04-15 18:00:49 -04:00
|
|
|
group_by_choices = self.choicify(gb_cols)
|
2016-06-24 01:43:52 -04:00
|
|
|
order_by_choices = []
|
|
|
|
for s in sorted(datasource.num_cols):
|
|
|
|
order_by_choices.append((json.dumps([s, True]), s + ' [asc]'))
|
|
|
|
order_by_choices.append((json.dumps([s, False]), s + ' [desc]'))
|
2016-03-29 00:55:58 -04:00
|
|
|
# Pool of all the fields that can be used in Caravel
|
2016-06-02 01:59:06 -04:00
|
|
|
field_data = {
|
|
|
|
'viz_type': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Viz"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'table',
|
|
|
|
"choices": [(k, v.verbose_name) for k, v in viz_types.items()],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("The type of visualization to display")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'metrics': (SelectMultipleSortableField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Metrics"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": datasource.metrics_combo,
|
|
|
|
"default": [default_metric],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("One or many metrics to display")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
2016-06-24 01:43:52 -04:00
|
|
|
'order_by_cols': (SelectMultipleSortableField, {
|
|
|
|
"label": _("Ordering"),
|
|
|
|
"choices": order_by_choices,
|
|
|
|
"description": _("One or many metrics to display")
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'metric': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Metric"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": datasource.metrics_combo,
|
|
|
|
"default": default_metric,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Choose the metric")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'stacked_style': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Chart Style"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('stack', _('stack')),
|
|
|
|
('stream', _('stream')),
|
|
|
|
('expand', _('expand')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'stack',
|
|
|
|
"description": ""
|
|
|
|
}),
|
|
|
|
'linear_color_scheme': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Color Scheme"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('fire', _('fire')),
|
|
|
|
('blue_white_yellow', _('blue_white_yellow')),
|
|
|
|
('white_black', _('white_black')),
|
|
|
|
('black_white', _('black_white')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'blue_white_yellow',
|
|
|
|
"description": ""
|
|
|
|
}),
|
|
|
|
'normalize_across': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Normalize Across"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('heatmap', _('heatmap')),
|
|
|
|
('x', _('x')),
|
|
|
|
('y', _('y')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'heatmap',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Color will be rendered based on a ratio "
|
|
|
|
"of the cell against the sum of across this "
|
2016-06-02 01:59:06 -04:00
|
|
|
"criteria")
|
|
|
|
}),
|
|
|
|
'horizon_color_scale': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Color Scale"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('series', _('series')),
|
|
|
|
('overall', _('overall')),
|
|
|
|
('change', _('change')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'series',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Defines how the color are attributed.")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'canvas_image_rendering': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Rendering"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": (
|
2016-06-08 20:38:43 -04:00
|
|
|
('pixelated', _('pixelated (Sharp)')),
|
|
|
|
('auto', _('auto (Smooth)')),
|
2016-03-18 02:44:58 -04:00
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'pixelated',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"image-rendering CSS attribute of the canvas object that "
|
2016-06-02 01:59:06 -04:00
|
|
|
"defines how the browser scales up the image")
|
|
|
|
}),
|
|
|
|
'xscale_interval': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("XScale Interval"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(range(1, 50)),
|
|
|
|
"default": '1',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Number of step to take between ticks when "
|
2016-06-02 01:59:06 -04:00
|
|
|
"printing the x scale")
|
|
|
|
}),
|
|
|
|
'yscale_interval': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("YScale Interval"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(range(1, 50)),
|
|
|
|
"default": '1',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Number of step to take between ticks when "
|
2016-06-02 01:59:06 -04:00
|
|
|
"printing the y scale")
|
|
|
|
}),
|
|
|
|
'bar_stacked': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Stacked Bars"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
|
|
|
"description": ""
|
|
|
|
}),
|
2016-08-26 14:45:46 -04:00
|
|
|
'show_markers': (BetterBooleanField, {
|
|
|
|
"label": _("Show Markers"),
|
|
|
|
"default": False,
|
|
|
|
"description": (
|
|
|
|
"Show data points as circle markers on top of the lines "
|
|
|
|
"in the chart")
|
|
|
|
}),
|
2016-07-25 23:38:26 -04:00
|
|
|
'show_bar_value': (BetterBooleanField, {
|
|
|
|
"label": _("Bar Values"),
|
|
|
|
"default": False,
|
|
|
|
"description": "Show the value on top of the bars or not"
|
|
|
|
}),
|
2016-06-24 01:43:40 -04:00
|
|
|
'show_controls': (BetterBooleanField, {
|
|
|
|
"label": _("Extra Controls"),
|
|
|
|
"default": False,
|
2016-07-01 17:31:22 -04:00
|
|
|
"description": _(
|
2016-06-24 01:43:40 -04:00
|
|
|
"Whether to show extra controls or not. Extra controls "
|
|
|
|
"include things like making mulitBar charts stacked "
|
|
|
|
"or side by side.")
|
|
|
|
}),
|
2016-06-17 15:31:20 -04:00
|
|
|
'reduce_x_ticks': (BetterBooleanField, {
|
|
|
|
"label": _("Reduce X ticks"),
|
|
|
|
"default": False,
|
2016-06-20 12:16:51 -04:00
|
|
|
"description": _(
|
2016-06-17 15:31:20 -04:00
|
|
|
"Reduces the number of X axis ticks to be rendered. "
|
|
|
|
"If true, the x axis wont overflow and labels may be "
|
|
|
|
"missing. If false, a minimum width will be applied "
|
|
|
|
"to columns and the width may overflow into an "
|
|
|
|
"horizontal scroll."),
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'include_series': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Include Series"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Include series name as an axis")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'secondary_metric': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Color Metric"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": datasource.metrics_combo,
|
|
|
|
"default": default_metric,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("A metric to use for color")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'country_fieldtype': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Country Field Type"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'cca2',
|
|
|
|
"choices": (
|
2016-06-08 20:38:43 -04:00
|
|
|
('name', _('Full name')),
|
|
|
|
('cioc', _('code International Olympic Committee (cioc)')),
|
|
|
|
('cca2', _('code ISO 3166-1 alpha-2 (cca2)')),
|
|
|
|
('cca3', _('code ISO 3166-1 alpha-3 (cca3)')),
|
2016-03-18 02:44:58 -04:00
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-29 00:55:58 -04:00
|
|
|
"The country code standard that Caravel should expect "
|
2016-06-02 01:59:06 -04:00
|
|
|
"to find in the [country] column")
|
|
|
|
}),
|
|
|
|
'groupby': (SelectMultipleSortableField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Group by"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(datasource.groupby_column_names),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("One or many fields to group by")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'columns': (SelectMultipleSortableField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Columns"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(datasource.groupby_column_names),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("One or many fields to pivot as columns")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'all_columns': (SelectMultipleSortableField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Columns"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(datasource.column_names),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Columns to display")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'all_columns_x': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("X"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(datasource.column_names),
|
2016-07-13 10:58:59 -04:00
|
|
|
"default": datasource.column_names[0],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Columns to display")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'all_columns_y': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Y"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(datasource.column_names),
|
2016-07-13 10:58:59 -04:00
|
|
|
"default": datasource.column_names[0],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Columns to display")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'druid_time_origin': (FreeFormSelectField, {
|
2016-08-04 18:30:33 -04:00
|
|
|
"label": _("Origin"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": (
|
2016-06-08 20:38:43 -04:00
|
|
|
('', _('default')),
|
|
|
|
('now', _('now')),
|
2016-04-15 20:00:44 -04:00
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-04-15 20:00:44 -04:00
|
|
|
"Defines the origin where time buckets start, "
|
2016-06-02 01:59:06 -04:00
|
|
|
"accepts natural dates as in 'now', 'sunday' or '1970-01-01'")
|
|
|
|
}),
|
2016-06-17 11:11:53 -04:00
|
|
|
'bottom_margin': (FreeFormSelectField, {
|
|
|
|
"label": _("Bottom Margin"),
|
2016-09-07 17:07:02 -04:00
|
|
|
"choices": self.choicify(['auto', 50, 75, 100, 125, 150, 200]),
|
|
|
|
"default": 'auto',
|
2016-06-17 11:11:53 -04:00
|
|
|
"description": _(
|
|
|
|
"Bottom marging, in pixels, allowing for more room for "
|
|
|
|
"axis labels"),
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'granularity': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Time Granularity"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "one day",
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('all', _('all')),
|
|
|
|
('5 seconds', _('5 seconds')),
|
|
|
|
('30 seconds', _('30 seconds')),
|
|
|
|
('1 minute', _('1 minute')),
|
|
|
|
('5 minutes', _('5 minutes')),
|
|
|
|
('1 hour', _('1 hour')),
|
|
|
|
('6 hour', _('6 hour')),
|
|
|
|
('1 day', _('1 day')),
|
|
|
|
('7 days', _('7 days')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The time granularity for the visualization. Note that you "
|
|
|
|
"can type and use simple natural language as in '10 seconds', "
|
2016-06-02 01:59:06 -04:00
|
|
|
"'1 day' or '56 weeks'")
|
|
|
|
}),
|
|
|
|
'domain_granularity': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Domain"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "month",
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('hour', _('hour')),
|
|
|
|
('day', _('day')),
|
|
|
|
('week', _('week')),
|
|
|
|
('month', _('month')),
|
|
|
|
('year', _('year')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-06-02 01:59:06 -04:00
|
|
|
"The time unit used for the grouping of blocks")
|
|
|
|
}),
|
|
|
|
'subdomain_granularity': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Subdomain"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "day",
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('min', _('min')),
|
|
|
|
('hour', _('hour')),
|
|
|
|
('day', _('day')),
|
|
|
|
('week', _('week')),
|
|
|
|
('month', _('month')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-05-16 20:59:38 -04:00
|
|
|
"The time unit for each block. Should be a smaller unit than "
|
2016-06-02 01:59:06 -04:00
|
|
|
"domain_granularity. Should be larger or equal to Time Grain")
|
|
|
|
}),
|
|
|
|
'link_length': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Link Length"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "200",
|
|
|
|
"choices": self.choicify([
|
2016-03-18 02:44:58 -04:00
|
|
|
'10',
|
|
|
|
'25',
|
|
|
|
'50',
|
|
|
|
'75',
|
|
|
|
'100',
|
|
|
|
'150',
|
|
|
|
'200',
|
|
|
|
'250',
|
|
|
|
]),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Link length in the force layout")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'charge': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Charge"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "-500",
|
|
|
|
"choices": self.choicify([
|
2016-03-18 02:44:58 -04:00
|
|
|
'-50',
|
|
|
|
'-75',
|
|
|
|
'-100',
|
|
|
|
'-150',
|
|
|
|
'-200',
|
|
|
|
'-250',
|
|
|
|
'-500',
|
|
|
|
'-1000',
|
|
|
|
'-2500',
|
|
|
|
'-5000',
|
|
|
|
]),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Charge in the force layout")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'granularity_sqla': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Time Column"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": datasource.main_dttm_col or datasource.any_dttm_col,
|
|
|
|
"choices": self.choicify(datasource.dttm_cols),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The time column for the visualization. Note that you "
|
|
|
|
"can define arbitrary expression that return a DATETIME "
|
|
|
|
"column in the table editor. Also note that the "
|
2016-07-19 17:30:06 -04:00
|
|
|
"filter below is applied against this column or "
|
2016-06-02 01:59:06 -04:00
|
|
|
"expression")
|
|
|
|
}),
|
|
|
|
'resample_rule': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Resample Rule"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '',
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('1T', _('1T')),
|
|
|
|
('1H', _('1H')),
|
|
|
|
('1D', _('1D')),
|
|
|
|
('7D', _('7D')),
|
|
|
|
('1M', _('1M')),
|
|
|
|
('1AS', _('1AS')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Pandas resample rule")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'resample_how': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Resample How"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '',
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('', ''),
|
|
|
|
('mean', _('mean')),
|
|
|
|
('sum', _('sum')),
|
|
|
|
('median', _('median')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Pandas resample how")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'resample_fillmethod': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Resample Fill Method"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '',
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('', ''),
|
|
|
|
('ffill', _('ffill')),
|
|
|
|
('bfill', _('bfill')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Pandas resample fill method")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'since': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Since"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "7 days ago",
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('1 hour ago', _('1 hour ago')),
|
|
|
|
('12 hours ago', _('12 hours ago')),
|
|
|
|
('1 day ago', _('1 day ago')),
|
|
|
|
('7 days ago', _('7 days ago')),
|
|
|
|
('28 days ago', _('28 days ago')),
|
|
|
|
('90 days ago', _('90 days ago')),
|
|
|
|
('1 year ago', _('1 year ago')),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Timestamp from filter. This supports free form typing and "
|
2016-06-02 01:59:06 -04:00
|
|
|
"natural language as in '1 day ago', '28 days' or '3 years'")
|
|
|
|
}),
|
|
|
|
'until': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Until"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "now",
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('now', _('now')),
|
|
|
|
('1 day ago', _('1 day ago')),
|
|
|
|
('7 days ago', _('7 days ago')),
|
|
|
|
('28 days ago', _('28 days ago')),
|
|
|
|
('90 days ago', _('90 days ago')),
|
|
|
|
('1 year ago', _('1 year ago')),
|
|
|
|
)
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'max_bubble_size': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Max Bubble Size"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "25",
|
|
|
|
"choices": self.choicify([
|
2016-03-18 02:44:58 -04:00
|
|
|
'5',
|
|
|
|
'10',
|
|
|
|
'15',
|
|
|
|
'25',
|
|
|
|
'50',
|
|
|
|
'75',
|
|
|
|
'100',
|
2016-03-16 23:25:41 -04:00
|
|
|
])
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'whisker_options': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Whisker/outlier options"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "Tukey",
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-04-10 19:15:25 -04:00
|
|
|
"Determines how whiskers and outliers are calculated."),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('Tukey', _('Tukey')),
|
|
|
|
('Min/max (no outliers)', _('Min/max (no outliers)')),
|
|
|
|
('2/98 percentiles', _('2/98 percentiles')),
|
|
|
|
('9/91 percentiles', _('9/91 percentiles')),
|
|
|
|
)
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'treemap_ratio': (DecimalField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Ratio"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 0.5 * (1 + math.sqrt(5)), # d3 default, golden ratio
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _('Target aspect ratio for treemap tiles.'),
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'number_format': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Number format"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '.3s',
|
|
|
|
"choices": [
|
2016-04-26 14:51:01 -04:00
|
|
|
('.3s', '".3s" | 12.3k'),
|
|
|
|
('.3%', '".3%" | 1234543.210%'),
|
|
|
|
('.4r', '".4r" | 12350'),
|
|
|
|
('.3f', '".3f" | 12345.432'),
|
|
|
|
('+,', '"+," | +12,345.4321'),
|
|
|
|
('$,.2f', '"$,.2f" | $12,345.43'),
|
|
|
|
],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("D3 format syntax for numbers "
|
2016-06-02 01:59:06 -04:00
|
|
|
"https: //github.com/mbostock/\n"
|
2016-06-08 20:38:43 -04:00
|
|
|
"d3/wiki/Formatting")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'row_limit': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _('Row limit'),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": config.get("ROW_LIMIT"),
|
|
|
|
"choices": self.choicify(
|
|
|
|
[10, 50, 100, 250, 500, 1000, 5000, 10000, 50000])
|
|
|
|
}),
|
|
|
|
'limit': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _('Series limit'),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": self.choicify(self.series_limits),
|
|
|
|
"default": 50,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-06-02 01:59:06 -04:00
|
|
|
"Limits the number of time series that get displayed")
|
|
|
|
}),
|
|
|
|
'rolling_type': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Rolling"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'None',
|
|
|
|
"choices": [(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Defines a rolling window function to apply, works along "
|
2016-06-02 01:59:06 -04:00
|
|
|
"with the [Periods] text box")
|
|
|
|
}),
|
|
|
|
'rolling_periods': (IntegerField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Periods"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"validators": [validators.optional()],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-16 23:25:41 -04:00
|
|
|
"Defines the size of the rolling window function, "
|
2016-06-02 01:59:06 -04:00
|
|
|
"relative to the time granularity selected")
|
|
|
|
}),
|
|
|
|
'series': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Series"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": group_by_choices,
|
|
|
|
"default": default_groupby,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Defines the grouping of entities. "
|
2016-07-10 22:37:02 -04:00
|
|
|
"Each series is shown as a specific color on the chart and "
|
2016-06-02 01:59:06 -04:00
|
|
|
"has a legend toggle")
|
|
|
|
}),
|
|
|
|
'entity': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Entity"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": group_by_choices,
|
|
|
|
"default": default_groupby,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("This define the element to be plotted on the chart")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'x': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("X Axis"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": datasource.metrics_combo,
|
|
|
|
"default": default_metric,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Metric assigned to the [X] axis")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'y': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Y Axis"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"choices": datasource.metrics_combo,
|
|
|
|
"default": default_metric,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Metric assigned to the [Y] axis")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'size': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _('Bubble Size'),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": default_metric,
|
|
|
|
"choices": datasource.metrics_combo
|
|
|
|
}),
|
|
|
|
'url': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("URL"),
|
|
|
|
"description": _(
|
2016-05-11 20:00:46 -04:00
|
|
|
"The URL, this field is templated, so you can integrate "
|
|
|
|
"{{ width }} and/or {{ height }} in your URL string."
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'https: //www.youtube.com/embed/JkI5rg_VcQ4',
|
|
|
|
}),
|
2016-06-17 11:11:53 -04:00
|
|
|
'x_axis_label': (TextField, {
|
|
|
|
"label": _("X Axis Label"),
|
|
|
|
"default": '',
|
|
|
|
}),
|
|
|
|
'y_axis_label': (TextField, {
|
2016-06-17 15:31:20 -04:00
|
|
|
"label": _("Y Axis Label"),
|
2016-06-17 11:11:53 -04:00
|
|
|
"default": '',
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'where': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Custom WHERE clause"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The text in this box gets included in your query's WHERE "
|
|
|
|
"clause, as an AND to other criteria. You can include "
|
|
|
|
"complex expression, parenthesis and anything else "
|
2016-06-02 01:59:06 -04:00
|
|
|
"supported by the backend it is directed towards.")
|
|
|
|
}),
|
|
|
|
'having': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Custom HAVING clause"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The text in this box gets included in your query's HAVING"
|
|
|
|
" clause, as an AND to other criteria. You can include "
|
|
|
|
"complex expression, parenthesis and anything else "
|
2016-06-02 01:59:06 -04:00
|
|
|
"supported by the backend it is directed towards.")
|
|
|
|
}),
|
|
|
|
'compare_lag': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Comparison Period Lag"),
|
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Based on granularity, number of time periods to "
|
2016-06-02 01:59:06 -04:00
|
|
|
"compare against")
|
|
|
|
}),
|
|
|
|
'compare_suffix': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Comparison suffix"),
|
|
|
|
"description": _("Suffix to apply after the percentage display")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'table_timestamp_format': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Table Timestamp Format"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'smart_date',
|
|
|
|
"choices": TIMESTAMP_CHOICES,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Timestamp Format")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'series_height': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Series Height"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 25,
|
|
|
|
"choices": self.choicify([10, 25, 40, 50, 75, 100, 150, 200]),
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Pixel height of each series")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'x_axis_format': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("X axis format"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'smart_date',
|
|
|
|
"choices": TIMESTAMP_CHOICES,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("D3 format syntax for y axis "
|
2016-06-02 01:59:06 -04:00
|
|
|
"https: //github.com/mbostock/\n"
|
2016-06-08 20:38:43 -04:00
|
|
|
"d3/wiki/Formatting")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'y_axis_format': (FreeFormSelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Y axis format"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": '.3s',
|
|
|
|
"choices": [
|
2016-03-18 02:44:58 -04:00
|
|
|
('.3s', '".3s" | 12.3k'),
|
|
|
|
('.3%', '".3%" | 1234543.210%'),
|
|
|
|
('.4r', '".4r" | 12350'),
|
|
|
|
('.3f', '".3f" | 12345.432'),
|
|
|
|
('+,', '"+," | +12,345.4321'),
|
|
|
|
('$,.2f', '"$,.2f" | $12,345.43'),
|
|
|
|
],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("D3 format syntax for y axis "
|
2016-06-02 01:59:06 -04:00
|
|
|
"https: //github.com/mbostock/\n"
|
2016-06-08 20:38:43 -04:00
|
|
|
"d3/wiki/Formatting")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'markup_type': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Markup Type"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('markdown', _('markdown')),
|
|
|
|
('html', _('html'))
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "markdown",
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Pick your favorite markup language")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'rotation': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Rotation"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('random', _('random')),
|
|
|
|
('flat', _('flat')),
|
|
|
|
('square', _('square')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "random",
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Rotation to apply to words in the cloud")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'line_interpolation': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Line Style"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('linear', _('linear')),
|
|
|
|
('basis', _('basis')),
|
|
|
|
('cardinal', _('cardinal')),
|
|
|
|
('monotone', _('monotone')),
|
|
|
|
('step-before', _('step-before')),
|
|
|
|
('step-after', _('step-after')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'linear',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Line interpolation as defined by d3.js")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
2016-07-28 14:39:29 -04:00
|
|
|
'pie_label_type': (SelectField, {
|
|
|
|
"label": _("Label Type"),
|
|
|
|
"default": 'key',
|
|
|
|
"choices": (
|
|
|
|
('key', _("Category Name")),
|
|
|
|
('value', _("Value")),
|
|
|
|
('percent', _("Percentage")),
|
|
|
|
),
|
|
|
|
"description": _("What should be shown on the label?")
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'code': (TextAreaField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Code"),
|
|
|
|
"description": _("Put your code here"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": ''
|
|
|
|
}),
|
|
|
|
'pandas_aggfunc': (SelectField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Aggregation function"),
|
2016-06-09 19:45:45 -04:00
|
|
|
"choices": (
|
|
|
|
('sum', _('sum')),
|
|
|
|
('mean', _('mean')),
|
|
|
|
('min', _('min')),
|
|
|
|
('max', _('max')),
|
|
|
|
('median', _('median')),
|
|
|
|
('stdev', _('stdev')),
|
|
|
|
('var', _('var')),
|
|
|
|
),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": 'sum',
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Aggregate function to apply when pivoting and "
|
2016-06-02 01:59:06 -04:00
|
|
|
"computing the total rows and columns")
|
|
|
|
}),
|
|
|
|
'size_from': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Font Size From"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "20",
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Font size for the smallest value in the list")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'size_to': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Font Size To"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "150",
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Font size for the biggest value in the list")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'show_brush': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Range Filter"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-06-02 01:59:06 -04:00
|
|
|
"Whether to display the time range interactive selector")
|
|
|
|
}),
|
2016-09-22 17:30:39 -04:00
|
|
|
'date_filter': (BetterBooleanField, {
|
|
|
|
"label": _("Date Filter"),
|
|
|
|
"default": False,
|
|
|
|
"description": _("Whether to include a time filter")
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'show_datatable': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Data Table"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Whether to display the interactive data table")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'include_search': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Search Box"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-06-02 01:59:06 -04:00
|
|
|
"Whether to include a client side search box")
|
|
|
|
}),
|
|
|
|
'show_bubbles': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Show Bubbles"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-06-02 01:59:06 -04:00
|
|
|
"Whether to display bubbles on top of countries")
|
|
|
|
}),
|
|
|
|
'show_legend': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Legend"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": True,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Whether to display the legend (toggles)")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'x_axis_showminmax': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("X bounds"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": True,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-06-02 01:59:06 -04:00
|
|
|
"Whether to display the min and max values of the X axis")
|
|
|
|
}),
|
|
|
|
'rich_tooltip': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Rich Tooltip"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": True,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The rich tooltip shows a list of all series for that"
|
2016-06-02 01:59:06 -04:00
|
|
|
" point in time")
|
|
|
|
}),
|
|
|
|
'y_axis_zero': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Y Axis Zero"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Force the Y axis to start at 0 instead of the minimum "
|
2016-06-02 01:59:06 -04:00
|
|
|
"value")
|
|
|
|
}),
|
|
|
|
'y_log_scale': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Y Log"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Use a log scale for the Y axis")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'x_log_scale': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("X Log"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Use a log scale for the X axis")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'donut': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Donut"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Do you want a donut or a pie?")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
2016-07-28 14:39:29 -04:00
|
|
|
'labels_outside': (BetterBooleanField, {
|
|
|
|
"label": _("Put labels outside"),
|
|
|
|
"default": True,
|
|
|
|
"description": _("Put the labels outside the pie?")
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'contribution': (BetterBooleanField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Contribution"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": False,
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _("Compute the contribution to the total")
|
2016-06-02 01:59:06 -04:00
|
|
|
}),
|
|
|
|
'num_period_compare': (IntegerField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Period Ratio"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": None,
|
|
|
|
"validators": [validators.optional()],
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"[integer] Number of period to compare against, "
|
2016-06-02 01:59:06 -04:00
|
|
|
"this is relative to the granularity selected")
|
|
|
|
}),
|
2016-08-17 15:26:10 -04:00
|
|
|
'period_ratio_type': (SelectField, {
|
|
|
|
"label": _("Period Ratio Type"),
|
|
|
|
"default": 'growth',
|
|
|
|
"choices": (
|
|
|
|
('factor', _('factor')),
|
|
|
|
('growth', _('growth')),
|
|
|
|
('value', _('value')),
|
|
|
|
),
|
|
|
|
"description": _(
|
|
|
|
"`factor` means (new/previous), `growth` is "
|
|
|
|
"((new/previous) - 1), `value` is (new-previous)")
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
'time_compare': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Time Shift"),
|
2016-06-02 01:59:06 -04:00
|
|
|
"default": "",
|
2016-06-08 20:38:43 -04:00
|
|
|
"description": _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Overlay a timeseries from a "
|
|
|
|
"relative time period. Expects relative time delta "
|
2016-06-02 01:59:06 -04:00
|
|
|
"in natural language (example: 24 hours, 7 days, "
|
|
|
|
"56 weeks, 365 days")
|
|
|
|
}),
|
|
|
|
'subheader': (TextField, {
|
2016-06-08 20:38:43 -04:00
|
|
|
"label": _("Subheader"),
|
|
|
|
"description": _(
|
2016-04-07 17:22:12 -04:00
|
|
|
"Description text that shows up below your Big "
|
2016-06-02 01:59:06 -04:00
|
|
|
"Number")
|
|
|
|
}),
|
Map visualization (#650)
* simple mapbox viz
use react-map-gl
superclustering of long/lat points
Added hook for map style, huge performance boost from bounding box fix, added count text on clusters
variable gradient size based on metric count
Ability to aggregate over any point property
This needed a change in the supercluster npm module, a PR was placed here:
https://github.com/mapbox/supercluster/pull/12
Aggregator function option in explore, tweaked visual defaults
better radius size management
clustering radius, point metric/unit options
scale cluster labels that don't fit, non-numeric labels for points
Minor fixes, label field affects points, text changes
serve mapbox apikey for slice
global opacity, viewport saves (hacky), bug in point labels
fixing mapbox-gl dependency
mapbox_api_key in config
expose row_limit, fix minor bugs
Add renderWhileDragging flag, groupby. Only show numerical columns for point radius
Implicitly group by lng/lat columns and error when label doesn't match groupby
'Fix' radius in miles problem, still some jankiness
derived fields cannot be typed as of now -> reverting numerical number change
better grouping error checking, expose count(*) for labelling
Custom colour for clusters/points + smart text colouring
Fixed bad positioning and overflow in explore view + small bugs + added thumbnail
* landscaping & eslint & use izip
* landscapin'
* address js code review
2016-06-24 17:16:51 -04:00
|
|
|
'mapbox_label': (SelectMultipleSortableField, {
|
|
|
|
"label": "Label",
|
|
|
|
"choices": self.choicify(["count"] + datasource.column_names),
|
|
|
|
"description": _(
|
|
|
|
"'count' is COUNT(*) if a group by is used. "
|
|
|
|
"Numerical columns will be aggregated with the aggregator. "
|
|
|
|
"Non-numerical columns will be used to label points. "
|
|
|
|
"Leave empty to get a count of points in each cluster."),
|
|
|
|
}),
|
|
|
|
'mapbox_style': (SelectField, {
|
|
|
|
"label": "Map Style",
|
|
|
|
"choices": [
|
|
|
|
("mapbox://styles/mapbox/streets-v9", "Streets"),
|
|
|
|
("mapbox://styles/mapbox/dark-v9", "Dark"),
|
|
|
|
("mapbox://styles/mapbox/light-v9", "Light"),
|
|
|
|
("mapbox://styles/mapbox/satellite-streets-v9", "Satellite Streets"),
|
|
|
|
("mapbox://styles/mapbox/satellite-v9", "Satellite"),
|
|
|
|
("mapbox://styles/mapbox/outdoors-v9", "Outdoors"),
|
|
|
|
],
|
2016-07-13 10:58:59 -04:00
|
|
|
"default": "mapbox://styles/mapbox/streets-v9",
|
Map visualization (#650)
* simple mapbox viz
use react-map-gl
superclustering of long/lat points
Added hook for map style, huge performance boost from bounding box fix, added count text on clusters
variable gradient size based on metric count
Ability to aggregate over any point property
This needed a change in the supercluster npm module, a PR was placed here:
https://github.com/mapbox/supercluster/pull/12
Aggregator function option in explore, tweaked visual defaults
better radius size management
clustering radius, point metric/unit options
scale cluster labels that don't fit, non-numeric labels for points
Minor fixes, label field affects points, text changes
serve mapbox apikey for slice
global opacity, viewport saves (hacky), bug in point labels
fixing mapbox-gl dependency
mapbox_api_key in config
expose row_limit, fix minor bugs
Add renderWhileDragging flag, groupby. Only show numerical columns for point radius
Implicitly group by lng/lat columns and error when label doesn't match groupby
'Fix' radius in miles problem, still some jankiness
derived fields cannot be typed as of now -> reverting numerical number change
better grouping error checking, expose count(*) for labelling
Custom colour for clusters/points + smart text colouring
Fixed bad positioning and overflow in explore view + small bugs + added thumbnail
* landscaping & eslint & use izip
* landscapin'
* address js code review
2016-06-24 17:16:51 -04:00
|
|
|
"description": _("Base layer map style")
|
|
|
|
}),
|
|
|
|
'clustering_radius': (FreeFormSelectField, {
|
|
|
|
"label": _("Clustering Radius"),
|
|
|
|
"default": "60",
|
|
|
|
"choices": self.choicify([
|
|
|
|
'0',
|
|
|
|
'20',
|
|
|
|
'40',
|
|
|
|
'60',
|
|
|
|
'80',
|
|
|
|
'100',
|
|
|
|
'200',
|
|
|
|
'500',
|
|
|
|
'1000',
|
|
|
|
]),
|
|
|
|
"description": _(
|
|
|
|
"The radius (in pixels) the algorithm uses to define a cluster. "
|
|
|
|
"Choose 0 to turn off clustering, but beware that a large "
|
|
|
|
"number of points (>1000) will cause lag.")
|
|
|
|
}),
|
|
|
|
'point_radius': (SelectField, {
|
|
|
|
"label": _("Point Radius"),
|
|
|
|
"default": "Auto",
|
|
|
|
"choices": self.choicify(["Auto"] + datasource.column_names),
|
|
|
|
"description": _(
|
|
|
|
"The radius of individual points (ones that are not in a cluster). "
|
|
|
|
"Either a numerical column or 'Auto', which scales the point based "
|
|
|
|
"on the largest cluster")
|
|
|
|
}),
|
|
|
|
'point_radius_unit': (SelectField, {
|
|
|
|
"label": _("Point Radius Unit"),
|
|
|
|
"default": "Pixels",
|
|
|
|
"choices": self.choicify([
|
|
|
|
"Pixels",
|
|
|
|
"Miles",
|
|
|
|
"Kilometers",
|
|
|
|
]),
|
|
|
|
"description": _("The unit of measure for the specified point radius")
|
|
|
|
}),
|
|
|
|
'global_opacity': (DecimalField, {
|
|
|
|
"label": _("Opacity"),
|
|
|
|
"default": 1,
|
|
|
|
"description": _(
|
|
|
|
"Opacity of all clusters, points, and labels. "
|
|
|
|
"Between 0 and 1."),
|
|
|
|
}),
|
|
|
|
'viewport_zoom': (DecimalField, {
|
|
|
|
"label": _("Zoom"),
|
|
|
|
"default": 11,
|
|
|
|
"validators": [validators.optional()],
|
|
|
|
"description": _("Zoom level of the map"),
|
|
|
|
"places": 8,
|
|
|
|
}),
|
|
|
|
'viewport_latitude': (DecimalField, {
|
|
|
|
"label": _("Default latitude"),
|
|
|
|
"default": 37.772123,
|
|
|
|
"description": _("Latitude of default viewport"),
|
|
|
|
"places": 8,
|
|
|
|
}),
|
|
|
|
'viewport_longitude': (DecimalField, {
|
|
|
|
"label": _("Default longitude"),
|
|
|
|
"default": -122.405293,
|
|
|
|
"description": _("Longitude of default viewport"),
|
|
|
|
"places": 8,
|
|
|
|
}),
|
|
|
|
'render_while_dragging': (BetterBooleanField, {
|
|
|
|
"label": _("Live render"),
|
|
|
|
"default": True,
|
|
|
|
"description": _("Points and clusters will update as viewport "
|
|
|
|
"is being changed")
|
|
|
|
}),
|
|
|
|
'mapbox_color': (FreeFormSelectField, {
|
|
|
|
"label": _("RGB Color"),
|
|
|
|
"default": "rgb(0, 122, 135)",
|
|
|
|
"choices": [
|
|
|
|
("rgb(0, 139, 139)", "Dark Cyan"),
|
|
|
|
("rgb(128, 0, 128)", "Purple"),
|
|
|
|
("rgb(255, 215, 0)", "Gold"),
|
|
|
|
("rgb(69, 69, 69)", "Dim Gray"),
|
|
|
|
("rgb(220, 20, 60)", "Crimson"),
|
|
|
|
("rgb(34, 139, 34)", "Forest Green"),
|
|
|
|
],
|
|
|
|
"description": _("The color for points and clusters in RGB")
|
|
|
|
}),
|
2016-06-02 01:59:06 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
# Override default arguments with form overrides
|
|
|
|
for field_name, override_map in viz.form_overrides.items():
|
|
|
|
if field_name in field_data:
|
|
|
|
field_data[field_name][1].update(override_map)
|
|
|
|
|
|
|
|
self.field_dict = {
|
|
|
|
field_name: v[0](**v[1])
|
|
|
|
for field_name, v in field_data.items()
|
2016-03-18 02:44:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def choicify(l):
|
|
|
|
return [("{}".format(obj), "{}".format(obj)) for obj in l]
|
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
"""Returns a form object based on the viz/datasource/context"""
|
|
|
|
viz = self.viz
|
|
|
|
field_css_classes = {}
|
|
|
|
for name, obj in self.field_dict.items():
|
2016-09-01 17:59:28 -04:00
|
|
|
field_css_classes[name] = ['form-control', 'input-sm']
|
2016-03-18 02:44:58 -04:00
|
|
|
s = self.fieltype_class.get(obj.field_class)
|
|
|
|
if s:
|
|
|
|
field_css_classes[name] += [s]
|
|
|
|
|
|
|
|
for field in ('show_brush', 'show_legend', 'rich_tooltip'):
|
|
|
|
field_css_classes[field] += ['input-sm']
|
|
|
|
|
|
|
|
class QueryForm(OmgWtForm):
|
2016-03-16 23:25:41 -04:00
|
|
|
|
|
|
|
"""The dynamic form object used for the explore view"""
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
fieldsets = copy(viz.fieldsets)
|
|
|
|
css_classes = field_css_classes
|
|
|
|
standalone = HiddenField()
|
|
|
|
async = HiddenField()
|
2016-03-16 23:25:41 -04:00
|
|
|
force = HiddenField()
|
2016-03-18 02:44:58 -04:00
|
|
|
extra_filters = HiddenField()
|
|
|
|
json = HiddenField()
|
|
|
|
slice_id = HiddenField()
|
|
|
|
slice_name = HiddenField()
|
|
|
|
previous_viz_type = HiddenField(default=viz.viz_type)
|
|
|
|
collapsed_fieldsets = HiddenField()
|
|
|
|
viz_type = self.field_dict.get('viz_type')
|
|
|
|
|
|
|
|
for field in viz.flat_form_fields():
|
|
|
|
setattr(QueryForm, field, self.field_dict[field])
|
|
|
|
|
|
|
|
def add_to_form(attrs):
|
|
|
|
for attr in attrs:
|
|
|
|
setattr(QueryForm, attr, self.field_dict[attr])
|
|
|
|
|
2016-05-23 16:06:35 -04:00
|
|
|
filter_choices = self.choicify(['in', 'not in'])
|
2016-06-21 12:43:10 -04:00
|
|
|
having_op_choices = []
|
|
|
|
filter_prefixes = ['flt']
|
2016-03-18 02:44:58 -04:00
|
|
|
# datasource type specific form elements
|
2016-05-23 16:06:35 -04:00
|
|
|
datasource_classname = viz.datasource.__class__.__name__
|
|
|
|
time_fields = None
|
|
|
|
if datasource_classname == 'SqlaTable':
|
2016-03-18 02:44:58 -04:00
|
|
|
QueryForm.fieldsets += ({
|
2016-06-08 20:38:43 -04:00
|
|
|
'label': _('SQL'),
|
2016-03-18 02:44:58 -04:00
|
|
|
'fields': ['where', 'having'],
|
2016-06-08 20:38:43 -04:00
|
|
|
'description': _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"This section exposes ways to include snippets of "
|
|
|
|
"SQL in your query"),
|
|
|
|
},)
|
|
|
|
add_to_form(('where', 'having'))
|
|
|
|
grains = viz.datasource.database.grains()
|
|
|
|
|
|
|
|
if grains:
|
2016-06-17 11:12:15 -04:00
|
|
|
grains_choices = [(grain.name, grain.label) for grain in grains]
|
2016-03-18 02:44:58 -04:00
|
|
|
time_fields = ('granularity_sqla', 'time_grain_sqla')
|
|
|
|
self.field_dict['time_grain_sqla'] = SelectField(
|
2016-06-08 20:38:43 -04:00
|
|
|
_('Time Grain'),
|
2016-06-17 11:12:15 -04:00
|
|
|
choices=grains_choices,
|
2016-03-18 02:44:58 -04:00
|
|
|
default="Time Column",
|
2016-06-08 20:38:43 -04:00
|
|
|
description=_(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The time granularity for the visualization. This "
|
|
|
|
"applies a date transformation to alter "
|
|
|
|
"your time column and defines a new time granularity."
|
|
|
|
"The options here are defined on a per database "
|
2016-03-29 00:55:58 -04:00
|
|
|
"engine basis in the Caravel source code"))
|
2016-03-18 02:44:58 -04:00
|
|
|
add_to_form(time_fields)
|
|
|
|
field_css_classes['time_grain_sqla'] = ['form-control', 'select2']
|
|
|
|
field_css_classes['granularity_sqla'] = ['form-control', 'select2']
|
|
|
|
else:
|
|
|
|
time_fields = 'granularity_sqla'
|
|
|
|
add_to_form((time_fields, ))
|
2016-05-23 16:06:35 -04:00
|
|
|
elif datasource_classname == 'DruidDatasource':
|
2016-04-15 20:00:44 -04:00
|
|
|
time_fields = ('granularity', 'druid_time_origin')
|
|
|
|
add_to_form(('granularity', 'druid_time_origin'))
|
2016-04-13 16:58:17 -04:00
|
|
|
field_css_classes['granularity'] = ['form-control', 'select2_freeform']
|
2016-04-15 20:00:44 -04:00
|
|
|
field_css_classes['druid_time_origin'] = ['form-control', 'select2_freeform']
|
2016-05-23 16:06:35 -04:00
|
|
|
filter_choices = self.choicify(['in', 'not in', 'regex'])
|
2016-07-01 17:45:04 -04:00
|
|
|
having_op_choices = self.choicify(
|
|
|
|
['==', '!=', '>', '<', '>=', '<='])
|
2016-06-21 12:43:10 -04:00
|
|
|
filter_prefixes += ['having']
|
2016-03-18 02:44:58 -04:00
|
|
|
add_to_form(('since', 'until'))
|
|
|
|
|
2016-07-01 17:45:04 -04:00
|
|
|
# filter_cols defaults to ''. Filters with blank col will be ignored
|
2016-06-21 12:43:10 -04:00
|
|
|
filter_cols = self.choicify(
|
2016-07-01 17:45:04 -04:00
|
|
|
([''] + viz.datasource.filterable_column_names) or [''])
|
2016-06-21 12:43:10 -04:00
|
|
|
having_cols = filter_cols + viz.datasource.metrics_combo
|
|
|
|
for field_prefix in filter_prefixes:
|
|
|
|
is_having_filter = field_prefix == 'having'
|
|
|
|
col_choices = filter_cols if not is_having_filter else having_cols
|
|
|
|
op_choices = filter_choices if not is_having_filter else \
|
|
|
|
having_op_choices
|
|
|
|
for i in range(10):
|
|
|
|
setattr(QueryForm, field_prefix + '_col_' + str(i),
|
|
|
|
SelectField(
|
|
|
|
_('Filter 1'),
|
|
|
|
default=col_choices[0][0],
|
|
|
|
choices=col_choices))
|
|
|
|
setattr(QueryForm, field_prefix + '_op_' + str(i), SelectField(
|
|
|
|
_('Filter 1'),
|
|
|
|
default=op_choices[0][0],
|
|
|
|
choices=op_choices))
|
|
|
|
setattr(
|
|
|
|
QueryForm, field_prefix + '_eq_' + str(i),
|
|
|
|
TextField(_("Super"), default=''))
|
2016-05-23 16:06:35 -04:00
|
|
|
|
|
|
|
if time_fields:
|
|
|
|
QueryForm.fieldsets = ({
|
2016-06-08 20:38:43 -04:00
|
|
|
'label': _('Time'),
|
2016-05-23 16:06:35 -04:00
|
|
|
'fields': (
|
|
|
|
time_fields,
|
|
|
|
('since', 'until'),
|
|
|
|
),
|
2016-06-08 20:38:43 -04:00
|
|
|
'description': _("Time related form attributes"),
|
2016-05-23 16:06:35 -04:00
|
|
|
},) + tuple(QueryForm.fieldsets)
|
2016-03-18 02:44:58 -04:00
|
|
|
return QueryForm
|