From aed473d0d242f1173bc9988b504291b82ae47158 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Thu, 22 Sep 2016 20:12:48 -0700 Subject: [PATCH] [filtering] define combo of slice/fields unafected by filtering (#1179) * [FilterBox] dashboard date range filtering * [filtering] define combo of slice/fields unafected by filtering * adding an entry to the docs * Addressed comments --- .../javascripts/dashboard/Dashboard.jsx | 26 ++++++++++ caravel/assets/javascripts/modules/caravel.js | 8 ++-- caravel/assets/visualizations/filter_box.jsx | 5 +- caravel/utils.py | 8 ++++ caravel/views.py | 4 ++ caravel/viz.py | 7 +-- docs/faq.rst | 48 +++++++++++++++++++ 7 files changed, 94 insertions(+), 12 deletions(-) diff --git a/caravel/assets/javascripts/dashboard/Dashboard.jsx b/caravel/assets/javascripts/dashboard/Dashboard.jsx index 13677ae4b1..96c4217ce9 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 f6e7e57777..9094a98554 100644 --- a/caravel/assets/javascripts/modules/caravel.js +++ b/caravel/assets/javascripts/modules/caravel.js @@ -81,9 +81,11 @@ 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 = 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/assets/visualizations/filter_box.jsx b/caravel/assets/visualizations/filter_box.jsx index 2cd0695575..b600a83e76 100644 --- a/caravel/assets/visualizations/filter_box.jsx +++ b/caravel/assets/visualizations/filter_box.jsx @@ -12,16 +12,15 @@ import './filter_box.css'; import { TIME_CHOICES } from './constants.js'; const propTypes = { + origSelectedValues: React.PropTypes.object, filtersChoices: React.PropTypes.object, onChange: React.PropTypes.func, - origSelectedValues: React.PropTypes.object, showDateFilter: React.PropTypes.bool, }; const defaultProps = { - filtersChoices: {}, - onChange: () => {}, origSelectedValues: {}, + onChange: () => {}, showDateFilter: false, }; diff --git a/caravel/utils.py b/caravel/utils.py index c80b94fc81..268fbc0ff0 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 46037cb469..64ac34158e 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -850,6 +850,8 @@ class DashboardModelView(CaravelModelView, DeleteMixin): # noqa 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) @@ -1369,6 +1371,8 @@ class Caravel(BaseCaravelView): 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 ec48ce7806..eb64ea68f8 100755 --- a/caravel/viz.py +++ b/caravel/viz.py @@ -196,12 +196,7 @@ class BaseViz(object): 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""" diff --git a/docs/faq.rst b/docs/faq.rst index 6dc15da22a..aa545ec3d2 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.