Browse Source

Implement badge mode (up/down vs up/late/down) selector

Fixes #282
pull/456/head
Pēteris Caune 4 years ago
parent
commit
524d1a7375
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
10 changed files with 133 additions and 95 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +36
    -40
      hc/api/tests/test_badge.py
  3. +8
    -4
      hc/api/views.py
  4. +7
    -3
      hc/front/views.py
  5. +7
    -6
      hc/lib/badges.py
  6. +16
    -0
      static/css/badges.css
  7. +0
    -22
      static/css/settings.css
  8. +13
    -2
      static/js/badges.js
  9. +1
    -0
      templates/base.html
  10. +44
    -18
      templates/front/badges.html

+ 1
- 0
CHANGELOG.md View File

@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
- Add retries to the the email sending logic - Add retries to the the email sending logic
- Require confirmation codes (sent to email) before sensitive actions - Require confirmation codes (sent to email) before sensitive actions
- Implement WebAuthn two-factor authentication - Implement WebAuthn two-factor authentication
- Implement badge mode (up/down vs up/late/down) selector (#282)
## v1.17.0 - 2020-10-14 ## v1.17.0 - 2020-10-14


+ 36
- 40
hc/api/tests/test_badge.py View File

@ -15,12 +15,15 @@ class BadgeTestCase(BaseTestCase):
sig = base64_hmac(str(self.project.badge_key), "foo", settings.SECRET_KEY) sig = base64_hmac(str(self.project.badge_key), "foo", settings.SECRET_KEY)
sig = sig[:8] 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): def test_it_rejects_bad_signature(self):
r = self.client.get("/badge/%s/12345678/foo.svg" % self.project.badge_key) 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): def test_it_returns_svg(self):
r = self.client.get(self.svg_url) r = self.client.get(self.svg_url)
@ -37,52 +40,24 @@ class BadgeTestCase(BaseTestCase):
self.assertEqual(r["Access-Control-Allow-Origin"], "*") self.assertEqual(r["Access-Control-Allow-Origin"], "*")
def test_it_handles_new(self): 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.last_start = now()
self.check.tags = "foo"
self.check.status = "down" self.check.status = "down"
self.check.save() 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.last_ping = now() - td(days=1, minutes=10)
self.check.tags = "foo"
self.check.status = "up" self.check.status = "up"
self.check.save() 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): def test_it_handles_special_characters(self):
self.check.tags = "db@dc1" self.check.tags = "db@dc1"
@ -94,3 +69,24 @@ class BadgeTestCase(BaseTestCase):
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) 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"})

+ 8
- 4
hc/api/views.py View File

@ -375,11 +375,15 @@ def flips_by_unique_key(request, unique_key):
@never_cache @never_cache
@cors("GET") @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() 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() return HttpResponseNotFound()
q = Check.objects.filter(project__badge_key=badge_key) q = Check.objects.filter(project__badge_key=badge_key)
@ -406,7 +410,7 @@ def badge(request, badge_key, signature, tag, fmt="svg"):
break break
elif check_status == "grace": elif check_status == "grace":
grace += 1 grace += 1
if status == "up":
if status == "up" and with_late:
status = "late" status = "late"
if fmt == "shields": if fmt == "shields":


+ 7
- 3
hc/front/views.py View File

@ -676,14 +676,18 @@ def badges(request, code):
sorted_tags = sorted(tags, key=lambda s: s.lower()) sorted_tags = sorted(tags, key=lambda s: s.lower())
sorted_tags.append("*") # For the "overall status" badge sorted_tags.append("*") # For the "overall status" badge
key = project.badge_key
urls = [] urls = []
for tag in sorted_tags: for tag in sorted_tags:
urls.append( urls.append(
{ {
"tag": tag, "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),
} }
) )


+ 7
- 6
hc/lib/badges.py View File

@ -99,16 +99,17 @@ def get_badge_svg(tag, status):
def check_signature(username, tag, sig): def check_signature(username, tag, sig):
ours = base64_hmac(str(username), tag, settings.SECRET_KEY) 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 == "*": if tag == "*":
url = reverse("hc-badge-all", args=[username, sig[:8], fmt])
url = reverse("hc-badge-all", args=[username, sig, fmt])
else: 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 return settings.SITE_ROOT + url

+ 16
- 0
static/css/badges.css View File

@ -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;
}

+ 0
- 22
static/css/settings.css View File

@ -25,28 +25,6 @@
background-color: #ffebea; 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 { .invite-suggestion {
color: #888; color: #888;
} }


+ 13
- 2
static/js/badges.js View File

@ -1,8 +1,8 @@
$(function() { $(function() {
$(".json-response").each(function(idx, el) {
$(".fetch-json").each(function(idx, el) {
$.getJSON(el.dataset.url, function(data) { $.getJSON(el.dataset.url, function(data) {
el.innerHTML = "<code>" + JSON.stringify(data) + "</code>";
el.innerText = JSON.stringify(data);
}); });
}); });
@ -23,4 +23,15 @@ $(function() {
$("#badges-json").hide(); $("#badges-json").hide();
$("#badges-shields").show(); $("#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();
})
}); });

+ 1
- 0
templates/base.html View File

@ -24,6 +24,7 @@
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/badges.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">


+ 44
- 18
templates/front/badges.html View File

@ -10,15 +10,27 @@
<p id="badges-description"> <p id="badges-description">
{{ site_name }} provides status badges for each of the tags {{ 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, URLs. You can use them in your READMEs,
dashboards or status pages. dashboards or status pages.
</p> </p>
<div id="b-format" class="btn-group" data-toggle="buttons">
<label id="show-svg" class="btn btn-default active">
<p>Each badge can be in one of the following states:</p>
<ul>
<li><strong>up</strong> – all matching checks are up.</li>
<li><strong>down</strong> – at least one check is currently down.</li>
</ul>
<p>
As an option, the badges can report a third state:
<strong>late</strong> (when at least one check is running late but has not
exceeded its grace time yet).
</p>
<br />
<div class="btn-group" data-toggle="buttons">
<label id="show-svg" class="btn btn-default active" data->
<input type="radio" autocomplete="off" checked> SVG <input type="radio" autocomplete="off" checked> SVG
</label> </label>
<label id="show-json" class="btn btn-default"> <label id="show-json" class="btn btn-default">
@ -28,12 +40,20 @@
<input type="radio" autocomplete="off"> Shields.io <input type="radio" autocomplete="off"> Shields.io
</label> </label>
</div> </div>
&nbsp;
<div class="btn-group" data-toggle="buttons">
<label id="show-no-late" class="btn btn-default active">
<input type="radio" autocomplete="off" checked> Badge states: <b>up</b> or <b>down</b>
</label>
<label id="show-with-late" class="btn btn-default">
<input type="radio" autocomplete="off"> Badge states: <b>up</b>, <b>late</b> or <b>down</b>
</label>
</div>
<table id="badges-svg" class="badges table">
<table id="badges-svg" class="table badge-preview">
{% if have_tags %} {% if have_tags %}
<tr>
<th colspan="2">Tags</th>
</tr>
<tr><th colspan="2">Tags</th></tr>
{% endif %} {% endif %}
{% for urldict in badges %} {% for urldict in badges %}
@ -45,15 +65,17 @@
<tr> <tr>
<td> <td>
<img src="{{ urldict.svg }}" alt="" />
<img class="no-late" src="{{ urldict.svg }}" alt="" />
<img class="with-late" src="{{ urldict.svg3 }}" alt="" />
</td> </td>
<td> <td>
<code>{{ urldict.svg }}</code>
<code class="no-late">{{ urldict.svg }}</code>
<code class="with-late">{{ urldict.svg3 }}</code>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<table id="badges-json" class="badges table">
<table id="badges-json" class="table badge-preview">
{% if have_tags %} {% if have_tags %}
<tr> <tr>
<th colspan="2">Tags</th> <th colspan="2">Tags</th>
@ -68,17 +90,19 @@
{% endif %} {% endif %}
<tr> <tr>
<td class="json-response" data-url="{{ urldict.json }}">
<td>
<code class="fetch-json no-late" data-url="{{ urldict.json }}"></code>
<code class="fetch-json with-late" data-url="{{ urldict.json3 }}"></code>
</td> </td>
<td> <td>
<code>{{ urldict.json }}</code>
<code class="no-late">{{ urldict.json }}</code>
<code class="with-late">{{ urldict.json3 }}</code>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div id="badges-shields">
<table class="badges table">
<table id="badges-shields" class="table badge-preview">
{% if have_tags %} {% if have_tags %}
<tr> <tr>
<th>Shields.io badge</th> <th>Shields.io badge</th>
@ -95,10 +119,12 @@
<tr> <tr>
<td> <td>
<img src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
<img class="no-late" src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
<img class="with-late" src="https://img.shields.io/endpoint?url={{ urldict.shields3|urlencode:"" }}" alt="" />
</td> </td>
<td> <td>
<code>{{ urldict.shields }}</code>
<code class="no-late">{{ urldict.shields }}</code>
<code class="with-late">{{ urldict.shields3 }}</code>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}


Loading…
Cancel
Save