forked from GithubBackups/healthchecks
Update alert email template: more information, less styling
Fixes: #348
This commit is contained in:
parent
c3b6d40012
commit
8fe8e0f605
@ -343,6 +343,14 @@ class Project(models.Model):
|
|||||||
break
|
break
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
def get_n_down(self):
|
||||||
|
result = 0
|
||||||
|
for check in self.check_set.all():
|
||||||
|
if check.get_status() == "down":
|
||||||
|
result += 1
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def have_channel_issues(self):
|
def have_channel_issues(self):
|
||||||
errors = list(self.channel_set.values_list("last_error", flat=True))
|
errors = list(self.channel_set.values_list("last_error", flat=True))
|
||||||
|
|
||||||
@ -363,6 +371,9 @@ class Project(models.Model):
|
|||||||
frag = urlencode({self.api_key_readonly: str(self)}, quote_via=quote)
|
frag = urlencode({self.api_key_readonly: str(self)}, quote_via=quote)
|
||||||
return reverse("hc-dashboard") + "#" + frag
|
return reverse("hc-dashboard") + "#" + frag
|
||||||
|
|
||||||
|
def checks_url(self):
|
||||||
|
return settings.SITE_ROOT + reverse("hc-checks", args=[self.code])
|
||||||
|
|
||||||
|
|
||||||
class Member(models.Model):
|
class Member(models.Model):
|
||||||
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
|
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
|
||||||
|
@ -329,58 +329,6 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
"get", "http://foo.com", headers=headers, timeout=5
|
"get", "http://foo.com", headers=headers, timeout=5
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_email(self):
|
|
||||||
self._setup_data("email", "alice@example.org")
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
|
|
||||||
n = Notification.objects.get()
|
|
||||||
self.assertEqual(n.error, "")
|
|
||||||
|
|
||||||
# And email should have been sent
|
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
|
||||||
|
|
||||||
email = mail.outbox[0]
|
|
||||||
self.assertEqual(email.to[0], "alice@example.org")
|
|
||||||
self.assertTrue("X-Status-Url" in email.extra_headers)
|
|
||||||
self.assertTrue("List-Unsubscribe" in email.extra_headers)
|
|
||||||
self.assertTrue("List-Unsubscribe-Post" in email.extra_headers)
|
|
||||||
|
|
||||||
def test_email_transport_handles_json_value(self):
|
|
||||||
payload = {"value": "alice@example.org", "up": True, "down": True}
|
|
||||||
self._setup_data("email", json.dumps(payload))
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
|
|
||||||
# And email should have been sent
|
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
|
||||||
|
|
||||||
email = mail.outbox[0]
|
|
||||||
self.assertEqual(email.to[0], "alice@example.org")
|
|
||||||
|
|
||||||
def test_it_reports_unverified_email(self):
|
|
||||||
self._setup_data("email", "alice@example.org", email_verified=False)
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
|
|
||||||
# If an email is not verified, it should say so in the notification:
|
|
||||||
n = Notification.objects.get()
|
|
||||||
self.assertEqual(n.error, "Email not verified")
|
|
||||||
|
|
||||||
def test_email_checks_up_down_flags(self):
|
|
||||||
payload = {"value": "alice@example.org", "up": True, "down": False}
|
|
||||||
self._setup_data("email", json.dumps(payload))
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
|
|
||||||
# This channel should not notify on "down" events:
|
|
||||||
self.assertEqual(Notification.objects.count(), 0)
|
|
||||||
self.assertEqual(len(mail.outbox), 0)
|
|
||||||
|
|
||||||
def test_email_handles_amperstand(self):
|
|
||||||
self._setup_data("email", "alice@example.org")
|
|
||||||
self.check.name = "Foo & Bar"
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
|
|
||||||
email = mail.outbox[0]
|
|
||||||
self.assertEqual(email.subject, "DOWN | Foo & Bar")
|
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
@patch("hc.api.transports.requests.request")
|
||||||
def test_pd(self, mock_post):
|
def test_pd(self, mock_post):
|
||||||
self._setup_data("pd", "123")
|
self._setup_data("pd", "123")
|
||||||
|
151
hc/api/tests/test_notify_email.py
Normal file
151
hc/api/tests/test_notify_email.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from datetime import timedelta as td
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core import mail
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from hc.api.models import Channel, Check, Notification, Ping
|
||||||
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyTestCase(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.check = Check(project=self.project)
|
||||||
|
self.check.name = "Daily Backup"
|
||||||
|
self.check.desc = "Line 1\nLine2"
|
||||||
|
self.check.tags = "foo bar"
|
||||||
|
self.check.status = "down"
|
||||||
|
self.check.last_ping = now() - td(minutes=61)
|
||||||
|
self.check.n_pings = 112233
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
self.ping = Ping(owner=self.check)
|
||||||
|
self.ping.remote_addr = "1.2.3.4"
|
||||||
|
self.ping.body = "Body Line 1\nBody Line 2"
|
||||||
|
self.ping.save()
|
||||||
|
|
||||||
|
self.channel = Channel(project=self.project)
|
||||||
|
self.channel.kind = "email"
|
||||||
|
self.channel.value = "alice@example.org"
|
||||||
|
self.channel.email_verified = True
|
||||||
|
self.channel.save()
|
||||||
|
self.channel.checks.add(self.check)
|
||||||
|
|
||||||
|
def test_email(self):
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
n = Notification.objects.get()
|
||||||
|
self.assertEqual(n.error, "")
|
||||||
|
|
||||||
|
# And email should have been sent
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
self.assertEqual(email.to[0], "alice@example.org")
|
||||||
|
self.assertTrue("X-Status-Url" in email.extra_headers)
|
||||||
|
self.assertTrue("List-Unsubscribe" in email.extra_headers)
|
||||||
|
self.assertTrue("List-Unsubscribe-Post" in email.extra_headers)
|
||||||
|
|
||||||
|
html = email.alternatives[0][0]
|
||||||
|
self.assertIn("Daily Backup", html)
|
||||||
|
self.assertIn("Line 1<br>Line2", html)
|
||||||
|
self.assertIn("Alices Project", html)
|
||||||
|
self.assertIn("foo</code>", html)
|
||||||
|
self.assertIn("bar</code>", html)
|
||||||
|
self.assertIn("1 day", html)
|
||||||
|
self.assertIn("from 1.2.3.4", html)
|
||||||
|
self.assertIn("112233", html)
|
||||||
|
self.assertIn("Body Line 1<br>Body Line 2", html)
|
||||||
|
|
||||||
|
def test_it_shows_cron_schedule(self):
|
||||||
|
self.check.kind = "cron"
|
||||||
|
self.check.schedule = "0 18-23,0-8 * * *"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
html = email.alternatives[0][0]
|
||||||
|
|
||||||
|
self.assertIn("<code>0 18-23,0-8 * * *</code>", html)
|
||||||
|
|
||||||
|
def test_it_truncates_long_body(self):
|
||||||
|
self.ping.body = "X" * 10000 + ", and the rest gets cut off"
|
||||||
|
self.ping.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
html = email.alternatives[0][0]
|
||||||
|
|
||||||
|
self.assertIn("[truncated]", html)
|
||||||
|
self.assertNotIn("the rest gets cut off", html)
|
||||||
|
|
||||||
|
def test_it_handles_missing_ping_object(self):
|
||||||
|
self.ping.delete()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
html = email.alternatives[0][0]
|
||||||
|
|
||||||
|
self.assertIn("Daily Backup", html)
|
||||||
|
|
||||||
|
def test_it_handles_missing_profile(self):
|
||||||
|
self.channel.value = "alice+notifications@example.org"
|
||||||
|
self.channel.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
self.assertEqual(email.to[0], "alice+notifications@example.org")
|
||||||
|
|
||||||
|
html = email.alternatives[0][0]
|
||||||
|
self.assertIn("Daily Backup", html)
|
||||||
|
self.assertNotIn("Projects Overview", html)
|
||||||
|
|
||||||
|
def test_email_transport_handles_json_value(self):
|
||||||
|
payload = {"value": "alice@example.org", "up": True, "down": True}
|
||||||
|
self.channel.value = json.dumps(payload)
|
||||||
|
self.channel.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
# And email should have been sent
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
self.assertEqual(email.to[0], "alice@example.org")
|
||||||
|
|
||||||
|
def test_it_reports_unverified_email(self):
|
||||||
|
self.channel.email_verified = False
|
||||||
|
self.channel.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
# If an email is not verified, it should say so in the notification:
|
||||||
|
n = Notification.objects.get()
|
||||||
|
self.assertEqual(n.error, "Email not verified")
|
||||||
|
|
||||||
|
def test_email_checks_up_down_flags(self):
|
||||||
|
payload = {"value": "alice@example.org", "up": True, "down": False}
|
||||||
|
self.channel.value = json.dumps(payload)
|
||||||
|
self.channel.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
# This channel should not notify on "down" events:
|
||||||
|
self.assertEqual(Notification.objects.count(), 0)
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
def test_email_handles_amperstand(self):
|
||||||
|
self.check.name = "Foo & Bar"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
email = mail.outbox[0]
|
||||||
|
self.assertEqual(email.subject, "DOWN | Foo & Bar")
|
@ -68,21 +68,20 @@ class Email(Transport):
|
|||||||
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
|
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
from hc.accounts.models import Profile
|
||||||
# Look up the sorting preference for this email address
|
|
||||||
p = Profile.objects.get(user__email=self.channel.email_value)
|
# If this email address has an associated account, include
|
||||||
sort = p.sort
|
# a summary of projects the account has access to
|
||||||
except Profile.DoesNotExist:
|
try:
|
||||||
# Default sort order is by check's creation time
|
profile = Profile.objects.get(user__email=self.channel.email_value)
|
||||||
sort = "created"
|
projects = list(profile.projects())
|
||||||
|
except Profile.DoesNotExist:
|
||||||
|
projects = None
|
||||||
|
|
||||||
# list() executes the query, to avoid DB access while
|
|
||||||
# rendering a template
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"check": check,
|
"check": check,
|
||||||
"checks": list(self.checks()),
|
"ping": check.ping_set.order_by("created").last(),
|
||||||
"sort": sort,
|
"projects": projects,
|
||||||
"now": timezone.now(),
|
|
||||||
"unsub_link": unsub_link,
|
"unsub_link": unsub_link,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -848,7 +848,7 @@ def unsubscribe_email(request, code, signed_token):
|
|||||||
def send_test_notification(request, code):
|
def send_test_notification(request, code):
|
||||||
channel, rw = _get_channel_for_user(request, code)
|
channel, rw = _get_channel_for_user(request, code)
|
||||||
|
|
||||||
dummy = Check(name="TEST", status="down")
|
dummy = Check(name="TEST", status="down", project=channel.project)
|
||||||
dummy.last_ping = timezone.now() - td(days=1)
|
dummy.last_ping = timezone.now() - td(days=1)
|
||||||
dummy.n_pings = 42
|
dummy.n_pings = 42
|
||||||
|
|
||||||
|
@ -1,32 +1,108 @@
|
|||||||
{% extends "emails/base.html" %}
|
{% load hc_extras humanize %}
|
||||||
{% load hc_extras %}
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
The check <a href="{{ check.details_url }}">{{ check.name_then_code|mangle_link }}</a>
|
<p>
|
||||||
has gone <strong>{{ check.status|upper }}</strong>.
|
"{{ check.name_then_code }}" is {{ check.status|upper }}.
|
||||||
<br>
|
<a href="{{ check.details_url }}">View on {% site_name %}…</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
{% if check.status == "down" and check.desc %}
|
|
||||||
Additional notes:<br><br>
|
{% if check.desc %}
|
||||||
<div style="padding: 10px 15px; background: #F2F4F6;">
|
<p>
|
||||||
{{ check.desc|linebreaksbr|urlize }}
|
<b>Description</b><br>
|
||||||
</div>
|
{{ check.desc|linebreaksbr }}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<br />
|
{% cycle '' '</tr><tr>' as trtr silent %}
|
||||||
A summary of your checks:
|
<table>
|
||||||
<br />
|
<tr>
|
||||||
|
{% if check.project.name %}
|
||||||
|
<td style="padding-right: 32px; padding-bottom: 8px; vertical-align: top;">
|
||||||
|
<b>Project</b><br>
|
||||||
|
{{ check.project.name }}
|
||||||
|
</td>
|
||||||
|
{{ trtr|safe }} {% cycle trtr %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include "emails/summary-html.html" %}
|
{% if check.tags_list %}
|
||||||
|
<td style="padding-right: 32px; padding-bottom: 8px; vertical-align: top;">
|
||||||
|
<b>Tags</b><br>
|
||||||
|
{% for tag in check.tags_list %}
|
||||||
|
<code style="background-color: #eeeeee; padding: 2px 4px; border-radius: 2px;">{{ tag }}</code>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
{{ trtr|safe }} {% cycle trtr %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
Thanks,<br>
|
{% if check.kind == "simple" %}
|
||||||
The {% site_name %} Team
|
<td style="padding-right: 32px; padding-bottom: 8px; vertical-align: top;">
|
||||||
|
<b>Period</b><br>
|
||||||
|
{{ check.timeout|hc_duration }}
|
||||||
|
</td>
|
||||||
|
{{ trtr|safe }} {% cycle trtr %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% if check.kind == "cron" %}
|
||||||
|
<td style="padding-right: 32px; padding-bottom: 8px; vertical-align: top;">
|
||||||
|
<b>Schedule</b><br>
|
||||||
|
<code>{{ check.schedule }}</code>
|
||||||
|
</td>
|
||||||
|
{% if trttr %}Yo!{% endif %}
|
||||||
|
{{ trtr|safe }} {% cycle trtr %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block unsub %}
|
{% if ping %}
|
||||||
<br>
|
<td style="padding-right: 32px; padding-bottom: 8px; vertical-align: top;">
|
||||||
|
<b>Last Ping</b><br>
|
||||||
|
{{ ping.created|naturaltime }}{% if ping.remote_addr %}, from {{ ping.remote_addr }}{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if trttr %}Yo!{% endif %}
|
||||||
|
{{ trtr|safe }} {% cycle trtr %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<td style="padding-right: 32px; padding-bottom: 8px; vertical-align: top;">
|
||||||
|
<b>Total Pings</b><br>
|
||||||
|
{{ check.n_pings }}
|
||||||
|
{% if check.created %}(since {{ check.created|date:'M j, Y' }}){% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if ping.body %}
|
||||||
|
<p><b>Last Ping Body</b></p>
|
||||||
|
<pre style="background: #eeeeee">{{ ping.body|slice:":10000"|linebreaksbr }}{% if ping.body|length > 10000 %} [truncated]{% endif %}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if projects %}
|
||||||
|
<p><b>Projects Overview</b></p>
|
||||||
|
<table>
|
||||||
|
{% for project in projects %}
|
||||||
|
<tr>
|
||||||
|
<td style="padding-right: 32px; padding-bottom: 4px;">
|
||||||
|
<a href="{{ project.checks_url }}">{{ project }}</a>
|
||||||
|
</td>
|
||||||
|
<td style="padding-right: 32px; padding-bottom: 4px;">
|
||||||
|
{% with project.get_n_down as n_down %}
|
||||||
|
{% if n_down %}
|
||||||
|
<b>{{ n_down }} check{{ n_down|pluralize }} down</b>
|
||||||
|
{% else %}
|
||||||
|
OK, all checks up
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p style="color: #666666">
|
||||||
|
—<br>
|
||||||
|
{% site_name %}<br>
|
||||||
<a href="{{ unsub_link }}" target="_blank" style="color: #666666; text-decoration: underline;">
|
<a href="{{ unsub_link }}" target="_blank" style="color: #666666; text-decoration: underline;">
|
||||||
|
{% if check.project.name %}
|
||||||
|
Unsubscribe from "{{ check.project.name }}" notifications
|
||||||
|
{% else %}
|
||||||
Unsubscribe
|
Unsubscribe
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
</p>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user