forked from GithubBackups/healthchecks
Badges
This commit is contained in:
parent
f9c1a174b7
commit
c15a4871c2
@ -1,4 +1,5 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
import re
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login as auth_login
|
from django.contrib.auth import login as auth_login
|
||||||
@ -15,6 +16,7 @@ from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm,
|
|||||||
SetPasswordForm, TeamNameForm)
|
SetPasswordForm, TeamNameForm)
|
||||||
from hc.accounts.models import Profile, Member
|
from hc.accounts.models import Profile, Member
|
||||||
from hc.api.models import Channel, Check
|
from hc.api.models import Channel, Check
|
||||||
|
from hc.lib.badges import get_badge_url
|
||||||
|
|
||||||
|
|
||||||
def _make_user(email):
|
def _make_user(email):
|
||||||
@ -186,7 +188,20 @@ def profile(request):
|
|||||||
profile.save()
|
profile.save()
|
||||||
messages.info(request, "Team Name updated!")
|
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 = {
|
ctx = {
|
||||||
|
"badge_urls": badge_urls,
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"show_api_key": show_api_key
|
"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-slash"),
|
||||||
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
||||||
url(r'^api/v1/checks/$', views.create_check),
|
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 import schemas
|
||||||
from hc.api.decorators import check_api_key, uuid_or_400, validate_json
|
from hc.api.decorators import check_api_key, uuid_or_400, validate_json
|
||||||
from hc.api.models import Check, Ping
|
from hc.api.models import Check, Ping
|
||||||
|
from hc.lib.badges import check_signature, get_badge_svg
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@ -84,3 +85,25 @@ def create_check(request):
|
|||||||
return HttpResponse(status=405)
|
return HttpResponse(status=405)
|
||||||
|
|
||||||
return JsonResponse(response, status=code)
|
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;
|
padding-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,4 +9,8 @@
|
|||||||
.settings-block h2 {
|
.settings-block h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#badges-description {
|
||||||
|
margin-bottom: 24px;
|
||||||
}
|
}
|
@ -7,7 +7,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<h1 id="settings-title">Settings</h1>
|
<h1 class="settings-title">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
@ -57,45 +57,6 @@
|
|||||||
</div>
|
</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 class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
@ -154,8 +115,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</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 id="revoke-api-key-modal" class="modal">
|
||||||
<div class="modal-dialog">
|
<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