diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx index ca5ee5be96a9..be746727057c 100644 --- a/superset-frontend/src/chart/Chart.jsx +++ b/superset-frontend/src/chart/Chart.jsx @@ -51,6 +51,7 @@ const propTypes = { timeout: PropTypes.number, vizType: PropTypes.string.isRequired, triggerRender: PropTypes.bool, + force: PropTypes.bool, isFiltersInitialized: PropTypes.bool, isDeactivatedViz: PropTypes.bool, // state @@ -84,6 +85,7 @@ const defaultProps = { dashboardId: null, chartStackTrace: null, isDeactivatedViz: false, + force: false, }; const Styles = styled.div` @@ -143,7 +145,7 @@ class Chart extends React.PureComponent { // Load saved chart with a GET request this.props.actions.getSavedChart( this.props.formData, - false, + this.props.force, this.props.timeout, this.props.chartId, this.props.dashboardId, @@ -153,7 +155,7 @@ class Chart extends React.PureComponent { // Create chart with POST request this.props.actions.postChartFormData( this.props.formData, - false, + this.props.force, this.props.timeout, this.props.chartId, this.props.dashboardId, diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx b/superset-frontend/src/explore/components/ExploreChartPanel.jsx index 7a930a7ff1c7..70c62941c1be 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx @@ -52,6 +52,7 @@ const propTypes = { form_data: PropTypes.object, ownState: PropTypes.object, standalone: PropTypes.number, + force: PropTypes.bool, timeout: PropTypes.number, refreshOverlayVisible: PropTypes.bool, chart: chartPropShape, @@ -131,7 +132,7 @@ const ExploreChartPanel = props => { if (slice && slice.query_context === null) { const queryContext = buildV1ChartDataPayload({ formData: slice.form_data, - force: false, + force: props.force, resultFormat: 'json', resultType: 'full', setDataMask: null, @@ -227,6 +228,7 @@ const ExploreChartPanel = props => { chartId={chart.id} chartStatus={chart.chartStatus} triggerRender={props.triggerRender} + force={props.force} datasource={props.datasource} errorMessage={props.errorMessage} formData={props.form_data} diff --git a/superset-frontend/src/explore/components/ExploreViewContainer.jsx b/superset-frontend/src/explore/components/ExploreViewContainer.jsx index b56f09a2ba2a..f5dbafc8f34f 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer.jsx @@ -73,6 +73,7 @@ const propTypes = { forcedHeight: PropTypes.string, form_data: PropTypes.object.isRequired, standalone: PropTypes.number.isRequired, + force: PropTypes.bool, timeout: PropTypes.number, impressionId: PropTypes.string, vizType: PropTypes.string, @@ -202,6 +203,8 @@ function ExploreViewContainer(props) { formData, props.standalone ? URL_PARAMS.standalone.name : null, false, + {}, + props.force, ); try { @@ -220,7 +223,7 @@ function ExploreViewContainer(props) { ); } }, - [props.form_data, props.standalone], + [props.form_data, props.standalone, props.force], ); const handlePopstate = useCallback(() => { @@ -229,7 +232,7 @@ function ExploreViewContainer(props) { props.actions.setExploreControls(formData); props.actions.postChartFormData( formData, - false, + props.force, props.timeout, props.chart.id, ); @@ -639,6 +642,7 @@ function mapStateToProps(state) { table_name: form_data.datasource_name, vizType: form_data.viz_type, standalone: explore.standalone, + force: explore.force, forcedHeight: explore.forced_height, chart, timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT, diff --git a/superset-frontend/src/explore/exploreUtils/getExploreLongUrl.test.ts b/superset-frontend/src/explore/exploreUtils/getExploreLongUrl.test.ts index 76b4dd91eeac..5672b26187e9 100644 --- a/superset-frontend/src/explore/exploreUtils/getExploreLongUrl.test.ts +++ b/superset-frontend/src/explore/exploreUtils/getExploreLongUrl.test.ts @@ -45,12 +45,27 @@ test('Get url when endpointType:standalone', () => { expect( getExploreLongUrl( params.formData, - params.endpointType, + 'standalone', params.allowOverflow, params.extraSearch, ), ).toBe( - '/superset/explore/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D', + '/superset/explore/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D&standalone=1', + ); +}); + +test('Get url when endpointType:standalone and force:true', () => { + const params = createParams(); + expect( + getExploreLongUrl( + params.formData, + 'standalone', + params.allowOverflow, + params.extraSearch, + true, + ), + ).toBe( + '/superset/explore/?same=any-string&form_data=%7B%22datasource%22%3A%22datasource%22%2C%22viz_type%22%3A%22viz_type%22%7D&force=1&standalone=1', ); }); diff --git a/superset-frontend/src/explore/exploreUtils/index.js b/superset-frontend/src/explore/exploreUtils/index.js index b9fbd12f681f..7cb3b8da69f1 100644 --- a/superset-frontend/src/explore/exploreUtils/index.js +++ b/superset-frontend/src/explore/exploreUtils/index.js @@ -99,6 +99,7 @@ export function getExploreLongUrl( endpointType, allowOverflow = true, extraSearch = {}, + force = false, ) { if (!formData.datasource) { return null; @@ -112,6 +113,9 @@ export function getExploreLongUrl( }); search.form_data = safeStringify(formData); if (endpointType === URL_PARAMS.standalone.name) { + if (force) { + search.force = '1'; + } search.standalone = DashboardStandaloneMode.HIDE_NAV; } const url = uri.directory(directory).search(search).toString(); @@ -120,9 +124,15 @@ export function getExploreLongUrl( datasource: formData.datasource, viz_type: formData.viz_type, }; - return getExploreLongUrl(minimalFormData, endpointType, false, { - URL_IS_TOO_LONG_TO_SHARE: null, - }); + return getExploreLongUrl( + minimalFormData, + endpointType, + false, + { + URL_IS_TOO_LONG_TO_SHARE: null, + }, + force, + ); } return url; } diff --git a/superset-frontend/src/explore/reducers/getInitialState.ts b/superset-frontend/src/explore/reducers/getInitialState.ts index b7f8a62f9408..08ed3d542a1a 100644 --- a/superset-frontend/src/explore/reducers/getInitialState.ts +++ b/superset-frontend/src/explore/reducers/getInitialState.ts @@ -48,6 +48,7 @@ export interface ExlorePageBootstrapData extends JsonObject { form_data: QueryFormData; slice: Slice | null; standalone: boolean; + force: boolean; user: UserWithPermissionsAndRoles; } diff --git a/superset/reports/commands/execute.py b/superset/reports/commands/execute.py index 297316b8a751..2789bb20fbaa 100644 --- a/superset/reports/commands/execute.py +++ b/superset/reports/commands/execute.py @@ -72,6 +72,7 @@ DashboardScreenshot, ) from superset.utils.urls import get_url_path +from superset.utils.webdriver import DashboardStandaloneMode logger = logging.getLogger(__name__) @@ -145,6 +146,16 @@ def _get_url( """ Get the url for this report schedule: chart or dashboard """ + # For alerts we always want to send a fresh screenshot, bypassing + # the cache. + # TODO (betodealmeida): allow to specify per report if users want + # to bypass the cache as well. + force = ( + "true" + if self._report_schedule.type == ReportScheduleType.ALERT + else "false" + ) + if self._report_schedule.chart: if result_format in { ChartDataResultFormat.CSV, @@ -155,17 +166,22 @@ def _get_url( pk=self._report_schedule.chart_id, format=result_format.value, type=ChartDataResultType.POST_PROCESSED.value, + force=force, ) return get_url_path( - "Superset.slice", + "Superset.explore", user_friendly=user_friendly, - slice_id=self._report_schedule.chart_id, + form_data=json.dumps({"slice_id": self._report_schedule.chart_id}), + standalone="true", + force=force, **kwargs, ) return get_url_path( "Superset.dashboard", user_friendly=user_friendly, dashboard_id_or_slug=self._report_schedule.dashboard_id, + standalone=DashboardStandaloneMode.REPORT.value, + force=force, **kwargs, ) @@ -187,7 +203,7 @@ def _get_screenshot(self) -> bytes: """ screenshot: Optional[BaseScreenshot] = None if self._report_schedule.chart: - url = self._get_url(standalone="true") + url = self._get_url() logger.info("Screenshotting chart at %s", url) screenshot = ChartScreenshot( url, diff --git a/superset/utils/core.py b/superset/utils/core.py index f864059db417..002592ae093f 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -303,7 +303,7 @@ class ReservedUrlParameters(str, Enum): @staticmethod def is_standalone_mode() -> Optional[bool]: standalone_param = request.args.get(ReservedUrlParameters.STANDALONE.value) - standalone: Optional[bool] = ( + standalone: Optional[bool] = bool( standalone_param and standalone_param != "false" and standalone_param != "0" ) return standalone diff --git a/superset/utils/webdriver.py b/superset/utils/webdriver.py index 9cedc0f93452..ac32ade0de09 100644 --- a/superset/utils/webdriver.py +++ b/superset/utils/webdriver.py @@ -21,7 +21,6 @@ from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING from flask import current_app -from requests.models import PreparedRequest from selenium.common.exceptions import ( StaleElementReferenceException, TimeoutException, @@ -107,11 +106,6 @@ def destroy(driver: WebDriver, tries: int = 2) -> None: def get_screenshot( self, url: str, element_name: str, user: "User" ) -> Optional[bytes]: - params = {"standalone": DashboardStandaloneMode.REPORT.value} - req = PreparedRequest() - req.prepare_url(url, params) - url = req.url or "" - driver = self.auth(user) driver.set_window_size(*self._window) driver.get(url) diff --git a/superset/views/core.py b/superset/views/core.py index 0ad97199fbdd..ecbd13df3267 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -842,6 +842,7 @@ def explore( query_context, ) standalone_mode = ReservedUrlParameters.is_standalone_mode() + force = request.args.get("force") in {"force", "1", "true"} dummy_datasource_data: Dict[str, Any] = { "type": datasource_type, "name": datasource_name, @@ -866,6 +867,7 @@ def explore( "datasource_type": datasource_type, "slice": slc.data if slc else None, "standalone": standalone_mode, + "force": force, "user": bootstrap_user_data(g.user, include_perms=True), "forced_height": request.args.get("height"), "common": common_bootstrap_payload(), diff --git a/tests/integration_tests/reports/commands_tests.py b/tests/integration_tests/reports/commands_tests.py index df584ec830d9..0e455b30dd9b 100644 --- a/tests/integration_tests/reports/commands_tests.py +++ b/tests/integration_tests/reports/commands_tests.py @@ -663,8 +663,47 @@ def test_email_chart_report_schedule( ) # assert that the link sent is correct assert ( - f'Explore in Superset' + 'Explore in Superset' + in email_mock.call_args[0][2] + ) + # Assert the email smtp address + assert email_mock.call_args[0][0] == notification_targets[0] + # Assert the email inline screenshot + smtp_images = email_mock.call_args[1]["images"] + assert smtp_images[list(smtp_images.keys())[0]] == SCREENSHOT_FILE + # Assert logs are correct + assert_log(ReportState.SUCCESS) + + +@pytest.mark.usefixtures( + "load_birth_names_dashboard_with_slices", "create_alert_email_chart" +) +@patch("superset.reports.notifications.email.send_email_smtp") +@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot") +def test_email_chart_alert_schedule( + screenshot_mock, email_mock, create_alert_email_chart, +): + """ + ExecuteReport Command: Test chart email alert schedule with screenshot + """ + # setup screenshot mock + screenshot_mock.return_value = SCREENSHOT_FILE + + with freeze_time("2020-01-01T00:00:00Z"): + AsyncExecuteReportScheduleCommand( + TEST_ID, create_alert_email_chart.id, datetime.utcnow() + ).run() + + notification_targets = get_target_from_report_schedule(create_alert_email_chart) + # assert that the link sent is correct + assert ( + 'Explore in Superset' in email_mock.call_args[0][2] ) # Assert the email smtp address @@ -729,8 +768,10 @@ def test_email_chart_report_schedule_with_csv( ) # assert that the link sent is correct assert ( - f'Explore in Superset' + 'Explore in Superset' in email_mock.call_args[0][2] ) # Assert the email smtp address