2016-03-29 00:55:58 -04:00
|
|
|
"""Flask web views for Caravel"""
|
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-18 02:44:58 -04:00
|
|
|
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import re
|
2016-06-10 18:49:33 -04:00
|
|
|
import sys
|
2016-03-24 17:11:29 -04:00
|
|
|
import time
|
2016-03-18 02:44:58 -04:00
|
|
|
import traceback
|
2016-04-08 02:01:40 -04:00
|
|
|
from datetime import datetime
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-06-21 12:42:54 -04:00
|
|
|
import functools
|
2016-04-08 02:01:40 -04:00
|
|
|
import pandas as pd
|
|
|
|
import sqlalchemy as sqla
|
2016-05-02 13:50:23 -04:00
|
|
|
|
2016-03-13 21:16:23 -04:00
|
|
|
from flask import (
|
|
|
|
g, request, redirect, flash, Response, render_template, Markup)
|
2016-06-06 00:37:03 -04:00
|
|
|
from flask_appbuilder import ModelView, CompactCRUDMixin, BaseView, expose
|
|
|
|
from flask_appbuilder.actions import action
|
|
|
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
2016-06-21 12:42:54 -04:00
|
|
|
from flask_appbuilder.security.decorators import has_access, has_access_api
|
2016-06-27 23:10:40 -04:00
|
|
|
from flask_babel import gettext as __
|
|
|
|
from flask_babel import lazy_gettext as _
|
2016-04-26 19:44:51 -04:00
|
|
|
from flask_appbuilder.models.sqla.filters import BaseFilter
|
2016-05-02 13:50:23 -04:00
|
|
|
|
2016-04-26 19:44:51 -04:00
|
|
|
from sqlalchemy import create_engine, select, text
|
2016-03-18 02:44:58 -04:00
|
|
|
from sqlalchemy.sql.expression import TextAsFrom
|
2016-03-31 12:27:21 -04:00
|
|
|
from werkzeug.routing import BaseConverter
|
2016-04-08 02:01:40 -04:00
|
|
|
from wtforms.validators import ValidationError
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-06-10 18:49:33 -04:00
|
|
|
import caravel
|
2016-03-29 00:55:58 -04:00
|
|
|
from caravel import appbuilder, db, models, viz, utils, app, sm, ascii_art
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
config = app.config
|
|
|
|
log_this = models.Log.log_this
|
2016-06-24 11:47:43 -04:00
|
|
|
can_access = utils.can_access
|
|
|
|
|
|
|
|
|
|
|
|
class BaseCaravelView(BaseView):
|
|
|
|
def can_access(self, permission_name, view_name):
|
|
|
|
return utils.can_access(appbuilder.sm, permission_name, view_name)
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
|
2016-06-21 12:42:54 -04:00
|
|
|
def get_error_msg():
|
|
|
|
if config.get("SHOW_STACKTRACE"):
|
|
|
|
error_msg = traceback.format_exc()
|
|
|
|
else:
|
|
|
|
error_msg = "FATAL ERROR \n"
|
|
|
|
error_msg += (
|
|
|
|
"Stacktrace is hidden. Change the SHOW_STACKTRACE "
|
|
|
|
"configuration setting to enable it")
|
|
|
|
return error_msg
|
|
|
|
|
|
|
|
|
|
|
|
def api(f):
|
|
|
|
"""
|
|
|
|
A decorator to label an endpoint as an API. Catches uncaught exceptions and
|
|
|
|
return the response in the JSON format
|
|
|
|
"""
|
|
|
|
def wraps(self, *args, **kwargs):
|
|
|
|
try:
|
|
|
|
return f(self, *args, **kwargs)
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
resp = Response(
|
|
|
|
json.dumps({
|
|
|
|
'message': get_error_msg()
|
|
|
|
}),
|
|
|
|
status=500,
|
|
|
|
mimetype="application/json")
|
|
|
|
return resp
|
|
|
|
|
|
|
|
return functools.update_wrapper(wraps, f)
|
|
|
|
|
|
|
|
|
2016-06-02 22:17:34 -04:00
|
|
|
def check_ownership(obj, raise_if_false=True):
|
|
|
|
"""Meant to be used in `pre_update` hooks on models to enforce ownership
|
|
|
|
|
|
|
|
Admin have all access, and other users need to be referenced on either
|
|
|
|
the created_by field that comes with the ``AuditMixin``, or in a field
|
|
|
|
named ``owners`` which is expected to be a one-to-many with the User
|
|
|
|
model. It is meant to be used in the ModelView's pre_update hook in
|
|
|
|
which raising will abort the update.
|
|
|
|
"""
|
2016-06-28 13:31:36 -04:00
|
|
|
if not obj:
|
|
|
|
return False
|
2016-06-02 22:17:34 -04:00
|
|
|
roles = (r.name for r in get_user_roles())
|
|
|
|
if 'Admin' in roles:
|
|
|
|
return True
|
|
|
|
session = db.create_scoped_session()
|
|
|
|
orig_obj = session.query(obj.__class__).filter_by(id=obj.id).first()
|
|
|
|
owner_names = (user.username for user in orig_obj.owners)
|
|
|
|
if (
|
|
|
|
hasattr(orig_obj, 'created_by') and
|
|
|
|
orig_obj.created_by and
|
|
|
|
orig_obj.created_by.username == g.user.username):
|
|
|
|
return True
|
2016-06-28 13:31:36 -04:00
|
|
|
if (
|
|
|
|
hasattr(orig_obj, 'owners') and
|
|
|
|
g.user and
|
|
|
|
hasattr(g.user, 'username') and
|
|
|
|
g.user.username in owner_names):
|
2016-06-02 22:17:34 -04:00
|
|
|
return True
|
|
|
|
if raise_if_false:
|
|
|
|
raise utils.CaravelSecurityException(
|
|
|
|
"You don't have the rights to alter [{}]".format(obj))
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2016-05-04 01:31:37 -04:00
|
|
|
def get_user_roles():
|
|
|
|
if g.user.is_anonymous():
|
|
|
|
return [appbuilder.sm.find_role('Public')]
|
|
|
|
return g.user.roles
|
|
|
|
|
|
|
|
|
2016-04-26 19:44:51 -04:00
|
|
|
class CaravelFilter(BaseFilter):
|
|
|
|
def get_perms(self):
|
|
|
|
perms = []
|
2016-05-04 01:31:37 -04:00
|
|
|
for role in get_user_roles():
|
2016-04-26 19:44:51 -04:00
|
|
|
for perm_view in role.permissions:
|
|
|
|
if perm_view.permission.name == 'datasource_access':
|
|
|
|
perms.append(perm_view.view_menu.name)
|
|
|
|
return perms
|
|
|
|
|
|
|
|
|
|
|
|
class FilterSlice(CaravelFilter):
|
|
|
|
def apply(self, query, func): # noqa
|
2016-05-04 01:31:37 -04:00
|
|
|
if any([r.name in ('Admin', 'Alpha') for r in get_user_roles()]):
|
2016-04-26 19:44:51 -04:00
|
|
|
return query
|
|
|
|
qry = query.filter(self.model.perm.in_(self.get_perms()))
|
|
|
|
return qry
|
|
|
|
|
|
|
|
|
|
|
|
class FilterDashboard(CaravelFilter):
|
|
|
|
def apply(self, query, func): # noqa
|
2016-05-04 01:31:37 -04:00
|
|
|
if any([r.name in ('Admin', 'Alpha') for r in get_user_roles()]):
|
2016-04-26 19:44:51 -04:00
|
|
|
return query
|
|
|
|
Slice = models.Slice # noqa
|
2016-06-02 22:17:34 -04:00
|
|
|
Dash = models.Dashboard # noqa
|
2016-04-26 19:44:51 -04:00
|
|
|
slice_ids_qry = (
|
|
|
|
db.session
|
|
|
|
.query(Slice.id)
|
|
|
|
.filter(Slice.perm.in_(self.get_perms()))
|
|
|
|
)
|
2016-06-02 22:17:34 -04:00
|
|
|
query = query.filter(
|
|
|
|
Dash.id.in_(
|
|
|
|
db.session.query(Dash.id)
|
|
|
|
.distinct()
|
|
|
|
.join(Dash.slices)
|
|
|
|
.filter(Slice.id.in_(slice_ids_qry))
|
2016-04-26 19:44:51 -04:00
|
|
|
)
|
|
|
|
)
|
2016-06-02 22:17:34 -04:00
|
|
|
return query
|
2016-04-26 19:44:51 -04:00
|
|
|
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
def validate_json(form, field): # noqa
|
|
|
|
try:
|
|
|
|
json.loads(field.data)
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
raise ValidationError("json isn't valid")
|
|
|
|
|
|
|
|
|
2016-04-06 11:22:49 -04:00
|
|
|
def generate_download_headers(extension):
|
|
|
|
filename = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
content_disp = "attachment; filename={}.{}".format(filename, extension)
|
|
|
|
headers = {
|
|
|
|
"Content-Disposition": content_disp,
|
|
|
|
}
|
|
|
|
return headers
|
|
|
|
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
class DeleteMixin(object):
|
|
|
|
@action(
|
|
|
|
"muldelete", "Delete", "Delete all Really?", "fa-trash", single=False)
|
|
|
|
def muldelete(self, items):
|
|
|
|
self.datamodel.delete_all(items)
|
|
|
|
self.update_redirect()
|
|
|
|
return redirect(self.get_redirect())
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class CaravelModelView(ModelView):
|
2016-03-18 02:44:58 -04:00
|
|
|
page_size = 500
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class TableColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.TableColumn)
|
|
|
|
can_delete = False
|
|
|
|
edit_columns = [
|
2016-05-02 13:00:28 -04:00
|
|
|
'column_name', 'verbose_name', 'description', 'groupby', 'filterable',
|
2016-05-10 12:29:29 -04:00
|
|
|
'table', 'count_distinct', 'sum', 'min', 'max', 'expression',
|
2016-06-28 00:33:44 -04:00
|
|
|
'is_dttm', 'python_date_format', 'database_expression']
|
2016-03-18 02:44:58 -04:00
|
|
|
add_columns = edit_columns
|
|
|
|
list_columns = [
|
|
|
|
'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
|
|
|
|
'sum', 'min', 'max', 'is_dttm']
|
|
|
|
page_size = 500
|
|
|
|
description_columns = {
|
2016-05-02 13:50:23 -04:00
|
|
|
'is_dttm': (_(
|
2016-03-18 02:44:58 -04:00
|
|
|
"Whether to make this column available as a "
|
|
|
|
"[Time Granularity] option, column has to be DATETIME or "
|
2016-05-02 13:50:23 -04:00
|
|
|
"DATETIME-like")),
|
2016-04-12 13:41:23 -04:00
|
|
|
'expression': utils.markdown(
|
|
|
|
"a valid SQL expression as supported by the underlying backend. "
|
|
|
|
"Example: `substr(name, 1, 1)`", True),
|
2016-06-28 00:33:44 -04:00
|
|
|
'python_date_format': utils.markdown(Markup(
|
|
|
|
"The pattern of timestamp format, use "
|
|
|
|
"<a href='https://docs.python.org/2/library/"
|
|
|
|
"datetime.html#strftime-strptime-behavior'>"
|
|
|
|
"python datetime string pattern</a> "
|
|
|
|
"expression. If time is stored in epoch "
|
|
|
|
"format, put `epoch_s` or `epoch_ms`. Leave `Database Expression` "
|
|
|
|
"below empty if timestamp is stored in "
|
|
|
|
"String or Integer(epoch) type"), True),
|
|
|
|
'database_expression': utils.markdown(
|
|
|
|
"The database expression to cast internal datetime "
|
|
|
|
"constants to database date/timestamp type according to the DBAPI. "
|
|
|
|
"The expression should follow the pattern of "
|
|
|
|
"%Y-%m-%d %H:%M:%S, based on different DBAPI. "
|
|
|
|
"The string should be a python string formatter \n"
|
|
|
|
"`Ex: TO_DATE('{}', 'YYYY-MM-DD HH24:MI:SS')` for Oracle"
|
|
|
|
"Caravel uses default expression based on DB URI if this "
|
|
|
|
"field is blank.", True),
|
2016-03-18 02:44:58 -04:00
|
|
|
}
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'column_name': _("Column"),
|
|
|
|
'verbose_name': _("Verbose Name"),
|
|
|
|
'description': _("Description"),
|
|
|
|
'groupby': _("Groupable"),
|
|
|
|
'filterable': _("Filterable"),
|
|
|
|
'table': _("Table"),
|
|
|
|
'count_distinct': _("Count Distinct"),
|
|
|
|
'sum': _("Sum"),
|
|
|
|
'min': _("Min"),
|
|
|
|
'max': _("Max"),
|
|
|
|
'expression': _("Expression"),
|
|
|
|
'is_dttm': _("Is temporal"),
|
2016-06-28 00:33:44 -04:00
|
|
|
'python_date_format': _("Datetime Format"),
|
|
|
|
'database_expression': _("Database Expression")
|
2016-05-23 14:46:33 -04:00
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
appbuilder.add_view_no_menu(TableColumnInlineView)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class DruidColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.DruidColumn)
|
|
|
|
edit_columns = [
|
|
|
|
'column_name', 'description', 'datasource', 'groupby',
|
|
|
|
'count_distinct', 'sum', 'min', 'max']
|
|
|
|
list_columns = [
|
|
|
|
'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
|
|
|
|
'sum', 'min', 'max']
|
|
|
|
can_delete = False
|
|
|
|
page_size = 500
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'column_name': _("Column"),
|
|
|
|
'type': _("Type"),
|
|
|
|
'datasource': _("Datasource"),
|
|
|
|
'groupby': _("Groupable"),
|
|
|
|
'filterable': _("Filterable"),
|
|
|
|
'count_distinct': _("Count Distinct"),
|
|
|
|
'sum': _("Sum"),
|
|
|
|
'min': _("Min"),
|
|
|
|
'max': _("Max"),
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
def post_update(self, col):
|
|
|
|
col.generate_metrics()
|
|
|
|
|
|
|
|
appbuilder.add_view_no_menu(DruidColumnInlineView)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class SqlMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.SqlMetric)
|
2016-06-24 11:47:43 -04:00
|
|
|
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
2016-03-18 02:44:58 -04:00
|
|
|
edit_columns = [
|
|
|
|
'metric_name', 'description', 'verbose_name', 'metric_type',
|
2016-06-10 18:49:33 -04:00
|
|
|
'expression', 'table', 'is_restricted']
|
2016-04-12 13:41:23 -04:00
|
|
|
description_columns = {
|
|
|
|
'expression': utils.markdown(
|
|
|
|
"a valid SQL expression as supported by the underlying backend. "
|
|
|
|
"Example: `count(DISTINCT userid)`", True),
|
2016-06-10 18:49:33 -04:00
|
|
|
'is_restricted': _("Whether the access to this metric is restricted "
|
|
|
|
"to certain roles. Only roles with the permission "
|
|
|
|
"'metric access on XXX (the name of this metric)' "
|
|
|
|
"are allowed to access this metric"),
|
2016-04-12 13:41:23 -04:00
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
add_columns = edit_columns
|
|
|
|
page_size = 500
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'metric_name': _("Metric"),
|
|
|
|
'description': _("Description"),
|
|
|
|
'verbose_name': _("Verbose Name"),
|
|
|
|
'metric_type': _("Type"),
|
|
|
|
'expression': _("SQL Expression"),
|
|
|
|
'table': _("Table"),
|
|
|
|
}
|
2016-06-10 18:49:33 -04:00
|
|
|
|
2016-06-24 11:47:43 -04:00
|
|
|
def post_add(self, metric):
|
|
|
|
utils.init_metrics_perm(caravel, [metric])
|
|
|
|
|
|
|
|
def post_update(self, metric):
|
|
|
|
utils.init_metrics_perm(caravel, [metric])
|
2016-06-10 18:49:33 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.DruidMetric)
|
2016-06-24 11:47:43 -04:00
|
|
|
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
2016-03-18 02:44:58 -04:00
|
|
|
edit_columns = [
|
2016-05-02 13:00:39 -04:00
|
|
|
'metric_name', 'description', 'verbose_name', 'metric_type', 'json',
|
2016-06-10 18:49:33 -04:00
|
|
|
'datasource', 'is_restricted']
|
2016-05-02 13:00:39 -04:00
|
|
|
add_columns = edit_columns
|
2016-03-18 02:44:58 -04:00
|
|
|
page_size = 500
|
|
|
|
validators_columns = {
|
|
|
|
'json': [validate_json],
|
|
|
|
}
|
2016-05-02 13:00:39 -04:00
|
|
|
description_columns = {
|
|
|
|
'metric_type': utils.markdown(
|
|
|
|
"use `postagg` as the metric type if you are defining a "
|
|
|
|
"[Druid Post Aggregation]"
|
|
|
|
"(http://druid.io/docs/latest/querying/post-aggregations.html)",
|
|
|
|
True),
|
2016-06-10 18:49:33 -04:00
|
|
|
'is_restricted': _("Whether the access to this metric is restricted "
|
|
|
|
"to certain roles. Only roles with the permission "
|
|
|
|
"'metric access on XXX (the name of this metric)' "
|
|
|
|
"are allowed to access this metric"),
|
2016-05-02 13:00:39 -04:00
|
|
|
}
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'metric_name': _("Metric"),
|
|
|
|
'description': _("Description"),
|
|
|
|
'verbose_name': _("Verbose Name"),
|
|
|
|
'metric_type': _("Type"),
|
|
|
|
'json': _("JSON"),
|
|
|
|
'datasource': _("Druid Datasource"),
|
|
|
|
}
|
2016-06-10 18:49:33 -04:00
|
|
|
|
2016-06-24 11:47:43 -04:00
|
|
|
def post_add(self, metric):
|
|
|
|
utils.init_metrics_perm(caravel, [metric])
|
|
|
|
|
|
|
|
def post_update(self, metric):
|
|
|
|
utils.init_metrics_perm(caravel, [metric])
|
2016-06-10 18:49:33 -04:00
|
|
|
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
appbuilder.add_view_no_menu(DruidMetricInlineView)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class DatabaseView(CaravelModelView, DeleteMixin): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.Database)
|
2016-05-12 00:05:32 -04:00
|
|
|
list_columns = ['database_name', 'sql_link', 'creator', 'changed_on_']
|
2016-04-04 19:13:08 -04:00
|
|
|
add_columns = [
|
|
|
|
'database_name', 'sqlalchemy_uri', 'cache_timeout', 'extra']
|
2016-03-18 02:44:58 -04:00
|
|
|
search_exclude_columns = ('password',)
|
|
|
|
edit_columns = add_columns
|
2016-03-29 00:55:58 -04:00
|
|
|
add_template = "caravel/models/database/add.html"
|
|
|
|
edit_template = "caravel/models/database/edit.html"
|
2016-03-16 23:25:41 -04:00
|
|
|
base_order = ('changed_on', 'desc')
|
2016-03-18 02:44:58 -04:00
|
|
|
description_columns = {
|
|
|
|
'sqlalchemy_uri': (
|
|
|
|
"Refer to the SqlAlchemy docs for more information on how "
|
|
|
|
"to structure your URI here: "
|
2016-04-04 19:13:08 -04:00
|
|
|
"http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html"),
|
|
|
|
'extra': utils.markdown(
|
|
|
|
"JSON string containing extra configuration elements. "
|
|
|
|
"The ``engine_params`` object gets unpacked into the "
|
|
|
|
"[sqlalchemy.create_engine]"
|
|
|
|
"(http://docs.sqlalchemy.org/en/latest/core/engines.html#"
|
|
|
|
"sqlalchemy.create_engine) call, while the ``metadata_params`` "
|
|
|
|
"gets unpacked into the [sqlalchemy.MetaData]"
|
|
|
|
"(http://docs.sqlalchemy.org/en/rel_1_0/core/metadata.html"
|
|
|
|
"#sqlalchemy.schema.MetaData) call. ", True),
|
2016-03-18 02:44:58 -04:00
|
|
|
}
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'database_name': _("Database"),
|
|
|
|
'sql_link': _("SQL link"),
|
|
|
|
'creator': _("Creator"),
|
|
|
|
'changed_on_': _("Last Changed"),
|
|
|
|
'sqlalchemy_uri': _("SQLAlchemy URI"),
|
|
|
|
'cache_timeout': _("Cache Timeout"),
|
|
|
|
'extra': _("Extra"),
|
|
|
|
}
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
def pre_add(self, db):
|
|
|
|
conn = sqla.engine.url.make_url(db.sqlalchemy_uri)
|
|
|
|
db.password = conn.password
|
|
|
|
conn.password = "X" * 10 if conn.password else None
|
|
|
|
db.sqlalchemy_uri = str(conn) # hides the password
|
|
|
|
|
|
|
|
def pre_update(self, db):
|
|
|
|
self.pre_add(db)
|
|
|
|
|
|
|
|
|
|
|
|
appbuilder.add_view(
|
|
|
|
DatabaseView,
|
|
|
|
"Databases",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Databases"),
|
2016-03-18 02:44:58 -04:00
|
|
|
icon="fa-database",
|
2016-06-07 20:43:51 -04:00
|
|
|
category="Sources",
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Sources"),
|
2016-03-18 02:44:58 -04:00
|
|
|
category_icon='fa-database',)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class TableModelView(CaravelModelView, DeleteMixin): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.SqlaTable)
|
|
|
|
list_columns = [
|
|
|
|
'table_link', 'database', 'sql_link', 'is_featured',
|
2016-05-12 00:05:32 -04:00
|
|
|
'changed_by_', 'changed_on_']
|
2016-06-30 01:20:25 -04:00
|
|
|
order_columns = [
|
|
|
|
'table_link', 'database', 'sql_link', 'is_featured', 'changed_on_']
|
2016-03-16 23:25:41 -04:00
|
|
|
add_columns = [
|
2016-04-13 20:28:12 -04:00
|
|
|
'table_name', 'database', 'schema',
|
|
|
|
'default_endpoint', 'offset', 'cache_timeout']
|
2016-03-18 02:44:58 -04:00
|
|
|
edit_columns = [
|
2016-06-28 00:33:44 -04:00
|
|
|
'table_name', 'is_featured', 'database', 'schema',
|
|
|
|
'description', 'owner',
|
2016-03-16 23:25:41 -04:00
|
|
|
'main_dttm_col', 'default_endpoint', 'offset', 'cache_timeout']
|
2016-03-18 02:44:58 -04:00
|
|
|
related_views = [TableColumnInlineView, SqlMetricInlineView]
|
2016-03-16 23:25:41 -04:00
|
|
|
base_order = ('changed_on', 'desc')
|
2016-03-18 02:44:58 -04:00
|
|
|
description_columns = {
|
|
|
|
'offset': "Timezone offset (in hours) for this datasource",
|
2016-04-13 20:28:12 -04:00
|
|
|
'schema': (
|
|
|
|
"Schema, as used only in some databases like Postgres, Redshift "
|
|
|
|
"and DB2"),
|
2016-03-18 02:44:58 -04:00
|
|
|
'description': Markup(
|
|
|
|
"Supports <a href='https://daringfireball.net/projects/markdown/'>"
|
|
|
|
"markdown</a>"),
|
|
|
|
}
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'table_link': _("Table"),
|
|
|
|
'changed_by_': _("Changed By"),
|
|
|
|
'database': _("Database"),
|
|
|
|
'changed_on_': _("Last Changed"),
|
|
|
|
'sql_link': _("SQL Editor"),
|
|
|
|
'is_featured': _("Is Featured"),
|
|
|
|
'schema': _("Schema"),
|
|
|
|
'default_endpoint': _("Default Endpoint"),
|
|
|
|
'offset': _("Offset"),
|
|
|
|
'cache_timeout': _("Cache Timeout"),
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
def post_add(self, table):
|
2016-06-01 00:16:32 -04:00
|
|
|
table_name = table.table_name
|
2016-03-18 02:44:58 -04:00
|
|
|
try:
|
|
|
|
table.fetch_metadata()
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
flash(
|
2016-03-16 23:25:41 -04:00
|
|
|
"Table [{}] doesn't seem to exist, "
|
2016-06-01 00:16:32 -04:00
|
|
|
"couldn't fetch metadata".format(table_name),
|
2016-03-16 23:25:41 -04:00
|
|
|
"danger")
|
2016-03-18 02:44:58 -04:00
|
|
|
utils.merge_perm(sm, 'datasource_access', table.perm)
|
|
|
|
|
|
|
|
def post_update(self, table):
|
|
|
|
self.post_add(table)
|
|
|
|
|
|
|
|
appbuilder.add_view(
|
|
|
|
TableModelView,
|
2016-06-07 20:43:51 -04:00
|
|
|
"Tables",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Tables"),
|
2016-06-07 20:43:51 -04:00
|
|
|
category="Sources",
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Sources"),
|
2016-03-18 02:44:58 -04:00
|
|
|
icon='fa-table',)
|
|
|
|
|
|
|
|
|
|
|
|
appbuilder.add_separator("Sources")
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class DruidClusterModelView(CaravelModelView, DeleteMixin): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.DruidCluster)
|
|
|
|
add_columns = [
|
|
|
|
'cluster_name',
|
|
|
|
'coordinator_host', 'coordinator_port', 'coordinator_endpoint',
|
|
|
|
'broker_host', 'broker_port', 'broker_endpoint',
|
|
|
|
]
|
|
|
|
edit_columns = add_columns
|
|
|
|
list_columns = ['cluster_name', 'metadata_last_refreshed']
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'cluster_name': _("Cluster"),
|
|
|
|
'coordinator_host': _("Coordinator Host"),
|
|
|
|
'coordinator_port': _("Coordinator Port"),
|
|
|
|
'coordinator_endpoint': _("Coordinator Endpoint"),
|
|
|
|
'broker_host': _("Broker Host"),
|
2016-06-01 00:10:28 -04:00
|
|
|
'broker_port': _("Broker Port"),
|
2016-05-23 14:46:33 -04:00
|
|
|
'broker_endpoint': _("Broker Endpoint"),
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-03-25 14:26:59 -04:00
|
|
|
|
|
|
|
if config['DRUID_IS_ACTIVE']:
|
|
|
|
appbuilder.add_view(
|
|
|
|
DruidClusterModelView,
|
2016-06-07 20:43:51 -04:00
|
|
|
name="Druid Clusters",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Druid Clusters"),
|
2016-03-25 14:26:59 -04:00
|
|
|
icon="fa-cubes",
|
2016-06-07 20:43:51 -04:00
|
|
|
category="Sources",
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Sources"),
|
2016-03-25 14:26:59 -04:00
|
|
|
category_icon='fa-database',)
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class SliceModelView(CaravelModelView, DeleteMixin): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.Slice)
|
2016-04-05 16:33:02 -04:00
|
|
|
add_template = "caravel/add_slice.html"
|
2016-03-18 02:44:58 -04:00
|
|
|
can_add = False
|
2016-03-24 17:11:29 -04:00
|
|
|
label_columns = {
|
|
|
|
'datasource_link': 'Datasource',
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
list_columns = [
|
2016-04-26 19:44:51 -04:00
|
|
|
'slice_link', 'viz_type', 'datasource_link', 'creator', 'modified']
|
2016-03-18 02:44:58 -04:00
|
|
|
edit_columns = [
|
|
|
|
'slice_name', 'description', 'viz_type', 'druid_datasource',
|
2016-04-26 19:44:51 -04:00
|
|
|
'table', 'owners', 'dashboards', 'params', 'cache_timeout']
|
2016-03-16 23:25:41 -04:00
|
|
|
base_order = ('changed_on', 'desc')
|
2016-03-18 02:44:58 -04:00
|
|
|
description_columns = {
|
|
|
|
'description': Markup(
|
|
|
|
"The content here can be displayed as widget headers in the "
|
|
|
|
"dashboard view. Supports "
|
|
|
|
"<a href='https://daringfireball.net/projects/markdown/'>"
|
|
|
|
"markdown</a>"),
|
2016-06-15 11:50:50 -04:00
|
|
|
'params': _(
|
|
|
|
"These parameters are generated dynamically when clicking "
|
|
|
|
"the save or overwrite button in the explore view. This JSON "
|
|
|
|
"object is exposed here for reference and for power users who may "
|
|
|
|
"want to alter specific parameters."),
|
|
|
|
'cache_timeout': _(
|
|
|
|
"Duration (in seconds) of the caching timeout for this slice."
|
|
|
|
),
|
2016-03-18 02:44:58 -04:00
|
|
|
}
|
2016-04-26 19:44:51 -04:00
|
|
|
base_filters = [['id', FilterSlice, lambda: []]]
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'cache_timeout': _("Cache Timeout"),
|
|
|
|
'creator': _("Creator"),
|
|
|
|
'dashboards': _("Dashboards"),
|
|
|
|
'datasource_link': _("Datasource"),
|
|
|
|
'description': _("Description"),
|
|
|
|
'modified': _("Last Modified"),
|
|
|
|
'owners': _("Owners"),
|
|
|
|
'params': _("Parameters"),
|
|
|
|
'slice_link': _("Slice"),
|
|
|
|
'slice_name': _("Name"),
|
|
|
|
'table': _("Table"),
|
|
|
|
'viz_type': _("Visualization Type"),
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-06-02 22:17:34 -04:00
|
|
|
def pre_update(self, obj):
|
|
|
|
check_ownership(obj)
|
|
|
|
|
2016-06-15 17:27:36 -04:00
|
|
|
def pre_delete(self, obj):
|
|
|
|
check_ownership(obj)
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
appbuilder.add_view(
|
|
|
|
SliceModelView,
|
2016-06-07 20:43:51 -04:00
|
|
|
"Slices",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Slices"),
|
2016-03-18 02:44:58 -04:00
|
|
|
icon="fa-bar-chart",
|
|
|
|
category="",
|
|
|
|
category_icon='',)
|
|
|
|
|
|
|
|
|
2016-03-24 17:11:29 -04:00
|
|
|
class SliceAsync(SliceModelView): # noqa
|
|
|
|
list_columns = [
|
|
|
|
'slice_link', 'viz_type',
|
2016-04-26 19:44:51 -04:00
|
|
|
'creator', 'modified', 'icons']
|
2016-03-27 03:18:26 -04:00
|
|
|
label_columns = {
|
|
|
|
'icons': ' ',
|
2016-05-23 14:46:33 -04:00
|
|
|
'viz_type': _('Type'),
|
|
|
|
'slice_link': _('Slice'),
|
|
|
|
'viz_type': _('Visualization Type'),
|
2016-03-27 03:18:26 -04:00
|
|
|
}
|
2016-03-24 17:11:29 -04:00
|
|
|
|
|
|
|
appbuilder.add_view_no_menu(SliceAsync)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class DashboardModelView(CaravelModelView, DeleteMixin): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.Dashboard)
|
2016-04-26 19:44:51 -04:00
|
|
|
list_columns = ['dashboard_link', 'creator', 'modified']
|
2016-03-18 02:44:58 -04:00
|
|
|
edit_columns = [
|
2016-04-26 19:44:51 -04:00
|
|
|
'dashboard_title', 'slug', 'slices', 'owners', 'position_json', 'css',
|
2016-03-18 02:44:58 -04:00
|
|
|
'json_metadata']
|
2016-06-21 12:41:48 -04:00
|
|
|
show_columns = edit_columns + ['table_names']
|
2016-03-18 02:44:58 -04:00
|
|
|
add_columns = edit_columns
|
2016-03-16 23:25:41 -04:00
|
|
|
base_order = ('changed_on', 'desc')
|
2016-03-18 02:44:58 -04:00
|
|
|
description_columns = {
|
2016-05-02 13:50:23 -04:00
|
|
|
'position_json': _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"This json object describes the positioning of the widgets in "
|
|
|
|
"the dashboard. It is dynamically generated when adjusting "
|
|
|
|
"the widgets size and positions by using drag & drop in "
|
|
|
|
"the dashboard view"),
|
2016-05-02 13:50:23 -04:00
|
|
|
'css': _(
|
2016-03-18 02:44:58 -04:00
|
|
|
"The css for individual dashboards can be altered here, or "
|
|
|
|
"in the dashboard view where changes are immediately "
|
|
|
|
"visible"),
|
2016-05-23 14:46:33 -04:00
|
|
|
'slug': _("To get a readable URL for your dashboard"),
|
2016-06-15 11:50:50 -04:00
|
|
|
'json_metadata': _(
|
|
|
|
"This JSON object is generated dynamically when clicking "
|
|
|
|
"the save or overwrite button in the dashboard view. It "
|
|
|
|
"is exposed here for reference and for power users who may "
|
|
|
|
"want to alter specific parameters."),
|
|
|
|
'owners': _("Owners is a list of users who can alter the dashboard."),
|
2016-03-18 02:44:58 -04:00
|
|
|
}
|
2016-04-26 19:44:51 -04:00
|
|
|
base_filters = [['slice', FilterDashboard, lambda: []]]
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'dashboard_link': _("Dashboard"),
|
|
|
|
'dashboard_title': _("Title"),
|
|
|
|
'slug': _("Slug"),
|
|
|
|
'slices': _("Slices"),
|
|
|
|
'owners': _("Owners"),
|
|
|
|
'creator': _("Creator"),
|
|
|
|
'modified': _("Modified"),
|
|
|
|
'position_json': _("Position JSON"),
|
|
|
|
'css': _("CSS"),
|
|
|
|
'json_metadata': _("JSON Metadata"),
|
2016-06-21 12:41:48 -04:00
|
|
|
'table_names': _("Underlying Tables"),
|
2016-05-23 14:46:33 -04:00
|
|
|
}
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
def pre_add(self, obj):
|
|
|
|
obj.slug = obj.slug.strip() or None
|
|
|
|
if obj.slug:
|
|
|
|
obj.slug = obj.slug.replace(" ", "-")
|
|
|
|
obj.slug = re.sub(r'\W+', '', obj.slug)
|
|
|
|
|
|
|
|
def pre_update(self, obj):
|
2016-06-07 14:07:25 -04:00
|
|
|
check_ownership(obj)
|
2016-03-18 02:44:58 -04:00
|
|
|
self.pre_add(obj)
|
|
|
|
|
|
|
|
|
|
|
|
appbuilder.add_view(
|
|
|
|
DashboardModelView,
|
|
|
|
"Dashboards",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Dashboards"),
|
2016-03-18 02:44:58 -04:00
|
|
|
icon="fa-dashboard",
|
|
|
|
category="",
|
|
|
|
category_icon='',)
|
|
|
|
|
|
|
|
|
2016-03-24 17:11:29 -04:00
|
|
|
class DashboardModelViewAsync(DashboardModelView): # noqa
|
2016-06-28 13:31:36 -04:00
|
|
|
list_columns = ['dashboard_link', 'creator', 'modified', 'dashboard_title']
|
2016-04-04 19:12:28 -04:00
|
|
|
label_columns = {
|
|
|
|
'dashboard_link': 'Dashboard',
|
|
|
|
}
|
2016-03-24 17:11:29 -04:00
|
|
|
|
|
|
|
appbuilder.add_view_no_menu(DashboardModelViewAsync)
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class LogModelView(CaravelModelView):
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.Log)
|
|
|
|
list_columns = ('user', 'action', 'dttm')
|
|
|
|
edit_columns = ('user', 'action', 'dttm', 'json')
|
2016-03-16 23:25:41 -04:00
|
|
|
base_order = ('dttm', 'desc')
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
|
|
|
'user': _("User"),
|
|
|
|
'action': _("Action"),
|
|
|
|
'dttm': _("dttm"),
|
|
|
|
'json': _("JSON"),
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
appbuilder.add_view(
|
|
|
|
LogModelView,
|
|
|
|
"Action Log",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Action Log"),
|
2016-06-07 20:43:51 -04:00
|
|
|
category="Security",
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Security"),
|
2016-03-18 02:44:58 -04:00
|
|
|
icon="fa-list-ol")
|
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class DruidDatasourceModelView(CaravelModelView, DeleteMixin): # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.DruidDatasource)
|
|
|
|
list_columns = [
|
2016-06-30 01:20:25 -04:00
|
|
|
'datasource_link', 'cluster', 'changed_by_', 'changed_on_', 'offset']
|
|
|
|
order_columns = [
|
|
|
|
'datasource_link', 'changed_on_', 'offset']
|
2016-04-26 19:44:51 -04:00
|
|
|
related_views = [DruidColumnInlineView, DruidMetricInlineView]
|
2016-03-18 02:44:58 -04:00
|
|
|
edit_columns = [
|
|
|
|
'datasource_name', 'cluster', 'description', 'owner',
|
2016-03-16 23:25:41 -04:00
|
|
|
'is_featured', 'is_hidden', 'default_endpoint', 'offset',
|
|
|
|
'cache_timeout']
|
2016-04-20 18:08:10 -04:00
|
|
|
add_columns = edit_columns
|
2016-03-18 02:44:58 -04:00
|
|
|
page_size = 500
|
|
|
|
base_order = ('datasource_name', 'asc')
|
|
|
|
description_columns = {
|
2016-05-23 14:46:33 -04:00
|
|
|
'offset': _("Timezone offset (in hours) for this datasource"),
|
2016-03-18 02:44:58 -04:00
|
|
|
'description': Markup(
|
|
|
|
"Supports <a href='"
|
|
|
|
"https://daringfireball.net/projects/markdown/'>markdown</a>"),
|
|
|
|
}
|
2016-05-23 14:46:33 -04:00
|
|
|
label_columns = {
|
2016-06-30 01:20:25 -04:00
|
|
|
'datasource_link': _("Data Source"),
|
2016-05-23 14:46:33 -04:00
|
|
|
'cluster': _("Cluster"),
|
|
|
|
'description': _("Description"),
|
|
|
|
'owner': _("Owner"),
|
|
|
|
'is_featured': _("Is Featured"),
|
|
|
|
'is_hidden': _("Is Hidden"),
|
|
|
|
'default_endpoint': _("Default Endpoint"),
|
|
|
|
'offset': _("Time Offset"),
|
|
|
|
'cache_timeout': _("Cache Timeout"),
|
|
|
|
}
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
def post_add(self, datasource):
|
|
|
|
datasource.generate_metrics()
|
|
|
|
utils.merge_perm(sm, 'datasource_access', datasource.perm)
|
|
|
|
|
|
|
|
def post_update(self, datasource):
|
|
|
|
self.post_add(datasource)
|
|
|
|
|
2016-03-25 14:26:59 -04:00
|
|
|
if config['DRUID_IS_ACTIVE']:
|
|
|
|
appbuilder.add_view(
|
|
|
|
DruidDatasourceModelView,
|
|
|
|
"Druid Datasources",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("Druid Datasources"),
|
2016-03-25 14:26:59 -04:00
|
|
|
category="Sources",
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Sources"),
|
2016-03-25 14:26:59 -04:00
|
|
|
icon="fa-cube")
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
|
@app.route('/health')
|
|
|
|
def health():
|
|
|
|
return "OK"
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/ping')
|
|
|
|
def ping():
|
|
|
|
return "OK"
|
|
|
|
|
|
|
|
|
2016-06-24 11:47:43 -04:00
|
|
|
class R(BaseCaravelView):
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-03-16 23:25:41 -04:00
|
|
|
"""used for short urls"""
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
@log_this
|
|
|
|
@expose("/<url_id>")
|
|
|
|
def index(self, url_id):
|
|
|
|
url = db.session.query(models.Url).filter_by(id=url_id).first()
|
|
|
|
if url:
|
|
|
|
return redirect('/' + url.url)
|
|
|
|
else:
|
|
|
|
flash("URL to nowhere...", "danger")
|
|
|
|
return redirect('/')
|
|
|
|
|
|
|
|
@log_this
|
|
|
|
@expose("/shortner/", methods=['POST', 'GET'])
|
|
|
|
def shortner(self):
|
|
|
|
url = request.form.get('data')
|
|
|
|
obj = models.Url(url=url)
|
|
|
|
db.session.add(obj)
|
|
|
|
db.session.commit()
|
|
|
|
return("{request.headers[Host]}/r/{obj.id}".format(
|
|
|
|
request=request, obj=obj))
|
|
|
|
|
2016-04-05 16:33:02 -04:00
|
|
|
@expose("/msg/")
|
|
|
|
def msg(self):
|
|
|
|
"""Redirects to specified url while flash a message"""
|
2016-06-15 17:23:25 -04:00
|
|
|
flash(Markup(request.args.get("msg")), "info")
|
2016-04-05 16:33:02 -04:00
|
|
|
return redirect(request.args.get("url"))
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
appbuilder.add_view_no_menu(R)
|
|
|
|
|
|
|
|
|
2016-06-24 11:47:43 -04:00
|
|
|
class Caravel(BaseCaravelView):
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
"""The base views for Caravel!"""
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
@has_access
|
|
|
|
@expose("/explore/<datasource_type>/<datasource_id>/")
|
|
|
|
@expose("/datasource/<datasource_type>/<datasource_id>/") # Legacy url
|
|
|
|
@log_this
|
|
|
|
def explore(self, datasource_type, datasource_id):
|
2016-06-28 13:31:36 -04:00
|
|
|
|
2016-04-14 23:25:40 -04:00
|
|
|
error_redirect = '/slicemodelview/list/'
|
2016-03-16 23:25:41 -04:00
|
|
|
datasource_class = models.SqlaTable \
|
|
|
|
if datasource_type == "table" else models.DruidDatasource
|
2016-04-06 11:23:27 -04:00
|
|
|
datasources = (
|
2016-03-16 23:25:41 -04:00
|
|
|
db.session
|
|
|
|
.query(datasource_class)
|
2016-04-06 11:23:27 -04:00
|
|
|
.all()
|
2016-03-16 23:25:41 -04:00
|
|
|
)
|
2016-04-06 11:23:27 -04:00
|
|
|
datasources = sorted(datasources, key=lambda ds: ds.full_name)
|
|
|
|
datasource = [ds for ds in datasources if int(datasource_id) == ds.id]
|
|
|
|
datasource = datasource[0] if datasource else None
|
2016-03-16 23:25:41 -04:00
|
|
|
slice_id = request.args.get("slice_id")
|
|
|
|
slc = None
|
2016-04-18 16:56:00 -04:00
|
|
|
|
2016-03-16 23:25:41 -04:00
|
|
|
if slice_id:
|
|
|
|
slc = (
|
|
|
|
db.session.query(models.Slice)
|
|
|
|
.filter_by(id=slice_id)
|
2016-03-18 02:44:58 -04:00
|
|
|
.first()
|
|
|
|
)
|
2016-03-16 23:25:41 -04:00
|
|
|
if not datasource:
|
2016-05-23 14:46:33 -04:00
|
|
|
flash(__("The datasource seems to have been deleted"), "alert")
|
2016-04-14 23:25:40 -04:00
|
|
|
return redirect(error_redirect)
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-06-28 13:31:36 -04:00
|
|
|
slice_add_perm = self.can_access('can_add', 'SliceModelView')
|
|
|
|
slice_edit_perm = check_ownership(slc, raise_if_false=False)
|
|
|
|
slice_download_perm = self.can_access('can_download', 'SliceModelView')
|
|
|
|
|
2016-06-24 11:47:43 -04:00
|
|
|
all_datasource_access = self.can_access(
|
2016-03-16 23:25:41 -04:00
|
|
|
'all_datasource_access', 'all_datasource_access')
|
2016-06-24 11:47:43 -04:00
|
|
|
datasource_access = self.can_access(
|
2016-03-16 23:25:41 -04:00
|
|
|
'datasource_access', datasource.perm)
|
|
|
|
if not (all_datasource_access or datasource_access):
|
2016-05-23 14:46:33 -04:00
|
|
|
flash(__("You don't seem to have access to this datasource"), "danger")
|
2016-04-14 23:25:40 -04:00
|
|
|
return redirect(error_redirect)
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
action = request.args.get('action')
|
2016-06-28 13:31:36 -04:00
|
|
|
if action in ('saveas', 'overwrite'):
|
2016-04-18 16:56:00 -04:00
|
|
|
return self.save_or_overwrite_slice(
|
|
|
|
request.args, slc, slice_add_perm, slice_edit_perm)
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
viz_type = request.args.get("viz_type")
|
|
|
|
if not viz_type and datasource.default_endpoint:
|
|
|
|
return redirect(datasource.default_endpoint)
|
|
|
|
if not viz_type:
|
|
|
|
viz_type = "table"
|
2016-04-15 18:00:49 -04:00
|
|
|
try:
|
|
|
|
obj = viz.viz_types[viz_type](
|
|
|
|
datasource,
|
|
|
|
form_data=request.args,
|
|
|
|
slice_=slc)
|
|
|
|
except Exception as e:
|
|
|
|
flash(str(e), "danger")
|
|
|
|
return redirect(error_redirect)
|
2016-03-16 23:25:41 -04:00
|
|
|
if request.args.get("json") == "true":
|
2016-03-18 02:44:58 -04:00
|
|
|
status = 200
|
2016-05-05 15:19:51 -04:00
|
|
|
if config.get("DEBUG"):
|
|
|
|
# Allows for nice debugger stack traces in debug mode
|
2016-03-16 23:25:41 -04:00
|
|
|
payload = obj.get_json()
|
2016-05-05 15:19:51 -04:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
payload = obj.get_json()
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
payload = str(e)
|
|
|
|
status = 500
|
2016-03-16 23:25:41 -04:00
|
|
|
resp = Response(
|
2016-03-18 02:44:58 -04:00
|
|
|
payload,
|
|
|
|
status=status,
|
2016-03-16 23:25:41 -04:00
|
|
|
mimetype="application/json")
|
|
|
|
return resp
|
|
|
|
elif request.args.get("csv") == "true":
|
|
|
|
payload = obj.get_csv()
|
2016-03-18 02:44:58 -04:00
|
|
|
return Response(
|
|
|
|
payload,
|
2016-06-09 21:06:20 -04:00
|
|
|
status=200,
|
2016-04-06 11:22:49 -04:00
|
|
|
headers=generate_download_headers("csv"),
|
2016-03-16 23:25:41 -04:00
|
|
|
mimetype="application/csv")
|
2016-03-18 02:44:58 -04:00
|
|
|
else:
|
2016-03-16 23:25:41 -04:00
|
|
|
if request.args.get("standalone") == "true":
|
2016-03-29 00:55:58 -04:00
|
|
|
template = "caravel/standalone.html"
|
2016-03-16 23:25:41 -04:00
|
|
|
else:
|
2016-03-29 00:55:58 -04:00
|
|
|
template = "caravel/explore.html"
|
2016-04-06 11:23:27 -04:00
|
|
|
resp = self.render_template(
|
2016-04-18 16:56:00 -04:00
|
|
|
template, viz=obj, slice=slc, datasources=datasources,
|
|
|
|
can_add=slice_add_perm, can_edit=slice_edit_perm,
|
2016-06-28 13:31:36 -04:00
|
|
|
can_download=slice_download_perm,
|
|
|
|
userid=g.user.get_id() if g.user else '')
|
2016-03-18 02:44:58 -04:00
|
|
|
try:
|
2016-03-29 11:40:18 -04:00
|
|
|
pass
|
2016-03-18 02:44:58 -04:00
|
|
|
except Exception as e:
|
|
|
|
if config.get("DEBUG"):
|
|
|
|
raise(e)
|
|
|
|
return Response(
|
|
|
|
str(e),
|
|
|
|
status=500,
|
|
|
|
mimetype="application/json")
|
|
|
|
return resp
|
|
|
|
|
2016-06-28 13:31:36 -04:00
|
|
|
def save_or_overwrite_slice(
|
|
|
|
self, args, slc, slice_add_perm, slice_edit_perm):
|
|
|
|
"""Save or overwrite a slice"""
|
2016-03-16 23:25:41 -04:00
|
|
|
slice_name = args.get('slice_name')
|
|
|
|
action = args.get('action')
|
|
|
|
|
|
|
|
# TODO use form processing form wtforms
|
|
|
|
d = args.to_dict(flat=False)
|
|
|
|
del d['action']
|
|
|
|
del d['previous_viz_type']
|
2016-06-28 13:31:36 -04:00
|
|
|
|
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
|
|
|
as_list = ('metrics', 'groupby', 'columns', 'all_columns', 'mapbox_label')
|
2016-03-16 23:25:41 -04:00
|
|
|
for k in d:
|
|
|
|
v = d.get(k)
|
|
|
|
if k in as_list and not isinstance(v, list):
|
|
|
|
d[k] = [v] if v else []
|
|
|
|
if k not in as_list and isinstance(v, list):
|
|
|
|
d[k] = v[0]
|
|
|
|
|
|
|
|
table_id = druid_datasource_id = None
|
|
|
|
datasource_type = args.get('datasource_type')
|
|
|
|
if datasource_type in ('datasource', 'druid'):
|
|
|
|
druid_datasource_id = args.get('datasource_id')
|
|
|
|
elif datasource_type == 'table':
|
|
|
|
table_id = args.get('datasource_id')
|
|
|
|
|
2016-06-28 13:31:36 -04:00
|
|
|
if action in ('saveas'):
|
|
|
|
slc = models.Slice(owners=[g.user] if g.user else [])
|
2016-03-16 23:25:41 -04:00
|
|
|
|
|
|
|
slc.params = json.dumps(d, indent=4, sort_keys=True)
|
|
|
|
slc.datasource_name = args.get('datasource_name')
|
|
|
|
slc.viz_type = args.get('viz_type')
|
|
|
|
slc.druid_datasource_id = druid_datasource_id
|
|
|
|
slc.table_id = table_id
|
|
|
|
slc.datasource_type = datasource_type
|
|
|
|
slc.slice_name = slice_name
|
|
|
|
|
2016-06-28 13:31:36 -04:00
|
|
|
if action in ('saveas') and slice_add_perm:
|
2016-04-18 16:56:00 -04:00
|
|
|
self.save_slice(slc)
|
|
|
|
elif action == 'overwrite' and slice_edit_perm:
|
|
|
|
self.overwrite_slice(slc)
|
|
|
|
|
2016-06-28 13:31:36 -04:00
|
|
|
# Adding slice to a dashboard if requested
|
|
|
|
dash = None
|
|
|
|
if request.args.get('add_to_dash') == 'existing':
|
|
|
|
dash = (
|
2016-06-28 15:58:09 -04:00
|
|
|
db.session.query(models.Dashboard)
|
2016-06-28 13:31:36 -04:00
|
|
|
.filter_by(id=int(request.args.get('save_to_dashboard_id')))
|
|
|
|
.one()
|
|
|
|
)
|
|
|
|
flash(
|
|
|
|
"Slice [{}] was added to dashboard [{}]".format(
|
|
|
|
slc.slice_name,
|
|
|
|
dash.dashboard_title),
|
|
|
|
"info")
|
|
|
|
elif request.args.get('add_to_dash') == 'new':
|
2016-06-28 15:58:09 -04:00
|
|
|
dash = models.Dashboard(
|
2016-06-28 13:31:36 -04:00
|
|
|
dashboard_title=request.args.get('new_dashboard_name'),
|
|
|
|
owners=[g.user] if g.user else [])
|
|
|
|
flash(
|
|
|
|
"Dashboard [{}] just got created and slice [{}] was added "
|
|
|
|
"to it".format(
|
|
|
|
dash.dashboard_title,
|
|
|
|
slc.slice_name),
|
|
|
|
"info")
|
|
|
|
|
|
|
|
if dash and slc not in dash.slices:
|
|
|
|
dash.slices.append(slc)
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
if request.args.get('goto_dash') == 'true':
|
|
|
|
return redirect(dash.url)
|
|
|
|
else:
|
|
|
|
return redirect(slc.slice_url)
|
2016-04-18 16:56:00 -04:00
|
|
|
|
|
|
|
def save_slice(self, slc):
|
|
|
|
session = db.session()
|
|
|
|
msg = "Slice [{}] has been saved".format(slc.slice_name)
|
|
|
|
session.add(slc)
|
|
|
|
session.commit()
|
|
|
|
flash(msg, "info")
|
|
|
|
|
|
|
|
def overwrite_slice(self, slc):
|
2016-06-02 22:17:34 -04:00
|
|
|
can_update = check_ownership(slc, raise_if_false=False)
|
|
|
|
if not can_update:
|
2016-06-15 17:19:50 -04:00
|
|
|
flash("You cannot overwrite [{}]".format(slc), "danger")
|
2016-06-02 22:17:34 -04:00
|
|
|
else:
|
|
|
|
session = db.session()
|
|
|
|
session.merge(slc)
|
|
|
|
session.commit()
|
|
|
|
msg = "Slice [{}] has been overwritten".format(slc.slice_name)
|
|
|
|
flash(msg, "info")
|
2016-03-16 23:25:41 -04:00
|
|
|
|
2016-06-21 12:42:54 -04:00
|
|
|
@api
|
|
|
|
@has_access_api
|
2016-03-18 02:44:58 -04:00
|
|
|
@expose("/checkbox/<model_view>/<id_>/<attr>/<value>", methods=['GET'])
|
|
|
|
def checkbox(self, model_view, id_, attr, value):
|
|
|
|
"""endpoint for checking/unchecking any boolean in a sqla model"""
|
2016-06-10 18:49:33 -04:00
|
|
|
views = sys.modules[__name__]
|
|
|
|
model_view_cls = getattr(views, model_view)
|
|
|
|
model = model_view_cls.datamodel.obj
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
obj = db.session.query(model).filter_by(id=id_).first()
|
|
|
|
if obj:
|
2016-03-16 23:25:41 -04:00
|
|
|
setattr(obj, attr, value == 'true')
|
2016-03-18 02:44:58 -04:00
|
|
|
db.session.commit()
|
|
|
|
return Response("OK", mimetype="application/json")
|
|
|
|
|
2016-06-21 12:42:54 -04:00
|
|
|
@api
|
|
|
|
@has_access_api
|
2016-03-24 17:11:29 -04:00
|
|
|
@expose("/activity_per_day")
|
|
|
|
def activity_per_day(self):
|
|
|
|
"""endpoint to power the calendar heatmap on the welcome page"""
|
2016-03-26 00:55:28 -04:00
|
|
|
Log = models.Log # noqa
|
2016-03-24 17:11:29 -04:00
|
|
|
qry = (
|
|
|
|
db.session
|
2016-03-26 00:55:28 -04:00
|
|
|
.query(
|
|
|
|
Log.dt,
|
|
|
|
sqla.func.count())
|
|
|
|
.group_by(Log.dt)
|
|
|
|
.all()
|
2016-03-24 17:11:29 -04:00
|
|
|
)
|
|
|
|
payload = {str(time.mktime(dt.timetuple())): ccount for dt, ccount in qry if dt}
|
|
|
|
return Response(json.dumps(payload), mimetype="application/json")
|
|
|
|
|
2016-06-21 12:42:54 -04:00
|
|
|
@api
|
|
|
|
@has_access_api
|
2016-03-18 02:44:58 -04:00
|
|
|
@expose("/save_dash/<dashboard_id>/", methods=['GET', 'POST'])
|
|
|
|
def save_dash(self, dashboard_id):
|
|
|
|
"""Save a dashboard's metadata"""
|
|
|
|
data = json.loads(request.form.get('data'))
|
|
|
|
positions = data['positions']
|
|
|
|
slice_ids = [int(d['slice_id']) for d in positions]
|
|
|
|
session = db.session()
|
2016-03-16 23:25:41 -04:00
|
|
|
Dash = models.Dashboard # noqa
|
2016-03-18 02:44:58 -04:00
|
|
|
dash = session.query(Dash).filter_by(id=dashboard_id).first()
|
2016-06-02 22:17:34 -04:00
|
|
|
check_ownership(dash, raise_if_false=True)
|
2016-03-18 02:44:58 -04:00
|
|
|
dash.slices = [o for o in dash.slices if o.id in slice_ids]
|
2016-06-23 18:28:23 -04:00
|
|
|
positions = sorted(data['positions'], key=lambda x: int(x['slice_id']))
|
|
|
|
dash.position_json = json.dumps(positions, indent=4, sort_keys=True)
|
2016-03-18 02:44:58 -04:00
|
|
|
md = dash.metadata_dejson
|
|
|
|
if 'filter_immune_slices' not in md:
|
|
|
|
md['filter_immune_slices'] = []
|
|
|
|
md['expanded_slices'] = data['expanded_slices']
|
|
|
|
dash.json_metadata = json.dumps(md, indent=4)
|
|
|
|
dash.css = data['css']
|
|
|
|
session.merge(dash)
|
|
|
|
session.commit()
|
|
|
|
session.close()
|
|
|
|
return "SUCCESS"
|
|
|
|
|
2016-06-21 12:42:54 -04:00
|
|
|
@api
|
|
|
|
@has_access_api
|
2016-03-18 02:44:58 -04:00
|
|
|
@expose("/testconn", methods=["POST", "GET"])
|
|
|
|
def testconn(self):
|
|
|
|
"""Tests a sqla connection"""
|
|
|
|
try:
|
2016-04-11 18:39:50 -04:00
|
|
|
uri = request.json.get('uri')
|
2016-06-28 13:31:36 -04:00
|
|
|
connect_args = (
|
|
|
|
request.json
|
|
|
|
.get('extras', {})
|
|
|
|
.get('engine_params', {})
|
|
|
|
.get('connect_args', {}))
|
2016-04-11 18:39:50 -04:00
|
|
|
engine = create_engine(uri, connect_args=connect_args)
|
2016-03-18 02:44:58 -04:00
|
|
|
engine.connect()
|
|
|
|
return json.dumps(engine.table_names(), indent=4)
|
|
|
|
except Exception:
|
|
|
|
return Response(
|
|
|
|
traceback.format_exc(),
|
|
|
|
status=500,
|
|
|
|
mimetype="application/json")
|
|
|
|
|
2016-03-13 21:16:23 -04:00
|
|
|
@expose("/favstar/<class_name>/<obj_id>/<action>/")
|
|
|
|
def favstar(self, class_name, obj_id, action):
|
|
|
|
session = db.session()
|
2016-04-11 01:49:08 -04:00
|
|
|
FavStar = models.FavStar # noqa
|
2016-03-13 21:16:23 -04:00
|
|
|
count = 0
|
|
|
|
favs = session.query(FavStar).filter_by(
|
2016-05-04 01:31:37 -04:00
|
|
|
class_name=class_name, obj_id=obj_id, user_id=g.user.get_id()).all()
|
2016-03-13 21:16:23 -04:00
|
|
|
if action == 'select':
|
|
|
|
if not favs:
|
|
|
|
session.add(
|
|
|
|
FavStar(
|
2016-05-04 01:31:37 -04:00
|
|
|
class_name=class_name, obj_id=obj_id, user_id=g.user.get_id(),
|
2016-03-13 21:16:23 -04:00
|
|
|
dttm=datetime.now()))
|
|
|
|
count = 1
|
|
|
|
elif action == 'unselect':
|
|
|
|
for fav in favs:
|
|
|
|
session.delete(fav)
|
|
|
|
else:
|
|
|
|
count = len(favs)
|
|
|
|
session.commit()
|
|
|
|
return Response(
|
|
|
|
json.dumps({'count': count}),
|
|
|
|
mimetype="application/json")
|
|
|
|
|
2016-06-20 12:18:03 -04:00
|
|
|
@has_access
|
|
|
|
@expose("/slice/<slice_id>/")
|
|
|
|
def slice(self, slice_id):
|
|
|
|
"""Redirects a request for a slice id to its corresponding URL"""
|
|
|
|
session = db.session()
|
|
|
|
qry = session.query(models.Slice).filter_by(id=int(slice_id))
|
|
|
|
slc = qry.first()
|
|
|
|
if slc:
|
|
|
|
return redirect(slc.slice_url)
|
|
|
|
else:
|
|
|
|
flash("The specified slice could not be found", "danger")
|
|
|
|
return redirect('/slicemodelview/list/')
|
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
@has_access
|
|
|
|
@expose("/dashboard/<dashboard_id>/")
|
|
|
|
def dashboard(self, dashboard_id):
|
|
|
|
"""Server side rendering for a dashboard"""
|
|
|
|
session = db.session()
|
|
|
|
qry = session.query(models.Dashboard)
|
|
|
|
if dashboard_id.isdigit():
|
|
|
|
qry = qry.filter_by(id=int(dashboard_id))
|
|
|
|
else:
|
|
|
|
qry = qry.filter_by(slug=dashboard_id)
|
|
|
|
|
|
|
|
templates = session.query(models.CssTemplate).all()
|
|
|
|
dash = qry.first()
|
|
|
|
|
|
|
|
# Hack to log the dashboard_id properly, even when getting a slug
|
|
|
|
@log_this
|
|
|
|
def dashboard(**kwargs): # noqa
|
|
|
|
pass
|
|
|
|
dashboard(dashboard_id=dash.id)
|
|
|
|
|
|
|
|
return self.render_template(
|
2016-03-29 00:55:58 -04:00
|
|
|
"caravel/dashboard.html", dashboard=dash,
|
2016-03-18 02:44:58 -04:00
|
|
|
templates=templates,
|
2016-06-24 11:47:43 -04:00
|
|
|
dash_save_perm=self.can_access('can_save_dash', 'Caravel'),
|
2016-06-28 13:31:36 -04:00
|
|
|
dash_edit_perm=check_ownership(dash, raise_if_false=False))
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
@has_access
|
|
|
|
@expose("/sql/<database_id>/")
|
|
|
|
@log_this
|
|
|
|
def sql(self, database_id):
|
2016-04-17 11:17:08 -04:00
|
|
|
if (
|
2016-06-24 11:47:43 -04:00
|
|
|
not self.can_access(
|
2016-04-17 11:17:08 -04:00
|
|
|
'all_datasource_access', 'all_datasource_access')):
|
|
|
|
flash(
|
|
|
|
"This view requires the `all_datasource_access` "
|
|
|
|
"permission", "danger")
|
|
|
|
return redirect("/tablemodelview/list/")
|
2016-03-18 02:44:58 -04:00
|
|
|
mydb = db.session.query(
|
|
|
|
models.Database).filter_by(id=database_id).first()
|
|
|
|
engine = mydb.get_sqla_engine()
|
|
|
|
tables = engine.table_names()
|
|
|
|
|
2016-03-16 23:25:41 -04:00
|
|
|
table_name = request.args.get('table_name')
|
2016-03-18 02:44:58 -04:00
|
|
|
return self.render_template(
|
2016-03-29 00:55:58 -04:00
|
|
|
"caravel/sql.html",
|
2016-03-18 02:44:58 -04:00
|
|
|
tables=tables,
|
|
|
|
table_name=table_name,
|
|
|
|
db=mydb)
|
|
|
|
|
|
|
|
@has_access
|
|
|
|
@expose("/table/<database_id>/<table_name>/")
|
|
|
|
@log_this
|
|
|
|
def table(self, database_id, table_name):
|
|
|
|
mydb = db.session.query(
|
|
|
|
models.Database).filter_by(id=database_id).first()
|
|
|
|
cols = mydb.get_columns(table_name)
|
|
|
|
df = pd.DataFrame([(c['name'], c['type']) for c in cols])
|
|
|
|
df.columns = ['col', 'type']
|
2016-04-12 00:20:42 -04:00
|
|
|
tbl_cls = (
|
|
|
|
"dataframe table table-striped table-bordered "
|
|
|
|
"table-condensed sql_results").split(' ')
|
2016-03-18 02:44:58 -04:00
|
|
|
return self.render_template(
|
2016-03-29 00:55:58 -04:00
|
|
|
"caravel/ajah.html",
|
2016-03-18 02:44:58 -04:00
|
|
|
content=df.to_html(
|
2016-03-16 23:25:41 -04:00
|
|
|
index=False,
|
|
|
|
na_rep='',
|
2016-04-12 00:20:42 -04:00
|
|
|
classes=tbl_cls))
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
@has_access
|
|
|
|
@expose("/select_star/<database_id>/<table_name>/")
|
|
|
|
@log_this
|
|
|
|
def select_star(self, database_id, table_name):
|
|
|
|
mydb = db.session.query(
|
|
|
|
models.Database).filter_by(id=database_id).first()
|
|
|
|
t = mydb.get_table(table_name)
|
|
|
|
fields = ", ".join(
|
|
|
|
[c.name for c in t.columns] or "*")
|
|
|
|
s = "SELECT\n{}\nFROM {}".format(fields, table_name)
|
|
|
|
return self.render_template(
|
2016-03-29 00:55:58 -04:00
|
|
|
"caravel/ajah.html",
|
2016-03-18 02:44:58 -04:00
|
|
|
content=s
|
|
|
|
)
|
|
|
|
|
|
|
|
@has_access
|
|
|
|
@expose("/runsql/", methods=['POST', 'GET'])
|
|
|
|
@log_this
|
|
|
|
def runsql(self):
|
2016-03-16 23:25:41 -04:00
|
|
|
"""Runs arbitrary sql and returns and html table"""
|
2016-03-18 02:44:58 -04:00
|
|
|
session = db.session()
|
|
|
|
limit = 1000
|
|
|
|
data = json.loads(request.form.get('data'))
|
|
|
|
sql = data.get('sql')
|
|
|
|
database_id = data.get('database_id')
|
|
|
|
mydb = session.query(models.Database).filter_by(id=database_id).first()
|
2016-04-12 00:20:42 -04:00
|
|
|
|
|
|
|
if (
|
2016-06-24 11:47:43 -04:00
|
|
|
not self.can_access(
|
2016-04-12 00:20:42 -04:00
|
|
|
'all_datasource_access', 'all_datasource_access')):
|
2016-06-02 22:17:34 -04:00
|
|
|
raise utils.CaravelSecurityException(_(
|
2016-05-02 13:50:23 -04:00
|
|
|
"This view requires the `all_datasource_access` permission"))
|
2016-03-18 02:44:58 -04:00
|
|
|
content = ""
|
|
|
|
if mydb:
|
|
|
|
eng = mydb.get_sqla_engine()
|
|
|
|
if limit:
|
|
|
|
sql = sql.strip().strip(';')
|
|
|
|
qry = (
|
|
|
|
select('*')
|
|
|
|
.select_from(TextAsFrom(text(sql), ['*']).alias('inner_qry'))
|
|
|
|
.limit(limit)
|
|
|
|
)
|
2016-03-16 23:25:41 -04:00
|
|
|
sql = str(qry.compile(eng, compile_kwargs={"literal_binds": True}))
|
2016-03-18 02:44:58 -04:00
|
|
|
try:
|
|
|
|
df = pd.read_sql_query(sql=sql, con=eng)
|
|
|
|
content = df.to_html(
|
|
|
|
index=False,
|
|
|
|
na_rep='',
|
|
|
|
classes=(
|
|
|
|
"dataframe table table-striped table-bordered "
|
2016-04-12 00:20:42 -04:00
|
|
|
"table-condensed sql_results").split(' '))
|
2016-03-18 02:44:58 -04:00
|
|
|
except Exception as e:
|
|
|
|
content = (
|
|
|
|
'<div class="alert alert-danger">'
|
|
|
|
"{}</div>"
|
|
|
|
).format(e.message)
|
|
|
|
session.commit()
|
|
|
|
return content
|
|
|
|
|
|
|
|
@has_access
|
|
|
|
@expose("/refresh_datasources/")
|
|
|
|
def refresh_datasources(self):
|
2016-03-16 23:25:41 -04:00
|
|
|
"""endpoint that refreshes druid datasources metadata"""
|
2016-03-18 02:44:58 -04:00
|
|
|
session = db.session()
|
|
|
|
for cluster in session.query(models.DruidCluster).all():
|
2016-06-01 00:16:32 -04:00
|
|
|
cluster_name = cluster.cluster_name
|
2016-03-18 02:44:58 -04:00
|
|
|
try:
|
|
|
|
cluster.refresh_datasources()
|
|
|
|
except Exception as e:
|
|
|
|
flash(
|
|
|
|
"Error while processing cluster '{}'\n{}".format(
|
2016-06-01 00:16:32 -04:00
|
|
|
cluster_name, str(e)),
|
2016-03-18 02:44:58 -04:00
|
|
|
"danger")
|
|
|
|
logging.exception(e)
|
|
|
|
return redirect('/druidclustermodelview/list/')
|
|
|
|
cluster.metadata_last_refreshed = datetime.now()
|
|
|
|
flash(
|
|
|
|
"Refreshed metadata from cluster "
|
|
|
|
"[" + cluster.cluster_name + "]",
|
|
|
|
'info')
|
|
|
|
session.commit()
|
2016-04-04 23:43:06 -04:00
|
|
|
return redirect("/druiddatasourcemodelview/list/")
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
@app.errorhandler(500)
|
|
|
|
def show_traceback(self):
|
2016-06-21 12:42:54 -04:00
|
|
|
error_msg = get_error_msg()
|
2016-03-18 02:44:58 -04:00
|
|
|
return render_template(
|
2016-03-29 00:55:58 -04:00
|
|
|
'caravel/traceback.html',
|
2016-03-18 02:44:58 -04:00
|
|
|
error_msg=error_msg,
|
|
|
|
title=ascii_art.stacktrace,
|
|
|
|
art=ascii_art.error), 500
|
|
|
|
|
|
|
|
@has_access
|
2016-03-24 17:11:29 -04:00
|
|
|
@expose("/welcome")
|
|
|
|
def welcome(self):
|
|
|
|
"""Personalized welcome page"""
|
2016-03-29 00:55:58 -04:00
|
|
|
return self.render_template('caravel/welcome.html', utils=utils)
|
2016-03-24 17:11:29 -04:00
|
|
|
|
2016-03-18 02:44:58 -04:00
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
appbuilder.add_view_no_menu(Caravel)
|
2016-03-25 14:26:59 -04:00
|
|
|
|
|
|
|
if config['DRUID_IS_ACTIVE']:
|
|
|
|
appbuilder.add_link(
|
2016-07-01 17:31:22 -04:00
|
|
|
_("Refresh Druid Metadata"),
|
2016-03-29 00:55:58 -04:00
|
|
|
href='/caravel/refresh_datasources/',
|
2016-03-25 14:26:59 -04:00
|
|
|
category='Sources',
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Sources"),
|
2016-03-25 14:26:59 -04:00
|
|
|
category_icon='fa-database',
|
|
|
|
icon="fa-cog")
|
2016-03-18 02:44:58 -04:00
|
|
|
|
|
|
|
|
2016-03-29 00:55:58 -04:00
|
|
|
class CssTemplateModelView(CaravelModelView, DeleteMixin):
|
2016-03-18 02:44:58 -04:00
|
|
|
datamodel = SQLAInterface(models.CssTemplate)
|
|
|
|
list_columns = ['template_name']
|
|
|
|
edit_columns = ['template_name', 'css']
|
|
|
|
add_columns = edit_columns
|
|
|
|
|
|
|
|
appbuilder.add_separator("Sources")
|
|
|
|
appbuilder.add_view(
|
|
|
|
CssTemplateModelView,
|
|
|
|
"CSS Templates",
|
2016-06-20 18:31:15 -04:00
|
|
|
label=__("CSS Templates"),
|
2016-03-18 02:44:58 -04:00
|
|
|
icon="fa-css3",
|
|
|
|
category="Sources",
|
2016-06-20 18:31:15 -04:00
|
|
|
category_label=__("Sources"),
|
2016-03-16 23:25:41 -04:00
|
|
|
category_icon='')
|
2016-03-31 12:27:21 -04:00
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
# Redirecting URL from previous names
|
|
|
|
class RegexConverter(BaseConverter):
|
|
|
|
def __init__(self, url_map, *items):
|
|
|
|
super(RegexConverter, self).__init__(url_map)
|
|
|
|
self.regex = items[0]
|
|
|
|
app.url_map.converters['regex'] = RegexConverter
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/<regex("panoramix\/.*"):url>')
|
|
|
|
def panoramix(url): # noqa
|
|
|
|
return redirect(request.full_path.replace('panoramix', 'caravel'))
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/<regex("dashed\/.*"):url>')
|
|
|
|
def dashed(url): # noqa
|
|
|
|
return redirect(request.full_path.replace('dashed', 'caravel'))
|
|
|
|
# ---------------------------------------------------------------------
|