From 0e77064c4418050222c921a74a5691a6f3fafa91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Wed, 14 Oct 2020 15:37:04 +0300
Subject: [PATCH] Update API to allow specifying channels by names
Fixes: #440
---
CHANGELOG.md | 1 +
hc/api/tests/test_update_check.py | 47 ++++++++++++++++++++++++++++++-
hc/api/views.py | 43 ++++++++++++++++------------
templates/docs/api.html | 36 +++++++++++++++++------
templates/docs/api.md | 47 +++++++++++++++++++++++++------
5 files changed, 139 insertions(+), 35 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45e7ad3a..23975fda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## Improvements
- Add a tooltip to the 'confirmation link' label (#436)
+- Update API to allow specifying channels by names (#440)
## v1.17.0 - 2020-10-14
diff --git a/hc/api/tests/test_update_check.py b/hc/api/tests/test_update_check.py
index cd6f5a16..68854a6c 100644
--- a/hc/api/tests/test_update_check.py
+++ b/hc/api/tests/test_update_check.py
@@ -123,6 +123,28 @@ class UpdateCheckTestCase(BaseTestCase):
self.check.refresh_from_db()
self.assertEqual(self.check.channel_set.count(), 1)
+ def test_it_sets_channel_by_name(self):
+ channel = Channel.objects.create(project=self.project, name="alerts")
+
+ r = self.post(self.check.code, {"api_key": "X" * 32, "channels": "alerts"})
+
+ self.assertEqual(r.status_code, 200)
+
+ self.check.refresh_from_db()
+ self.assertEqual(self.check.channel_set.count(), 1)
+ self.assertEqual(self.check.channel_set.first().code, channel.code)
+
+ def test_it_sets_channel_by_name_formatted_as_uuid(self):
+ name = "102eaa82-a274-4b15-a499-c1bb6bbcd7b6"
+ channel = Channel.objects.create(project=self.project, name=name)
+
+ r = self.post(self.check.code, {"api_key": "X" * 32, "channels": name})
+ self.assertEqual(r.status_code, 200)
+
+ self.check.refresh_from_db()
+ self.assertEqual(self.check.channel_set.count(), 1)
+ self.assertEqual(self.check.channel_set.first().code, channel.code)
+
def test_it_handles_comma_separated_channel_codes(self):
c1 = Channel.objects.create(project=self.project)
c2 = Channel.objects.create(project=self.project)
@@ -187,10 +209,33 @@ class UpdateCheckTestCase(BaseTestCase):
self.check.refresh_from_db()
self.assertEqual(self.check.channel_set.count(), 0)
- def test_it_rejects_non_uuid_channel_code(self):
+ def test_it_handles_channel_lookup_by_name_with_no_results(self):
r = self.post(self.check.code, {"api_key": "X" * 32, "channels": "foo"})
self.assertEqual(r.status_code, 400)
+ self.assertEqual(r.json()["error"], "invalid channel identifier: foo")
+
+ self.check.refresh_from_db()
+ self.assertEqual(self.check.channel_set.count(), 0)
+
+ def test_it_handles_channel_lookup_by_name_with_multiple_results(self):
+ Channel.objects.create(project=self.project, name="foo")
+ Channel.objects.create(project=self.project, name="foo")
+
+ r = self.post(self.check.code, {"api_key": "X" * 32, "channels": "foo"})
+
+ self.assertEqual(r.status_code, 400)
+ self.assertEqual(r.json()["error"], "non-unique channel identifier: foo")
+
+ self.check.refresh_from_db()
+ self.assertEqual(self.check.channel_set.count(), 0)
+
+ def test_it_rejects_multiple_empty_channel_names(self):
+ Channel.objects.create(project=self.project, name="")
+
+ r = self.post(self.check.code, {"api_key": "X" * 32, "channels": ","})
+ self.assertEqual(r.status_code, 400)
+ self.assertEqual(r.json()["error"], "empty channel identifier")
self.check.refresh_from_db()
self.assertEqual(self.check.channel_set.count(), 0)
diff --git a/hc/api/views.py b/hc/api/views.py
index ee52152d..6236c1d5 100644
--- a/hc/api/views.py
+++ b/hc/api/views.py
@@ -1,6 +1,5 @@
from datetime import timedelta as td
import time
-import uuid
from django.conf import settings
from django.db import connection
@@ -71,20 +70,32 @@ def _lookup(project, spec):
def _update(check, spec):
- channels = set()
- # First, validate the supplied channel codes
- if "channels" in spec and spec["channels"] not in ("*", ""):
- q = Channel.objects.filter(project=check.project)
+ # First, validate the supplied channel codes/names
+ if "channels" not in spec:
+ # If the channels key is not present, don't update check's channels
+ new_channels = None
+ elif spec["channels"] == "*":
+ # "*" means "all project's channels"
+ new_channels = Channel.objects.filter(project=check.project)
+ elif spec.get("channels") == "":
+ # "" means "empty list"
+ new_channels = []
+ else:
+ # expect a comma-separated list of channel codes or names
+ new_channels = set()
+ available = list(Channel.objects.filter(project=check.project))
+
for s in spec["channels"].split(","):
- try:
- code = uuid.UUID(s)
- except ValueError:
- raise BadChannelException("invalid channel identifier: %s" % s)
+ if s == "":
+ raise BadChannelException("empty channel identifier")
- try:
- channels.add(q.get(code=code))
- except Channel.DoesNotExist:
+ matches = [c for c in available if str(c.code) == s or c.name == s]
+ if len(matches) == 0:
raise BadChannelException("invalid channel identifier: %s" % s)
+ elif len(matches) > 1:
+ raise BadChannelException("non-unique channel identifier: %s" % s)
+
+ new_channels.add(matches[0])
if "name" in spec:
check.name = spec["name"]
@@ -119,12 +130,8 @@ def _update(check, spec):
# This needs to be done after saving the check, because of
# the M2M relation between checks and channels:
- if spec.get("channels") == "*":
- check.assign_all_channels()
- elif spec.get("channels") == "":
- check.channel_set.clear()
- elif channels:
- check.channel_set.set(channels)
+ if new_channels is not None:
+ check.channel_set.set(new_channels)
return check
diff --git a/templates/docs/api.html b/templates/docs/api.html
index 30e9192f..b38e071e 100644
--- a/templates/docs/api.html
+++ b/templates/docs/api.html
@@ -347,10 +347,20 @@ and POST requests.
By default, this API call assigns no integrations to the newly created
check.
Set this field to a special value "*" to automatically assign all existing
-integrations.
+integrations. Example:
+{"channels": "*"}
To assign specific integrations, use a comma-separated list of integration
-identifiers. Use the Get a List of Existing Integrations
-API call to look up the available integration identifiers.
+UUIDs. You can look up integration UUIDs using the
+Get a List of Existing Integrations API call.
+Example:
+{"channels":
+ "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc"}
+Alternatively, if you have named your integrations in SITE_NAME dashboard,
+you can specify integrations by their names. For this to work, your integrations
+need non-empty and unique names, and they must not contain commas. The names
+must match exactly, whitespace is significant.
+Example:
+{"channels": "Email to Alice,SMS to Alice"}
unique
@@ -496,13 +506,23 @@ and POST requests.
string, optional.
Set this field to a special value "*" to automatically assign all existing
-notification channels.
+integrations. Example:
+{"channels": "*"}
Set this field to a special value "" (empty string) to automatically unassign
-all notification channels.
-Set this field to a comma-separated list of channel identifiers to assign
-specific notification channels.
+all existing integrations. Example:
+{"channels": ""}
+To assign specific integrations, use a comma-separated list of integration
+UUIDs. You can look up integration UUIDs using the
+Get a List of Existing Integrations API call.
+Example:
+{"channels":
+ "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc"}
+Alternatively, if you have named your integrations in SITE_NAME dashboard,
+you can specify integrations by their names. For this to work, your integrations
+need non-empty and unique names, and they must not contain commas. The names
+must match exactly, whitespace is significant.
Example:
-{"channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc"}
+{"channels": "Email to Alice,SMS to Alice"}
Response Codes
diff --git a/templates/docs/api.md b/templates/docs/api.md
index 850e7f72..07516cc0 100644
--- a/templates/docs/api.md
+++ b/templates/docs/api.md
@@ -360,11 +360,27 @@ channels
check.
Set this field to a special value "*" to automatically assign all existing
- integrations.
+ integrations. Example:
+
+ {"channels": "*"}
To assign specific integrations, use a comma-separated list of integration
- identifiers. Use the [Get a List of Existing Integrations](#list-channels)
- API call to look up the available integration identifiers.
+ UUIDs. You can look up integration UUIDs using the
+ [Get a List of Existing Integrations](#list-channels) API call.
+
+ Example:
+
+ {"channels":
+ "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc"}
+
+ Alternatively, if you have named your integrations in SITE_NAME dashboard,
+ you can specify integrations by their names. For this to work, your integrations
+ need non-empty and unique names, and they must not contain commas. The names
+ must match exactly, whitespace is significant.
+
+ Example:
+
+ {"channels": "Email to Alice,SMS to Alice"}
unique
: array of string values, optional, default value: [].
@@ -540,17 +556,32 @@ channels
: string, optional.
Set this field to a special value "*" to automatically assign all existing
- notification channels.
+ integrations. Example:
+
+ {"channels": "*"}
Set this field to a special value "" (empty string) to automatically *unassign*
- all notification channels.
+ all existing integrations. Example:
+
+ {"channels": ""}
+
+ To assign specific integrations, use a comma-separated list of integration
+ UUIDs. You can look up integration UUIDs using the
+ [Get a List of Existing Integrations](#list-channels) API call.
+
+ Example:
+
+ {"channels":
+ "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc"}
- Set this field to a comma-separated list of channel identifiers to assign
- specific notification channels.
+ Alternatively, if you have named your integrations in SITE_NAME dashboard,
+ you can specify integrations by their names. For this to work, your integrations
+ need non-empty and unique names, and they must not contain commas. The names
+ must match exactly, whitespace is significant.
Example:
- {"channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc"}
+ {"channels": "Email to Alice,SMS to Alice"}
### Response Codes