Skip to content

Commit

Permalink
feat: add force option to report screenshots (#17853)
Browse files Browse the repository at this point in the history
* Update existing tests

* Add backend test

* feat: add force option to report screenshots

* Add tests

* Rebase fixes

* Do not force screenshot on dashboard alerts
  • Loading branch information
betodealmeida authored Dec 22, 2021
1 parent cb0b7a2 commit 2cd8054
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 8 deletions.
2 changes: 2 additions & 0 deletions superset-frontend/src/components/ReportModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface ReportObject {
validator_type: string;
working_timeout: number;
creation_method: string;
force_screenshot: boolean;
}

interface ChartObject {
Expand Down Expand Up @@ -227,6 +228,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
active: true,
report_format: currentReport?.report_format || defaultNotificationFormat,
timezone: currentReport?.timezone,
force_screenshot: false,
};

if (isEditMode) {
Expand Down
2 changes: 2 additions & 0 deletions superset-frontend/src/views/CRUD/alert/AlertReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const DEFAULT_ALERT = {
sql: '',
validator_config_json: {},
validator_type: '',
force_screenshot: false,
grace_period: undefined,
};

Expand Down Expand Up @@ -512,6 +513,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
const data: any = {
...currentAlert,
type: isReport ? 'Report' : 'Alert',
force_screenshot: contentType === 'chart' && !isReport ? 'true' : 'false',
validator_type: conditionNotNull ? 'not null' : 'operator',
validator_config_json: conditionNotNull
? {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Add force_screenshot to alerts/reports
Revision ID: bb38f40aa3ff
Revises: 31bb738bd1d2
Create Date: 2021-12-10 19:25:29.802949
"""

# revision identifiers, used by Alembic.
revision = "bb38f40aa3ff"
down_revision = "31bb738bd1d2"

import sqlalchemy as sa
from alembic import op
from sqlalchemy.ext.declarative import declarative_base

from superset import db

Base = declarative_base()


class ReportSchedule(Base):
__tablename__ = "report_schedule"

id = sa.Column(sa.Integer, primary_key=True)
type = sa.Column(sa.String(50), nullable=False)
force_screenshot = sa.Column(sa.Boolean, default=False)


def upgrade():
with op.batch_alter_table("report_schedule") as batch_op:
batch_op.add_column(sa.Column("force_screenshot", sa.Boolean(), default=False))

bind = op.get_bind()
session = db.Session(bind=bind)

for report in session.query(ReportSchedule).all():
# Update existing alerts that send chart screenshots so that the cache is
# bypassed. We don't turn this one for dashboards because (1) it's currently
# not supported but also because (2) it can be very expensive.
report.force_screenshot = report.type == "Alert" and report.chart_id is not None

session.commit()


def downgrade():
with op.batch_alter_table("report_schedule") as batch_op:
batch_op.drop_column("force_screenshot")
3 changes: 3 additions & 0 deletions superset/models/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ class ReportSchedule(Model, AuditMixinNullable):
# Store the selected dashboard tabs etc.
extra = Column(Text, default="{}")

# (Reports) When generating a screenshot, bypass the cache?
force_screenshot = Column(Boolean, default=False)

def __repr__(self) -> str:
return str(self.name)

Expand Down
10 changes: 2 additions & 8 deletions superset/reports/commands/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,7 @@ def _get_url(
"""
# 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"
)
force = "true" if self._report_schedule.force_screenshot else "false"

if self._report_schedule.chart:
if result_format in {
Expand All @@ -181,7 +175,7 @@ def _get_url(
user_friendly=user_friendly,
dashboard_id_or_slug=self._report_schedule.dashboard_id,
standalone=DashboardStandaloneMode.REPORT.value,
force=force,
# force=force, TODO (betodealmeida) implement this
**kwargs,
)

Expand Down
2 changes: 2 additions & 0 deletions superset/reports/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ class ReportSchedulePostSchema(Schema):
default=ReportDataFormat.VISUALIZATION,
validate=validate.OneOf(choices=tuple(key.value for key in ReportDataFormat)),
)
force_screenshot = fields.Boolean(default=False)

@validates_schema
def validate_report_references( # pylint: disable=unused-argument,no-self-use
Expand Down Expand Up @@ -292,3 +293,4 @@ class ReportSchedulePutSchema(Schema):
default=ReportDataFormat.VISUALIZATION,
validate=validate.OneOf(choices=tuple(key.value for key in ReportDataFormat)),
)
force_screenshot = fields.Boolean(default=False)
58 changes: 58 additions & 0 deletions tests/integration_tests/reports/commands_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def create_report_notification(
grace_period: Optional[int] = None,
report_format: Optional[ReportDataFormat] = None,
name: Optional[str] = None,
force_screenshot: bool = False,
) -> ReportSchedule:
report_type = report_type or ReportScheduleType.REPORT
target = email_target or slack_channel
Expand Down Expand Up @@ -174,6 +175,7 @@ def create_report_notification(
validator_config_json=validator_config_json,
grace_period=grace_period,
report_format=report_format or ReportDataFormat.VISUALIZATION,
force_screenshot=force_screenshot,
)
return report_schedule

Expand Down Expand Up @@ -218,6 +220,18 @@ def create_report_email_chart():
cleanup_report_schedule(report_schedule)


@pytest.fixture()
def create_report_email_chart_force_screenshot():
with app.app_context():
chart = db.session.query(Slice).first()
report_schedule = create_report_notification(
email_target="[email protected]", chart=chart, force_screenshot=True
)
yield report_schedule

cleanup_report_schedule(report_schedule)


@pytest.fixture()
def create_report_email_chart_with_csv():
with app.app_context():
Expand Down Expand Up @@ -480,6 +494,7 @@ def create_alert_email_chart(request):
validator_config_json=param_config[request.param][
"validator_config_json"
],
force_screenshot=True,
)
yield report_schedule

Expand Down Expand Up @@ -678,6 +693,49 @@ def test_email_chart_report_schedule(
assert_log(ReportState.SUCCESS)


@pytest.mark.usefixtures(
"load_birth_names_dashboard_with_slices",
"create_report_email_chart_force_screenshot",
)
@patch("superset.reports.notifications.email.send_email_smtp")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
def test_email_chart_report_schedule_force_screenshot(
screenshot_mock, email_mock, create_report_email_chart_force_screenshot,
):
"""
ExecuteReport Command: Test chart email report schedule with screenshot
In this test ``force_screenshot`` is true, and the screenshot URL should
reflect that.
"""
# setup screenshot mock
screenshot_mock.return_value = SCREENSHOT_FILE

with freeze_time("2020-01-01T00:00:00Z"):
AsyncExecuteReportScheduleCommand(
TEST_ID, create_report_email_chart_force_screenshot.id, datetime.utcnow()
).run()

notification_targets = get_target_from_report_schedule(
create_report_email_chart_force_screenshot
)
# assert that the link sent is correct
assert (
'<a href="http://0.0.0.0:8080/superset/explore/?'
"form_data=%7B%22slice_id%22%3A+"
f"{create_report_email_chart_force_screenshot.chart.id}%7D&"
'standalone=true&force=true">Explore in Superset</a>'
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"
)
Expand Down
2 changes: 2 additions & 0 deletions tests/integration_tests/reports/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def insert_report_schedule(
recipients: Optional[List[ReportRecipients]] = None,
report_format: Optional[ReportDataFormat] = None,
logs: Optional[List[ReportExecutionLog]] = None,
force_screenshot: bool = False,
) -> ReportSchedule:
owners = owners or []
recipients = recipients or []
Expand All @@ -75,6 +76,7 @@ def insert_report_schedule(
logs=logs,
last_state=last_state,
report_format=report_format,
force_screenshot=force_screenshot,
)
db.session.add(report_schedule)
db.session.commit()
Expand Down

0 comments on commit 2cd8054

Please sign in to comment.