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 %} diff --git a/templates/front/channels.html b/templates/front/channels.html index b9fea3e0..8b6f62b1 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -42,7 +42,13 @@ {% endif %}

{% if ch.kind == "email" %} - Email to {{ ch.value }} + Email to {{ ch.email_value }} + {% if ch.email_notify_down and not ch.email_notify_up %} + (down only) + {% endif %} + {% if ch.email_notify_up and not ch.email_notify_down %} + (up only) + {% endif %} {% elif ch.kind == "pd" %} PagerDuty account {{ ch.pd_account }} {% elif ch.kind == "pagertree" %} diff --git a/templates/front/details_events.html b/templates/front/details_events.html index f01f2468..9ee93ff1 100644 --- a/templates/front/details_events.html +++ b/templates/front/details_events.html @@ -54,7 +54,7 @@ {% if event.channel.kind == "email" %} - Sent email alert to {{ event.channel.value }} + Sent email alert to {{ event.channel.email_value }} {% elif event.channel.kind == "slack" %} Sent Slack alert {% if event.channel.slack_channel %} diff --git a/templates/front/log.html b/templates/front/log.html index 35e9f215..1a2443b6 100644 --- a/templates/front/log.html +++ b/templates/front/log.html @@ -97,7 +97,7 @@ {% if event.channel.kind == "email" %} - Sent email alert to {{ event.channel.value }} + Sent email alert to {{ event.channel.email_value }} {% elif event.channel.kind == "slack" %} Sent Slack alert {% if event.channel.slack_channel %} diff --git a/templates/integrations/add_email.html b/templates/integrations/add_email.html index 9149a044..402968a7 100644 --- a/templates/integrations/add_email.html +++ b/templates/integrations/add_email.html @@ -1,26 +1,20 @@ {% extends "base.html" %} {% load humanize static hc_extras %} -{% block title %}Notification Channels - {% site_name %}{% endblock %} +{% block title %}Set Up Email Notifications - {% site_name %}{% endblock %} {% block content %}

Email

-

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 %}
+ +
+ +
+ + +
+
+
diff --git a/templates/integrations/add_pushover.html b/templates/integrations/add_pushover.html index ddfe4df6..25bf90a8 100644 --- a/templates/integrations/add_pushover.html +++ b/templates/integrations/add_pushover.html @@ -7,6 +7,7 @@ {% block content %}
+

Pushbover

{% if request.user.is_authenticated %}