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