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: elif "create_api_key" in request.POST:
profile.set_api_key() profile.set_api_key()
show_api_key = True 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: elif "revoke_api_key" in request.POST:
profile.api_key = "" profile.api_key = ""
profile.save() profile.save()
@ -149,7 +149,7 @@ def profile(request):
if form.is_valid(): if form.is_valid():
profile.reports_allowed = form.cleaned_data["reports_allowed"] profile.reports_allowed = form.cleaned_data["reports_allowed"]
profile.save() 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: elif "invite_team_member" in request.POST:
if not profile.team_access_allowed: if not profile.team_access_allowed:
return HttpResponseForbidden() return HttpResponseForbidden()
@ -164,7 +164,7 @@ def profile(request):
user = _make_user(email) user = _make_user(email)
profile.invite(user) 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: elif "remove_team_member" in request.POST:
form = RemoveTeamMemberForm(request.POST) form = RemoveTeamMemberForm(request.POST)
if form.is_valid(): if form.is_valid():
@ -186,7 +186,7 @@ def profile(request):
if form.is_valid(): if form.is_valid():
profile.team_name = form.cleaned_data["team_name"] profile.team_name = form.cleaned_data["team_name"]
profile.save() profile.save()
messages.info(request, "Team Name updated!")
messages.success(request, "Team Name updated!")
tags = set() tags = set()
for check in Check.objects.filter(user=request.team.user): 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) u = authenticate(username=request.user.email, password=password)
auth_login(request, u) 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 redirect("hc-profile")
return render(request, "accounts/set_password.html", {}) return render(request, "accounts/set_password.html", {})


+ 28
- 0
hc/api/models.py View File

@ -1,6 +1,7 @@
# coding: utf-8 # coding: utf-8
import hashlib import hashlib
import json
import uuid import uuid
from datetime import timedelta as td from datetime import timedelta as td
@ -197,6 +198,33 @@ class Channel(models.Model):
parts = self.value.split("\n") parts = self.value.split("\n")
return parts[1] if len(parts) == 2 else "" 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): def latest_notification(self):
return Notification.objects.filter(channel=self).latest() 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.api.models import Channel, Check, Notification
from hc.test import BaseTestCase from hc.test import BaseTestCase
from mock import patch
from requests.exceptions import ConnectionError, Timeout
class NotifyTestCase(BaseTestCase): class NotifyTestCase(BaseTestCase):
@ -27,7 +28,7 @@ class NotifyTestCase(BaseTestCase):
self.channel.notify(self.check) self.channel.notify(self.check)
mock_get.assert_called_with( mock_get.assert_called_with(
"get", u"http://example",
"get", u"http://example",
headers={"User-Agent": "healthchecks.io"}, timeout=5) headers={"User-Agent": "healthchecks.io"}, timeout=5)
@patch("hc.api.transports.requests.request", side_effect=Timeout) @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"]} fields = {f["title"]: f["value"] for f in attachment["fields"]}
self.assertEqual(fields["Last Ping"], "Never") 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") @patch("hc.api.transports.requests.request")
def test_slack_handles_500(self, mock_post): def test_slack_handles_500(self, mock_post):
self._setup_data("slack", "123") self._setup_data("slack", "123")


+ 1
- 1
hc/api/transports.py View File

@ -118,7 +118,7 @@ class Slack(HttpTransport):
def notify(self, check): def notify(self, check):
text = tmpl("slack_message.json", check=check) text = tmpl("slack_message.json", check=check)
payload = json.loads(text) payload = json.loads(text)
return self.post(self.channel.value, payload)
return self.post(self.channel.slack_webhook_url, payload)
class HipChat(HttpTransport): 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_webhook/$', views.add_webhook, name="hc-add-webhook"),
url(r'^integrations/add_pd/$', views.add_pd, name="hc-add-pd"), 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/$', 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_hipchat/$', views.add_hipchat, name="hc-add-hipchat"),
url(r'^integrations/add_pushover/$', views.add_pushover, name="hc-add-pushover"), url(r'^integrations/add_pushover/$', views.add_pushover, name="hc-add-pushover"),
url(r'^integrations/add_victorops/$', views.add_victorops, name="hc-add-victorops"), 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 datetime import timedelta as td
from itertools import tee from itertools import tee
import requests
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Count 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.crypto import get_random_string
from django.utils.six.moves.urllib.parse import urlencode from django.utils.six.moves.urllib.parse import urlencode
from hc.api.decorators import uuid_or_400 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, from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm,
TimeoutForm) TimeoutForm)
@ -269,6 +271,7 @@ def channels(request):
"channels": channels, "channels": channels,
"num_checks": num_checks, "num_checks": num_checks,
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
"slack_client_id": settings.SLACK_CLIENT_ID
} }
return render(request, "front/channels.html", ctx) return render(request, "front/channels.html", ctx)
@ -377,6 +380,34 @@ def add_slack(request):
return render(request, "integrations/add_slack.html", ctx) 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 @login_required
def add_hipchat(request): def add_hipchat(request):
ctx = {"page": "channels"} ctx = {"page": "channels"}


+ 4
- 0
hc/settings.py View File

@ -136,6 +136,10 @@ COMPRESS_OFFLINE = True
EMAIL_BACKEND = "djmail.backends.default.EmailBackend" 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 integration -- override these in local_settings
PUSHOVER_API_TOKEN = None PUSHOVER_API_TOKEN = None
PUSHOVER_SUBSCRIPTION_URL = 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 font-weight: bold
} }
.preposition {
.preposition, .description {
color: #888; color: #888;
} }
@ -102,7 +102,7 @@ table.channels-table > tbody > tr > th {
background: #eee; background: #eee;
} }
.add-integration img {
.add-integration .icon {
position: absolute; position: absolute;
left: 16px; left: 16px;
top: 50%; top: 50%;
@ -125,6 +125,10 @@ table.channels-table > tbody > tr > th {
right: 16px; right: 16px;
top: 50%; top: 50%;
margin-top: -17px; 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 %} {% if messages %}
<div class="col-sm-12"> <div class="col-sm-12">
{% for message in messages %} {% for message in messages %}
<p class="alert alert-success">{{ message }}</p>
<p class="alert alert-{{ message.tags }}">{{ message }}</p>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}


+ 30
- 7
templates/front/channels.html View File

@ -6,6 +6,14 @@
{% block content %} {% block content %}
<div class="row"> <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"> <div class="col-sm-12">
<table class="table channels-table"> <table class="table channels-table">
{% if channels %} {% if channels %}
@ -44,6 +52,15 @@
<span class="preposition">user key</span> <span class="preposition">user key</span>
{{ ch.po_value|first }} {{ ch.po_value|first }}
({{ ch.po_value|last }} priority) ({{ 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" %} {% elif ch.kind == "webhook" %}
<table> <table>
{% if ch.value_down %} {% if ch.value_down %}
@ -107,16 +124,22 @@
<ul class="add-integration"> <ul class="add-integration">
<li> <li>
<img src="{% static 'img/integrations/slack.png' %}" <img src="{% static 'img/integrations/slack.png' %}"
alt="Slack icon" />
class="icon" alt="Slack icon" />
<h2>Slack</h2> <h2>Slack</h2>
<p>A messaging app for teams.</p> <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> <a href="{% url 'hc-add-slack' %}" class="btn btn-primary">Add Integration</a>
{% endif %}
</li> </li>
<li> <li>
<img src="{% static 'img/integrations/email.png' %}" <img src="{% static 'img/integrations/email.png' %}"
alt="Email icon" />
class="icon" alt="Email icon" />
<h2>Email</h2> <h2>Email</h2>
<p>Get an email message when check goes up or down.</p> <p>Get an email message when check goes up or down.</p>
@ -125,7 +148,7 @@
</li> </li>
<li> <li>
<img src="{% static 'img/integrations/webhook.png' %}" <img src="{% static 'img/integrations/webhook.png' %}"
alt="Webhook icon" />
class="icon" alt="Webhook icon" />
<h2>Webhook</h2> <h2>Webhook</h2>
<p>Receive a HTTP callback when a check goes down.</p> <p>Receive a HTTP callback when a check goes down.</p>
@ -134,7 +157,7 @@
</li> </li>
<li> <li>
<img src="{% static 'img/integrations/pd.png' %}" <img src="{% static 'img/integrations/pd.png' %}"
alt="PagerDuty icon" />
class="icon" alt="PagerDuty icon" />
<h2>PagerDuty</h2> <h2>PagerDuty</h2>
<p>On-call scheduling, alerting, and incident tracking.</p> <p>On-call scheduling, alerting, and incident tracking.</p>
@ -143,7 +166,7 @@
</li> </li>
<li> <li>
<img src="{% static 'img/integrations/hipchat.png' %}" <img src="{% static 'img/integrations/hipchat.png' %}"
alt="HipChat icon" />
class="icon" alt="HipChat icon" />
<h2>HipChat</h2> <h2>HipChat</h2>
<p>Group and private chat, file sharing, and integrations.</p> <p>Group and private chat, file sharing, and integrations.</p>
@ -152,7 +175,7 @@
</li> </li>
<li> <li>
<img src="{% static 'img/integrations/victorops.png' %}" <img src="{% static 'img/integrations/victorops.png' %}"
alt="VictorOps icon" />
class="icon" alt="VictorOps icon" />
<h2>VictorOps</h2> <h2>VictorOps</h2>
<p>On-call scheduling, alerting, and incident tracking.</p> <p>On-call scheduling, alerting, and incident tracking.</p>
@ -162,7 +185,7 @@
{% if enable_pushover %} {% if enable_pushover %}
<li> <li>
<img src="{% static 'img/integrations/pushover.png' %}" <img src="{% static 'img/integrations/pushover.png' %}"
alt="Pushover icon" />
class="icon" alt="Pushover icon" />
<h2>Pushover</h2> <h2>Pushover</h2>
<p>Receive instant push notifications on your phone or tablet.</p> <p>Receive instant push notifications on your phone or tablet.</p>


Loading…
Cancel
Save