diff --git a/.landscape.yml b/.landscape.yml
index 7a9ab62102..93e2e8d444 100644
--- a/.landscape.yml
+++ b/.landscape.yml
@@ -9,6 +9,7 @@ pylint:
disable:
- cyclic-import
- invalid-name
+ - logging-format-interpolation
options:
docstring-min-length: 10
pep8:
@@ -20,6 +21,3 @@ ignore-paths:
ignore-patterns:
- ^example/doc_.*\.py$
- (^|/)docs(/|$)
-python-targets:
- - 2
- - 3
diff --git a/README.md b/README.md
index d703f25e4f..b51403197c 100644
--- a/README.md
+++ b/README.md
@@ -44,32 +44,24 @@ Dashed provides:
slicing and dicing large, realtime datasets
-Buzz Phrases
-------------
-
-* Analytics at the speed of thought!
-* Instantaneous learning curve
-* Realtime analytics when querying [Druid.io](http://druid.io)
-* Extentsible to infinity
-
Database Support
----------------
Dashed was originally designed on to of Druid.io, but quickly broadened
its scope to support other databases through the use of SqlAlchemy, a Python
ORM that is compatible with
-[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
+[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
What is Druid?
-------------
From their website at http://druid.io
-*Druid is an open-source analytics data store designed for
-business intelligence (OLAP) queries on event data. Druid provides low
-latency (real-time) data ingestion, flexible data exploration,
-and fast data aggregation. Existing Druid deployments have scaled to
-trillions of events and petabytes of data. Druid is best used to
+*Druid is an open-source analytics data store designed for
+business intelligence (OLAP) queries on event data. Druid provides low
+latency (real-time) data ingestion, flexible data exploration,
+and fast data aggregation. Existing Druid deployments have scaled to
+trillions of events and petabytes of data. Druid is best used to
power analytic dashboards and applications.*
@@ -109,50 +101,28 @@ your datasources for Dashed to be aware of, and they should show up in
`Menu -> Datasources`, from where you can start playing with your data!
Configuration
+=======
+[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
+
+
+Installation & Configuration
+----------------------------
+
+(See in the documentation)
+[http://mistercrunch.github.io/panoramix-docs/installation.html]
+
+
+What is Druid?
-------------
+From their website at http://druid.io
-To configure your application, you need to create a file (module)
-`dashed_config.py` and make sure it is in your PYTHONPATH. Here are some
-of the parameters you can copy / paste in that configuration module:
+*Druid is an open-source analytics data store designed for
+business intelligence (OLAP) queries on event data. Druid provides low
+latency (real-time) data ingestion, flexible data exploration,
+and fast data aggregation. Existing Druid deployments have scaled to
+trillions of events and petabytes of data. Druid is best used to
+power analytic dashboards and applications.*
-```
-#---------------------------------------------------------
-# Dashed specifix config
-#---------------------------------------------------------
-ROW_LIMIT = 5000
-WEBSERVER_THREADS = 8
-
-DASHED_WEBSERVER_PORT = 8088
-#---------------------------------------------------------
-
-#---------------------------------------------------------
-# Flask App Builder configuration
-#---------------------------------------------------------
-# Your App secret key
-SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
-
-# The SQLAlchemy connection string.
-SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dashed.db'
-
-# Flask-WTF flag for CSRF
-CSRF_ENABLED = True
-
-# Whether to run the web server in debug mode or not
-DEBUG = True
-```
-
-This file also allows you to define configuration parameters used by
-Flask App Builder, the web framework used by Dashed. Please consult
-the [Flask App Builder Documentation](http://flask-appbuilder.readthedocs.org/en/latest/config.html) for more information on how to configure Dashed.
-
-
-* From the UI, enter the information about your clusters in the
-``Admin->Clusters`` menu by hitting the + sign.
-
-* Once the Druid cluster connection information is entered, hit the
-``Admin->Refresh Metadata`` menu item to populate
-
-* Navigate to your datasources
More screenshots
----------------
diff --git a/TODO.md b/TODO.md
index 089db8488e..bf783b1aba 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,7 +2,6 @@
List of TODO items for Dashed
## Important
-* **Caching:** integrate with flask-cache
* **Getting proper JS testing:** unit tests on the Python side are pretty
solid, but now we need a test suite for the JS part of the site,
testing all the ajax-type calls
@@ -14,7 +13,6 @@ List of TODO items for Dashed
* **Stars:** set dashboards, slices and datasets as favorites
* **Homepage:** a page that has links to your Slices and Dashes, favorited
content, feed of recent actions (people viewing your objects)
-* **Comments:** allow for people to comment on slices and dashes
* **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2`
* **Default slice:** choose a default slice for the dataset instead of
default endpoint
@@ -34,9 +32,11 @@ List of TODO items for Dashed
* **Slack integration** - TBD
* **Sexy Viz Selector:** the visualization selector should be a nice large
modal with nice thumbnails for each one of the viz
+* **Comments:** allow for people to comment on slices and dashes
## Easy-ish fix
+* Kill switch for Druid in docs
* CREATE VIEW button from SQL editor
* Test button for when editing SQL expression
* Slider form element
diff --git a/dashed/__init__.py b/dashed/__init__.py
index 808ed9ce63..83e6cce5b4 100644
--- a/dashed/__init__.py
+++ b/dashed/__init__.py
@@ -6,6 +6,7 @@ from flask import Flask, redirect
from flask.ext.appbuilder import SQLA, AppBuilder, IndexView
from flask.ext.appbuilder.baseviews import expose
from flask.ext.migrate import Migrate
+from flask.ext.cache import Cache
APP_DIR = os.path.dirname(__file__)
@@ -18,6 +19,9 @@ logging.getLogger().setLevel(logging.DEBUG)
app = Flask(__name__)
app.config.from_object(CONFIG_MODULE)
db = SQLA(app)
+
+cache = Cache(app, config=app.config.get('CACHE_CONFIG'))
+
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")
diff --git a/dashed/assets/javascripts/dashboard.js b/dashed/assets/javascripts/dashboard.js
index 1ea68ee9c1..d5768df056 100644
--- a/dashed/assets/javascripts/dashboard.js
+++ b/dashed/assets/javascripts/dashboard.js
@@ -22,7 +22,7 @@ var Dashboard = function (dashboardData) {
dashboard.slices.forEach(function (data) {
var slice = px.Slice(data, dash);
$("#slice_" + data.slice_id).find('a.refresh').click(function () {
- slice.render();
+ slice.render(true);
});
sliceObjects.push(slice);
slice.render();
@@ -90,7 +90,7 @@ var Dashboard = function (dashboardData) {
var gridster = $(".gridster ul").gridster({
autogrow_cols: true,
widget_margins: [10, 10],
- widget_base_dimensions: [100, 100],
+ widget_base_dimensions: [95, 95],
draggable: {
handle: '.drag'
},
@@ -113,6 +113,16 @@ var Dashboard = function (dashboardData) {
};
}
}).data('gridster');
+
+ // Displaying widget controls on hover
+ $('.chart-header').hover(
+ function () {
+ $(this).find('.chart-controls').fadeIn(300);
+ },
+ function () {
+ $(this).find('.chart-controls').fadeOut(300);
+ }
+ );
$("div.gridster").css('visibility', 'visible');
$("#savedash").click(function () {
var expanded_slices = {};
@@ -168,6 +178,11 @@ var Dashboard = function (dashboardData) {
$('#filters').click(function () {
alert(dashboard.readFilters());
});
+ $('#refresh_dash').click(function () {
+ dashboard.slices.forEach(function (slice) {
+ slice.render(true);
+ });
+ });
$("a.remove-chart").click(function () {
var li = $(this).parents("li");
gridster.remove_widget(li);
@@ -226,4 +241,5 @@ var Dashboard = function (dashboardData) {
$(document).ready(function () {
Dashboard($('.dashboard').data('dashboard'));
+ $('[data-toggle="tooltip"]').tooltip({ container: 'body' });
});
diff --git a/dashed/assets/javascripts/explore.js b/dashed/assets/javascripts/explore.js
index 8abe4dc901..7ea48bcf8e 100644
--- a/dashed/assets/javascripts/explore.js
+++ b/dashed/assets/javascripts/explore.js
@@ -53,19 +53,21 @@ function prepForm() {
});
}
-function renderSlice() {
+function druidify(force) {
+ if (force === undefined) {
+ force = false;
+ }
+ $('.query-and-save button').attr('disabled', 'disabled');
+ $('.btn-group.results span,a').attr('disabled', 'disabled');
+ $('div.alert').remove();
+ $('#is_cached').hide();
+ history.pushState({}, document.title, slice.querystring());
prepForm();
- slice.render();
+ slice.render(force);
}
function initExploreView() {
- function druidify() {
- $('div.alert').remove();
- history.pushState({}, document.title, slice.querystring());
- renderSlice();
- }
-
function get_collapsed_fieldsets() {
var collapsed_fieldsets = $("#collapsed_fieldsets").val();
@@ -199,9 +201,7 @@ function initExploreView() {
bindOrder: 'sortableStop'
});
$("form").show();
- $('[data-toggle="tooltip"]').tooltip({
- container: 'body'
- });
+ $('[data-toggle="tooltip"]').tooltip({ container: 'body' });
$(".ui-helper-hidden-accessible").remove(); // jQuery-ui 1.11+ creates a div for every tooltip
function set_filters() {
@@ -256,7 +256,9 @@ function initExploreView() {
}
});
- $(".druidify").click(druidify);
+ $(".druidify").click(function () {
+ druidify(true);
+ });
function create_choices(term, data) {
var filtered = $(data).filter(function () {
@@ -319,7 +321,7 @@ $(document).ready(function () {
$('.slice').data('slice', slice);
// call vis render method, which issues ajax
- renderSlice();
+ druidify(false);
// make checkbox inputs display as toggles
$(':checkbox')
diff --git a/dashed/assets/javascripts/modules/dashed.js b/dashed/assets/javascripts/modules/dashed.js
index d0389362d0..dd0e4af65a 100644
--- a/dashed/assets/javascripts/modules/dashed.js
+++ b/dashed/assets/javascripts/modules/dashed.js
@@ -169,16 +169,51 @@ var px = (function () {
}
return qrystr;
},
+ getWidgetHeader: function () {
+ return this.container.parents("li.widget").find(".chart-header");
+ },
jsonEndpoint: function () {
var parser = document.createElement('a');
parser.href = data.json_endpoint;
- var endpoint = parser.pathname + this.querystring() + "&json=true";
+ var endpoint = parser.pathname + this.querystring();
+ endpoint += "&json=true";
+ endpoint += "&force=" + this.force;
return endpoint;
},
done: function (data) {
clearInterval(timer);
token.find("img.loading").hide();
container.show();
+
+ var cachedSelector = null;
+ if (dashboard === undefined) {
+ cachedSelector = $('#is_cached');
+ if (data !== undefined && data.is_cached) {
+ cachedSelector
+ .attr('title', 'Served from data cached at ' + data.cached_dttm + '. Click to force-refresh')
+ .show()
+ .tooltip('fixTitle');
+ } else {
+ cachedSelector.hide();
+ }
+ } else {
+ var refresh = this.getWidgetHeader().find('.refresh');
+ if (data !== undefined && data.is_cached) {
+ refresh
+ .addClass('danger')
+ .attr(
+ 'title',
+ 'Served from data cached at ' + data.cached_dttm + '. Click to force-refresh')
+ .tooltip('fixTitle');
+ } else {
+ refresh
+ .removeClass('danger')
+ .attr(
+ 'title',
+ 'Click to force-refresh')
+ .tooltip('fixTitle');
+ }
+ }
if (data !== undefined) {
$("#query_container").html(data.query);
}
@@ -194,7 +229,8 @@ var px = (function () {
$('#csv').click(function () {
window.location = data.csv_endpoint;
});
- $('.btn-group.results span').removeAttr('disabled');
+ $('.btn-group.results span,a').removeAttr('disabled');
+ $('.query-and-save button').removeAttr('disabled');
always(data);
},
error: function (msg) {
@@ -204,6 +240,8 @@ var px = (function () {
container.show();
$('span.query').removeClass('disabled');
$('#timer').addClass('btn-danger');
+ $('.btn-group.results span,a').removeAttr('disabled');
+ $('.query-and-save button').removeAttr('disabled');
always(data);
},
width: function () {
@@ -228,8 +266,11 @@ var px = (function () {
}, 500);
});
},
- render: function () {
- $('.btn-group.results span').attr('disabled', 'disabled');
+ render: function (force) {
+ if (force === undefined) {
+ force = false;
+ }
+ this.force = force;
token.find("img.loading").show();
container.hide();
container.html('');
diff --git a/dashed/assets/stylesheets/dashed.css b/dashed/assets/stylesheets/dashed.css
index 6e5a822561..8f67c8bcc1 100644
--- a/dashed/assets/stylesheets/dashed.css
+++ b/dashed/assets/stylesheets/dashed.css
@@ -5,11 +5,18 @@ body {
.modal-dialog {
z-index: 1100;
}
+.label {
+ font-size: 100%;
+}
input.form-control {
background-color: white;
}
+.chart-header a.danger {
+ color: red;
+}
+
.col-left-fixed {
width:350px;
position: absolute;
@@ -57,8 +64,17 @@ form div {
color: white;
}
-.header span{
- margin-left: 3px;
+.header span {
+ margin-left: 5px;
+}
+
+.widget-is-cached {
+ display: none;
+}
+
+.header span.label {
+ margin-left: 5px;
+ margin-right: 5px;
}
#timer {
@@ -234,9 +250,18 @@ li.widget .chart-header a {
margin-left: 5px;
}
-li.widget .chart-controls {
+#is_cached {
display: none;
+}
+
+li.widget .chart-controls {
background-color: #f1f1f1;
+ position: absolute;
+ right: 0;
+ left: 0;
+ padding: 0px 5px;
+ opacity: 0.75;
+ display: none;
}
li.widget .slice_container {
diff --git a/dashed/assets/visualizations/filter_box.js b/dashed/assets/visualizations/filter_box.js
index 64959a4b2e..f820549610 100644
--- a/dashed/assets/visualizations/filter_box.js
+++ b/dashed/assets/visualizations/filter_box.js
@@ -56,7 +56,7 @@ function filterBox(slice) {
})
.on('change', fltChanged);
}
- slice.done();
+ slice.done(payload);
function select2Formatter(result, container /*, query, escapeMarkup*/) {
var perc = Math.round((result.metric / maxes[result.filter]) * 100);
diff --git a/dashed/assets/visualizations/word_cloud.js b/dashed/assets/visualizations/word_cloud.js
index 503807a7e0..a83442ba52 100644
--- a/dashed/assets/visualizations/word_cloud.js
+++ b/dashed/assets/visualizations/word_cloud.js
@@ -78,7 +78,7 @@ function wordCloudChart(slice) {
return d.text;
});
}
- slice.done(data);
+ slice.done(json);
});
}
diff --git a/dashed/config.py b/dashed/config.py
index a6561114f1..915b9a9e5a 100644
--- a/dashed/config.py
+++ b/dashed/config.py
@@ -1,16 +1,15 @@
-"""
-All configuration in this file can be overridden by providing a local_config
-in your PYTHONPATH.
+"""The main config file for Dashed
-There' a ``from local_config import *`` at the end of this file.
+All configuration in this file can be overridden by providing a local_config
+in your PYTHONPATH as there is a ``from local_config import *``
+at the end of this file.
"""
import os
from flask_appbuilder.security.manager import AUTH_DB
-# from flask_appbuilder.security.manager import (
-# AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH)
-BASE_DIR = os.path.abspath(os.path.dirname(__file__))
from dateutil import tz
+BASE_DIR = os.path.abspath(os.path.dirname(__file__))
+
# ---------------------------------------------------------
# Dashed specifix config
@@ -112,6 +111,9 @@ IMG_UPLOAD_URL = '/static/uploads/'
# Setup image size default is (300, 200, True)
# IMG_SIZE = (300, 200, True)
+CACHE_DEFAULT_TIMEOUT = None
+CACHE_CONFIG = {'CACHE_TYPE': 'null'}
+
try:
from dashed_config import * # noqa
except Exception:
diff --git a/dashed/data/__init__.py b/dashed/data/__init__.py
index b02339ad1e..348a14ebb6 100644
--- a/dashed/data/__init__.py
+++ b/dashed/data/__init__.py
@@ -111,8 +111,7 @@ def load_world_bank_health_n_pop():
params=get_slice_json(
defaults,
viz_type='filter_box',
- groupby=['region'],
- )),
+ groupby=['region', 'country_name'])),
Slice(
slice_name="World's Population",
viz_type='big_number',
@@ -155,8 +154,8 @@ def load_world_bank_health_n_pop():
params=get_slice_json(
defaults,
viz_type='world_map',
- metric= "sum__SP_RUR_TOTL_ZS",
- num_period_compare="10",)),
+ metric="sum__SP_RUR_TOTL_ZS",
+ num_period_compare="10")),
Slice(
slice_name="Life Expexctancy VS Rural %",
viz_type='bubble',
@@ -165,8 +164,8 @@ def load_world_bank_health_n_pop():
params=get_slice_json(
defaults,
viz_type='bubble',
- since= "2011-01-01",
- until= "2011-01-01",
+ since="2011-01-01",
+ until="2011-01-01",
series="region",
limit="0",
entity="country_name",
@@ -175,7 +174,7 @@ def load_world_bank_health_n_pop():
size="sum__SP_POP_TOTL",
max_bubble_size="50",
flt_col_1="country_code",
- flt_op_1= "not in",
+ flt_op_1="not in",
flt_eq_1="TCA,MNP,DMA,MHL,MCO,SXM,CYM,TUV,IMY,KNA,ASM,ADO,AMA,PLW",
num_period_compare="10",)),
Slice(
@@ -188,8 +187,8 @@ def load_world_bank_health_n_pop():
viz_type='sunburst',
groupby=["region", "country_name"],
secondary_metric="sum__SP_RUR_TOTL",
- since= "2011-01-01",
- until= "2011-01-01",)),
+ since="2011-01-01",
+ until="2011-01-01",)),
Slice(
slice_name="World's Pop Growth",
viz_type='area',
@@ -214,60 +213,60 @@ def load_world_bank_health_n_pop():
js = """\
[
{
- "size_y": 1,
+ "size_y": 2,
"size_x": 3,
"col": 1,
- "slice_id": "269",
+ "slice_id": "1",
"row": 1
},
{
"size_y": 3,
"size_x": 3,
"col": 1,
- "slice_id": "270",
- "row": 2
+ "slice_id": "2",
+ "row": 3
},
{
- "size_y": 7,
+ "size_y": 8,
"size_x": 3,
"col": 10,
- "slice_id": "271",
+ "slice_id": "3",
"row": 1
},
{
"size_y": 3,
"size_x": 6,
"col": 1,
- "slice_id": "272",
- "row": 5
+ "slice_id": "4",
+ "row": 6
},
{
- "size_y": 4,
+ "size_y": 5,
"size_x": 6,
"col": 4,
- "slice_id": "273",
+ "slice_id": "5",
"row": 1
},
{
"size_y": 4,
"size_x": 6,
"col": 7,
- "slice_id": "274",
- "row": 8
+ "slice_id": "6",
+ "row": 9
},
{
"size_y": 3,
"size_x": 3,
"col": 7,
- "slice_id": "275",
- "row": 5
+ "slice_id": "7",
+ "row": 6
},
{
"size_y": 4,
"size_x": 6,
"col": 1,
- "slice_id": "276",
- "row": 8
+ "slice_id": "8",
+ "row": 9
}
]
"""
@@ -287,7 +286,7 @@ def load_world_bank_health_n_pop():
def load_css_templates():
"""Loads 2 css templates to demonstrate the feature"""
print('Creating default CSS templates')
- CSS = models.CssTemplate
+ CSS = models.CssTemplate # noqa
obj = db.session.query(CSS).filter_by(template_name='Flat').first()
if not obj:
@@ -387,6 +386,7 @@ def load_css_templates():
def load_birth_names():
+ """Loading birth name dataset from a zip file in the repo"""
with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f:
pdf = pd.read_json(f)
pdf.ds = pd.to_datetime(pdf.ds, unit='ms')
@@ -409,7 +409,7 @@ def load_birth_names():
print("Creating table reference")
obj = db.session.query(TBL).filter_by(table_name='birth_names').first()
if not obj:
- obj = TBL(table_name = 'birth_names')
+ obj = TBL(table_name='birth_names')
obj.main_dttm_col = 'ds'
obj.database = get_or_create_db(db.session)
obj.is_featured = True
@@ -514,8 +514,7 @@ def load_birth_names():
-"""
- )),
+""")),
Slice(
slice_name="Name Cloud",
viz_type='word_cloud',
diff --git a/dashed/data/countries.py b/dashed/data/countries.py
index f81ef32df2..663c7d5759 100644
--- a/dashed/data/countries.py
+++ b/dashed/data/countries.py
@@ -1,6 +1,4 @@
-"""
-This module contains data related to countries and is used for geo mapping
-"""
+"""This module contains data related to countries and is used for geo mapping"""
countries = [
{
@@ -2482,6 +2480,7 @@ for lookup in lookups:
for country in countries:
all_lookups[lookup][country[lookup].lower()] = country
+
def get(field, symbol):
"""
Get country data based on a standard code and a symbol
diff --git a/dashed/forms.py b/dashed/forms.py
index e5332ce5eb..c4df90b405 100644
--- a/dashed/forms.py
+++ b/dashed/forms.py
@@ -1,3 +1,5 @@
+"""Contains the logic to create cohesive forms on the explore view"""
+
from wtforms import (
Form, SelectMultipleField, SelectField, TextField, TextAreaField,
BooleanField, IntegerField, HiddenField)
@@ -10,8 +12,8 @@ config = app.config
class BetterBooleanField(BooleanField):
- """
- Fixes behavior of html forms omitting non checked
+ """Fixes the html checkbox to distinguish absent from unchecked
+
(which doesn't distinguish False from NULL/missing )
If value is unchecked, this hidden fills in False value
"""
@@ -61,9 +63,10 @@ class FreeFormSelect(widgets.Select):
class FreeFormSelectField(SelectField):
- """ A WTF SelectField that allows for free form input """
+ """A WTF SelectField that allows for free form input"""
widget = FreeFormSelect()
+
def pre_validate(self, form):
return
@@ -85,7 +88,9 @@ class OmgWtForm(Form):
class FormFactory(object):
+
"""Used to create the forms in the explore view dynamically"""
+
series_limits = [0, 5, 10, 25, 50, 100, 500]
fieltype_class = {
SelectField: 'select2',
@@ -278,7 +283,8 @@ class FormFactory(object):
description=(
"Timestamp from filter. This supports free form typing and "
"natural language as in '1 day ago', '28 days' or '3 years'")),
- 'until': FreeFormSelectField('Until', default="now",
+ 'until': FreeFormSelectField(
+ 'Until', default="now",
choices=self.choicify([
'now',
'1 day ago',
@@ -286,7 +292,7 @@ class FormFactory(object):
'28 days ago',
'90 days ago',
'1 year ago'])
- ),
+ ),
'max_bubble_size': FreeFormSelectField(
'Max Bubble Size', default="25",
choices=self.choicify([
@@ -297,8 +303,8 @@ class FormFactory(object):
'50',
'75',
'100',
- ])
- ),
+ ])
+ ),
'row_limit':
FreeFormSelectField(
'Row limit',
@@ -323,8 +329,8 @@ class FormFactory(object):
'Periods',
validators=[validators.optional()],
description=(
- "Defines the size of the rolling window function, "
- "relative to the time granularity selected")),
+ "Defines the size of the rolling window function, "
+ "relative to the time granularity selected")),
'series': SelectField(
'Series', choices=group_by_choices,
default=default_groupby,
@@ -332,14 +338,16 @@ class FormFactory(object):
"Defines the grouping of entities. "
"Each serie is shown as a specific color on the chart and "
"has a legend toggle")),
- 'entity': SelectField('Entity', choices=group_by_choices,
+ 'entity': SelectField(
+ 'Entity', choices=group_by_choices,
default=default_groupby,
description="This define the element to be plotted on the chart"),
'x': SelectField(
'X Axis', choices=datasource.metrics_combo,
default=default_metric,
description="Metric assigned to the [X] axis"),
- 'y': SelectField('Y Axis', choices=datasource.metrics_combo,
+ 'y': SelectField(
+ 'Y Axis', choices=datasource.metrics_combo,
default=default_metric,
description="Metric assigned to the [Y] axis"),
'size': SelectField(
@@ -355,19 +363,23 @@ class FormFactory(object):
"clause, as an AND to other criteria. You can include "
"complex expression, parenthesis and anything else "
"supported by the backend it is directed towards.")),
- 'having': TextField('Custom HAVING clause', default='',
+ 'having': TextField(
+ 'Custom HAVING clause', default='',
description=(
"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 "
"supported by the backend it is directed towards.")),
- 'compare_lag': TextField('Comparison Period Lag',
+ 'compare_lag': TextField(
+ 'Comparison Period Lag',
description=(
"Based on granularity, number of time periods to "
"compare against")),
- 'compare_suffix': TextField('Comparison suffix',
+ 'compare_suffix': TextField(
+ 'Comparison suffix',
description="Suffix to apply after the percentage display"),
- 'x_axis_format': FreeFormSelectField('X axis format',
+ 'x_axis_format': FreeFormSelectField(
+ 'X axis format',
default='smart_date',
choices=[
('smart_date', 'Adaptative formating'),
@@ -380,7 +392,8 @@ class FormFactory(object):
description="D3 format syntax for y axis "
"https://github.com/mbostock/\n"
"d3/wiki/Formatting"),
- 'y_axis_format': FreeFormSelectField('Y axis format',
+ 'y_axis_format': FreeFormSelectField(
+ 'Y axis format',
default='.3s',
choices=[
('.3s', '".3s" | 12.3k'),
@@ -506,10 +519,14 @@ class FormFactory(object):
field_css_classes[field] += ['input-sm']
class QueryForm(OmgWtForm):
+
+ """The dynamic form object used for the explore view"""
+
fieldsets = copy(viz.fieldsets)
css_classes = field_css_classes
standalone = HiddenField()
async = HiddenField()
+ force = HiddenField()
extra_filters = HiddenField()
json = HiddenField()
slice_id = HiddenField()
diff --git a/dashed/migrations/versions/836c0bf75904_cache_timeouts.py b/dashed/migrations/versions/836c0bf75904_cache_timeouts.py
new file mode 100644
index 0000000000..d050c49c0b
--- /dev/null
+++ b/dashed/migrations/versions/836c0bf75904_cache_timeouts.py
@@ -0,0 +1,28 @@
+"""cache_timeouts
+
+Revision ID: 836c0bf75904
+Revises: 18e88e1cc004
+Create Date: 2016-03-17 08:40:03.186534
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '836c0bf75904'
+down_revision = '18e88e1cc004'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('datasources', sa.Column('cache_timeout', sa.Integer(), nullable=True))
+ op.add_column('dbs', sa.Column('cache_timeout', sa.Integer(), nullable=True))
+ op.add_column('slices', sa.Column('cache_timeout', sa.Integer(), nullable=True))
+ op.add_column('tables', sa.Column('cache_timeout', sa.Integer(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('tables', 'cache_timeout')
+ op.drop_column('slices', 'cache_timeout')
+ op.drop_column('dbs', 'cache_timeout')
+ op.drop_column('datasources', 'cache_timeout')
diff --git a/dashed/models.py b/dashed/models.py
index 025806ad04..358c167851 100644
--- a/dashed/models.py
+++ b/dashed/models.py
@@ -1,6 +1,4 @@
-"""
-A collection of ORM sqlalchemy models for Dashed
-"""
+"""A collection of ORM sqlalchemy models for Dashed"""
from copy import deepcopy, copy
from collections import namedtuple
@@ -58,7 +56,8 @@ class AuditMixinNullable(AuditMixin):
@declared_attr
def changed_by_fk(cls):
- return Column(Integer, ForeignKey('ab_user.id'),
+ return Column(
+ Integer, ForeignKey('ab_user.id'),
default=cls.get_user_id, onupdate=cls.get_user_id, nullable=True)
@property
@@ -103,6 +102,7 @@ class Slice(Model, AuditMixinNullable):
viz_type = Column(String(250))
params = Column(Text)
description = Column(Text)
+ cache_timeout = Column(Integer)
table = relationship(
'SqlaTable', foreign_keys=[table_id], backref='slices')
@@ -177,7 +177,8 @@ class Slice(Model, AuditMixinNullable):
url=url, self=self)
-dashboard_slices = Table('dashboard_slices', Model.metadata,
+dashboard_slices = Table(
+ 'dashboard_slices', Model.metadata,
Column('id', Integer, primary_key=True),
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
Column('slice_id', Integer, ForeignKey('slices.id')),
@@ -229,7 +230,9 @@ class Dashboard(Model, AuditMixinNullable):
class Queryable(object):
+
"""A common interface to objects that are queryable (tables and datasources)"""
+
@property
def column_names(self):
return sorted([c.column_name for c in self.columns])
@@ -260,6 +263,7 @@ class Database(Model, AuditMixinNullable):
database_name = Column(String(250), unique=True)
sqlalchemy_uri = Column(String(1024))
password = Column(EncryptedType(String(1024), config.get('SECRET_KEY')))
+ cache_timeout = Column(Integer)
def __repr__(self):
return self.database_name
@@ -280,7 +284,7 @@ class Database(Model, AuditMixinNullable):
this allows a mapping between database engines and actual functions.
"""
Grain = namedtuple('Grain', 'name function')
- DB_TIME_GRAINS = {
+ db_time_grains = {
'presto': (
Grain('Time Column', '{col}'),
Grain('week', "date_trunc('week', CAST({col} AS DATE))"),
@@ -297,7 +301,7 @@ class Database(Model, AuditMixinNullable):
Grain('month', 'DATE_SUB({col}, INTERVAL DAYOFMONTH({col}) - 1 DAY)'),
),
}
- for db_type, grains in DB_TIME_GRAINS.items():
+ for db_type, grains in db_time_grains.items():
if self.sqlalchemy_uri.startswith(db_type):
return grains
@@ -350,6 +354,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
database = relationship(
'Database', backref='tables', foreign_keys=[database_id])
offset = Column(Integer, default=0)
+ cache_timeout = Column(Integer)
baselink = "tablemodelview"
@@ -428,7 +433,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
def sql_link(self):
return 'SQL'.format(self.sql_url)
- def query(
+ def query( # sqla
self, groupby, metrics,
granularity,
from_dttm, to_dttm,
@@ -438,7 +443,7 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
inner_from_dttm=None, inner_to_dttm=None,
extras=None,
columns=None):
-
+ """Querying any sqla table from this common interface"""
# For backward compatibility
if granularity not in self.dttm_cols:
granularity = self.main_dttm_col
@@ -586,8 +591,8 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
"couldn't fetch column information", "danger")
return
- TC = TableColumn
- M = SqlMetric
+ TC = TableColumn # noqa shortcut to class
+ M = SqlMetric # noqa
metrics = []
any_date_col = None
for col in table.columns:
@@ -778,6 +783,7 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
cluster = relationship(
'DruidCluster', backref='datasources', foreign_keys=[cluster_name])
offset = Column(Integer, default=0)
+ cache_timeout = Column(Integer)
@property
def metrics_combo(self):
@@ -892,7 +898,7 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
row_limit=None,
inner_from_dttm=None, inner_to_dttm=None,
extras=None, # noqa
- select=None):
+ select=None,): # noqa
"""Runs a query against Druid and returns a dataframe.
This query interface is common to SqlAlchemy and Druid
@@ -1074,8 +1080,6 @@ class Log(Model):
return wrapper
-
-
class DruidMetric(Model):
"""ORM object referencing Druid metrics for a datasource"""
@@ -1131,7 +1135,7 @@ class DruidColumn(Model):
def generate_metrics(self):
"""Generate metrics based on the column metadata"""
- M = DruidMetric
+ M = DruidMetric # noqa
metrics = []
metrics.append(DruidMetric(
metric_name='count',
diff --git a/dashed/templates/dashed/dashboard.html b/dashed/templates/dashed/dashboard.html
index 905435cfea..5f1c5bb078 100644
--- a/dashed/templates/dashed/dashboard.html
+++ b/dashed/templates/dashed/dashboard.html
@@ -40,24 +40,27 @@