forked from GithubBackups/healthchecks
Support for "Add to Slack" button
This commit is contained in:
parent
af997446f3
commit
760b5b4fdb
@ -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", {})
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
24
hc/front/tests/test_channels.py
Normal file
24
hc/front/tests/test_channels.py
Normal 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")
|
53
hc/front/tests/test_slack_callback.py
Normal file
53
hc/front/tests/test_slack_callback.py
Normal 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")
|
@ -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"),
|
||||
|
@ -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"}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user