forked from GithubBackups/healthchecks
Badges
This commit is contained in:
parent
f9c1a174b7
commit
c15a4871c2
@ -1,4 +1,5 @@
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login as auth_login
|
||||
@ -15,6 +16,7 @@ from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm,
|
||||
SetPasswordForm, TeamNameForm)
|
||||
from hc.accounts.models import Profile, Member
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.lib.badges import get_badge_url
|
||||
|
||||
|
||||
def _make_user(email):
|
||||
@ -186,7 +188,20 @@ def profile(request):
|
||||
profile.save()
|
||||
messages.info(request, "Team Name updated!")
|
||||
|
||||
tags = set()
|
||||
for check in Check.objects.filter(user=request.team.user):
|
||||
tags.update(check.tags_list())
|
||||
|
||||
username = request.team.user.username
|
||||
badge_urls = []
|
||||
for tag in sorted(tags, key=lambda s: s.lower()):
|
||||
if not re.match("^[\w-]+$", tag):
|
||||
continue
|
||||
|
||||
badge_urls.append(get_badge_url(username, tag))
|
||||
|
||||
ctx = {
|
||||
"badge_urls": badge_urls,
|
||||
"profile": profile,
|
||||
"show_api_key": show_api_key
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ urlpatterns = [
|
||||
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
||||
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
||||
url(r'^api/v1/checks/$', views.create_check),
|
||||
url(r'^badge/([\w-]+)/([\w-]{8})/([\w-]+).svg$', views.badge, name="hc-badge"),
|
||||
]
|
||||
|
@ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from hc.api import schemas
|
||||
from hc.api.decorators import check_api_key, uuid_or_400, validate_json
|
||||
from hc.api.models import Check, Ping
|
||||
from hc.lib.badges import check_signature, get_badge_svg
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@ -84,3 +85,25 @@ def create_check(request):
|
||||
return HttpResponse(status=405)
|
||||
|
||||
return JsonResponse(response, status=code)
|
||||
|
||||
|
||||
@never_cache
|
||||
def badge(request, username, signature, tag):
|
||||
if not check_signature(username, tag, signature):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
status = "up"
|
||||
q = Check.objects.filter(user__username=username, tags__contains=tag)
|
||||
for check in q:
|
||||
if tag not in check.tags_list():
|
||||
continue
|
||||
|
||||
if status == "up" and check.in_grace_period():
|
||||
status = "late"
|
||||
|
||||
if check.get_status() == "down":
|
||||
status = "down"
|
||||
break
|
||||
|
||||
svg = get_badge_svg(tag, status)
|
||||
return HttpResponse(svg, content_type="image/svg+xml")
|
||||
|
55
hc/lib/badges.py
Normal file
55
hc/lib/badges.py
Normal file
@ -0,0 +1,55 @@
|
||||
from django.conf import settings
|
||||
from django.core.signing import base64_hmac
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
WIDTHS = {"a": 7, "b": 7, "c": 6, "d": 7, "e": 6, "f": 4, "g": 7, "h": 7,
|
||||
"i": 3, "j": 3, "k": 7, "l": 3, "m": 10, "n": 7, "o": 7, "p": 7,
|
||||
"q": 7, "r": 4, "s": 6, "t": 5, "u": 7, "v": 7, "w": 9, "x": 6,
|
||||
"y": 7, "z": 7, "0": 7, "1": 6, "2": 7, "3": 7, "4": 7, "5": 7,
|
||||
"6": 7, "7": 7, "8": 7, "9": 7, "A": 8, "B": 7, "C": 8, "D": 8,
|
||||
"E": 7, "F": 6, "G": 9, "H": 8, "I": 3, "J": 4, "K": 7, "L": 6,
|
||||
"M": 10, "N": 8, "O": 9, "P": 6, "Q": 9, "R": 7, "S": 7, "T": 7,
|
||||
"U": 8, "V": 8, "W": 11, "X": 7, "Y": 7, "Z": 7, "-": 4, "_": 6}
|
||||
|
||||
COLORS = {
|
||||
"up": "#4c1",
|
||||
"late": "#fe7d37",
|
||||
"down": "#e05d44"
|
||||
}
|
||||
|
||||
|
||||
def get_width(s):
|
||||
total = 0
|
||||
for c in s:
|
||||
total += WIDTHS.get(c, 7)
|
||||
return total
|
||||
|
||||
|
||||
def get_badge_svg(tag, status):
|
||||
w1 = get_width(tag) + 10
|
||||
w2 = get_width(status) + 10
|
||||
ctx = {
|
||||
"width": w1 + w2,
|
||||
"tag_width": w1,
|
||||
"status_width": w2,
|
||||
"tag_center_x": w1 / 2,
|
||||
"status_center_x": w1 + w2 / 2,
|
||||
"tag": tag,
|
||||
"status": status,
|
||||
"color": COLORS[status]
|
||||
}
|
||||
|
||||
return render_to_string("badge.svg", ctx)
|
||||
|
||||
|
||||
def check_signature(username, tag, sig):
|
||||
ours = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
||||
ours = ours[:8].decode("utf-8")
|
||||
return ours == sig
|
||||
|
||||
|
||||
def get_badge_url(username, tag):
|
||||
sig = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
||||
url = reverse("hc-badge", args=[username, sig[:8], tag])
|
||||
return settings.SITE_ROOT + url
|
@ -1,4 +1,4 @@
|
||||
#settings-title {
|
||||
.settings-title {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
@ -9,4 +9,8 @@
|
||||
.settings-block h2 {
|
||||
margin: 0;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
#badges-description {
|
||||
margin-bottom: 24px;
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1 id="settings-title">Settings</h1>
|
||||
<h1 class="settings-title">Settings</h1>
|
||||
</div>
|
||||
{% if messages %}
|
||||
<div class="col-sm-12">
|
||||
@ -57,45 +57,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body settings-block">
|
||||
<h2>API Access</h2>
|
||||
{% if profile.api_key %}
|
||||
{% if show_api_key %}
|
||||
API key: <code>{{ profile.api_key }}</code>
|
||||
<button
|
||||
data-toggle="modal"
|
||||
data-target="#revoke-api-key-modal"
|
||||
class="btn btn-danger pull-right">Revoke</button>
|
||||
|
||||
{% else %}
|
||||
<span class="text-success glyphicon glyphicon-ok"></span>
|
||||
API access is enabled.
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
name="show_api_key"
|
||||
class="btn btn-default pull-right">Show API key</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
API access is disabled.
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
name="create_api_key"
|
||||
class="btn btn-default pull-right">Create API key</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body settings-block">
|
||||
@ -154,8 +115,77 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body settings-block">
|
||||
<h2>API Access</h2>
|
||||
{% if profile.api_key %}
|
||||
{% if show_api_key %}
|
||||
API key: <code>{{ profile.api_key }}</code>
|
||||
<button
|
||||
data-toggle="modal"
|
||||
data-target="#revoke-api-key-modal"
|
||||
class="btn btn-danger pull-right">Revoke</button>
|
||||
|
||||
{% else %}
|
||||
<span class="text-success glyphicon glyphicon-ok"></span>
|
||||
API access is enabled.
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
name="show_api_key"
|
||||
class="btn btn-default pull-right">Show API key</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
API access is disabled.
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
name="create_api_key"
|
||||
class="btn btn-default pull-right">Create API key</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if badge_urls %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body settings-block">
|
||||
<h2 class="settings-title">Status Badges</h2>
|
||||
<p id="badges-description">
|
||||
Here are status badges for each of the tags you have used. The
|
||||
badges have public, but hard-to-guess URLs. If you wish, you can
|
||||
add them to your READMEs, dashboards or status pages.
|
||||
</p>
|
||||
<table class="badges table">
|
||||
{% for badge_url in badge_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ badge_url }}" alt="" />
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ badge_url }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<div id="revoke-api-key-modal" class="modal">
|
||||
<div class="modal-dialog">
|
||||
|
23
templates/badge.svg
Normal file
23
templates/badge.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{ width }}" height="20">
|
||||
<linearGradient id="smooth" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
|
||||
<mask id="round">
|
||||
<rect width="{{ width }}" height="20" rx="3" fill="#fff"/>
|
||||
</mask>
|
||||
|
||||
<g mask="url(#round)">
|
||||
<rect width="{{ tag_width }}" height="20" fill="#555"/>
|
||||
<rect x="{{ tag_width }}" width="{{ status_width }}" height="20" fill="{{ color }}"/>
|
||||
<rect width="{{ width }}" height="20" fill="url(#smooth)"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text x="{{ tag_center_x }}" y="15" fill="#010101" fill-opacity=".3">{{ tag }}</text>
|
||||
<text x="{{ tag_center_x }}" y="14">{{ tag }}</text>
|
||||
<text x="{{ status_center_x }}" y="15" fill="#010101" fill-opacity=".3">{{ status }}</text>
|
||||
<text x="{{ status_center_x }}" y="14">{{ status }}</text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
Loading…
x
Reference in New Issue
Block a user