From 98eb7cc14a13c06c381fde1f31257b40c292db40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Wed, 18 Aug 2021 17:47:57 +0300
Subject: [PATCH] Add /api/v1/badges/ endpoint
cc: #552
---
CHANGELOG.md | 3 ++
hc/api/tests/test_get_badges.py | 43 ++++++++++++++++++++++
hc/api/urls.py | 1 +
hc/api/views.py | 24 ++++++++++++-
templates/docs/api.html | 58 ++++++++++++++++++++++++++++++
templates/docs/api.md | 64 +++++++++++++++++++++++++++++++++
6 files changed, 192 insertions(+), 1 deletion(-)
create mode 100644 hc/api/tests/test_get_badges.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a0dcbcd..3cfd7f92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
## v1.23.0 - Unreleased
+### Improvements
+- Add /api/v1/badges/ endpoint (#552)
+
### Bug Fixes
- Add handling for non-latin-1 characters in webhook headers
- Fix dark mode bug in selectpicker widgets
diff --git a/hc/api/tests/test_get_badges.py b/hc/api/tests/test_get_badges.py
new file mode 100644
index 00000000..c9771be7
--- /dev/null
+++ b/hc/api/tests/test_get_badges.py
@@ -0,0 +1,43 @@
+from datetime import timedelta as td
+
+from hc.api.models import Check
+from hc.test import BaseTestCase
+
+
+class GetBadgesTestCase(BaseTestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.a1 = Check(project=self.project, name="Alice 1")
+ self.a1.timeout = td(seconds=3600)
+ self.a1.grace = td(seconds=900)
+ self.a1.n_pings = 0
+ self.a1.status = "new"
+ self.a1.tags = "foo bar"
+ self.a1.save()
+
+ self.url = "/api/v1/badges/"
+
+ def get(self, api_key="X" * 32, qs=""):
+ return self.client.get(self.url + qs, HTTP_X_API_KEY=api_key)
+
+ def test_it_works(self):
+
+ r = self.get()
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r["Access-Control-Allow-Origin"], "*")
+
+ doc = r.json()
+ self.assertTrue("foo" in doc["badges"])
+ self.assertTrue("svg" in doc["badges"]["foo"])
+
+ def test_readonly_key_is_allowed(self):
+ self.project.api_key_readonly = "R" * 32
+ self.project.save()
+
+ r = self.get(api_key=self.project.api_key_readonly)
+ self.assertEqual(r.status_code, 200)
+
+ def test_it_rejects_post(self):
+ r = self.client.post(self.url, HTTP_X_API_KEY="X" * 32)
+ self.assertEqual(r.status_code, 405)
diff --git a/hc/api/urls.py b/hc/api/urls.py
index 7546b68a..8aeeb222 100644
--- a/hc/api/urls.py
+++ b/hc/api/urls.py
@@ -46,6 +46,7 @@ urlpatterns = [
path("api/v1/checks//flips/", views.flips_by_uuid, name="hc-api-flips"),
path("api/v1/checks//flips/", views.flips_by_unique_key),
path("api/v1/channels/", views.channels),
+ path("api/v1/badges/", views.badges),
path(
"badge///.",
views.badge,
diff --git a/hc/api/views.py b/hc/api/views.py
index 03ebb6ef..9f40779a 100644
--- a/hc/api/views.py
+++ b/hc/api/views.py
@@ -21,7 +21,7 @@ from hc.api import schemas
from hc.api.decorators import authorize, authorize_read, cors, validate_json
from hc.api.forms import FlipsFiltersForm
from hc.api.models import MAX_DELTA, Flip, Channel, Check, Notification, Ping
-from hc.lib.badges import check_signature, get_badge_svg
+from hc.lib.badges import check_signature, get_badge_svg, get_badge_url
class BadChannelException(Exception):
@@ -377,6 +377,28 @@ def flips_by_unique_key(request, unique_key):
return HttpResponseNotFound()
+@cors("GET")
+@authorize_read
+def badges(request):
+ tags = set(["*"])
+ for check in Check.objects.filter(project=request.project):
+ tags.update(check.tags_list())
+
+ key = request.project.badge_key
+ badges = {}
+ for tag in tags:
+ badges[tag] = {
+ "svg": get_badge_url(key, tag),
+ "svg3": get_badge_url(key, tag, with_late=True),
+ "json": get_badge_url(key, tag, fmt="json"),
+ "json3": get_badge_url(key, tag, fmt="json", with_late=True),
+ "shields": get_badge_url(key, tag, fmt="shields"),
+ "shields3": get_badge_url(key, tag, fmt="shields", with_late=True),
+ }
+
+ return JsonResponse({"badges": badges})
+
+
@never_cache
@cors("GET")
def badge(request, badge_key, signature, tag, fmt):
diff --git a/templates/docs/api.html b/templates/docs/api.html
index f26c6caf..42df1407 100644
--- a/templates/docs/api.html
+++ b/templates/docs/api.html
@@ -46,6 +46,10 @@ in your account.
Get a list of existing integrations |
GET SITE_ROOT/api/v1/channels/ |
+
+Get project's badges |
+GET SITE_ROOT/api/v1/badges/ |
+
Authentication
@@ -63,6 +67,7 @@ and read-only API keys on the Project Settings page.
Get a list of existing checks
Get a single check
Get a list of check's status changes
+Get project's badges
Omits sensitive information from the API responses. See the documentation of
individual API endpoints for details.
@@ -807,4 +812,57 @@ number of returned pings depends on the account's billing plan: 100 for free acc
}
]
}
+
+
+Get Project's Badges
+GET SITE_ROOT/api/v1/badges/
+Returns a map of all tags in the project, with badge URLs for each tag.
+Response Codes
+
+- 200 OK
+- The request succeeded.
+- 401 Unauthorized
+- The API key is either missing or invalid.
+
+Example Request
+curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/badges/
+
+
+Example Response
+{
+ "badges": {
+ "backup": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.shields"
+ },
+ "db": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm-2/db.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm/db.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm-2/db.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm/db.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm-2/db.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm/db.shields"
+ },
+ "production": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8-2/production.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8/production.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8-2/production.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8/production.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8-2/production.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8/production.shields"
+ },
+ "*": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe-2.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe-2.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe-2.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe.shields"
+ }
+ }
+}
\ No newline at end of file
diff --git a/templates/docs/api.md b/templates/docs/api.md
index 99d6b5b7..addab3bb 100644
--- a/templates/docs/api.md
+++ b/templates/docs/api.md
@@ -16,6 +16,7 @@ Endpoint Name | Endpoint Address
[Get a list of check's logged pings](#list-pings) | `GET SITE_ROOT/api/v1/checks//pings/`
[Get a list of check's status changes](#list-flips) | `GET SITE_ROOT/api/v1/checks//flips/`
`GET SITE_ROOT/api/v1/checks//flips/`
[Get a list of existing integrations](#list-channels) | `GET SITE_ROOT/api/v1/channels/`
+[Get project's badges](#list-badges) | `GET SITE_ROOT/api/v1/badges/`
## Authentication
@@ -33,6 +34,7 @@ read-only key
* [Get a list of existing checks](#list-checks)
* [Get a single check](#get-check)
* [Get a list of check's status changes](#list-flips)
+ * [Get project's badges](#list-badges)
Omits sensitive information from the API responses. See the documentation of
individual API endpoints for details.
@@ -946,3 +948,65 @@ curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/channels/
]
}
```
+
+## Get Project's Badges {: #list-badges .rule }
+
+`GET SITE_ROOT/api/v1/badges/`
+
+Returns a map of all tags in the project, with badge URLs for each tag.
+
+### Response Codes
+
+200 OK
+: The request succeeded.
+
+401 Unauthorized
+: The API key is either missing or invalid.
+
+### Example Request
+
+```bash
+curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/badges/
+```
+
+### Example Response
+
+```json
+{
+ "badges": {
+ "backup": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M-2/backup.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/LOegDs5M/backup.shields"
+ },
+ "db": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm-2/db.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm/db.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm-2/db.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm/db.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm-2/db.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/99MuQaKm/db.shields"
+ },
+ "production": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8-2/production.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8/production.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8-2/production.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8/production.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8-2/production.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/1TEhqie8/production.shields"
+ },
+ "*": {
+ "svg": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe-2.svg",
+ "svg3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe.svg",
+ "json": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe-2.json",
+ "json3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe.json",
+ "shields": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe-2.shields",
+ "shields3": "SITE_ROOT/badge/67541b37-8b9c-4d17-b952-690eae/9X7kcZoe.shields"
+ }
+ }
+}
+```
+