schedules.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. import enum
  18. from typing import Optional, Type
  19. import simplejson as json
  20. from croniter import croniter
  21. from flask import flash, g
  22. from flask_appbuilder import expose
  23. from flask_appbuilder.models.sqla.interface import SQLAInterface
  24. from flask_appbuilder.security.decorators import has_access
  25. from flask_babel import lazy_gettext as _
  26. from wtforms import BooleanField, StringField
  27. from superset import db, security_manager
  28. from superset.constants import RouteMethod
  29. from superset.exceptions import SupersetException
  30. from superset.models.dashboard import Dashboard
  31. from superset.models.schedules import (
  32. DashboardEmailSchedule,
  33. ScheduleType,
  34. SliceEmailSchedule,
  35. )
  36. from superset.models.slice import Slice
  37. from superset.tasks.schedules import schedule_email_report
  38. from superset.utils.core import get_email_address_list, json_iso_dttm_ser
  39. from superset.views.core import json_success
  40. from .base import DeleteMixin, SupersetModelView
  41. class EmailScheduleView(
  42. SupersetModelView, DeleteMixin
  43. ): # pylint: disable=too-many-ancestors
  44. include_route_methods = RouteMethod.CRUD_SET
  45. _extra_data = {"test_email": False, "test_email_recipients": None}
  46. schedule_type: Optional[Type] = None
  47. schedule_type_model: Optional[Type] = None
  48. page_size = 20
  49. add_exclude_columns = [
  50. "user",
  51. "created_on",
  52. "changed_on",
  53. "created_by",
  54. "changed_by",
  55. ]
  56. edit_exclude_columns = add_exclude_columns
  57. description_columns = {
  58. "deliver_as_group": "If enabled, send a single email to all "
  59. "recipients (in email/To: field)",
  60. "crontab": "Unix style crontab schedule to deliver emails. "
  61. "Changes to schedules reflect in one hour.",
  62. "delivery_type": "Indicates how the rendered content is delivered",
  63. }
  64. add_form_extra_fields = {
  65. "test_email": BooleanField(
  66. "Send Test Email",
  67. default=False,
  68. description="If enabled, we send a test mail on create / update",
  69. ),
  70. "test_email_recipients": StringField(
  71. "Test Email Recipients",
  72. default=None,
  73. description="List of recipients to send test email to. "
  74. "If empty, we send it to the original recipients",
  75. ),
  76. }
  77. edit_form_extra_fields = add_form_extra_fields
  78. def process_form(self, form, is_created):
  79. if form.test_email_recipients.data:
  80. test_email_recipients = form.test_email_recipients.data.strip()
  81. else:
  82. test_email_recipients = None
  83. self._extra_data["test_email"] = form.test_email.data
  84. self._extra_data["test_email_recipients"] = test_email_recipients
  85. def pre_add(self, item):
  86. try:
  87. recipients = get_email_address_list(item.recipients)
  88. item.recipients = ", ".join(recipients)
  89. except Exception:
  90. raise SupersetException("Invalid email list")
  91. item.user = item.user or g.user
  92. if not croniter.is_valid(item.crontab):
  93. raise SupersetException("Invalid crontab format")
  94. def pre_update(self, item):
  95. self.pre_add(item)
  96. def post_add(self, item):
  97. # Schedule a test mail if the user requested for it.
  98. if self._extra_data["test_email"]:
  99. recipients = self._extra_data["test_email_recipients"] or item.recipients
  100. args = (self.schedule_type, item.id)
  101. kwargs = dict(recipients=recipients)
  102. schedule_email_report.apply_async(args=args, kwargs=kwargs)
  103. # Notify the user that schedule changes will be activate only in the
  104. # next hour
  105. if item.active:
  106. flash("Schedule changes will get applied in one hour", "warning")
  107. def post_update(self, item):
  108. self.post_add(item)
  109. @has_access
  110. @expose("/fetch/<int:item_id>/", methods=["GET"])
  111. def fetch_schedules(self, item_id):
  112. query = db.session.query(self.datamodel.obj)
  113. query = query.join(self.schedule_type_model).filter(
  114. self.schedule_type_model.id == item_id
  115. )
  116. schedules = []
  117. for schedule in query.all():
  118. info = {"schedule": schedule.id}
  119. for col in self.list_columns + self.add_exclude_columns:
  120. info[col] = getattr(schedule, col)
  121. if isinstance(info[col], enum.Enum):
  122. info[col] = info[col].name
  123. elif isinstance(info[col], security_manager.user_model):
  124. info[col] = info[col].username
  125. info["user"] = schedule.user.username
  126. info[self.schedule_type] = getattr(schedule, self.schedule_type).id
  127. schedules.append(info)
  128. return json_success(json.dumps(schedules, default=json_iso_dttm_ser))
  129. class DashboardEmailScheduleView(
  130. EmailScheduleView
  131. ): # pylint: disable=too-many-ancestors
  132. schedule_type = ScheduleType.dashboard.value
  133. schedule_type_model = Dashboard
  134. add_title = _("Schedule Email Reports for Dashboards")
  135. edit_title = add_title
  136. list_title = _("Manage Email Reports for Dashboards")
  137. datamodel = SQLAInterface(DashboardEmailSchedule)
  138. order_columns = ["user", "dashboard", "created_on"]
  139. list_columns = [
  140. "dashboard",
  141. "active",
  142. "crontab",
  143. "user",
  144. "deliver_as_group",
  145. "delivery_type",
  146. ]
  147. add_columns = [
  148. "dashboard",
  149. "active",
  150. "crontab",
  151. "recipients",
  152. "deliver_as_group",
  153. "delivery_type",
  154. "test_email",
  155. "test_email_recipients",
  156. ]
  157. edit_columns = add_columns
  158. search_columns = [
  159. "dashboard",
  160. "active",
  161. "user",
  162. "deliver_as_group",
  163. "delivery_type",
  164. ]
  165. label_columns = {
  166. "dashboard": _("Dashboard"),
  167. "created_on": _("Created On"),
  168. "changed_on": _("Changed On"),
  169. "user": _("User"),
  170. "active": _("Active"),
  171. "crontab": _("Crontab"),
  172. "recipients": _("Recipients"),
  173. "deliver_as_group": _("Deliver As Group"),
  174. "delivery_type": _("Delivery Type"),
  175. }
  176. def pre_add(self, item):
  177. if item.dashboard is None:
  178. raise SupersetException("Dashboard is mandatory")
  179. super(DashboardEmailScheduleView, self).pre_add(item)
  180. class SliceEmailScheduleView(EmailScheduleView): # pylint: disable=too-many-ancestors
  181. schedule_type = ScheduleType.slice.value
  182. schedule_type_model = Slice
  183. add_title = _("Schedule Email Reports for Charts")
  184. edit_title = add_title
  185. list_title = _("Manage Email Reports for Charts")
  186. datamodel = SQLAInterface(SliceEmailSchedule)
  187. order_columns = ["user", "slice", "created_on"]
  188. list_columns = [
  189. "slice",
  190. "active",
  191. "crontab",
  192. "user",
  193. "deliver_as_group",
  194. "delivery_type",
  195. "email_format",
  196. ]
  197. add_columns = [
  198. "slice",
  199. "active",
  200. "crontab",
  201. "recipients",
  202. "deliver_as_group",
  203. "delivery_type",
  204. "email_format",
  205. "test_email",
  206. "test_email_recipients",
  207. ]
  208. edit_columns = add_columns
  209. search_columns = [
  210. "slice",
  211. "active",
  212. "user",
  213. "deliver_as_group",
  214. "delivery_type",
  215. "email_format",
  216. ]
  217. label_columns = {
  218. "slice": _("Chart"),
  219. "created_on": _("Created On"),
  220. "changed_on": _("Changed On"),
  221. "user": _("User"),
  222. "active": _("Active"),
  223. "crontab": _("Crontab"),
  224. "recipients": _("Recipients"),
  225. "deliver_as_group": _("Deliver As Group"),
  226. "delivery_type": _("Delivery Type"),
  227. "email_format": _("Email Format"),
  228. }
  229. def pre_add(self, item):
  230. if item.slice is None:
  231. raise SupersetException("Slice is mandatory")
  232. super(SliceEmailScheduleView, self).pre_add(item)