"Edit" function for webhook integrations (#176)

This commit is contained in:
Pēteris Caune 2020-04-06 14:48:47 +03:00
parent f12a649c72
commit 609f78c5ed
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
12 changed files with 192 additions and 18 deletions

View File

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Improvements
- Rate limiting for Telegram notifications (10 notifications per chat per minute)
- Use Slack V2 OAuth flow
- "Edit" function for webhook integrations (#176)
### Bug Fixes
- "Get a single check" API call now supports read-only API keys (#346)

View File

@ -136,8 +136,9 @@ class AddUrlForm(forms.Form):
METHODS = ("GET", "POST", "PUT")
class AddWebhookForm(forms.Form):
class WebhookForm(forms.Form):
error_css_class = "has-error"
name = forms.CharField(max_length=100, required=False)
method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
body_down = forms.CharField(max_length=1000, required=False)
@ -160,7 +161,8 @@ class AddWebhookForm(forms.Form):
url_up = self.cleaned_data.get("url_up")
if not url_down and not url_up:
self.add_error("url_down", "Enter a valid URL.")
if not self.has_error("url_down"):
self.add_error("url_down", "Enter a valid URL.")
def get_value(self):
return json.dumps(dict(self.cleaned_data), sort_keys=True)

View File

@ -12,6 +12,22 @@ class AddWebhookTestCase(BaseTestCase):
r = self.client.get(self.url)
self.assertContains(r, "Executes an HTTP request")
def test_it_saves_name(self):
form = {
"name": "Call foo.com",
"method_down": "GET",
"url_down": "http://foo.com",
"method_up": "GET",
"url_up": "",
}
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, self.channels_url)
c = Channel.objects.get()
self.assertEqual(c.name, "Call foo.com")
def test_it_adds_two_webhook_urls_and_redirects(self):
form = {
"method_down": "GET",

View File

@ -0,0 +1,84 @@
import json
from hc.api.models import Channel
from hc.test import BaseTestCase
class EditWebhookTestCase(BaseTestCase):
def setUp(self):
super(EditWebhookTestCase, self).setUp()
definition = {
"method_down": "GET",
"url_down": "http://example.org/down",
"body_down": "$NAME is down",
"headers_down": {"User-Agent": "My-Custom-UA"},
"method_up": "GET",
"url_up": "http://example.org/up",
"body_up": "$NAME is up",
"headers_up": {},
}
self.channel = Channel(project=self.project, kind="webhook")
self.channel.name = "Call example.org"
self.channel.value = json.dumps(definition)
self.channel.save()
self.url = "/integrations/%s/edit_webhook/" % self.channel.code
def test_it_shows_form(self):
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Webhook Settings")
self.assertContains(r, "Call example.org")
# down
self.assertContains(r, "http://example.org/down")
self.assertContains(r, "My-Custom-UA")
self.assertContains(r, "$NAME is down")
# up
self.assertContains(r, "http://example.org/up")
self.assertContains(r, "$NAME is up")
def test_it_saves_form_and_redirects(self):
form = {
"name": "Call foo.com / bar.com",
"method_down": "POST",
"url_down": "http://foo.com",
"headers_down": "X-Foo: 1\nX-Bar: 2",
"body_down": "going down",
"method_up": "POST",
"url_up": "https://bar.com",
"headers_up": "Content-Type: text/plain",
"body_up": "going up",
}
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, self.channels_url)
self.channel.refresh_from_db()
self.assertEqual(self.channel.name, "Call foo.com / bar.com")
down_spec = self.channel.down_webhook_spec
self.assertEqual(down_spec["method"], "POST")
self.assertEqual(down_spec["url"], "http://foo.com")
self.assertEqual(down_spec["body"], "going down")
self.assertEqual(down_spec["headers"], {"X-Foo": "1", "X-Bar": "2"})
up_spec = self.channel.up_webhook_spec
self.assertEqual(up_spec["method"], "POST")
self.assertEqual(up_spec["url"], "https://bar.com")
self.assertEqual(up_spec["body"], "going up")
self.assertEqual(up_spec["headers"], {"Content-Type": "text/plain"})
def test_it_requires_kind_webhook(self):
self.channel.kind = "email"
self.channel.value = "foo@example.org"
self.channel.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)

View File

@ -7,10 +7,7 @@ class UpdateChannelTestCase(BaseTestCase):
def setUp(self):
super(UpdateChannelTestCase, self).setUp()
self.check = Check.objects.create(project=self.project)
self.channel = Channel(project=self.project, kind="email")
self.channel.email = "alice@example.org"
self.channel.save()
self.channel = Channel.objects.create(project=self.project, kind="email")
def test_it_works(self):
payload = {"channel": self.channel.code, "check-%s" % self.check.code: True}

View File

@ -39,6 +39,7 @@ channel_urls = [
path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"),
path("<uuid:code>/checks/", views.channel_checks, name="hc-channel-checks"),
path("<uuid:code>/name/", views.update_channel_name, name="hc-channel-name"),
path("<uuid:code>/edit_webhook/", views.edit_webhook, name="hc-edit-webhook"),
path("<uuid:code>/test/", views.send_test_notification, name="hc-channel-test"),
path("<uuid:code>/remove/", views.remove_channel, name="hc-remove-channel"),
path(

View File

@ -843,16 +843,18 @@ def add_webhook(request, code):
project = _get_project_for_user(request, code)
if request.method == "POST":
form = forms.AddWebhookForm(request.POST)
form = forms.WebhookForm(request.POST)
if form.is_valid():
channel = Channel(project=project, kind="webhook")
channel.name = form.cleaned_data["name"]
channel.value = form.get_value()
channel.save()
channel.assign_all_checks()
return redirect("hc-p-channels", project.code)
else:
form = forms.AddWebhookForm()
form = forms.WebhookForm()
ctx = {
"page": "channels",
@ -860,7 +862,43 @@ def add_webhook(request, code):
"form": form,
"now": timezone.now().replace(microsecond=0).isoformat(),
}
return render(request, "integrations/add_webhook.html", ctx)
return render(request, "integrations/webhook_form.html", ctx)
@login_required
def edit_webhook(request, code):
channel = _get_channel_for_user(request, code)
if channel.kind != "webhook":
return HttpResponseBadRequest()
if request.method == "POST":
form = forms.WebhookForm(request.POST)
if form.is_valid():
channel.name = form.cleaned_data["name"]
channel.value = form.get_value()
channel.save()
return redirect("hc-p-channels", channel.project.code)
else:
def flatten(d):
return "\n".join("%s: %s" % pair for pair in d.items())
doc = json.loads(channel.value)
doc["headers_down"] = flatten(doc["headers_down"])
doc["headers_up"] = flatten(doc["headers_up"])
doc["name"] = channel.name
form = forms.WebhookForm(doc)
ctx = {
"page": "channels",
"project": channel.project,
"channel": channel,
"form": form,
"now": timezone.now().replace(microsecond=0).isoformat(),
}
return render(request, "integrations/webhook_form.html", ctx)
@require_setting("SHELL_ENABLED")

View File

@ -105,6 +105,10 @@ table.channels-table > tbody > tr > th {
color: #000;
}
.channel-row .actions {
text-align: right;
}
.channel-row .actions form {
display: inline;
}

View File

@ -37,4 +37,8 @@
.label-up {
color: #5cb85c
}
#webhook-form-name {
max-width: 400px;
}

View File

@ -22,7 +22,7 @@
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/add_webhook.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">

View File

@ -131,6 +131,9 @@
{% endif %}
</td>
<td class="actions">
{% if ch.kind == "webhook" %}
<a class="btn btn-sm btn-default" href="{% url 'hc-edit-webhook' ch.code %}">Edit</a>
{% endif %}
<form action="{% url 'hc-channel-test' ch.code %}" method="post">
{% csrf_token %}
<button

View File

@ -1,7 +1,13 @@
{% extends "base.html" %}
{% load compress humanize static hc_extras %}
{% block title %}Add Webhook Integration - {% site_name %}{% endblock %}
{% block title %}
{% if channel %}
Webhook Settings - {% site_name %}
{% else %}
Add Webhook Integration - {% site_name %}
{% endif %}
{% endblock %}
{% block content %}
@ -9,18 +15,30 @@
<div class="col-sm-12">
<h1>Webhook</h1>
<p>Executes an HTTP request to your specified URL when a check
goes up or down.</p>
<p>
You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong> and others in webhook URLs,
request body and header values
<a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
Executes an HTTP request to your specified URL when a check
goes up or down. You can use placeholders <strong>$NAME</strong>,
<strong>$STATUS</strong> and others in webhook URLs, request body and
header values
<a href="#" data-toggle="modal" data-target="#reference-modal">(available placeholders)</a>.
</p>
<form id="add-webhook-form" method="post">
{% csrf_token %}
<div class="form-group {{ form.name.css_classes }}">
<br>
<label>Name</label>
<input
id="webhook-form-name"
name="name"
value="{{ form.name.value|default:"" }}"
type="text"
class="form-control" />
<div class="help-block">Give this integration a descriptive name, so you can easily recognize it later. </div>
</div>
<div class="row">
<div class="col-sm-6">
<h2>Execute when a check goes <strong class="label-down">down</strong></h2>
@ -140,7 +158,13 @@
<br>
<br>
<div class="text-right">
<button type="submit" class="btn btn-primary">Save Integration</button>
<button type="submit" class="btn btn-primary">
{% if channel %}
Save Changes
{% else %}
Save Integration
{% endif %}
</button>
</div>
</div>
</form>