Browse Source

Notification Channels WIP

pull/7/head
Pēteris Caune 9 years ago
parent
commit
061fc4f6a9
14 changed files with 435 additions and 15 deletions
  1. +11
    -1
      hc/accounts/views.py
  2. +8
    -2
      hc/api/admin.py
  3. +30
    -0
      hc/api/migrations/0010_channel.py
  4. +17
    -0
      hc/api/models.py
  5. +9
    -0
      hc/front/forms.py
  6. +13
    -10
      hc/front/urls.py
  7. +69
    -2
      hc/front/views.py
  8. +26
    -0
      static/css/channel_checks.css
  9. +44
    -0
      static/css/channels.css
  10. +4
    -0
      static/css/my_checks_desktop.css
  11. +38
    -0
      static/js/channels.js
  12. +2
    -0
      templates/base.html
  13. +47
    -0
      templates/front/channel_checks.html
  14. +117
    -0
      templates/front/channels.html

+ 11
- 1
hc/accounts/views.py View File

@ -9,7 +9,7 @@ from django.http import HttpResponseBadRequest
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from hc.accounts.forms import EmailForm from hc.accounts.forms import EmailForm
from hc.api.models import Check
from hc.api.models import Channel, Check
from hc.lib.emails import send from hc.lib.emails import send
@ -18,6 +18,13 @@ def _make_user(email):
user = User(username=username, email=email) user = User(username=username, email=email)
user.save() user.save()
channel = Channel()
channel.user = user
channel.kind = "email"
channel.value = email
channel.email_verified = True
channel.save()
return user return user
@ -29,6 +36,9 @@ def _associate_demo_check(request, user):
if check.user is None: if check.user is None:
check.user = user check.user = user
check.save() check.save()
check.assign_all_channels()
del request.session["welcome_code"] del request.session["welcome_code"]


+ 8
- 2
hc/api/admin.py View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from hc.api.models import Check, Ping
from hc.api.models import Channel, Check, Ping
class OwnershipListFilter(admin.SimpleListFilter): class OwnershipListFilter(admin.SimpleListFilter):
@ -55,4 +55,10 @@ class PingsAdmin(admin.ModelAdmin):
return obj.owner.name if obj.owner.name else obj.owner.code return obj.owner.name if obj.owner.name else obj.owner.code
def email(self, obj): def email(self, obj):
return obj.owner.user.email if obj.owner.user else None
return obj.owner.user.email if obj.owner.user else None
@admin.register(Channel)
class ChannelsAdmin(admin.ModelAdmin):
list_select_related = ("user", )
list_display = ("id", "code", "user", "kind", "value")

+ 30
- 0
hc/api/migrations/0010_channel.py View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0009_auto_20150801_1250'),
]
operations = [
migrations.CreateModel(
name='Channel',
fields=[
('id', models.AutoField(primary_key=True, auto_created=True, verbose_name='ID', serialize=False)),
('code', models.UUIDField(editable=False, default=uuid.uuid4)),
('created', models.DateTimeField(auto_now_add=True)),
('kind', models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('pd', 'PagerDuty')], max_length=20)),
('value', models.CharField(max_length=200, blank=True)),
('email_verified', models.BooleanField(default=False)),
('checks', models.ManyToManyField(to='api.Check')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
]

+ 17
- 0
hc/api/models.py View File

@ -13,6 +13,8 @@ from hc.lib.emails import send
STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New")) STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"))
DEFAULT_TIMEOUT = td(days=1) DEFAULT_TIMEOUT = td(days=1)
DEFAULT_GRACE = td(hours=1) DEFAULT_GRACE = td(hours=1)
CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"),
("pd", "PagerDuty"))
class Check(models.Model): class Check(models.Model):
@ -62,6 +64,11 @@ class Check(models.Model):
return "down" return "down"
def assign_all_channels(self):
for channel in Channel.objects.filter(user=self.user):
channel.checks.add(self)
channel.save()
class Ping(models.Model): class Ping(models.Model):
owner = models.ForeignKey(Check) owner = models.ForeignKey(Check)
@ -71,3 +78,13 @@ class Ping(models.Model):
method = models.CharField(max_length=10, blank=True) method = models.CharField(max_length=10, blank=True)
ua = models.CharField(max_length=200, blank=True) ua = models.CharField(max_length=200, blank=True)
body = models.TextField(blank=True) body = models.TextField(blank=True)
class Channel(models.Model):
code = models.UUIDField(default=uuid.uuid4, editable=False)
user = models.ForeignKey(User)
created = models.DateTimeField(auto_now_add=True)
kind = models.CharField(max_length=20, choices=CHANNEL_KINDS)
value = models.CharField(max_length=200, blank=True)
email_verified = models.BooleanField(default=False)
checks = models.ManyToManyField(Check)

+ 9
- 0
hc/front/forms.py View File

@ -1,6 +1,15 @@
from django import forms from django import forms
from hc.api.models import Channel
class TimeoutForm(forms.Form): class TimeoutForm(forms.Form):
timeout = forms.IntegerField(min_value=60, max_value=604800) timeout = forms.IntegerField(min_value=60, max_value=604800)
grace = forms.IntegerField(min_value=60, max_value=604800) grace = forms.IntegerField(min_value=60, max_value=604800)
class AddChannelForm(forms.ModelForm):
class Meta:
model = Channel
fields = ['kind', 'value']

+ 13
- 10
hc/front/urls.py View File

@ -3,14 +3,17 @@ from django.conf.urls import url
from hc.front import views from hc.front import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.index, name="hc-index"),
url(r'^checks/add/$', views.add_check, name="hc-add-check"),
url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"),
url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"),
url(r'^checks/([\w-]+)/email/$', views.email_preview),
url(r'^checks/([\w-]+)/remove/$', views.remove, name="hc-remove-check"),
url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"),
url(r'^pricing/$', views.pricing, name="hc-pricing"),
url(r'^docs/$', views.docs, name="hc-docs"),
url(r'^about/$', views.about, name="hc-about"),
url(r'^$', views.index, name="hc-index"),
url(r'^checks/add/$', views.add_check, name="hc-add-check"),
url(r'^checks/([\w-]+)/name/$', views.update_name, name="hc-update-name"),
url(r'^checks/([\w-]+)/timeout/$', views.update_timeout, name="hc-update-timeout"),
url(r'^checks/([\w-]+)/email/$', views.email_preview),
url(r'^checks/([\w-]+)/remove/$', views.remove, name="hc-remove-check"),
url(r'^checks/([\w-]+)/log/$', views.log, name="hc-log"),
url(r'^pricing/$', views.pricing, name="hc-pricing"),
url(r'^docs/$', views.docs, name="hc-docs"),
url(r'^about/$', views.about, name="hc-about"),
url(r'^channels/$', views.channels, name="hc-channels"),
url(r'^channels/add/$', views.add_channel, name="hc-add-channel"),
url(r'^channels/([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"),
] ]

+ 69
- 2
hc/front/views.py View File

@ -7,8 +7,8 @@ from django.shortcuts import redirect, render
from django.utils import timezone from django.utils import timezone
from hc.api.decorators import uuid_or_400 from hc.api.decorators import uuid_or_400
from hc.api.models import Check, Ping
from hc.front.forms import TimeoutForm
from hc.api.models import Channel, Check, Ping
from hc.front.forms import AddChannelForm, TimeoutForm
def _welcome(request): def _welcome(request):
@ -79,6 +79,9 @@ def add_check(request):
check = Check(user=request.user) check = Check(user=request.user)
check.save() check.save()
check.assign_all_channels()
return redirect("hc-index") return redirect("hc-index")
@ -169,3 +172,67 @@ def log(request, code):
} }
return render(request, "front/log.html", ctx) return render(request, "front/log.html", ctx)
@login_required
def channels(request):
if request.method == "POST":
code = request.POST["channel"]
channel = Channel.objects.get(code=code)
assert channel.user == request.user
channel.checks = []
print (request.POST)
for key in request.POST:
if key.startswith("check-"):
code = key[6:]
check = Check.objects.get(code=code)
assert check.user == request.user
channel.checks.add(check)
channel.save()
return redirect("hc-channels")
channels = Channel.objects.filter(user=request.user).order_by("created")
num_checks = Check.objects.filter(user=request.user).count()
ctx = {
"channels": channels,
"num_checks": num_checks
}
return render(request, "front/channels.html", ctx)
@login_required
def add_channel(request):
assert request.method == "POST"
form = AddChannelForm(request.POST)
if form.is_valid():
channel = form.save(commit=False)
channel.user = request.user
channel.save()
checks = Check.objects.filter(user=request.user)
channel.checks.add(*checks)
channel.save()
return redirect("hc-channels")
@login_required
@uuid_or_400
def channel_checks(request, code):
channel = Channel.objects.get(code=code)
assigned = set([check.code for check in channel.checks.all()])
checks = Check.objects.filter(user=request.user).order_by("created")
ctx = {
"checks": checks,
"assigned": assigned,
"channel": channel
}
return render(request, "front/channel_checks.html", ctx)

+ 26
- 0
static/css/channel_checks.css View File

@ -0,0 +1,26 @@
.channel-checks-table tr:first-child td {
border-top: 0;
}
.channel-checks-table td:first-child, .channel-checks-table th:first-child {
padding-left: 16px;
}
.channel-checks-table .check-all-cell {
background: #EEE;
}
.channel-checks-table .check-all-cell .cbx-container {
background: #FFF;
}
.channel-checks-table input[type=checkbox]:checked + label:after {
font-family: 'Glyphicons Halflings';
content: "\e013";
}
.channel-checks-table label:after {
padding-left: 4px;
padding-top: 2px;
font-size: 9px;
}

+ 44
- 0
static/css/channels.css View File

@ -0,0 +1,44 @@
.channels-table {
margin-top: 36px;
}
table.channels-table > tbody > tr > th {
border-top: 0;
}
.channels-table .channels-add-title {
border-top: 0;
padding-top: 20px
}
.channels-table .channels-add-help {
color: #888;
border-top: 0;
}
.word-up {
color: #5cb85c;
font-weight: bold
}
.word-down {
color: #d9534f;
font-weight: bold
}
.preposition {
color: #888;
}
.channel-unconfirmed {
font-size: small;
}
.channels-help-hidden {
display: none;
}
.channels-table .channels-num-checks {
padding-left: 40px;
}

+ 4
- 0
static/css/my_checks_desktop.css View File

@ -1,3 +1,7 @@
#checks-table {
margin-top: 36px;
}
.my-checks-name.unnamed { .my-checks-name.unnamed {
color: #999; color: #999;
font-style: italic; font-style: italic;


+ 38
- 0
static/js/channels.js View File

@ -0,0 +1,38 @@
$(function() {
var placeholders = {
email: "[email protected]",
webhook: "http://",
pd: "service key"
}
$("#add-check-kind").change(function() {
$(".channels-add-help p").hide();
var v = $("#add-check-kind").val();
$(".channels-add-help p." + v).show();
$("#add-check-value").attr("placeholder", placeholders[v]);
});
$(".edit-checks").click(function() {
$("#checks-modal").modal("show");
var url = $(this).attr("href");
$.ajax(url).done(function(data) {
$("#checks-modal .modal-content").html(data);
})
return false;
});
var $cm = $("#checks-modal");
$cm.on("click", "#toggle-all", function() {
var value = $(this).prop("checked");
$cm.find(".toggle").prop("checked", value);
console.log("aaa", value);
});
});

+ 2
- 0
templates/base.html View File

@ -21,6 +21,8 @@
<link rel="stylesheet" href="{% static 'css/my_checks_desktop.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/my_checks_desktop.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/pricing.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'css/syntax.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/channels.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
{% endcompress %} {% endcompress %}
</head> </head>
<body class="page-{{ page }}"> <body class="page-{{ page }}">


+ 47
- 0
templates/front/channel_checks.html View File

@ -0,0 +1,47 @@
{% load compress humanize staticfiles hc_extras %}
<form method="post">
{% csrf_token %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button>
<h4 class="update-timeout-title">Assign Checks to Channel {{ channel.value }}</h4>
</div>
<input type="hidden" name="channel" value="{{ channel.code }}" />
<table class="table channel-checks-table">
<tr>
<th class="check-all-cell">
<input
id="toggle-all"
type="checkbox"
class="toggle" />
</th>
<th class="check-all-cell">
Check / Uncheck All
</th>
</tr>
{% for check in checks %}
<tr>
<td>
<input
type="checkbox"
class="toggle"
data-toggle="checkbox-x"
{% if check.code in assigned %} checked {% endif %}
name="check-{{ check.code }}">
</td>
<td>
{{ check.name|default:check.code }}
</td>
</tr>
{% endfor %}
</table>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>

+ 117
- 0
templates/front/channels.html View File

@ -0,0 +1,117 @@
{% extends "base.html" %}
{% load compress humanize staticfiles hc_extras %}
{% block title %}Notification Channels - healthchecks.io{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1>Notification Channels</h1>
<table class="table channels-table">
<tr>
<th>Type</th>
<th>Value</th>
<th>Assigned Checks</th>
<th></th>
</tr>
{% for ch in channels %}
<tr>
<td>
{% if ch.kind == "email" %} Email {% endif %}
{% if ch.kind == "webhook" %} Webhook {% endif %}
{% if ch.kind == "pd" %} PagerDuty {% endif %}
</td>
<td>
<span class="preposition">
{% if ch.kind == "email" %} to {% endif %}
{% if ch.kind == "pd" %} service key {% endif %}
</span>
{{ ch.value }}
{% if ch.kind == "email" and not ch.email_verified %}
<span class="channel-unconfirmed">(unconfirmed)
{% endif %}
</td>
<td class="channels-num-checks">
<a
class="edit-checks"
href="{% url 'hc-channel-checks' ch.code %}">
{{ ch.checks.count }} of {{ num_checks }}
</a>
</td>
</tr>
{% endfor %}
<tr>
<th colspan="2" class="channels-add-title">
Add Notification Channel
</th>
</tr>
<tr>
<form method="post" action="{% url 'hc-add-channel' %}">
<td>
<select id="add-check-kind" class="form-control" name="kind">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
<option value="pd">PagerDuty</option>
</select>
</td>
<td class="form-inline">
{% csrf_token %}
<input
id="add-check-value"
name="value"
class="form-control"
type="text"
placeholder="[email protected]" />
<button type="submit" class="btn btn-success">Add</button>
</td>
<td>
</td>
</form>
</tr>
<tr>
<td colspan="3" class="channels-add-help">
<p class="email">
Healthchecks.io will send an email to the specified
address when a check goes
<span class="word-up">up</span> or <span class="word-down">down</span>.
</p>
<p class="channels-help-hidden webhook">
Healthchecks.io will request the specified URL when
a check goes
<span class="word-down">down</span>.
</p>
<p class="channels-help-hidden pd">
Healthchecks.io will create an incident on PagerDuty when
a check goes
<span class="word-down">down</span> and will resolve it
when same check goes <span class="word-up">up</span>
</p>
</td>
</tr>
</table>
</div>
</div>
<div id="checks-modal" class="modal">
<div class="modal-dialog">
<div class="modal-content">
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/channels.js' %}"></script>
{% endcompress %}
{% endblock %}

Loading…
Cancel
Save