diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9c8498a..ae4ceee3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
- Add the EMAIL_USE_VERIFICATION configuration setting (#232)
- Show "Badges" and "Settings" in top navigation (#234)
- Upgrade to Django 2.2
+- Can configure the email integration to only report the "down" events (#231)
## 1.6.0 - 2019-04-01
diff --git a/hc/api/models.py b/hc/api/models.py
index 002bbc71..9c97dbb2 100644
--- a/hc/api/models.py
+++ b/hc/api/models.py
@@ -272,7 +272,7 @@ class Channel(models.Model):
if self.name:
return self.name
if self.kind == "email":
- return "Email to %s" % self.value
+ return "Email to %s" % self.email_value
elif self.kind == "sms":
return "SMS to %s" % self.sms_number
elif self.kind == "slack":
@@ -302,7 +302,7 @@ class Channel(models.Model):
args = [self.code, self.make_token()]
verify_link = reverse("hc-verify-email", args=args)
verify_link = settings.SITE_ROOT + verify_link
- emails.verify_email(self.value, {"verify_link": verify_link})
+ emails.verify_email(self.email_value, {"verify_link": verify_link})
def get_unsub_link(self):
args = [self.code, self.make_token()]
@@ -526,6 +526,33 @@ class Channel(models.Model):
doc = json.loads(self.value)
return doc["list_id"]
+ @property
+ def email_value(self):
+ assert self.kind == "email"
+ if not self.value.startswith("{"):
+ return self.value
+
+ doc = json.loads(self.value)
+ return doc.get("value")
+
+ @property
+ def email_notify_up(self):
+ assert self.kind == "email"
+ if not self.value.startswith("{"):
+ return True
+
+ doc = json.loads(self.value)
+ return doc.get("up")
+
+ @property
+ def email_notify_down(self):
+ assert self.kind == "email"
+ if not self.value.startswith("{"):
+ return True
+
+ doc = json.loads(self.value)
+ return doc.get("down")
+
class Notification(models.Model):
class Meta:
diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py
index ca5ffb19..1438687f 100644
--- a/hc/api/tests/test_notify.py
+++ b/hc/api/tests/test_notify.py
@@ -244,9 +244,21 @@ class NotifyTestCase(BaseTestCase):
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
+ self.assertEqual(email.to[0], "alice@example.org")
self.assertTrue("X-Bounce-Url" in email.extra_headers)
self.assertTrue("List-Unsubscribe" 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_skips_unverified_email(self):
self._setup_data("email", "alice@example.org", email_verified=False)
self.channel.notify(self.check)
@@ -256,6 +268,15 @@ class NotifyTestCase(BaseTestCase):
self.assertEqual(Notification.objects.count(), 0)
self.assertEqual(len(mail.outbox), 0)
+ 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)
+
@patch("hc.api.transports.requests.request")
def test_pd(self, mock_post):
self._setup_data("pd", "123")
diff --git a/hc/api/transports.py b/hc/api/transports.py
index a9797289..5263042c 100644
--- a/hc/api/transports.py
+++ b/hc/api/transports.py
@@ -59,7 +59,7 @@ class Email(Transport):
try:
# Look up the sorting preference for this email address
- p = Profile.objects.get(user__email=self.channel.value)
+ p = Profile.objects.get(user__email=self.channel.email_value)
sort = p.sort
except Profile.DoesNotExist:
# Default sort order is by check's creation time
@@ -75,10 +75,16 @@ class Email(Transport):
"unsub_link": unsub_link
}
- emails.alert(self.channel.value, ctx, headers)
+ emails.alert(self.channel.email_value, ctx, headers)
def is_noop(self, check):
- return not self.channel.email_verified
+ if not self.channel.email_verified:
+ return True
+
+ if check.status == "down":
+ return not self.channel.email_notify_down
+ else:
+ return not self.channel.email_notify_up
class HttpTransport(Transport):
diff --git a/hc/front/forms.py b/hc/front/forms.py
index 28a48890..cad5c03a 100644
--- a/hc/front/forms.py
+++ b/hc/front/forms.py
@@ -57,6 +57,8 @@ class AddOpsGenieForm(forms.Form):
class AddEmailForm(forms.Form):
error_css_class = "has-error"
value = forms.EmailField(max_length=100)
+ down = forms.BooleanField(required=False, initial=True)
+ up = forms.BooleanField(required=False, initial=True)
class AddUrlForm(forms.Form):
diff --git a/hc/front/tests/test_add_email.py b/hc/front/tests/test_add_email.py
index 13830987..9bba4430 100644
--- a/hc/front/tests/test_add_email.py
+++ b/hc/front/tests/test_add_email.py
@@ -1,3 +1,5 @@
+import json
+
from django.core import mail
from django.test.utils import override_settings
@@ -12,7 +14,7 @@ class AddEmailTestCase(BaseTestCase):
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Get an email message")
- self.assertContains(r, "Confirmation needed")
+ self.assertContains(r, "Requires confirmation")
def test_it_creates_channel(self):
form = {"value": "alice@example.org"}
@@ -22,8 +24,9 @@ class AddEmailTestCase(BaseTestCase):
self.assertRedirects(r, "/integrations/")
c = Channel.objects.get()
+ doc = json.loads(c.value)
self.assertEqual(c.kind, "email")
- self.assertEqual(c.value, "alice@example.org")
+ self.assertEqual(doc["value"], "alice@example.org")
self.assertFalse(c.email_verified)
self.assertEqual(c.project, self.project)
@@ -32,6 +35,8 @@ class AddEmailTestCase(BaseTestCase):
email = mail.outbox[0]
self.assertTrue(email.subject.startswith("Verify email address on"))
+ # Make sure we're sending to an email address, not a JSON string:
+ self.assertEqual(email.to[0], "alice@example.org")
def test_team_access_works(self):
form = {"value": "bob@example.org"}
@@ -57,13 +62,14 @@ class AddEmailTestCase(BaseTestCase):
self.client.post(self.url, form)
c = Channel.objects.get()
- self.assertEqual(c.value, "alice@example.org")
+ doc = json.loads(c.value)
+ self.assertEqual(doc["value"], "alice@example.org")
@override_settings(EMAIL_USE_VERIFICATION=False)
def test_it_hides_confirmation_needed_notice(self):
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
- self.assertNotContains(r, "Confirmation needed")
+ self.assertNotContains(r, "Requires confirmation")
@override_settings(EMAIL_USE_VERIFICATION=False)
def test_it_auto_verifies_email(self):
@@ -74,8 +80,9 @@ class AddEmailTestCase(BaseTestCase):
self.assertRedirects(r, "/integrations/")
c = Channel.objects.get()
+ doc = json.loads(c.value)
self.assertEqual(c.kind, "email")
- self.assertEqual(c.value, "alice@example.org")
+ self.assertEqual(doc["value"], "alice@example.org")
self.assertTrue(c.email_verified)
# Email should *not* have been sent
diff --git a/hc/front/tests/test_channels.py b/hc/front/tests/test_channels.py
index 521dc69a..36c33c7d 100644
--- a/hc/front/tests/test_channels.py
+++ b/hc/front/tests/test_channels.py
@@ -74,6 +74,34 @@ class ChannelsTestCase(BaseTestCase):
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Unconfirmed")
+ def test_it_shows_down_only_note_for_email(self):
+ channel = Channel(project=self.project, kind="email")
+ channel.value = json.dumps({
+ "value": "alice@example.org",
+ "up": False,
+ "down": True
+ })
+ channel.save()
+
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get("/integrations/")
+ self.assertEqual(r.status_code, 200)
+ self.assertContains(r, "(down only)")
+
+ def test_it_shows_up_only_note_for_email(self):
+ channel = Channel(project=self.project, kind="email")
+ channel.value = json.dumps({
+ "value": "alice@example.org",
+ "up": True,
+ "down": False
+ })
+ channel.save()
+
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get("/integrations/")
+ self.assertEqual(r.status_code, 200)
+ self.assertContains(r, "(up only)")
+
def test_it_shows_sms_label(self):
ch = Channel(kind="sms", project=self.project)
ch.value = json.dumps({"value": "+123", "label": "My Phone"})
diff --git a/hc/front/tests/test_log.py b/hc/front/tests/test_log.py
index 8559d344..7e9735eb 100644
--- a/hc/front/tests/test_log.py
+++ b/hc/front/tests/test_log.py
@@ -1,3 +1,5 @@
+import json
+
from hc.api.models import Channel, Check, Notification, Ping
from hc.test import BaseTestCase
@@ -15,20 +17,20 @@ class LogTestCase(BaseTestCase):
ping.created = "2000-01-01T00:00:00+00:00"
ping.save()
+ self.url = "/checks/%s/log/" % self.check.code
+
def test_it_works(self):
- url = "/checks/%s/log/" % self.check.code
self.client.login(username="alice@example.org", password="password")
- r = self.client.get(url)
+ r = self.client.get(self.url)
self.assertContains(r, "Local Time", status_code=200)
def test_team_access_works(self):
- url = "/checks/%s/log/" % self.check.code
# Logging in as bob, not alice. Bob has team access so this
# should work.
self.client.login(username="bob@example.org", password="password")
- r = self.client.get(url)
+ r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
def test_it_handles_bad_uuid(self):
@@ -47,40 +49,50 @@ class LogTestCase(BaseTestCase):
self.assertEqual(r.status_code, 404)
def test_it_checks_ownership(self):
- url = "/checks/%s/log/" % self.check.code
self.client.login(username="charlie@example.org", password="password")
- r = self.client.get(url)
+ r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)
- def test_it_shows_pushover_notifications(self):
- ch = Channel.objects.create(kind="po", project=self.project)
+ def test_it_shows_email_notification(self):
+ ch = Channel(kind="email", project=self.project)
+ ch.value = json.dumps({
+ "value": "alice@example.org",
+ "up": True,
+ "down": True
+ })
+ ch.save()
Notification(owner=self.check, channel=ch, check_status="down").save()
- url = "/checks/%s/log/" % self.check.code
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get(self.url)
+ self.assertContains(r, "Sent email alert to alice@example.org",
+ status_code=200)
+
+ def test_it_shows_pushover_notification(self):
+ ch = Channel.objects.create(kind="po", project=self.project)
+
+ Notification(owner=self.check, channel=ch, check_status="down").save()
self.client.login(username="alice@example.org", password="password")
- r = self.client.get(url)
+ r = self.client.get(self.url)
self.assertContains(r, "Sent a Pushover notification", status_code=200)
- def test_it_shows_webhook_notifications(self):
+ def test_it_shows_webhook_notification(self):
ch = Channel(kind="webhook", project=self.project)
ch.value = "foo/$NAME"
ch.save()
Notification(owner=self.check, channel=ch, check_status="down").save()
- url = "/checks/%s/log/" % self.check.code
-
self.client.login(username="alice@example.org", password="password")
- r = self.client.get(url)
+ r = self.client.get(self.url)
self.assertContains(r, "Called webhook foo/$NAME", status_code=200)
def test_it_allows_cross_team_access(self):
self.bobs_profile.current_project = None
self.bobs_profile.save()
- url = "/checks/%s/log/" % self.check.code
self.client.login(username="bob@example.org", password="password")
- r = self.client.get(url)
+ r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
diff --git a/hc/front/views.py b/hc/front/views.py
index 76992785..47d81336 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -678,7 +678,11 @@ def add_email(request):
form = AddEmailForm(request.POST)
if form.is_valid():
channel = Channel(project=request.project, kind="email")
- channel.value = form.cleaned_data["value"]
+ channel.value = json.dumps({
+ "value": form.cleaned_data["value"],
+ "up": form.cleaned_data["up"],
+ "down": form.cleaned_data["down"]
+ })
channel.save()
channel.assign_all_checks()
diff --git a/static/css/base.css b/static/css/base.css
index 8a8dd8ca..68bb6026 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -77,13 +77,13 @@ body {
.status.icon-grace { color: #f0ad4e; }
.status.icon-down { color: #d9534f; }
-.label-start {
+.label.label-start {
background-color: #FFF;
color: #117a3f;;
border: 1px solid #117a3f;
}
-.label-ign {
+.label.label-ign {
background: #e0e0e0;
color: #333;
}
diff --git a/static/css/radio.css b/static/css/radio.css
index 8d4be112..5df3ac62 100644
--- a/static/css/radio.css
+++ b/static/css/radio.css
@@ -13,6 +13,11 @@
font-weight: normal;
}
+.form-horizontal .radio-container, .form-horizontal .checkbox-container {
+ margin-top: 7px;
+ margin-left: 0;
+}
+
/* Hide the browser's default radio button */
.radio-container input {
position: absolute;
diff --git a/templates/accounts/notifications.html b/templates/accounts/notifications.html
index dcea4e06..83346f0f 100644
--- a/templates/accounts/notifications.html
+++ b/templates/accounts/notifications.html
@@ -73,7 +73,7 @@
-
+
Reports will be delivered to {{ profile.user.email }}.
{% if profile.next_report_date %}
Next monthly report date is
diff --git a/templates/base.html b/templates/base.html
index 4529c0a6..fc8fa7ee 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -16,35 +16,35 @@
{% compress css %}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
{% endcompress %}
Get an email message when check goes up or down.
-- Tip: - Add multiple email addresses, to notify multiple team members. -
- {% if use_verification %} -- Confirmation needed. +
+ Requires confirmation. After entering an email address, {% site_name %} will send out a confirmation link. - Only confirmed addresses will receive notifications. + Only confirmed addresses receive notifications.
{% endif %} @@ -37,6 +31,7 @@ class="form-control" name="value" placeholder="you@example.org" + required value="{{ form.value.value|default:"" }}"> {% if form.value.errors %} @@ -46,6 +41,36 @@ {% endif %}