From 524d1a73752b5f402ad10331f6a6e8d7482e1079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Fri, 27 Nov 2020 12:57:25 +0200 Subject: [PATCH] Implement badge mode (up/down vs up/late/down) selector Fixes #282 --- CHANGELOG.md | 1 + hc/api/tests/test_badge.py | 76 ++++++++++++++++++------------------- hc/api/views.py | 12 ++++-- hc/front/views.py | 10 +++-- hc/lib/badges.py | 13 ++++--- static/css/badges.css | 16 ++++++++ static/css/settings.css | 22 ----------- static/js/badges.js | 15 +++++++- templates/base.html | 1 + templates/front/badges.html | 62 +++++++++++++++++++++--------- 10 files changed, 133 insertions(+), 95 deletions(-) create mode 100644 static/css/badges.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 633b5033..c1a8fd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Add retries to the the email sending logic - Require confirmation codes (sent to email) before sensitive actions - Implement WebAuthn two-factor authentication +- Implement badge mode (up/down vs up/late/down) selector (#282) ## v1.17.0 - 2020-10-14 diff --git a/hc/api/tests/test_badge.py b/hc/api/tests/test_badge.py index 70d24c93..a34daba2 100644 --- a/hc/api/tests/test_badge.py +++ b/hc/api/tests/test_badge.py @@ -15,12 +15,15 @@ class BadgeTestCase(BaseTestCase): sig = base64_hmac(str(self.project.badge_key), "foo", settings.SECRET_KEY) sig = sig[:8] - self.svg_url = "/badge/%s/%s/foo.svg" % (self.project.badge_key, sig) - self.json_url = "/badge/%s/%s/foo.json" % (self.project.badge_key, sig) + + self.svg_url = "/badge/%s/%s-2/foo.svg" % (self.project.badge_key, sig) + self.json_url = "/badge/%s/%s-2/foo.json" % (self.project.badge_key, sig) + self.with_late_url = "/badge/%s/%s/foo.json" % (self.project.badge_key, sig) + self.shields_url = "/badge/%s/%s-2/foo.shields" % (self.project.badge_key, sig) def test_it_rejects_bad_signature(self): r = self.client.get("/badge/%s/12345678/foo.svg" % self.project.badge_key) - assert r.status_code == 404 + self.assertEqual(r.status_code, 404) def test_it_returns_svg(self): r = self.client.get(self.svg_url) @@ -37,52 +40,24 @@ class BadgeTestCase(BaseTestCase): self.assertEqual(r["Access-Control-Allow-Origin"], "*") def test_it_handles_new(self): - r = self.client.get(self.json_url) - doc = r.json() - self.assertEqual(doc["status"], "up") - self.assertEqual(doc["total"], 1) - self.assertEqual(doc["grace"], 0) - self.assertEqual(doc["down"], 0) - - def test_it_handles_started_but_down(self): + doc = self.client.get(self.json_url).json() + self.assertEqual(doc, {"status": "up", "total": 1, "grace": 0, "down": 0}) + + def test_it_ignores_started_when_down(self): self.check.last_start = now() - self.check.tags = "foo" self.check.status = "down" self.check.save() - r = self.client.get(self.json_url) - doc = r.json() - self.assertEqual(doc["status"], "down") - self.assertEqual(doc["total"], 1) - self.assertEqual(doc["grace"], 0) - self.assertEqual(doc["down"], 1) + doc = self.client.get(self.json_url).json() + self.assertEqual(doc, {"status": "down", "total": 1, "grace": 0, "down": 1}) - def test_it_shows_grace_badge(self): + def test_it_treats_late_as_up(self): self.check.last_ping = now() - td(days=1, minutes=10) - self.check.tags = "foo" self.check.status = "up" self.check.save() - r = self.client.get(self.json_url) - doc = r.json() - self.assertEqual(doc["status"], "late") - self.assertEqual(doc["total"], 1) - self.assertEqual(doc["grace"], 1) - self.assertEqual(doc["down"], 0) - - def test_it_shows_started_but_grace_badge(self): - self.check.last_start = now() - self.check.last_ping = now() - td(days=1, minutes=10) - self.check.tags = "foo" - self.check.status = "up" - self.check.save() - - r = self.client.get(self.json_url) - doc = r.json() - self.assertEqual(doc["status"], "late") - self.assertEqual(doc["total"], 1) - self.assertEqual(doc["grace"], 1) - self.assertEqual(doc["down"], 0) + doc = self.client.get(self.json_url).json() + self.assertEqual(doc, {"status": "up", "total": 1, "grace": 1, "down": 0}) def test_it_handles_special_characters(self): self.check.tags = "db@dc1" @@ -94,3 +69,24 @@ class BadgeTestCase(BaseTestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) + + def test_late_mode_returns_late_status(self): + self.check.last_ping = now() - td(days=1, minutes=10) + self.check.status = "up" + self.check.save() + + doc = self.client.get(self.with_late_url).json() + self.assertEqual(doc, {"status": "late", "total": 1, "grace": 1, "down": 0}) + + def test_late_mode_ignores_started_when_late(self): + self.check.last_start = now() + self.check.last_ping = now() - td(days=1, minutes=10) + self.check.status = "up" + self.check.save() + + doc = self.client.get(self.with_late_url).json() + self.assertEqual(doc, {"status": "late", "total": 1, "grace": 1, "down": 0}) + + def test_it_returns_shields_json(self): + doc = self.client.get(self.shields_url).json() + self.assertEqual(doc, {"label": "foo", "message": "up", "color": "success"}) diff --git a/hc/api/views.py b/hc/api/views.py index 0702f803..d16b8e95 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -375,11 +375,15 @@ def flips_by_unique_key(request, unique_key): @never_cache @cors("GET") -def badge(request, badge_key, signature, tag, fmt="svg"): - if not check_signature(badge_key, tag, signature): +def badge(request, badge_key, signature, tag, fmt): + if fmt not in ("svg", "json", "shields"): return HttpResponseNotFound() - if fmt not in ("svg", "json", "shields"): + with_late = True + if len(signature) == 10 and signature.endswith("-2"): + with_late = False + + if not check_signature(badge_key, tag, signature): return HttpResponseNotFound() q = Check.objects.filter(project__badge_key=badge_key) @@ -406,7 +410,7 @@ def badge(request, badge_key, signature, tag, fmt="svg"): break elif check_status == "grace": grace += 1 - if status == "up": + if status == "up" and with_late: status = "late" if fmt == "shields": diff --git a/hc/front/views.py b/hc/front/views.py index 6d1350f3..f7350d58 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -676,14 +676,18 @@ def badges(request, code): sorted_tags = sorted(tags, key=lambda s: s.lower()) sorted_tags.append("*") # For the "overall status" badge + key = project.badge_key urls = [] for tag in sorted_tags: urls.append( { "tag": tag, - "svg": get_badge_url(project.badge_key, tag), - "json": get_badge_url(project.badge_key, tag, fmt="json"), - "shields": get_badge_url(project.badge_key, tag, fmt="shields"), + "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), } ) diff --git a/hc/lib/badges.py b/hc/lib/badges.py index c503df77..1bcdb226 100644 --- a/hc/lib/badges.py +++ b/hc/lib/badges.py @@ -99,16 +99,17 @@ def get_badge_svg(tag, status): def check_signature(username, tag, sig): ours = base64_hmac(str(username), tag, settings.SECRET_KEY) - ours = ours[:8] - return ours == sig + return ours[:8] == sig[:8] -def get_badge_url(username, tag, fmt="svg"): - sig = base64_hmac(str(username), tag, settings.SECRET_KEY) +def get_badge_url(username, tag, fmt="svg", with_late=False): + sig = base64_hmac(str(username), tag, settings.SECRET_KEY)[:8] + if not with_late: + sig += "-2" if tag == "*": - url = reverse("hc-badge-all", args=[username, sig[:8], fmt]) + url = reverse("hc-badge-all", args=[username, sig, fmt]) else: - url = reverse("hc-badge", args=[username, sig[:8], tag, fmt]) + url = reverse("hc-badge", args=[username, sig, tag, fmt]) return settings.SITE_ROOT + url diff --git a/static/css/badges.css b/static/css/badges.css new file mode 100644 index 00000000..8d7bb249 --- /dev/null +++ b/static/css/badges.css @@ -0,0 +1,16 @@ +.table.badge-preview th { + border-top: 0; + color: #777777; + font-weight: normal; + font-size: 12px; + padding-top: 32px; +} + +#badges-json .fetch-json { + background: #eee; + padding: 3px; +} + +#badges-json, #badges-shields, .badge-preview .with-late { + display: none; +} \ No newline at end of file diff --git a/static/css/settings.css b/static/css/settings.css index 3faf50c3..7bc0ccc1 100644 --- a/static/css/settings.css +++ b/static/css/settings.css @@ -25,28 +25,6 @@ background-color: #ffebea; } -.table.badges th { - border-top: 0; - color: #777777; - font-weight: normal; - font-size: 12px; - padding-top: 32px; -} - -#badges-json, #badges-shields { - display: none; -} - -#badges-shields label:first-child { - margin: 20px 0 10px 0; -} - -.json-response code { - display: inline-block; - background: #eee; - padding: 3px; -} - .invite-suggestion { color: #888; } diff --git a/static/js/badges.js b/static/js/badges.js index 1b83d549..4681f92a 100644 --- a/static/js/badges.js +++ b/static/js/badges.js @@ -1,8 +1,8 @@ $(function() { - $(".json-response").each(function(idx, el) { + $(".fetch-json").each(function(idx, el) { $.getJSON(el.dataset.url, function(data) { - el.innerHTML = "" + JSON.stringify(data) + ""; + el.innerText = JSON.stringify(data); }); }); @@ -23,4 +23,15 @@ $(function() { $("#badges-json").hide(); $("#badges-shields").show(); }) + + $("#show-with-late").click(function() { + $(".no-late").hide(); + $(".with-late").show(); + }) + + $("#show-no-late").click(function() { + $(".with-late").hide(); + $(".no-late").show(); + }) + }); diff --git a/templates/base.html b/templates/base.html index c5521f2e..ef3c8736 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,7 @@ + diff --git a/templates/front/badges.html b/templates/front/badges.html index a82f7c79..a447f131 100644 --- a/templates/front/badges.html +++ b/templates/front/badges.html @@ -10,15 +10,27 @@

{{ site_name }} provides status badges for each of the tags - you have used. Additionally, the "{{ site_name }}" - badge shows the overall status of all checks in a - project. The badges have public, but hard-to-guess + you have used. The badges have public, but hard-to-guess URLs. You can use them in your READMEs, dashboards or status pages.

-
-