From 609f78c5ed139d866685b760399873933e8e3d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Mon, 6 Apr 2020 14:48:47 +0300 Subject: [PATCH] "Edit" function for webhook integrations (#176) --- CHANGELOG.md | 1 + hc/front/forms.py | 6 +- hc/front/tests/test_add_webhook.py | 16 ++++ hc/front/tests/test_edit_webhook.py | 84 +++++++++++++++++++ hc/front/tests/test_update_channel.py | 5 +- hc/front/urls.py | 1 + hc/front/views.py | 44 +++++++++- static/css/channels.css | 4 + .../css/{add_webhook.css => webhook_form.css} | 4 + templates/base.html | 2 +- templates/front/channels.html | 3 + .../{add_webhook.html => webhook_form.html} | 40 +++++++-- 12 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 hc/front/tests/test_edit_webhook.py rename static/css/{add_webhook.css => webhook_form.css} (92%) rename templates/integrations/{add_webhook.html => webhook_form.html} (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa6cd1a4..13505378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/hc/front/forms.py b/hc/front/forms.py index febca0d0..7e68d36c 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -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) diff --git a/hc/front/tests/test_add_webhook.py b/hc/front/tests/test_add_webhook.py index d42d3946..919248bb 100644 --- a/hc/front/tests/test_add_webhook.py +++ b/hc/front/tests/test_add_webhook.py @@ -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", diff --git a/hc/front/tests/test_edit_webhook.py b/hc/front/tests/test_edit_webhook.py new file mode 100644 index 00000000..f04720b2 --- /dev/null +++ b/hc/front/tests/test_edit_webhook.py @@ -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) diff --git a/hc/front/tests/test_update_channel.py b/hc/front/tests/test_update_channel.py index 168019c7..a45ea66d 100644 --- a/hc/front/tests/test_update_channel.py +++ b/hc/front/tests/test_update_channel.py @@ -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} diff --git a/hc/front/urls.py b/hc/front/urls.py index 0b344300..53ba4bfe 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -39,6 +39,7 @@ channel_urls = [ path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"), path("/checks/", views.channel_checks, name="hc-channel-checks"), path("/name/", views.update_channel_name, name="hc-channel-name"), + path("/edit_webhook/", views.edit_webhook, name="hc-edit-webhook"), path("/test/", views.send_test_notification, name="hc-channel-test"), path("/remove/", views.remove_channel, name="hc-remove-channel"), path( diff --git a/hc/front/views.py b/hc/front/views.py index 6d3e4c53..2c6eedaf 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -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") diff --git a/static/css/channels.css b/static/css/channels.css index a7e0122a..7e8eea90 100644 --- a/static/css/channels.css +++ b/static/css/channels.css @@ -105,6 +105,10 @@ table.channels-table > tbody > tr > th { color: #000; } +.channel-row .actions { + text-align: right; +} + .channel-row .actions form { display: inline; } diff --git a/static/css/add_webhook.css b/static/css/webhook_form.css similarity index 92% rename from static/css/add_webhook.css rename to static/css/webhook_form.css index c90883cb..445d9991 100644 --- a/static/css/add_webhook.css +++ b/static/css/webhook_form.css @@ -37,4 +37,8 @@ .label-up { color: #5cb85c +} + +#webhook-form-name { + max-width: 400px; } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index f5f85f60..46bb97ac 100644 --- a/templates/base.html +++ b/templates/base.html @@ -22,7 +22,7 @@ - + diff --git a/templates/front/channels.html b/templates/front/channels.html index 5ecbe4a9..506589c3 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -131,6 +131,9 @@ {% endif %} + {% if ch.kind == "webhook" %} + Edit + {% endif %}
{% csrf_token %} +