From 770619a9e961117afbd89fad1e191ada93be5abd Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 21 Sep 2016 12:57:00 -0700 Subject: [PATCH 1/4] [FilterBox] dashboard date range filtering --- caravel/assets/visualizations/filter_box.jsx | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/caravel/assets/visualizations/filter_box.jsx b/caravel/assets/visualizations/filter_box.jsx index 2cd0695575671..dd5426a879e6f 100644 --- a/caravel/assets/visualizations/filter_box.jsx +++ b/caravel/assets/visualizations/filter_box.jsx @@ -25,6 +25,29 @@ const defaultProps = { showDateFilter: false, }; +const TIME_CHOICES = [ + '1 hour ago', + '12 hours ago', + '1 day ago', + '7 days ago', + '28 days ago', + '90 days ago', + '1 year ago', +]; + +const propTypes = { + origSelectedValues: React.PropTypes.object, + filtersChoices: React.PropTypes.object, + onChange: React.PropTypes.func, + showDateFilter: React.PropTypes.bool, +}; + +const defaultProps = { + origSelectedValues: {}, + onChange: () => {}, + showDateFilter: false, +}; + class FilterBox extends React.Component { constructor(props) { super(props); From 75f99d10ab37c86240ea28b381ed46df858d64da Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 21 Sep 2016 15:53:57 -0700 Subject: [PATCH 2/4] [filtering] define combo of slice/fields unafected by filtering --- .../javascripts/dashboard/Dashboard.jsx | 26 +++++++++++++++++++ caravel/assets/javascripts/modules/caravel.js | 10 ++++--- caravel/views.py | 2 ++ caravel/viz.py | 7 +---- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/caravel/assets/javascripts/dashboard/Dashboard.jsx b/caravel/assets/javascripts/dashboard/Dashboard.jsx index 13677ae4b1f3a..96c4217ce9766 100644 --- a/caravel/assets/javascripts/dashboard/Dashboard.jsx +++ b/caravel/assets/javascripts/dashboard/Dashboard.jsx @@ -82,6 +82,32 @@ function dashboardContainer(dashboardData) { setFilter(sliceId, col, vals, refresh) { this.addFilter(sliceId, col, vals, false, refresh); }, + effectiveExtraFilters(sliceId) { + // Summarized filter, not defined by sliceId + // returns k=field, v=array of values + const f = {}; + if (sliceId && this.metadata.filter_immune_slices.includes(sliceId)) { + // The slice is immune to dashboard fiterls + return f; + } + + // Building a list of fields the slice is immune to filters on + let immuneToFields = []; + if ( + sliceId && + this.metadata.filter_immune_slice_fields && + this.metadata.filter_immune_slice_fields[sliceId]) { + immuneToFields = this.metadata.filter_immune_slice_fields[sliceId]; + } + for (const filteringSliceId in this.filters) { + for (const field in this.filters[filteringSliceId]) { + if (!immuneToFields.includes(field)) { + f[field] = this.filters[filteringSliceId][field]; + } + } + } + return f; + }, addFilter(sliceId, col, vals, merge = true, refresh = true) { if (!(sliceId in this.filters)) { this.filters[sliceId] = {}; diff --git a/caravel/assets/javascripts/modules/caravel.js b/caravel/assets/javascripts/modules/caravel.js index f6e7e5777785e..c74d16cba1144 100644 --- a/caravel/assets/javascripts/modules/caravel.js +++ b/caravel/assets/javascripts/modules/caravel.js @@ -81,9 +81,13 @@ const px = function () { const parser = document.createElement('a'); parser.href = data.json_endpoint; if (dashboard !== undefined) { - const flts = - newParams.extraFilters === false ? '' : - encodeURIComponent(JSON.stringify(dashboard.filters)); + let flts; + if (newParams.extraFilters === false) { + flts = ''; + } else { + flts = dashboard.effectiveExtraFilters(sliceId); + flts = encodeURIComponent(JSON.stringify(flts)); + } qrystr = parser.search + '&extra_filters=' + flts; } else if ($('#query').length === 0) { qrystr = parser.search; diff --git a/caravel/views.py b/caravel/views.py index 46037cb4699b0..7d954942ae24f 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -1369,6 +1369,8 @@ def save_dash(self, dashboard_id): md = dash.metadata_dejson if 'filter_immune_slices' not in md: md['filter_immune_slices'] = [] + if 'filter_immune_slice_fields' not in md: + md['filter_immune_slice_fields'] = {} md['expanded_slices'] = data['expanded_slices'] dash.json_metadata = json.dumps(md, indent=4) dash.css = data['css'] diff --git a/caravel/viz.py b/caravel/viz.py index ec48ce78068eb..eb64ea68f8d7a 100755 --- a/caravel/viz.py +++ b/caravel/viz.py @@ -196,12 +196,7 @@ def get_extra_filters(self): extra_filters = self.form_data.get('extra_filters') if not extra_filters: return {} - extra_filters = json.loads(extra_filters) - # removing per-slice details - summary = {} - for flt in extra_filters.values(): - summary.update(flt) - return summary + return json.loads(extra_filters) def query_filters(self, is_having_filter=False): """Processes the filters for the query""" From 71a8d9d8104be9321e945784eb3df2f63175a856 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 21 Sep 2016 16:46:12 -0700 Subject: [PATCH 3/4] adding an entry to the docs --- caravel/utils.py | 8 ++++++++ caravel/views.py | 2 ++ docs/faq.rst | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/caravel/utils.py b/caravel/utils.py index c80b94fc813d7..268fbc0ff092d 100644 --- a/caravel/utils.py +++ b/caravel/utils.py @@ -445,6 +445,14 @@ def generic_find_constraint_name(table, columns, referenced, db): return fk.name +def validate_json(obj): + if obj: + try: + json.loads(obj) + except Exception: + raise CaravelException("JSON is not valid") + + class timeout(object): """ To be used in a ``with`` block and timeout its content. diff --git a/caravel/views.py b/caravel/views.py index 7d954942ae24f..64ac34158e1f3 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -850,6 +850,8 @@ def pre_add(self, obj): obj.slug = re.sub(r'\W+', '', obj.slug) if g.user not in obj.owners: obj.owners.append(g.user) + utils.validate_json(obj.json_metadata) + utils.validate_json(obj.position_json) def pre_update(self, obj): check_ownership(obj) diff --git a/docs/faq.rst b/docs/faq.rst index 6dc15da22a9f8..aa545ec3d2f92 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -61,3 +61,51 @@ Why is the map not visible in the mapbox visualization? You need to register to mapbox.com, get an API key and configure it as ``MAPBOX_API_KEY`` in ``caravel_config.py``. + + +How to add dynamic filters to a dashboard? +------------------------------------------ + +It's easy: use the ``Filter Box`` widget, build a slice, and add it to your +dashboard. + +The ``Filter Box`` widget allows you to define a query to populate dropdowns +that can be use for filtering. To build the list of distinct values, we +run a query, and sort the result by the metric you provide, sorting +descending. + +The widget also has a checkbox ``Date Filter``, which enables time filtering +capabilities to your dashboard. After checking the box and refreshing, you'll +see a ``from`` and a ``to`` dropdown show up. + +But what about if you don't want certain widgets to get filtered on your +dashboard? You can do that by editing your dashboard, and in the form, +edit the ``JSON Metadata`` field, more specifically the +``filter_immune_slices`` key, that receives an array of sliceIds that should +never be affected by any dashboard level filtering. + + +..code:: + + { + "filter_immune_slices": [324, 65, 92], + "expanded_slices": {}, + "filter_immune_slice_fields": { + "177": ["country_name", "__from", "__to"], + "32": ["__from", "__to"] + } + } + +In the json blob above, slices 324, 65 and 92 won't be affected by any +dashboard level filtering. + +Now note the ``filter_immune_slice_fields`` key. This one allows you to +be more specific and define for a specific slice_id, which filter fields +should be disregarded. + +Note the use of the ``__from`` and ``__to`` keywords, those are reserved +for dealing with the time boundary filtering mentioned above. + +But what happens with filtering when dealing with slices coming from +different tables or databases? If the column name is shared, the filter will +be applied, it's as simple as that. From 37513e79e4676e0eb567cc5ae17b9b2a3b6d990a Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Thu, 22 Sep 2016 15:32:56 -0700 Subject: [PATCH 4/4] Addressed comments --- caravel/assets/javascripts/modules/caravel.js | 6 ++--- caravel/assets/visualizations/filter_box.jsx | 24 ------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/caravel/assets/javascripts/modules/caravel.js b/caravel/assets/javascripts/modules/caravel.js index c74d16cba1144..9094a985541c0 100644 --- a/caravel/assets/javascripts/modules/caravel.js +++ b/caravel/assets/javascripts/modules/caravel.js @@ -81,10 +81,8 @@ const px = function () { const parser = document.createElement('a'); parser.href = data.json_endpoint; if (dashboard !== undefined) { - let flts; - if (newParams.extraFilters === false) { - flts = ''; - } else { + let flts = ''; + if (newParams.extraFilters !== false) { flts = dashboard.effectiveExtraFilters(sliceId); flts = encodeURIComponent(JSON.stringify(flts)); } diff --git a/caravel/assets/visualizations/filter_box.jsx b/caravel/assets/visualizations/filter_box.jsx index dd5426a879e6f..b600a83e76263 100644 --- a/caravel/assets/visualizations/filter_box.jsx +++ b/caravel/assets/visualizations/filter_box.jsx @@ -11,30 +11,6 @@ import '../stylesheets/react-select/select.less'; import './filter_box.css'; import { TIME_CHOICES } from './constants.js'; -const propTypes = { - filtersChoices: React.PropTypes.object, - onChange: React.PropTypes.func, - origSelectedValues: React.PropTypes.object, - showDateFilter: React.PropTypes.bool, -}; - -const defaultProps = { - filtersChoices: {}, - onChange: () => {}, - origSelectedValues: {}, - showDateFilter: false, -}; - -const TIME_CHOICES = [ - '1 hour ago', - '12 hours ago', - '1 day ago', - '7 days ago', - '28 days ago', - '90 days ago', - '1 year ago', -]; - const propTypes = { origSelectedValues: React.PropTypes.object, filtersChoices: React.PropTypes.object,