diff --git a/client/app/pages/alert/AlertEdit.jsx b/client/app/pages/alert/AlertEdit.jsx
index 64bff87631..be3446c3ac 100644
--- a/client/app/pages/alert/AlertEdit.jsx
+++ b/client/app/pages/alert/AlertEdit.jsx
@@ -103,6 +103,8 @@ export default class AlertEdit extends React.Component {
setSubject={subject => onNotificationTemplateChange({ custom_subject: subject })}
body={options.custom_body}
setBody={body => onNotificationTemplateChange({ custom_body: body })}
+ allowHtml={options.allow_html}
+ setAllowHtml={allowHtml => onNotificationTemplateChange({ allow_html: allowHtml })}
/>
>
diff --git a/client/app/pages/alert/AlertNew.jsx b/client/app/pages/alert/AlertNew.jsx
index 9e128ed2fc..1392b8ceeb 100644
--- a/client/app/pages/alert/AlertNew.jsx
+++ b/client/app/pages/alert/AlertNew.jsx
@@ -70,6 +70,8 @@ export default class AlertNew extends React.Component {
setSubject={subject => onNotificationTemplateChange({ custom_subject: subject })}
body={options.custom_body}
setBody={body => onNotificationTemplateChange({ custom_body: body })}
+ allowHtml={options.allow_html}
+ setAllowHtml={allowHtml => onNotificationTemplateChange({ allow_html: allowHtml })}
/>
>
diff --git a/client/app/pages/alert/components/NotificationTemplate.jsx b/client/app/pages/alert/components/NotificationTemplate.jsx
index 5aa750207b..173c88f887 100644
--- a/client/app/pages/alert/components/NotificationTemplate.jsx
+++ b/client/app/pages/alert/components/NotificationTemplate.jsx
@@ -4,12 +4,14 @@ import { head, isEmpty, isNull, isUndefined } from "lodash";
import Mustache from "mustache";
import HelpTrigger from "@/components/HelpTrigger";
+import Tooltip from "@/components/Tooltip";
import { Alert as AlertType, Query as QueryType } from "@/components/proptypes";
import Input from "antd/lib/input";
import Select from "antd/lib/select";
import Modal from "antd/lib/modal";
import Switch from "antd/lib/switch";
+import Checkbox from "antd/lib/checkbox";
import "./NotificationTemplate.less";
@@ -30,7 +32,18 @@ function normalizeCustomTemplateData(alert, query, columnNames, resultValues) {
};
}
-function NotificationTemplate({ alert, query, columnNames, resultValues, subject, setSubject, body, setBody }) {
+function NotificationTemplate({
+ alert,
+ query,
+ columnNames,
+ resultValues,
+ subject,
+ setSubject,
+ body,
+ setBody,
+ allowHtml,
+ setAllowHtml,
+}) {
const hasContent = !!(subject || body);
const [enabled, setEnabled] = useState(hasContent ? 1 : 0);
const [showPreview, setShowPreview] = useState(false);
@@ -49,6 +62,7 @@ function NotificationTemplate({ alert, query, columnNames, resultValues, subject
onOk: () => {
setSubject(null);
setBody(null);
+ setAllowHtml(false);
setEnabled(value);
setShowPreview(false);
},
@@ -100,6 +114,16 @@ function NotificationTemplate({ alert, query, columnNames, resultValues, subject
Formatting guide{" "}
(help)
+ setAllowHtml(e.target.checked)}
+ data-test="AlertTemplateAllowHTML">
+ Allow HTML content
+
+
+
+
)}
@@ -115,11 +139,14 @@ NotificationTemplate.propTypes = {
setSubject: PropTypes.func.isRequired,
body: PropTypes.string,
setBody: PropTypes.func.isRequired,
+ allowHtml: PropTypes.bool,
+ setAllowHtml: PropTypes.func.isRequired,
};
NotificationTemplate.defaultProps = {
subject: "",
body: "",
+ allowHtml: false,
};
export default NotificationTemplate;
diff --git a/client/app/pages/alert/components/NotificationTemplate.less b/client/app/pages/alert/components/NotificationTemplate.less
index 15a4907c34..661bba4bfe 100644
--- a/client/app/pages/alert/components/NotificationTemplate.less
+++ b/client/app/pages/alert/components/NotificationTemplate.less
@@ -33,4 +33,15 @@
.alert-template-preview {
margin: 0 0 0 5px !important;
}
+
+ .alert-template-allow-html {
+ display: block;
+ margin-top: 8px;
+
+ .fa-question-circle {
+ margin-left: 5px;
+ color: rgba(0, 0, 0, 0.45);
+ cursor: help;
+ }
+ }
}
\ No newline at end of file
diff --git a/redash/models/__init__.py b/redash/models/__init__.py
index 5f5918cd60..c51ea7fb8d 100644
--- a/redash/models/__init__.py
+++ b/redash/models/__init__.py
@@ -1051,7 +1051,7 @@ def evaluate(self):
def subscribers(self):
return User.query.join(AlertSubscription).filter(AlertSubscription.alert == self)
- def render_template(self, template):
+ def render_template(self, template, escape=True):
if template is None:
return ""
@@ -1081,17 +1081,18 @@ def render_template(self, template):
"QUERY_RESULT_COLS": data["columns"],
"QUERY_RESULT_TABLE": result_table,
}
- return mustache_render_escape(template, context)
+ render = mustache_render_escape if escape else mustache_render
+ return render(template, context)
@property
def custom_body(self):
template = self.options.get("custom_body", self.options.get("template"))
- return self.render_template(template)
+ return self.render_template(template, escape=self.options.get("allow_html") is not True)
@property
def custom_subject(self):
template = self.options.get("custom_subject")
- return self.render_template(template)
+ return self.render_template(template, escape=self.options.get("allow_html") is not True)
@property
def groups(self):
diff --git a/tests/models/test_alerts.py b/tests/models/test_alerts.py
index fbd82dcce1..20b2157d16 100644
--- a/tests/models/test_alerts.py
+++ b/tests/models/test_alerts.py
@@ -216,3 +216,32 @@ def test_render_custom_alert_template_query_table(self):
"""
result = alert.render_template(textwrap.dedent(custom_alert))
self.assertMultiLineEqual(result, textwrap.dedent(expected))
+
+ def test_custom_body_escapes_html_by_default(self):
+ alert = self.create_alert(get_results("bold"))
+ alert.options["custom_body"] = "Value: {{QUERY_RESULT_VALUE}}"
+ self.assertEqual(alert.custom_body, "Value: <b>bold</b>")
+
+ def test_custom_body_renders_html_when_allow_html_enabled(self):
+ alert = self.create_alert(get_results("bold"))
+ alert.options["custom_body"] = "Value: {{QUERY_RESULT_VALUE}}"
+ alert.options["allow_html"] = True
+ self.assertEqual(alert.custom_body, "Value: bold")
+
+ def test_custom_subject_respects_allow_html(self):
+ alert = self.create_alert(get_results("bold"))
+ alert.options["custom_subject"] = "{{QUERY_RESULT_VALUE}}"
+ self.assertEqual(alert.custom_subject, "<b>bold</b>")
+ alert.options["allow_html"] = True
+ self.assertEqual(alert.custom_subject, "bold")
+
+ def test_allow_html_does_not_affect_default_render_template(self):
+ alert = self.create_alert(get_results("bold"))
+ alert.options["allow_html"] = True
+ self.assertEqual(alert.render_template("{{QUERY_RESULT_VALUE}}"), "<b>bold</b>")
+
+ def test_allow_html_requires_boolean_true(self):
+ alert = self.create_alert(get_results("bold"))
+ alert.options["custom_body"] = "Value: {{QUERY_RESULT_VALUE}}"
+ alert.options["allow_html"] = "false"
+ self.assertEqual(alert.custom_body, "Value: <b>bold</b>")