diff --git a/hc/accounts/views.py b/hc/accounts/views.py index a0c03c9e..aed21d19 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -137,7 +137,7 @@ def profile(request): elif "create_api_key" in request.POST: profile.set_api_key() show_api_key = True - messages.info(request, "The API key has been created!") + messages.success(request, "The API key has been created!") elif "revoke_api_key" in request.POST: profile.api_key = "" profile.save() @@ -149,7 +149,7 @@ def profile(request): if form.is_valid(): profile.reports_allowed = form.cleaned_data["reports_allowed"] profile.save() - messages.info(request, "Your settings have been updated!") + messages.success(request, "Your settings have been updated!") elif "invite_team_member" in request.POST: if not profile.team_access_allowed: return HttpResponseForbidden() @@ -164,7 +164,7 @@ def profile(request): user = _make_user(email) profile.invite(user) - messages.info(request, "Invitation to %s sent!" % email) + messages.success(request, "Invitation to %s sent!" % email) elif "remove_team_member" in request.POST: form = RemoveTeamMemberForm(request.POST) if form.is_valid(): @@ -186,7 +186,7 @@ def profile(request): if form.is_valid(): profile.team_name = form.cleaned_data["team_name"] profile.save() - messages.info(request, "Team Name updated!") + messages.success(request, "Team Name updated!") tags = set() for check in Check.objects.filter(user=request.team.user): @@ -230,7 +230,7 @@ def set_password(request, token): u = authenticate(username=request.user.email, password=password) auth_login(request, u) - messages.info(request, "Your password has been set!") + messages.success(request, "Your password has been set!") return redirect("hc-profile") return render(request, "accounts/set_password.html", {}) diff --git a/hc/api/models.py b/hc/api/models.py index d126f6a6..2dee4d82 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -1,6 +1,7 @@ # coding: utf-8 import hashlib +import json import uuid from datetime import timedelta as td @@ -197,6 +198,33 @@ class Channel(models.Model): parts = self.value.split("\n") return parts[1] if len(parts) == 2 else "" + @property + def slack_team(self): + assert self.kind == "slack" + if not self.value.startswith("{"): + return None + + doc = json.loads(self.value) + return doc["team_name"] + + @property + def slack_channel(self): + assert self.kind == "slack" + if not self.value.startswith("{"): + return None + + doc = json.loads(self.value) + return doc["incoming_webhook"]["channel"] + + @property + def slack_webhook_url(self): + assert self.kind == "slack" + if not self.value.startswith("{"): + return self.value + + doc = json.loads(self.value) + return doc["incoming_webhook"]["url"] + def latest_notification(self): return Notification.objects.filter(channel=self).latest() diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index cdc0cba7..5d5c5883 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -1,9 +1,10 @@ -from django.core import mail -from mock import patch -from requests.exceptions import ConnectionError, Timeout +import json +from django.core import mail from hc.api.models import Channel, Check, Notification from hc.test import BaseTestCase +from mock import patch +from requests.exceptions import ConnectionError, Timeout class NotifyTestCase(BaseTestCase): @@ -27,7 +28,7 @@ class NotifyTestCase(BaseTestCase): self.channel.notify(self.check) mock_get.assert_called_with( - "get", u"http://example", + "get", u"http://example", headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request", side_effect=Timeout) @@ -152,6 +153,18 @@ class NotifyTestCase(BaseTestCase): fields = {f["title"]: f["value"] for f in attachment["fields"]} self.assertEqual(fields["Last Ping"], "Never") + @patch("hc.api.transports.requests.request") + def test_slack_with_complex_value(self, mock_post): + v = json.dumps({"incoming_webhook": {"url": "123"}}) + self._setup_data("slack", v) + mock_post.return_value.status_code = 200 + + self.channel.notify(self.check) + assert Notification.objects.count() == 1 + + args, kwargs = mock_post.call_args + self.assertEqual(args[1], "123") + @patch("hc.api.transports.requests.request") def test_slack_handles_500(self, mock_post): self._setup_data("slack", "123") diff --git a/hc/api/transports.py b/hc/api/transports.py index e222346d..f651acba 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -118,7 +118,7 @@ class Slack(HttpTransport): def notify(self, check): text = tmpl("slack_message.json", check=check) payload = json.loads(text) - return self.post(self.channel.value, payload) + return self.post(self.channel.slack_webhook_url, payload) class HipChat(HttpTransport): diff --git a/hc/front/tests/test_channels.py b/hc/front/tests/test_channels.py new file mode 100644 index 00000000..a133073b --- /dev/null +++ b/hc/front/tests/test_channels.py @@ -0,0 +1,24 @@ +import json + +from hc.api.models import Channel +from hc.test import BaseTestCase + + +class ChannelsTestCase(BaseTestCase): + + def test_it_formats_complex_slack_value(self): + ch = Channel(kind="slack", user=self.alice) + ch.value = json.dumps({ + "ok": True, + "team_name": "foo-team", + "incoming_webhook": { + "url": "http://example.org", + "channel": "#bar" + } + }) + ch.save() + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/integrations/") + self.assertContains(r, "foo-team", status_code=200) + self.assertContains(r, "#bar") diff --git a/hc/front/tests/test_slack_callback.py b/hc/front/tests/test_slack_callback.py new file mode 100644 index 00000000..abb7c1ae --- /dev/null +++ b/hc/front/tests/test_slack_callback.py @@ -0,0 +1,53 @@ +import json + +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase +from mock import patch + + +@override_settings(PUSHOVER_API_TOKEN="token", PUSHOVER_SUBSCRIPTION_URL="url") +class SlackCallbackTestCase(BaseTestCase): + + @patch("hc.front.views.requests.post") + def test_it_works(self, mock_post): + oauth_response = { + "ok": True, + "team_name": "foo", + "incoming_webhook": { + "url": "http://example.org", + "channel": "bar" + } + } + + mock_post.return_value.text = json.dumps(oauth_response) + mock_post.return_value.json.return_value = oauth_response + + url = "/integrations/add_slack_btn/?code=12345678" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url, follow=True) + self.assertRedirects(r, "/integrations/") + self.assertContains(r, "The Slack integration has been added!") + + ch = Channel.objects.get() + self.assertEqual(ch.slack_team, "foo") + self.assertEqual(ch.slack_channel, "bar") + self.assertEqual(ch.slack_webhook_url, "http://example.org") + + @patch("hc.front.views.requests.post") + def test_it_handles_error(self, mock_post): + oauth_response = { + "ok": False, + "error": "something went wrong" + } + + mock_post.return_value.text = json.dumps(oauth_response) + mock_post.return_value.json.return_value = oauth_response + + url = "/integrations/add_slack_btn/?code=12345678" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url, follow=True) + self.assertRedirects(r, "/integrations/") + self.assertContains(r, "something went wrong") diff --git a/hc/front/urls.py b/hc/front/urls.py index 2b7badc1..89599e13 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ url(r'^integrations/add_webhook/$', views.add_webhook, name="hc-add-webhook"), url(r'^integrations/add_pd/$', views.add_pd, name="hc-add-pd"), url(r'^integrations/add_slack/$', views.add_slack, name="hc-add-slack"), + url(r'^integrations/add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"), url(r'^integrations/add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), url(r'^integrations/add_pushover/$', views.add_pushover, name="hc-add-pushover"), url(r'^integrations/add_victorops/$', views.add_victorops, name="hc-add-victorops"), diff --git a/hc/front/views.py b/hc/front/views.py index 2e1d17aa..5f88622a 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -2,7 +2,9 @@ from collections import Counter from datetime import timedelta as td from itertools import tee +import requests from django.conf import settings +from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse from django.db.models import Count @@ -12,7 +14,7 @@ from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.six.moves.urllib.parse import urlencode from hc.api.decorators import uuid_or_400 -from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE +from hc.api.models import DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm, TimeoutForm) @@ -269,6 +271,7 @@ def channels(request): "channels": channels, "num_checks": num_checks, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, + "slack_client_id": settings.SLACK_CLIENT_ID } return render(request, "front/channels.html", ctx) @@ -377,6 +380,34 @@ def add_slack(request): return render(request, "integrations/add_slack.html", ctx) +@login_required +def add_slack_btn(request): + code = request.GET.get("code", "") + if len(code) < 8: + return HttpResponseBadRequest() + + result = requests.post("https://slack.com/api/oauth.access", { + "client_id": settings.SLACK_CLIENT_ID, + "client_secret": settings.SLACK_CLIENT_SECRET, + "code": code + }) + + doc = result.json() + if doc.get("ok"): + channel = Channel() + channel.user = request.team.user + channel.kind = "slack" + channel.value = result.text + channel.save() + channel.assign_all_checks() + messages.info(request, "The Slack integration has been added!") + else: + s = doc.get("error") + messages.warning(request, "Error message from slack: %s" % s) + + return redirect("hc-channels") + + @login_required def add_hipchat(request): ctx = {"page": "channels"} diff --git a/hc/settings.py b/hc/settings.py index c4f5b928..32321507 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -136,6 +136,10 @@ COMPRESS_OFFLINE = True EMAIL_BACKEND = "djmail.backends.default.EmailBackend" +# Slack integration -- override these in local_settings +SLACK_CLIENT_ID = None +SLACK_CLIENT_SECRET = None + # Pushover integration -- override these in local_settings PUSHOVER_API_TOKEN = None PUSHOVER_SUBSCRIPTION_URL = None diff --git a/static/css/channels.css b/static/css/channels.css index 03e455c9..f4eee091 100644 --- a/static/css/channels.css +++ b/static/css/channels.css @@ -43,7 +43,7 @@ table.channels-table > tbody > tr > th { font-weight: bold } -.preposition { +.preposition, .description { color: #888; } @@ -102,7 +102,7 @@ table.channels-table > tbody > tr > th { background: #eee; } -.add-integration img { +.add-integration .icon { position: absolute; left: 16px; top: 50%; @@ -125,6 +125,10 @@ table.channels-table > tbody > tr > th { right: 16px; top: 50%; margin-top: -17px; + width: 139px; + height: 40px; + padding: 0; + line-height: 40px; } diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index e7181e87..a794cfcd 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -12,7 +12,7 @@ {% if messages %}
{% for message in messages %} -

{{ message }}

+

{{ message }}

{% endfor %}
{% endif %} diff --git a/templates/front/channels.html b/templates/front/channels.html index d146b1b1..f84ab537 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -6,6 +6,14 @@ {% block content %}
+{% if messages %} +
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+{% endif %} +
{% if channels %} @@ -44,6 +52,15 @@ user key {{ ch.po_value|first }} ({{ ch.po_value|last }} priority) + {% elif ch.kind == "slack" %} + {% if ch.slack_team %} + team + {{ ch.slack_team }}, + channel + {{ ch.slack_channel }} + {% else %} + {{ ch.value }} + {% endif %} {% elif ch.kind == "webhook" %}
{% if ch.value_down %} @@ -107,16 +124,22 @@