Files
superset2/superset/connectors/base.py
Maxime Beauchemin 16141ecb94 [explore] improve metric(s) and groupby(s) controls (#2921)
* [explore] improve metric(s) and groupby(s) controls

- surface verbose_name, description & expression in controls
- [table viz] surface verbose name in table header

* Fixing tests

* Addressing comments

* Fixing tests (once more)
2017-06-09 11:29:55 -07:00

257 lines
7.8 KiB
Python

import json
from sqlalchemy import (
Column, Integer, String, Text, Boolean,
)
from superset import utils
from superset.models.helpers import AuditMixinNullable, ImportMixin
class BaseDatasource(AuditMixinNullable, ImportMixin):
"""A common interface to objects that are queryable (tables and datasources)"""
# ---------------------------------------------------------------
# class attributes to define when deriving BaseDatasource
# ---------------------------------------------------------------
__tablename__ = None # {connector_name}_datasource
type = None # datasoure type, str to be defined when deriving this class
baselink = None # url portion pointing to ModelView endpoint
column_class = None # link to derivative of BaseColumn
metric_class = None # link to derivative of BaseMetric
# Used to do code highlighting when displaying the query in the UI
query_language = None
name = None # can be a Column or a property pointing to one
# ---------------------------------------------------------------
# Columns
id = Column(Integer, primary_key=True)
description = Column(Text)
default_endpoint = Column(Text)
is_featured = Column(Boolean, default=False) # TODO deprecating
filter_select_enabled = Column(Boolean, default=False)
offset = Column(Integer, default=0)
cache_timeout = Column(Integer)
params = Column(String(1000))
perm = Column(String(1000))
# placeholder for a relationship to a derivative of BaseColumn
columns = []
# placeholder for a relationship to a derivative of BaseMetric
metrics = []
@property
def uid(self):
"""Unique id across datasource types"""
return "{self.id}__{self.type}".format(**locals())
@property
def column_names(self):
return sorted([c.column_name for c in self.columns])
@property
def main_dttm_col(self):
return "timestamp"
@property
def groupby_column_names(self):
return sorted([c.column_name for c in self.columns if c.groupby])
@property
def filterable_column_names(self):
return sorted([c.column_name for c in self.columns if c.filterable])
@property
def dttm_cols(self):
return []
@property
def url(self):
return '/{}/edit/{}'.format(self.baselink, self.id)
@property
def explore_url(self):
if self.default_endpoint:
return self.default_endpoint
else:
return "/superset/explore/{obj.type}/{obj.id}/".format(obj=self)
@property
def column_formats(self):
return {
m.metric_name: m.d3format
for m in self.metrics
if m.d3format
}
@property
def metrics_combo(self):
return sorted(
[
(m.metric_name, m.verbose_name or m.metric_name)
for m in self.metrics],
key=lambda x: x[1])
@property
def data(self):
"""Data representation of the datasource sent to the frontend"""
order_by_choices = []
for s in sorted(self.column_names):
order_by_choices.append((json.dumps([s, True]), s + ' [asc]'))
order_by_choices.append((json.dumps([s, False]), s + ' [desc]'))
verbose_map = {
o.metric_name: o.verbose_name or o.metric_name
for o in self.metrics
}
verbose_map.update({
o.column_name: o.verbose_name or o.column_name
for o in self.columns
})
return {
'all_cols': utils.choicify(self.column_names),
'column_formats': self.column_formats,
'edit_url': self.url,
'filter_select': self.filter_select_enabled,
'filterable_cols': utils.choicify(self.filterable_column_names),
'gb_cols': utils.choicify(self.groupby_column_names),
'id': self.id,
'metrics_combo': self.metrics_combo,
'name': self.name,
'order_by_choices': order_by_choices,
'type': self.type,
'metrics': [o.data for o in self.metrics],
'columns': [o.data for o in self.columns],
'verbose_map': verbose_map,
}
def get_query_str(self, query_obj):
"""Returns a query as a string
This is used to be displayed to the user so that she/he can
understand what is taking place behind the scene"""
raise NotImplementedError()
def query(self, query_obj):
"""Executes the query and returns a dataframe
query_obj is a dictionary representing Superset's query interface.
Should return a ``superset.models.helpers.QueryResult``
"""
raise NotImplementedError()
def values_for_column(self, column_name, limit=10000):
"""Given a column, returns an iterable of distinct values
This is used to populate the dropdown showing a list of
values in filters in the explore view"""
raise NotImplementedError()
class BaseColumn(AuditMixinNullable, ImportMixin):
"""Interface for column"""
__tablename__ = None # {connector_name}_column
id = Column(Integer, primary_key=True)
column_name = Column(String(255))
verbose_name = Column(String(1024))
is_active = Column(Boolean, default=True)
type = Column(String(32))
groupby = Column(Boolean, default=False)
count_distinct = Column(Boolean, default=False)
sum = Column(Boolean, default=False)
avg = Column(Boolean, default=False)
max = Column(Boolean, default=False)
min = Column(Boolean, default=False)
filterable = Column(Boolean, default=False)
description = Column(Text)
# [optional] Set this to support import/export functionality
export_fields = []
def __repr__(self):
return self.column_name
num_types = (
'DOUBLE', 'FLOAT', 'INT', 'BIGINT',
'LONG', 'REAL', 'NUMERIC', 'DECIMAL'
)
date_types = ('DATE', 'TIME', 'DATETIME')
str_types = ('VARCHAR', 'STRING', 'CHAR')
@property
def is_num(self):
return (
self.type and
any([t in self.type.upper() for t in self.num_types])
)
@property
def is_time(self):
return (
self.type and
any([t in self.type.upper() for t in self.date_types])
)
@property
def is_string(self):
return (
self.type and
any([t in self.type.upper() for t in self.str_types])
)
@property
def expression(self):
raise NotImplementedError()
@property
def data(self):
attrs = ('column_name', 'verbose_name', 'description', 'expression')
return {s: getattr(self, s) for s in attrs}
class BaseMetric(AuditMixinNullable, ImportMixin):
"""Interface for Metrics"""
__tablename__ = None # {connector_name}_metric
id = Column(Integer, primary_key=True)
metric_name = Column(String(512))
verbose_name = Column(String(1024))
metric_type = Column(String(32))
description = Column(Text)
is_restricted = Column(Boolean, default=False, nullable=True)
d3format = Column(String(128))
"""
The interface should also declare a datasource relationship pointing
to a derivative of BaseDatasource, along with a FK
datasource_name = Column(
String(255),
ForeignKey('datasources.datasource_name'))
datasource = relationship(
# needs to be altered to point to {Connector}Datasource
'BaseDatasource',
backref=backref('metrics', cascade='all, delete-orphan'),
enable_typechecks=False)
"""
@property
def perm(self):
raise NotImplementedError()
@property
def expression(self):
raise NotImplementedError()
@property
def data(self):
attrs = ('metric_name', 'verbose_name', 'description', 'expression')
return {s: getattr(self, s) for s in attrs}