This is an automated email from the ASF dual-hosted git repository.
graceguo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new adebd40 [cache warm_up] warm_up slice with dashboard default_filters
(#9311)
adebd40 is described below
commit adebd40d300fb86a53220ee99b453fb853fb2ef5
Author: Grace Guo <[email protected]>
AuthorDate: Wed Mar 18 08:21:10 2020 -0700
[cache warm_up] warm_up slice with dashboard default_filters (#9311)
* [cache warm_up] warm_up slice with dashboard default_filters
* update Celery warmup tasks
* fix code review comments
* add try catch and type checking for parsed dash metadata
* extra code review fix
---
superset/tasks/cache.py | 31 +++---
superset/views/core.py | 6 ++
superset/views/utils.py | 88 ++++++++++++++++
tests/strategy_tests.py | 22 ++++
tests/utils_tests.py | 265 ++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 396 insertions(+), 16 deletions(-)
diff --git a/superset/tasks/cache.py b/superset/tasks/cache.py
index ae95f20..67c366b 100644
--- a/superset/tasks/cache.py
+++ b/superset/tasks/cache.py
@@ -18,6 +18,7 @@
import json
import logging
+from typing import Any, Dict, Optional
from urllib import request
from urllib.error import URLError
@@ -31,6 +32,7 @@ from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.tags import Tag, TaggedObject
from superset.utils.core import parse_human_datetime
+from superset.views.utils import build_extra_filters
logger = get_task_logger(__name__)
logger.setLevel(logging.INFO)
@@ -54,27 +56,23 @@ def get_form_data(chart_id, dashboard=None):
if not default_filters:
return form_data
- # do not apply filters if chart is immune to them
- immune_fields = []
filter_scopes = json_metadata.get("filter_scopes", {})
- if filter_scopes:
- for scopes in filter_scopes.values():
- for (field, scope) in scopes.items():
- if chart_id in scope.get("immune", []):
- immune_fields.append(field)
-
- extra_filters = []
- for filters in default_filters.values():
- for col, val in filters.items():
- if col not in immune_fields:
- extra_filters.append({"col": col, "op": "in", "val": val})
+ layout = json.loads(dashboard.position_json or "{}")
+ if (
+ isinstance(layout, dict)
+ and isinstance(filter_scopes, dict)
+ and isinstance(default_filters, dict)
+ ):
+ extra_filters = build_extra_filters(
+ layout, filter_scopes, default_filters, chart_id
+ )
if extra_filters:
form_data["extra_filters"] = extra_filters
return form_data
-def get_url(chart):
+def get_url(chart, extra_filters: Optional[Dict[str, Any]] = None):
"""Return external URL for warming up a given chart/table cache."""
with app.test_request_context():
baseurl = (
@@ -82,7 +80,7 @@ def get_url(chart):
"{SUPERSET_WEBSERVER_ADDRESS}:"
"{SUPERSET_WEBSERVER_PORT}".format(**app.config)
)
- return f"{baseurl}{chart.url}"
+ return f"{baseurl}{chart.get_explore_url(overrides=extra_filters)}"
class Strategy:
@@ -181,7 +179,8 @@ class TopNDashboardsStrategy(Strategy):
dashboards =
session.query(Dashboard).filter(Dashboard.id.in_(dash_ids)).all()
for dashboard in dashboards:
for chart in dashboard.slices:
- urls.append(get_url(chart))
+ form_data_with_filters = get_form_data(chart.id, dashboard)
+ urls.append(get_url(chart, form_data_with_filters))
return urls
diff --git a/superset/views/core.py b/superset/views/core.py
index ff6dcbf..447461e 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -90,6 +90,7 @@ from superset.utils.dashboard_filter_scopes_converter import
copy_filter_scopes
from superset.utils.dates import now_as_float
from superset.utils.decorators import etag_cache, stats_timing
from superset.views.database.filters import DatabaseFilter
+from superset.views.utils import get_dashboard_extra_filters
from .base import (
api,
@@ -1651,6 +1652,7 @@ class Superset(BaseSupersetView):
slices = None
session = db.session()
slice_id = request.args.get("slice_id")
+ dashboard_id = request.args.get("dashboard_id")
table_name = request.args.get("table_name")
db_name = request.args.get("db_name")
@@ -1696,6 +1698,10 @@ class Superset(BaseSupersetView):
for slc in slices:
try:
form_data = get_form_data(slc.id, use_slice_data=True)[0]
+ if dashboard_id:
+ form_data["extra_filters"] = get_dashboard_extra_filters(
+ slc.id, dashboard_id
+ )
obj = get_viz(
datasource_type=slc.datasource.type,
datasource_id=slc.datasource.id,
diff --git a/superset/views/utils.py b/superset/views/utils.py
index 9bcfc6e..40de71e 100644
--- a/superset/views/utils.py
+++ b/superset/views/utils.py
@@ -27,6 +27,7 @@ from superset import app, db, viz
from superset.connectors.connector_registry import ConnectorRegistry
from superset.exceptions import SupersetException
from superset.legacy import update_time_range
+from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils.core import QueryStatus, TimeRangeEndpoint
@@ -262,3 +263,90 @@ def get_time_range_endpoints(
return (TimeRangeEndpoint(start), TimeRangeEndpoint(end))
return (TimeRangeEndpoint.INCLUSIVE, TimeRangeEndpoint.EXCLUSIVE)
+
+
+# see all dashboard components type in
+# /superset-frontend/src/dashboard/util/componentTypes.js
+CONTAINER_TYPES = ["COLUMN", "GRID", "TABS", "TAB", "ROW"]
+
+
+def get_dashboard_extra_filters(
+ slice_id: int, dashboard_id: int
+) -> List[Dict[str, Any]]:
+ session = db.session()
+ dashboard =
session.query(Dashboard).filter_by(id=dashboard_id).one_or_none()
+
+ # is chart in this dashboard?
+ if (
+ dashboard is None
+ or not dashboard.json_metadata
+ or not dashboard.slices
+ or not any([slc for slc in dashboard.slices if slc.id == slice_id])
+ ):
+ return []
+
+ try:
+ # does this dashboard have default filters?
+ json_metadata = json.loads(dashboard.json_metadata)
+ default_filters = json.loads(json_metadata.get("default_filters",
"null"))
+ if not default_filters:
+ return []
+
+ # are default filters applicable to the given slice?
+ filter_scopes = json_metadata.get("filter_scopes", {})
+ layout = json.loads(dashboard.position_json or "{}")
+
+ if (
+ isinstance(layout, dict)
+ and isinstance(filter_scopes, dict)
+ and isinstance(default_filters, dict)
+ ):
+ return build_extra_filters(layout, filter_scopes, default_filters,
slice_id)
+ except json.JSONDecodeError:
+ pass
+
+ return []
+
+
+def build_extra_filters(
+ layout: Dict,
+ filter_scopes: Dict,
+ default_filters: Dict[str, Dict[str, List]],
+ slice_id: int,
+) -> List[Dict[str, Any]]:
+ extra_filters = []
+
+ # do not apply filters if chart is not in filter's scope or
+ # chart is immune to the filter
+ for filter_id, columns in default_filters.items():
+ scopes_by_filter_field = filter_scopes.get(filter_id, {})
+ for col, val in columns.items():
+ current_field_scopes = scopes_by_filter_field.get(col, {})
+ scoped_container_ids = current_field_scopes.get("scope",
["ROOT_ID"])
+ immune_slice_ids = current_field_scopes.get("immune", [])
+
+ for container_id in scoped_container_ids:
+ if slice_id not in immune_slice_ids and is_slice_in_container(
+ layout, container_id, slice_id
+ ):
+ extra_filters.append({"col": col, "op": "in", "val": val})
+
+ return extra_filters
+
+
+def is_slice_in_container(layout: Dict, container_id: str, slice_id: int) ->
bool:
+ if container_id == "ROOT_ID":
+ return True
+
+ node = layout[container_id]
+ node_type = node.get("type")
+ if node_type == "CHART" and node.get("meta", {}).get("chartId") ==
slice_id:
+ return True
+
+ if node_type in CONTAINER_TYPES:
+ children = node.get("children", [])
+ return any(
+ is_slice_in_container(layout, child_id, slice_id) for child_id in
children
+ )
+
+ return False
diff --git a/tests/strategy_tests.py b/tests/strategy_tests.py
index 646931f..08d25b2 100644
--- a/tests/strategy_tests.py
+++ b/tests/strategy_tests.py
@@ -33,6 +33,22 @@ from .base_tests import SupersetTestCase
URL_PREFIX = "http://0.0.0.0:8081"
+mock_positions = {
+ "DASHBOARD_VERSION_KEY": "v2",
+ "DASHBOARD_CHART_TYPE-1": {
+ "type": "CHART",
+ "id": "DASHBOARD_CHART_TYPE-1",
+ "children": [],
+ "meta": {"width": 4, "height": 50, "chartId": 1},
+ },
+ "DASHBOARD_CHART_TYPE-2": {
+ "type": "CHART",
+ "id": "DASHBOARD_CHART_TYPE-2",
+ "children": [],
+ "meta": {"width": 4, "height": 50, "chartId": 2},
+ },
+}
+
class CacheWarmUpTests(SupersetTestCase):
def __init__(self, *args, **kwargs):
@@ -48,6 +64,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
dashboard = MagicMock()
dashboard.json_metadata = None
+ dashboard.position_json = json.dumps(mock_positions)
result = get_form_data(chart_id, dashboard)
expected = {"slice_id": chart_id}
self.assertEqual(result, expected)
@@ -56,6 +73,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
+ dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"filter_scopes": {
@@ -76,6 +94,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
dashboard = MagicMock()
dashboard.json_metadata = json.dumps({})
+ dashboard.position_json = json.dumps(mock_positions)
result = get_form_data(chart_id, dashboard)
expected = {"slice_id": chart_id}
self.assertEqual(result, expected)
@@ -84,6 +103,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
+ dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"default_filters": json.dumps(
@@ -112,6 +132,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
+ dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"default_filters": json.dumps(
@@ -132,6 +153,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
+ dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"default_filters": json.dumps(
diff --git a/tests/utils_tests.py b/tests/utils_tests.py
index cc19c48..5332007 100644
--- a/tests/utils_tests.py
+++ b/tests/utils_tests.py
@@ -56,6 +56,7 @@ from superset.utils.core import (
zlib_decompress,
)
from superset.views.utils import get_time_range_endpoints
+from superset.views.utils import build_extra_filters
from tests.base_tests import SupersetTestCase
@@ -956,3 +957,267 @@ class UtilsTestCase(SupersetTestCase):
self.assertListEqual(get_iterable(123), [123])
self.assertListEqual(get_iterable([123]), [123])
self.assertListEqual(get_iterable("foo"), ["foo"])
+
+ def test_build_extra_filters(self):
+ layout = {
+ "CHART-2ee52f30": {
+ "children": [],
+ "id": "CHART-2ee52f30",
+ "meta": {
+ "chartId": 1020,
+ "height": 38,
+ "sliceName": "Chart 927",
+ "width": 6,
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "TABS-N1zN4CIZP0",
+ "TAB-asWdJzKmTN",
+ "ROW-i_sG4ccXE",
+ ],
+ "type": "CHART",
+ },
+ "CHART-36bfc934": {
+ "children": [],
+ "id": "CHART-36bfc934",
+ "meta": {
+ "chartId": 1018,
+ "height": 26,
+ "sliceName": "Region Filter",
+ "width": 2,
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-W62P60D88",
+ "ROW-1e064e3c",
+ "COLUMN-fe3914b8",
+ ],
+ "type": "CHART",
+ },
+ "CHART-E_y2cuNHTv": {
+ "children": [],
+ "id": "CHART-E_y2cuNHTv",
+ "meta": {"chartId": 998, "height": 55, "sliceName": "MAP",
"width": 6},
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-W62P60D88",
+ "ROW-1e064e3c",
+ ],
+ "type": "CHART",
+ },
+ "CHART-JNxDOsAfEb": {
+ "children": [],
+ "id": "CHART-JNxDOsAfEb",
+ "meta": {
+ "chartId": 1015,
+ "height": 27,
+ "sliceName": "Population",
+ "width": 4,
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-W62P60D88",
+ "ROW-1e064e3c",
+ "COLUMN-fe3914b8",
+ ],
+ "type": "CHART",
+ },
+ "CHART-KoOwqalV80": {
+ "children": [],
+ "id": "CHART-KoOwqalV80",
+ "meta": {
+ "chartId": 927,
+ "height": 20,
+ "sliceName": "Chart 927",
+ "width": 4,
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "TABS-N1zN4CIZP0",
+ "TAB-cHNWcBZC9",
+ "ROW-9b9vrWKPY",
+ ],
+ "type": "CHART",
+ },
+ "CHART-YCQAPVK7mQ": {
+ "children": [],
+ "id": "CHART-YCQAPVK7mQ",
+ "meta": {
+ "chartId": 1023,
+ "height": 38,
+ "sliceName": "World's Population",
+ "width": 4,
+ },
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "ROW-UfxFT36oV5",
+ ],
+ "type": "CHART",
+ },
+ "COLUMN-fe3914b8": {
+ "children": ["CHART-36bfc934", "CHART-JNxDOsAfEb"],
+ "id": "COLUMN-fe3914b8",
+ "meta": {"background": "BACKGROUND_TRANSPARENT", "width": 6},
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-W62P60D88",
+ "ROW-1e064e3c",
+ ],
+ "type": "COLUMN",
+ },
+ "DASHBOARD_VERSION_KEY": "v2",
+ "GRID_ID": {
+ "children": [],
+ "id": "GRID_ID",
+ "parents": ["ROOT_ID"],
+ "type": "GRID",
+ },
+ "HEADER_ID": {
+ "id": "HEADER_ID",
+ "meta": {"text": "Test warmup 1023"},
+ "type": "HEADER",
+ },
+ "ROOT_ID": {
+ "children": ["TABS-Qq4sdkANSY"],
+ "id": "ROOT_ID",
+ "type": "ROOT",
+ },
+ "ROW-1e064e3c": {
+ "children": ["COLUMN-fe3914b8", "CHART-E_y2cuNHTv"],
+ "id": "ROW-1e064e3c",
+ "meta": {"background": "BACKGROUND_TRANSPARENT"},
+ "parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-W62P60D88"],
+ "type": "ROW",
+ },
+ "ROW-9b9vrWKPY": {
+ "children": ["CHART-KoOwqalV80"],
+ "id": "ROW-9b9vrWKPY",
+ "meta": {"background": "BACKGROUND_TRANSPARENT"},
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "TABS-N1zN4CIZP0",
+ "TAB-cHNWcBZC9",
+ ],
+ "type": "ROW",
+ },
+ "ROW-UfxFT36oV5": {
+ "children": ["CHART-YCQAPVK7mQ"],
+ "id": "ROW-UfxFT36oV5",
+ "meta": {"background": "BACKGROUND_TRANSPARENT"},
+ "parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-VrhTX2WUlO"],
+ "type": "ROW",
+ },
+ "ROW-i_sG4ccXE": {
+ "children": ["CHART-2ee52f30"],
+ "id": "ROW-i_sG4ccXE",
+ "meta": {"background": "BACKGROUND_TRANSPARENT"},
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "TABS-N1zN4CIZP0",
+ "TAB-asWdJzKmTN",
+ ],
+ "type": "ROW",
+ },
+ "TAB-VrhTX2WUlO": {
+ "children": ["ROW-UfxFT36oV5", "TABS-N1zN4CIZP0"],
+ "id": "TAB-VrhTX2WUlO",
+ "meta": {"text": "New Tab"},
+ "parents": ["ROOT_ID", "TABS-Qq4sdkANSY"],
+ "type": "TAB",
+ },
+ "TAB-W62P60D88": {
+ "children": ["ROW-1e064e3c"],
+ "id": "TAB-W62P60D88",
+ "meta": {"text": "Tab 2"},
+ "parents": ["ROOT_ID", "TABS-Qq4sdkANSY"],
+ "type": "TAB",
+ },
+ "TAB-asWdJzKmTN": {
+ "children": ["ROW-i_sG4ccXE"],
+ "id": "TAB-asWdJzKmTN",
+ "meta": {"text": "nested tab 1"},
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "TABS-N1zN4CIZP0",
+ ],
+ "type": "TAB",
+ },
+ "TAB-cHNWcBZC9": {
+ "children": ["ROW-9b9vrWKPY"],
+ "id": "TAB-cHNWcBZC9",
+ "meta": {"text": "test2d tab 2"},
+ "parents": [
+ "ROOT_ID",
+ "TABS-Qq4sdkANSY",
+ "TAB-VrhTX2WUlO",
+ "TABS-N1zN4CIZP0",
+ ],
+ "type": "TAB",
+ },
+ "TABS-N1zN4CIZP0": {
+ "children": ["TAB-asWdJzKmTN", "TAB-cHNWcBZC9"],
+ "id": "TABS-N1zN4CIZP0",
+ "meta": {},
+ "parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-VrhTX2WUlO"],
+ "type": "TABS",
+ },
+ "TABS-Qq4sdkANSY": {
+ "children": ["TAB-VrhTX2WUlO", "TAB-W62P60D88"],
+ "id": "TABS-Qq4sdkANSY",
+ "meta": {},
+ "parents": ["ROOT_ID"],
+ "type": "TABS",
+ },
+ }
+ filter_scopes = {
+ "1018": {
+ "region": {"scope": ["TAB-W62P60D88"], "immune": [998]},
+ "country_name": {"scope": ["ROOT_ID"], "immune": [927, 998]},
+ }
+ }
+ default_filters = {
+ "1018": {"region": ["North America"], "country_name": ["United
States"]}
+ }
+
+ # immune to all filters
+ slice_id = 998
+ extra_filters = build_extra_filters(
+ layout, filter_scopes, default_filters, slice_id
+ )
+ expected = []
+ self.assertEqual(extra_filters, expected)
+
+ # in scope
+ slice_id = 1015
+ extra_filters = build_extra_filters(
+ layout, filter_scopes, default_filters, slice_id
+ )
+ expected = [
+ {"col": "region", "op": "in", "val": ["North America"]},
+ {"col": "country_name", "op": "in", "val": ["United States"]},
+ ]
+ self.assertEqual(extra_filters, expected)
+
+ # not in scope
+ slice_id = 927
+ extra_filters = build_extra_filters(
+ layout, filter_scopes, default_filters, slice_id
+ )
+ expected = []
+ self.assertEqual(extra_filters, expected)