diff --git a/hc/accounts/models.py b/hc/accounts/models.py
index a67e9396..878944d3 100644
--- a/hc/accounts/models.py
+++ b/hc/accounts/models.py
@@ -343,6 +343,14 @@ class Project(models.Model):
break
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):
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)
return reverse("hc-dashboard") + "#" + frag
+ def checks_url(self):
+ return settings.SITE_ROOT + reverse("hc-checks", args=[self.code])
+
class Member(models.Model):
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py
index 637966da..a5679eee 100644
--- a/hc/api/tests/test_notify.py
+++ b/hc/api/tests/test_notify.py
@@ -329,58 +329,6 @@ class NotifyTestCase(BaseTestCase):
"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")
def test_pd(self, mock_post):
self._setup_data("pd", "123")
diff --git a/hc/api/tests/test_notify_email.py b/hc/api/tests/test_notify_email.py
new file mode 100644
index 00000000..cbcc4408
--- /dev/null
+++ b/hc/api/tests/test_notify_email.py
@@ -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
Line2", html)
+ self.assertIn("Alices Project", html)
+ self.assertIn("foo", html)
+ self.assertIn("bar", html)
+ self.assertIn("1 day", html)
+ self.assertIn("from 1.2.3.4", html)
+ self.assertIn("112233", html)
+ self.assertIn("Body Line 1
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("0 18-23,0-8 * * *
", 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")
diff --git a/hc/api/transports.py b/hc/api/transports.py
index 4da12981..ca5ca3ac 100644
--- a/hc/api/transports.py
+++ b/hc/api/transports.py
@@ -68,21 +68,20 @@ class Email(Transport):
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
}
+ from hc.accounts.models import Profile
+
+ # If this email address has an associated account, include
+ # a summary of projects the account has access to
try:
- # Look up the sorting preference for this email address
- p = Profile.objects.get(user__email=self.channel.email_value)
- sort = p.sort
+ profile = Profile.objects.get(user__email=self.channel.email_value)
+ projects = list(profile.projects())
except Profile.DoesNotExist:
- # Default sort order is by check's creation time
- sort = "created"
+ projects = None
- # list() executes the query, to avoid DB access while
- # rendering a template
ctx = {
"check": check,
- "checks": list(self.checks()),
- "sort": sort,
- "now": timezone.now(),
+ "ping": check.ping_set.order_by("created").last(),
+ "projects": projects,
"unsub_link": unsub_link,
}
diff --git a/hc/front/views.py b/hc/front/views.py
index f4ece3c6..7d045a8a 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -848,7 +848,7 @@ def unsubscribe_email(request, code, signed_token):
def send_test_notification(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.n_pings = 42
diff --git a/templates/emails/alert-body-html.html b/templates/emails/alert-body-html.html
index 54bf31e4..a99a5bb4 100644
--- a/templates/emails/alert-body-html.html
+++ b/templates/emails/alert-body-html.html
@@ -1,32 +1,108 @@
-{% extends "emails/base.html" %}
-{% load hc_extras %}
-{% block content %}
-
-The check {{ check.name_then_code|mangle_link }}
-has gone {{ check.status|upper }}.
-
-
-{% if check.status == "down" and check.desc %}
-Additional notes:
-
+ "{{ check.name_then_code }}" is {{ check.status|upper }}. + View on {% site_name %}… +
+ + +{% if check.desc %} +
+ Description
+ {{ check.desc|linebreaksbr }}
+
+ Project + {{ check.project.name }} + |
+ {{ trtr|safe }} {% cycle trtr %}
+ {% endif %}
+
+ {% if check.tags_list %}
+
+ Tags + {% for tag in check.tags_list %} + {{ tag }}
+ {% endfor %}
+ |
+ {{ trtr|safe }} {% cycle trtr %}
+ {% endif %}
+
+ {% if check.kind == "simple" %}
+
+ Period + {{ check.timeout|hc_duration }} + |
+ {{ trtr|safe }} {% cycle trtr %}
+ {% endif %}
-{% include "emails/summary-html.html" %}
+ {% if check.kind == "cron" %}
+
+ Schedule + {{ check.schedule }}
+ |
+ {% if trttr %}Yo!{% endif %}
+ {{ trtr|safe }} {% cycle trtr %}
+ {% endif %}
-Thanks,
+ Last Ping + {{ ping.created|naturaltime }}{% if ping.remote_addr %}, from {{ ping.remote_addr }}{% endif %} + |
+ {% if trttr %}Yo!{% endif %}
+ {{ trtr|safe }} {% cycle trtr %}
+ {% endif %}
-{% endblock %}
+
+ Total Pings + {{ check.n_pings }} + {% if check.created %}(since {{ check.created|date:'M j, Y' }}){% endif %} + |
+
Last Ping Body
+{{ ping.body|slice:":10000"|linebreaksbr }}{% if ping.body|length > 10000 %} [truncated]{% endif %}+{% endif %} + +{% if projects %} +
Projects Overview
++ {{ project }} + | ++ {% with project.get_n_down as n_down %} + {% if n_down %} + {{ n_down }} check{{ n_down|pluralize }} down + {% else %} + OK, all checks up + {% endif %} + {% endwith %} + | +
+—
+{% site_name %}
+ {% if check.project.name %}
+ Unsubscribe from "{{ check.project.name }}" notifications
+ {% else %}
Unsubscribe
+ {% endif %}
-{% endblock %}
+