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.
-