Browse Source

Support for "Add to Slack" button

pull/72/head
Pēteris Caune 8 years ago
parent
commit
760b5b4fdb
12 changed files with 202 additions and 21 deletions
  1. +5
    -5
      hc/accounts/views.py
  2. +28
    -0
      hc/api/models.py
  3. +17
    -4
      hc/api/tests/test_notify.py
  4. +1
    -1
      hc/api/transports.py
  5. +24
    -0
      hc/front/tests/test_channels.py
  6. +53
    -0
      hc/front/tests/test_slack_callback.py
  7. +1
    -0
      hc/front/urls.py
  8. +32
    -1
      hc/front/views.py
  9. +4
    -0
      hc/settings.py
  10. +6
    -2
      static/css/channels.css
  11. +1
    -1
      templates/accounts/profile.html
  12. +30
    -7
      templates/front/channels.html

+ 5
- 5
hc/accounts/views.py 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", {})


+ 28
- 0
hc/api/models.py 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()


+ 17
- 4
hc/api/tests/test_notify.py 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")


+ 1
- 1
hc/api/transports.py 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):


+ 24
- 0
hc/front/tests/test_channels.py 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="[email protected]", password="password")
r = self.client.get("/integrations/")
self.assertContains(r, "foo-team", status_code=200)
self.assertContains(r, "#bar")

+ 53
- 0
hc/front/tests/test_slack_callback.py 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="[email protected]", 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="[email protected]", password="password")
r = self.client.get(url, follow=True)
self.assertRedirects(r, "/integrations/")
self.assertContains(r, "something went wrong")

+ 1
- 0
hc/front/urls.py 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"),


+ 32
- 1
hc/front/views.py 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"}


+ 4
- 0
hc/settings.py 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


+ 6
- 2
static/css/channels.css 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;
}


+ 1
- 1
templates/accounts/profile.html 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 %}


+ 30
- 7
templates/front/channels.html 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/[email protected] 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…
Cancel
Save