From 01d8e982cc2ef8009a6b3e38d2b326d77e673c17 Mon Sep 17 00:00:00 2001 From: Umit Akkaya Date: Fri, 1 Sep 2017 17:12:34 +0200 Subject: [PATCH 1/3] tags querystring param to filter checks by tags --- hc/api/tests/test_list_checks.py | 36 ++++++++++++++++++++++++++++++++ hc/api/views.py | 5 +++++ 2 files changed, 41 insertions(+) diff --git a/hc/api/tests/test_list_checks.py b/hc/api/tests/test_list_checks.py index 1d7b4a41..6c167b03 100644 --- a/hc/api/tests/test_list_checks.py +++ b/hc/api/tests/test_list_checks.py @@ -20,6 +20,7 @@ class ListChecksTestCase(BaseTestCase): self.a1.last_ping = self.now self.a1.n_pings = 1 self.a1.status = "new" + self.a1.tags = "a1-tag a1-additional-tag" self.a1.save() self.a2 = Check(user=self.alice, name="Alice 2") @@ -27,6 +28,7 @@ class ListChecksTestCase(BaseTestCase): self.a2.grace = td(seconds=3600) self.a2.last_ping = self.now self.a2.status = "up" + self.a2.tags = "a2-tag" self.a2.save() def get(self): @@ -79,3 +81,37 @@ class ListChecksTestCase(BaseTestCase): self.assertEqual(r.status_code, 200) self.assertContains(r, "Alice") + + def test_it_works_with_tags_param(self): + r = self.client.get("/api/v1/checks/?tags=a2-tag", HTTP_X_API_KEY="abc") + self.assertEqual(r.status_code, 200) + + doc = r.json() + self.assertTrue("checks" in doc) + self.assertEqual(len(doc["checks"]), 1) + + check = doc["checks"][0] + + self.assertEqual(check["name"], "Alice 2") + self.assertEqual(check["tags"], "a2-tag") + + def test_it_filters_with_multiple_tags_param(self): + r = self.client.get("/api/v1/checks/?tags=a1-tag&tags=a1-additional-tag", HTTP_X_API_KEY="abc") + self.assertEqual(r.status_code, 200) + + doc = r.json() + self.assertTrue("checks" in doc) + self.assertEqual(len(doc["checks"]), 1) + + check = doc["checks"][0] + + self.assertEqual(check["name"], "Alice 1") + self.assertEqual(check["tags"], "a1-tag a1-additional-tag") + + def test_non_existing_tags_filter_returns_empty_result(self): + r = self.client.get("/api/v1/checks/?tags=non_existing_tag_with_no_checks", HTTP_X_API_KEY="abc") + self.assertEqual(r.status_code, 200) + + doc = r.json() + self.assertTrue("checks" in doc) + self.assertEqual(len(doc["checks"]), 0) diff --git a/hc/api/views.py b/hc/api/views.py index d351b867..6388c3ec 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -104,7 +104,12 @@ def _update(check, spec): @validate_json(schemas.check) def checks(request): if request.method == "GET": + tags = request.GET.getlist('tags', []) q = Check.objects.filter(user=request.user) + + for tag in tags: + q = q.filter(tags__icontains=tag.strip()) + doc = {"checks": [check.to_dict() for check in q]} return JsonResponse(doc) From afe4ec6df8fad8f3181d9359152a35f32171235d Mon Sep 17 00:00:00 2001 From: Umit Akkaya Date: Thu, 7 Sep 2017 20:32:05 +0200 Subject: [PATCH 2/3] Ensure filter matches tag exactly --- hc/api/tests/test_list_checks.py | 14 +++++++++++--- hc/api/views.py | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/hc/api/tests/test_list_checks.py b/hc/api/tests/test_list_checks.py index 6c167b03..db060379 100644 --- a/hc/api/tests/test_list_checks.py +++ b/hc/api/tests/test_list_checks.py @@ -83,7 +83,7 @@ class ListChecksTestCase(BaseTestCase): self.assertContains(r, "Alice") def test_it_works_with_tags_param(self): - r = self.client.get("/api/v1/checks/?tags=a2-tag", HTTP_X_API_KEY="abc") + r = self.client.get("/api/v1/checks/?tag=a2-tag", HTTP_X_API_KEY="abc") self.assertEqual(r.status_code, 200) doc = r.json() @@ -96,7 +96,7 @@ class ListChecksTestCase(BaseTestCase): self.assertEqual(check["tags"], "a2-tag") def test_it_filters_with_multiple_tags_param(self): - r = self.client.get("/api/v1/checks/?tags=a1-tag&tags=a1-additional-tag", HTTP_X_API_KEY="abc") + r = self.client.get("/api/v1/checks/?tag=a1-tag&tag=a1-additional-tag", HTTP_X_API_KEY="abc") self.assertEqual(r.status_code, 200) doc = r.json() @@ -108,8 +108,16 @@ class ListChecksTestCase(BaseTestCase): self.assertEqual(check["name"], "Alice 1") self.assertEqual(check["tags"], "a1-tag a1-additional-tag") + def test_it_does_not_match_tag_partially(self): + r = self.client.get("/api/v1/checks/?tag=tag", HTTP_X_API_KEY="abc") + self.assertEqual(r.status_code, 200) + + doc = r.json() + self.assertTrue("checks" in doc) + self.assertEqual(len(doc["checks"]), 0) + def test_non_existing_tags_filter_returns_empty_result(self): - r = self.client.get("/api/v1/checks/?tags=non_existing_tag_with_no_checks", HTTP_X_API_KEY="abc") + r = self.client.get("/api/v1/checks/?tag=non_existing_tag_with_no_checks", HTTP_X_API_KEY="abc") self.assertEqual(r.status_code, 200) doc = r.json() diff --git a/hc/api/views.py b/hc/api/views.py index 6388c3ec..1f7aa27c 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -104,13 +104,23 @@ def _update(check, spec): @validate_json(schemas.check) def checks(request): if request.method == "GET": - tags = request.GET.getlist('tags', []) + tags = request.GET.getlist('tag', []) q = Check.objects.filter(user=request.user) - for tag in tags: - q = q.filter(tags__icontains=tag.strip()) + doc = {"checks": []} + + if len(tags) > 0: + for tag in tags: + q = q.filter(tags__contains=tag.strip()) + + tags_set = set(tags) + + for check in q: + if tags_set.issubset(set(check.tags_list())): + doc["checks"].append(check.to_dict()) + else: + doc["checks"] = [check.to_dict() for check in q] - doc = {"checks": [check.to_dict() for check in q]} return JsonResponse(doc) elif request.method == "POST": From 8f88fbf24f6891b4f1a14b9c6faf875613c1977e Mon Sep 17 00:00:00 2001 From: Umit Akkaya Date: Thu, 7 Sep 2017 20:44:34 +0200 Subject: [PATCH 3/3] Add filtering checks by tag to api documentation --- templates/front/docs_api.html | 10 ++++++++-- .../list_checks_request_filtered.html | 2 ++ .../snippets/list_checks_request_filtered.txt | 1 + .../list_checks_response_filtered.html | 19 +++++++++++++++++++ .../list_checks_response_filtered.txt | 18 ++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 templates/front/snippets/list_checks_request_filtered.html create mode 100644 templates/front/snippets/list_checks_request_filtered.txt create mode 100644 templates/front/snippets/list_checks_response_filtered.html create mode 100644 templates/front/snippets/list_checks_response_filtered.txt diff --git a/templates/front/docs_api.html b/templates/front/docs_api.html index a5c9d39b..af5275e6 100644 --- a/templates/front/docs_api.html +++ b/templates/front/docs_api.html @@ -61,8 +61,8 @@ The response may contain a JSON document with additional data.
GET {{ SITE_ROOT }}/api/v1/checks/

- Returns a list of checks. This API call takes no parameters and returns - a JSON document with all checks in user's account. + Returns a list of checks. This API call takes only optional tag querystring parameter to filter checks by their tags. + If no parameter provided it returns a JSON document with all checks in user's account.

Example Request

{% include "front/snippets/list_checks_request.html" %} @@ -70,6 +70,12 @@ The response may contain a JSON document with additional data.

Example Response

{% include "front/snippets/list_checks_response.html" %} +

Example Request To Filter Checks By Their Tags

+{% include "front/snippets/list_checks_request_filtered.html" %} + +

Example Response Of Filtered Checks

+{% include "front/snippets/list_checks_response_filtered.html" %} + diff --git a/templates/front/snippets/list_checks_request_filtered.html b/templates/front/snippets/list_checks_request_filtered.html new file mode 100644 index 00000000..a604acf6 --- /dev/null +++ b/templates/front/snippets/list_checks_request_filtered.html @@ -0,0 +1,2 @@ +
curl --header "X-Api-Key: your-api-key" {{ SITE_ROOT }}/api/v1/checks/?tag=bar&tag=baz
+
diff --git a/templates/front/snippets/list_checks_request_filtered.txt b/templates/front/snippets/list_checks_request_filtered.txt new file mode 100644 index 00000000..b393688d --- /dev/null +++ b/templates/front/snippets/list_checks_request_filtered.txt @@ -0,0 +1 @@ +curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/?tag=bar&tag=baz \ No newline at end of file diff --git a/templates/front/snippets/list_checks_response_filtered.html b/templates/front/snippets/list_checks_response_filtered.html new file mode 100644 index 00000000..530c1c7e --- /dev/null +++ b/templates/front/snippets/list_checks_response_filtered.html @@ -0,0 +1,19 @@ +
{
+  "checks": [
+    {
+      "last_ping": null,
+      "ping_url": "{{ PING_ENDPOINT }}9d17c61f-5c4f-4cab-b517-11e6b2679ced",
+      "next_ping": null,
+      "grace": 3600,
+      "name": "Api test 2",
+      "n_pings": 0,
+      "tags": "bar baz",
+      "pause_url": "{{ SITE_ROOT }}/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause",
+      "tz": "UTC",
+      "schedule": "0/10 * * * *",
+      "status": "new",
+      "update_url": "{{ SITE_ROOT }}/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced"
+    }
+  ]
+}
+
diff --git a/templates/front/snippets/list_checks_response_filtered.txt b/templates/front/snippets/list_checks_response_filtered.txt new file mode 100644 index 00000000..870cfc1f --- /dev/null +++ b/templates/front/snippets/list_checks_response_filtered.txt @@ -0,0 +1,18 @@ +{ + "checks": [ + { + "last_ping": null, + "ping_url": "PING_ENDPOINT9d17c61f-5c4f-4cab-b517-11e6b2679ced", + "next_ping": null, + "grace": 3600, + "name": "Api test 2", + "n_pings": 0, + "tags": "bar baz", + "pause_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause", + "tz": "UTC", + "schedule": "0/10 * * * *", + "status": "new", + "update_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced" + } + ] +} \ No newline at end of file