Adding orderby to Table 'not grouped by' and fixing metrics ordering (#669)

This commit is contained in:
Maxime Beauchemin 2016-06-23 22:43:52 -07:00 committed by GitHub
parent 51024b5f8a
commit 131372740e
4 changed files with 59 additions and 31 deletions

View File

@ -119,6 +119,7 @@ function tableVis(slice) {
var height = slice.container.height(); var height = slice.container.height();
var datatable = slice.container.find('.dataTable').DataTable({ var datatable = slice.container.find('.dataTable').DataTable({
paging: false, paging: false,
aaSorting: [],
searching: form_data.include_search, searching: form_data.include_search,
bInfo: false, bInfo: false,
scrollY: height + "px", scrollY: height + "px",

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from copy import copy from copy import copy
import json
import math import math
from flask_babelpkg import lazy_gettext as _ from flask_babelpkg import lazy_gettext as _
@ -129,6 +130,10 @@ class FormFactory(object):
gb_cols = datasource.groupby_column_names gb_cols = datasource.groupby_column_names
default_groupby = gb_cols[0] if gb_cols else None default_groupby = gb_cols[0] if gb_cols else None
group_by_choices = self.choicify(gb_cols) group_by_choices = self.choicify(gb_cols)
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]'))
# Pool of all the fields that can be used in Caravel # Pool of all the fields that can be used in Caravel
field_data = { field_data = {
'viz_type': (SelectField, { 'viz_type': (SelectField, {
@ -143,6 +148,11 @@ class FormFactory(object):
"default": [default_metric], "default": [default_metric],
"description": _("One or many metrics to display") "description": _("One or many metrics to display")
}), }),
'order_by_cols': (SelectMultipleSortableField, {
"label": _("Ordering"),
"choices": order_by_choices,
"description": _("One or many metrics to display")
}),
'metric': (SelectField, { 'metric': (SelectField, {
"label": _("Metric"), "label": _("Metric"),
"choices": datasource.metrics_combo, "choices": datasource.metrics_combo,

View File

@ -32,7 +32,7 @@ from pydruid.utils.having import Having, Aggregation
from six import string_types from six import string_types
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, String, ForeignKey, Text, Boolean, DateTime, Date, Column, Integer, String, ForeignKey, Text, Boolean, DateTime, Date,
Table, create_engine, MetaData, desc, select, and_, func) Table, create_engine, MetaData, desc, asc, select, and_, func)
from sqlalchemy.engine import reflection from sqlalchemy.engine import reflection
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -533,6 +533,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
'database_id', 'schema', 'table_name', 'database_id', 'schema', 'table_name',
name='_customer_location_uc'),) name='_customer_location_uc'),)
def __repr__(self): def __repr__(self):
return self.table_name return self.table_name
@ -561,6 +562,10 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
l.append(self.main_dttm_col) l.append(self.main_dttm_col)
return l return l
@property
def num_cols(self):
return [c.column_name for c in self.columns if c.isnum]
@property @property
def any_dttm_col(self): def any_dttm_col(self):
cols = self.dttm_cols cols = self.dttm_cols
@ -610,6 +615,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
is_timeseries=True, is_timeseries=True,
timeseries_limit=15, row_limit=None, timeseries_limit=15, row_limit=None,
inner_from_dttm=None, inner_to_dttm=None, inner_from_dttm=None, inner_to_dttm=None,
orderby=None,
extras=None, extras=None,
columns=None): columns=None):
"""Querying any sqla table from this common interface""" """Querying any sqla table from this common interface"""
@ -618,6 +624,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
granularity = self.main_dttm_col granularity = self.main_dttm_col
cols = {col.column_name: col for col in self.columns} cols = {col.column_name: col for col in self.columns}
metrics_dict = {m.metric_name: m for m in self.metrics}
qry_start_dttm = datetime.now() qry_start_dttm = datetime.now()
if not granularity and is_timeseries: if not granularity and is_timeseries:
@ -625,14 +632,10 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
"Datetime column not provided as part table configuration " "Datetime column not provided as part table configuration "
"and is required by this type of chart")) "and is required by this type of chart"))
metrics_exprs = [ metrics_exprs = [metrics_dict.get(m).sqla_col for m in metrics]
m.sqla_col
for m in self.metrics if m.metric_name in metrics]
if metrics: if metrics:
main_metric_expr = [ main_metric_expr = metrics_exprs[0]
m.sqla_col for m in self.metrics
if m.metric_name == metrics[0]][0]
else: else:
main_metric_expr = literal_column("COUNT(*)").label("ccount") main_metric_expr = literal_column("COUNT(*)").label("ccount")
@ -720,6 +723,11 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
qry = qry.having(and_(*having_clause_and)) qry = qry.having(and_(*having_clause_and))
if groupby: if groupby:
qry = qry.order_by(desc(main_metric_expr)) qry = qry.order_by(desc(main_metric_expr))
elif orderby:
for col, ascending in orderby:
direction = asc if ascending else desc
qry = qry.order_by(direction(col))
qry = qry.limit(row_limit) qry = qry.limit(row_limit)
if timeseries_limit and groupby: if timeseries_limit and groupby:
@ -768,9 +776,12 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
any_date_col = None any_date_col = None
for col in table.columns: for col in table.columns:
try: try:
datatype = str(col.type) datatype = "{}".format(col.type).upper()
except Exception as e: except Exception as e:
datatype = "UNKNOWN" datatype = "UNKNOWN"
logging.error(
"Unrecognized data type in {}.{}".format(table, col.name))
logging.exception(e)
dbcol = ( dbcol = (
db.session db.session
.query(TC) .query(TC)
@ -780,22 +791,16 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
) )
db.session.flush() db.session.flush()
if not dbcol: if not dbcol:
dbcol = TableColumn(column_name=col.name) dbcol = TableColumn(column_name=col.name, type=datatype)
num_types = ('DOUBLE', 'FLOAT', 'INT', 'BIGINT', 'LONG') dbcol.groupby = dbcol.is_string
date_types = ('DATE', 'TIME') dbcol.filterable = dbcol.is_string
str_types = ('VARCHAR', 'STRING') dbcol.sum = dbcol.isnum
datatype = str(datatype).upper() dbcol.is_dttm = dbcol.is_time
if any([t in datatype for t in str_types]):
dbcol.groupby = True
dbcol.filterable = True
elif any([t in datatype for t in num_types]):
dbcol.sum = True
elif any([t in datatype for t in date_types]):
dbcol.is_dttm = True
db.session.merge(self) db.session.merge(self)
self.columns.append(dbcol) self.columns.append(dbcol)
if not any_date_col and 'date' in datatype.lower(): if not any_date_col and dbcol.is_time:
any_date_col = col.name any_date_col = col.name
quoted = "{}".format( quoted = "{}".format(
@ -905,13 +910,24 @@ class TableColumn(Model, AuditMixinNullable):
expression = Column(Text, default='') expression = Column(Text, default='')
description = Column(Text, default='') description = Column(Text, default='')
num_types = ('DOUBLE', 'FLOAT', 'INT', 'BIGINT', 'LONG')
date_types = ('DATE', 'TIME')
str_types = ('VARCHAR', 'STRING', 'CHAR')
def __repr__(self): def __repr__(self):
return self.column_name return self.column_name
@property @property
def isnum(self): def isnum(self):
types = ('LONG', 'DOUBLE', 'FLOAT', 'BIGINT', 'INT') return any([t in self.type.upper() for t in self.num_types])
return any([t in self.type.upper() for t in types])
@property
def is_time(self):
return any([t in self.type.upper() for t in self.date_types])
@property
def is_string(self):
return any([t in self.type.upper() for t in self.str_types])
@property @property
def sqla_col(self): def sqla_col(self):
@ -999,6 +1015,10 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
[(m.metric_name, m.verbose_name) for m in self.metrics], [(m.metric_name, m.verbose_name) for m in self.metrics],
key=lambda x: x[1]) key=lambda x: x[1])
@property
def num_cols(self):
return [c.column_name for c in self.columns if c.isnum]
@property @property
def name(self): def name(self):
return self.datasource_name return self.datasource_name
@ -1119,6 +1139,7 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
timeseries_limit=None, timeseries_limit=None,
row_limit=None, row_limit=None,
inner_from_dttm=None, inner_to_dttm=None, inner_from_dttm=None, inner_to_dttm=None,
orderby=None,
extras=None, # noqa extras=None, # noqa
select=None,): # noqa select=None,): # noqa
"""Runs a query against Druid and returns a dataframe. """Runs a query against Druid and returns a dataframe.
@ -1458,7 +1479,7 @@ class DruidColumn(Model, AuditMixinNullable):
@property @property
def isnum(self): def isnum(self):
return self.type in ('LONG', 'DOUBLE', 'FLOAT') return self.type in ('LONG', 'DOUBLE', 'FLOAT', 'INT')
def generate_metrics(self): def generate_metrics(self):
"""Generate metrics based on the column metadata""" """Generate metrics based on the column metadata"""

View File

@ -350,16 +350,11 @@ class TableViz(BaseViz):
fieldsets = ({ fieldsets = ({
'label': _("GROUP BY"), 'label': _("GROUP BY"),
'description': _('Use this section if you want a query that aggregates'), 'description': _('Use this section if you want a query that aggregates'),
'fields': ( 'fields': ('groupby', 'metrics')
'groupby',
'metrics',
)
}, { }, {
'label': _("NOT GROUPED BY"), 'label': _("NOT GROUPED BY"),
'description': _('Use this section if you want to query atomic rows'), 'description': _('Use this section if you want to query atomic rows'),
'fields': ( 'fields': ('all_columns', 'order_by_cols'),
'all_columns',
)
}, { }, {
'label': _("Options"), 'label': _("Options"),
'fields': ( 'fields': (
@ -385,6 +380,7 @@ class TableViz(BaseViz):
if fd.get('all_columns'): if fd.get('all_columns'):
d['columns'] = fd.get('all_columns') d['columns'] = fd.get('all_columns')
d['groupby'] = [] d['groupby'] = []
d['orderby'] = [json.loads(t) for t in fd.get('order_by_cols', [])]
return d return d
def get_df(self, query_obj=None): def get_df(self, query_obj=None):