blob: d463390673f587d20e1f1241b7acd8db58cfebfb [file] [log] [blame]
# 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.
"""
DEPRECATION NOTICE: this module is deprecated and will be removed on 2.0.
"""
import enum
from typing import Type, Union
import simplejson as json
from croniter import croniter
from flask import flash, g, Markup
from flask_appbuilder import expose
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access
from flask_babel import lazy_gettext as _
from wtforms import BooleanField, Form, StringField
from superset import db, security_manager
from superset.constants import RouteMethod
from superset.exceptions import SupersetException
from superset.models.dashboard import Dashboard
from superset.models.schedules import (
DashboardEmailSchedule,
ScheduleType,
SliceEmailSchedule,
)
from superset.models.slice import Slice
from superset.tasks.schedules import schedule_email_report
from superset.typing import FlaskResponse
from superset.utils.core import get_email_address_list, json_iso_dttm_ser
from superset.views.core import json_success
from .base import DeleteMixin, SupersetModelView
class EmailScheduleView(
SupersetModelView, DeleteMixin
): # pylint: disable=too-many-ancestors
include_route_methods = RouteMethod.CRUD_SET
_extra_data = {"test_email": False, "test_email_recipients": None}
@property
def schedule_type(self) -> str:
raise NotImplementedError()
@property
def schedule_type_model(self) -> Type[Union[Dashboard, Slice]]:
raise NotImplementedError()
page_size = 20
add_exclude_columns = [
"user",
"created_on",
"changed_on",
"created_by",
"changed_by",
]
edit_exclude_columns = add_exclude_columns
description_columns = {
"deliver_as_group": "If enabled, send a single email to all "
"recipients (in email/To: field)",
"crontab": "Unix style crontab schedule to deliver emails. "
"Changes to schedules reflect in one hour.",
"delivery_type": "Indicates how the rendered content is delivered",
}
add_form_extra_fields = {
"test_email": BooleanField(
"Send Test Email",
default=False,
description="If enabled, we send a test mail on create / update",
),
"test_email_recipients": StringField(
"Test Email Recipients",
default=None,
description="List of recipients to send test email to. "
"If empty, we send it to the original recipients",
),
"test_slack_channel": StringField(
"Test Slack Channel",
default=None,
description="A slack channel to send a test message to.",
),
}
edit_form_extra_fields = add_form_extra_fields
def process_form(self, form: Form, is_created: bool) -> None:
if form.test_email_recipients.data:
test_email_recipients = form.test_email_recipients.data.strip()
else:
test_email_recipients = None
test_slack_channel = (
form.test_slack_channel.data.strip()
if form.test_slack_channel.data
else None
)
self._extra_data["test_email"] = form.test_email.data
self._extra_data["test_email_recipients"] = test_email_recipients
self._extra_data["test_slack_channel"] = test_slack_channel
def pre_add(self, item: "EmailScheduleView") -> None:
try:
recipients = get_email_address_list(item.recipients)
item.recipients = ", ".join(recipients)
except Exception:
raise SupersetException("Invalid email list")
item.user = item.user or g.user
if not croniter.is_valid(item.crontab):
raise SupersetException("Invalid crontab format")
def pre_update(self, item: "EmailScheduleView") -> None:
self.pre_add(item)
def post_add(self, item: "EmailScheduleView") -> None:
# Schedule a test mail if the user requested for it.
if self._extra_data["test_email"]:
recipients = self._extra_data["test_email_recipients"] or item.recipients
slack_channel = self._extra_data["test_slack_channel"] or item.slack_channel
args = (self.schedule_type, item.id)
kwargs = dict(recipients=recipients, slack_channel=slack_channel)
schedule_email_report.apply_async(args=args, kwargs=kwargs)
# Notify the user that schedule changes will be activate only in the
# next hour
if item.active:
flash("Schedule changes will get applied in one hour", "warning")
def post_update(self, item: "EmailScheduleView") -> None:
self.post_add(item)
@has_access
@expose("/fetch/<int:item_id>/", methods=["GET"])
def fetch_schedules(self, item_id: int) -> FlaskResponse:
query = db.session.query(self.datamodel.obj)
query = query.join(self.schedule_type_model).filter(
self.schedule_type_model.id == item_id
)
schedules = []
for schedule in query.all():
info = {"schedule": schedule.id}
for col in self.list_columns + self.add_exclude_columns:
info[col] = getattr(schedule, col)
if isinstance(info[col], enum.Enum):
info[col] = info[col].name
elif isinstance(info[col], security_manager.user_model):
info[col] = info[col].username
info["user"] = schedule.user.username
info[self.schedule_type] = getattr(schedule, self.schedule_type).id
schedules.append(info)
return json_success(json.dumps(schedules, default=json_iso_dttm_ser))
class DashboardEmailScheduleView(
EmailScheduleView
): # pylint: disable=too-many-ancestors
schedule_type = ScheduleType.dashboard
schedule_type_model = Dashboard
add_title = _("Schedule Email Reports for Dashboards")
edit_title = add_title
list_title = _("Manage Email Reports for Dashboards")
datamodel = SQLAInterface(DashboardEmailSchedule)
order_columns = ["user", "dashboard", "created_on"]
list_columns = [
"dashboard",
"active",
"crontab",
"user",
"deliver_as_group",
"delivery_type",
]
add_columns = [
"dashboard",
"active",
"crontab",
"recipients",
"slack_channel",
"deliver_as_group",
"delivery_type",
"test_email",
"test_email_recipients",
"test_slack_channel",
]
edit_columns = add_columns
search_columns = [
"dashboard",
"active",
"user",
"deliver_as_group",
"delivery_type",
]
label_columns = {
"dashboard": _("Dashboard"),
"created_on": _("Created On"),
"changed_on": _("Changed On"),
"user": _("User"),
"active": _("Active"),
"crontab": _("Crontab"),
"recipients": _("Recipients"),
"slack_channel": _("Slack Channel"),
"deliver_as_group": _("Deliver As Group"),
"delivery_type": _("Delivery Type"),
}
@expose("/list/")
@has_access
def list(self) -> FlaskResponse:
flash(
Markup(
_(
"This feature is deprecated and will be removed on 2.0. "
"Take a look at the replacement feature "
"<a href="
"'https://superset.apache.org/docs/installation/alerts-reports'>"
"Alerts & Reports documentation</a>"
)
),
"warning",
)
return super().list()
def pre_add(self, item: "DashboardEmailScheduleView") -> None:
if item.dashboard is None:
raise SupersetException("Dashboard is mandatory")
super(DashboardEmailScheduleView, self).pre_add(item)
class SliceEmailScheduleView(EmailScheduleView): # pylint: disable=too-many-ancestors
schedule_type = ScheduleType.slice
schedule_type_model = Slice
add_title = _("Schedule Email Reports for Charts")
edit_title = add_title
list_title = _("Manage Email Reports for Charts")
datamodel = SQLAInterface(SliceEmailSchedule)
order_columns = ["user", "slice", "created_on"]
list_columns = [
"slice",
"active",
"crontab",
"user",
"deliver_as_group",
"delivery_type",
"email_format",
]
add_columns = [
"slice",
"active",
"crontab",
"recipients",
"slack_channel",
"deliver_as_group",
"delivery_type",
"email_format",
"test_email",
"test_email_recipients",
"test_slack_channel",
]
edit_columns = add_columns
search_columns = [
"slice",
"active",
"user",
"deliver_as_group",
"delivery_type",
"email_format",
]
label_columns = {
"slice": _("Chart"),
"created_on": _("Created On"),
"changed_on": _("Changed On"),
"user": _("User"),
"active": _("Active"),
"crontab": _("Crontab"),
"recipients": _("Recipients"),
"slack_channel": _("Slack Channel"),
"deliver_as_group": _("Deliver As Group"),
"delivery_type": _("Delivery Type"),
"email_format": _("Email Format"),
}
@expose("/list/")
@has_access
def list(self) -> FlaskResponse:
flash(
Markup(
_(
"This feature is deprecated and will be removed on 2.0. "
"Take a look at the replacement feature "
"<a href="
"'https://superset.apache.org/docs/installation/alerts-reports'>"
"Alerts & Reports documentation</a>"
)
),
"warning",
)
return super().list()
def pre_add(self, item: "SliceEmailScheduleView") -> None:
if item.slice is None:
raise SupersetException("Slice is mandatory")
super(SliceEmailScheduleView, self).pre_add(item)