Support for "Add to Slack" button

This commit is contained in:
Pēteris Caune 2016-07-08 00:05:05 +03:00
parent af997446f3
commit 760b5b4fdb
12 changed files with 202 additions and 21 deletions

View File

@ -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", {})

View File

@ -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()

View File

@ -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")

View File

@ -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):

View File

@ -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")

View File

@ -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")

View File

@ -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"),

View File

@ -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"}

View File

@ -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

View File

@ -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;
}

View File

@ -12,7 +12,7 @@
{% if messages %}
<div class="col-sm-12">
{% for message in messages %}
<p class="alert alert-success">{{ message }}</p>
<p class="alert alert-{{ message.tags }}">{{ message }}</p>
{% endfor %}
</div>
{% endif %}

View File

@ -6,6 +6,14 @@
{% block content %}
<div class="row">
{% if messages %}
<div class="col-sm-12">
{% for message in messages %}
<p class="alert alert-{{ message.tags }}">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
<div class="col-sm-12">
<table class="table channels-table">
{% if channels %}
@ -44,6 +52,15 @@
<span class="preposition">user key</span>
{{ ch.po_value|first }}
({{ ch.po_value|last }} priority)
{% elif ch.kind == "slack" %}
{% if ch.slack_team %}
<span class="preposition">team</span>
{{ ch.slack_team }},
<span class="preposition">channel</span>
{{ ch.slack_channel }}
{% else %}
{{ ch.value }}
{% endif %}
{% elif ch.kind == "webhook" %}
<table>
{% if ch.value_down %}
@ -107,16 +124,22 @@
<ul class="add-integration">
<li>
<img src="{% static 'img/integrations/slack.png' %}"
alt="Slack icon" />
class="icon" alt="Slack icon" />
<h2>Slack</h2>
<p>A messaging app for teams.</p>
{% if slack_client_id %}
<a href="https://slack.com/oauth/authorize?scope=incoming-webhook&client_id={{ slack_client_id }}">
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />
</a>
{% else %}
<a href="{% url 'hc-add-slack' %}" class="btn btn-primary">Add Integration</a>
{% endif %}
</li>
<li>
<img src="{% static 'img/integrations/email.png' %}"
alt="Email icon" />
class="icon" alt="Email icon" />
<h2>Email</h2>
<p>Get an email message when check goes up or down.</p>
@ -125,7 +148,7 @@
</li>
<li>
<img src="{% static 'img/integrations/webhook.png' %}"
alt="Webhook icon" />
class="icon" alt="Webhook icon" />
<h2>Webhook</h2>
<p>Receive a HTTP callback when a check goes down.</p>
@ -134,7 +157,7 @@
</li>
<li>
<img src="{% static 'img/integrations/pd.png' %}"
alt="PagerDuty icon" />
class="icon" alt="PagerDuty icon" />
<h2>PagerDuty</h2>
<p>On-call scheduling, alerting, and incident tracking.</p>
@ -143,7 +166,7 @@
</li>
<li>
<img src="{% static 'img/integrations/hipchat.png' %}"
alt="HipChat icon" />
class="icon" alt="HipChat icon" />
<h2>HipChat</h2>
<p>Group and private chat, file sharing, and integrations.</p>
@ -152,7 +175,7 @@
</li>
<li>
<img src="{% static 'img/integrations/victorops.png' %}"
alt="VictorOps icon" />
class="icon" alt="VictorOps icon" />
<h2>VictorOps</h2>
<p>On-call scheduling, alerting, and incident tracking.</p>
@ -162,7 +185,7 @@
{% if enable_pushover %}
<li>
<img src="{% static 'img/integrations/pushover.png' %}"
alt="Pushover icon" />
class="icon" alt="Pushover icon" />
<h2>Pushover</h2>
<p>Receive instant push notifications on your phone or tablet.</p>