From 061fc4f6a9c37520ce464fae769107b5df0edbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Wed, 12 Aug 2015 23:51:45 +0300 Subject: [PATCH] Notification Channels WIP --- hc/accounts/views.py | 12 ++- hc/api/admin.py | 10 ++- hc/api/migrations/0010_channel.py | 30 +++++++ hc/api/models.py | 17 ++++ hc/front/forms.py | 9 +++ hc/front/urls.py | 23 +++--- hc/front/views.py | 71 ++++++++++++++++- static/css/channel_checks.css | 26 +++++++ static/css/channels.css | 44 +++++++++++ static/css/my_checks_desktop.css | 4 + static/js/channels.js | 38 +++++++++ templates/base.html | 2 + templates/front/channel_checks.html | 47 +++++++++++ templates/front/channels.html | 117 ++++++++++++++++++++++++++++ 14 files changed, 435 insertions(+), 15 deletions(-) create mode 100644 hc/api/migrations/0010_channel.py create mode 100644 static/css/channel_checks.css create mode 100644 static/css/channels.css create mode 100644 static/js/channels.js create mode 100644 templates/front/channel_checks.html create mode 100644 templates/front/channels.html diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 604664b4..1b751537 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -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"] diff --git a/hc/api/admin.py b/hc/api/admin.py index de15171a..97caf282 100644 --- a/hc/api/admin.py +++ b/hc/api/admin.py @@ -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 \ No newline at end of file + 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") diff --git a/hc/api/migrations/0010_channel.py b/hc/api/migrations/0010_channel.py new file mode 100644 index 00000000..280f2c81 --- /dev/null +++ b/hc/api/migrations/0010_channel.py @@ -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)), + ], + ), + ] diff --git a/hc/api/models.py b/hc/api/models.py index 7c5ffd6e..b374c367 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -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) diff --git a/hc/front/forms.py b/hc/front/forms.py index ec5da871..b70de41f 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -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'] diff --git a/hc/front/urls.py b/hc/front/urls.py index 0a059843..01cbd12f 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -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"), ] diff --git a/hc/front/views.py b/hc/front/views.py index e850f514..be596748 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -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) diff --git a/static/css/channel_checks.css b/static/css/channel_checks.css new file mode 100644 index 00000000..ef595f2b --- /dev/null +++ b/static/css/channel_checks.css @@ -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; +} \ No newline at end of file diff --git a/static/css/channels.css b/static/css/channels.css new file mode 100644 index 00000000..7267e2df --- /dev/null +++ b/static/css/channels.css @@ -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; +} + diff --git a/static/css/my_checks_desktop.css b/static/css/my_checks_desktop.css index 89b86472..1aa8eb82 100644 --- a/static/css/my_checks_desktop.css +++ b/static/css/my_checks_desktop.css @@ -1,3 +1,7 @@ +#checks-table { + margin-top: 36px; +} + .my-checks-name.unnamed { color: #999; font-style: italic; diff --git a/static/js/channels.js b/static/js/channels.js new file mode 100644 index 00000000..428183f9 --- /dev/null +++ b/static/js/channels.js @@ -0,0 +1,38 @@ +$(function() { + var placeholders = { + email: "address@example.org", + 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); + + }); + +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 3fbea1cf..0a05dd0f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -21,6 +21,8 @@ + + {% endcompress %} diff --git a/templates/front/channel_checks.html b/templates/front/channel_checks.html new file mode 100644 index 00000000..6c0e0a91 --- /dev/null +++ b/templates/front/channel_checks.html @@ -0,0 +1,47 @@ +{% load compress humanize staticfiles hc_extras %} + +
+{% csrf_token %} + + + + + + + + + + {% for check in checks %} + + + + + {% endfor %} +
+ + + Check / Uncheck All +
+ + + + {{ check.name|default:check.code }} +
+ + +
+ diff --git a/templates/front/channels.html b/templates/front/channels.html new file mode 100644 index 00000000..06fe24ef --- /dev/null +++ b/templates/front/channels.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} +{% load compress humanize staticfiles hc_extras %} + +{% block title %}Notification Channels - healthchecks.io{% endblock %} + + +{% block content %} +
+
+

Notification Channels

+ + + + + + + + {% for ch in channels %} + + + + + + {% endfor %} + + + + + + + + + + + + + + +
TypeValueAssigned Checks
+ {% if ch.kind == "email" %} Email {% endif %} + {% if ch.kind == "webhook" %} Webhook {% endif %} + {% if ch.kind == "pd" %} PagerDuty {% endif %} + + + {% if ch.kind == "email" %} to {% endif %} + {% if ch.kind == "pd" %} service key {% endif %} + + + {{ ch.value }} + + {% if ch.kind == "email" and not ch.email_verified %} + (unconfirmed) + {% endif %} + + + {{ ch.checks.count }} of {{ num_checks }} + +
+ Add Notification Channel +
+ + + {% csrf_token %} + + + + +
+ +

+ Healthchecks.io will request the specified URL when + a check goes + down. +

+

+ Healthchecks.io will create an incident on PagerDuty when + a check goes + down and will resolve it + when same check goes up +

+
+
+
+ + + +{% endblock %} + +{% block scripts %} +{% compress js %} + + + +{% endcompress %} +{% endblock %} \ No newline at end of file