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


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

@ -1,6 +1,6 @@
from django.contrib import admin
from hc.api.models import Check, Ping
from hc.api.models import Channel, Check, Ping
class OwnershipListFilter(admin.SimpleListFilter):
@ -55,4 +55,10 @@ class PingsAdmin(admin.ModelAdmin):
return obj.owner.name if obj.owner.name else obj.owner.code
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"))
DEFAULT_TIMEOUT = td(days=1)
DEFAULT_GRACE = td(hours=1)
CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"),
("pd", "PagerDuty"))
class Check(models.Model):
@ -62,6 +64,11 @@ class Check(models.Model):
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):
owner = models.ForeignKey(Check)
@ -71,3 +78,13 @@ class Ping(models.Model):
method = models.CharField(max_length=10, blank=True)
ua = models.CharField(max_length=200, 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 hc.api.models import Channel
class TimeoutForm(forms.Form):
timeout = 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
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 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):
@ -79,6 +79,9 @@ def add_check(request):
check = Check(user=request.user)
check.save()
check.assign_all_channels()
return redirect("hc-index")
@ -169,3 +172,67 @@ def log(request, code):
}
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 {
color: #999;
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/pricing.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 %}
</head>
<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