forked from GithubBackups/healthchecks
Send emails using djmail, can verify email addresses in /channels/
This commit is contained in:
parent
d2e483a8a0
commit
f0089e2cd2
@ -10,7 +10,7 @@ from django.shortcuts import redirect, render
|
||||
|
||||
from hc.accounts.forms import EmailForm
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.lib.emails import send
|
||||
from hc.lib import emails
|
||||
|
||||
|
||||
def _make_user(email):
|
||||
@ -51,7 +51,7 @@ def _send_login_link(user):
|
||||
login_link = settings.SITE_ROOT + login_link
|
||||
ctx = {"login_link": login_link}
|
||||
|
||||
send(user.email, "emails/login", ctx)
|
||||
emails.login(user.email, ctx)
|
||||
|
||||
|
||||
def login(request):
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from hc.api.models import Channel, Check, Ping
|
||||
from hc.api.models import Channel, Check, Notification, Ping
|
||||
|
||||
|
||||
class OwnershipListFilter(admin.SimpleListFilter):
|
||||
@ -61,4 +61,32 @@ class PingsAdmin(admin.ModelAdmin):
|
||||
@admin.register(Channel)
|
||||
class ChannelsAdmin(admin.ModelAdmin):
|
||||
list_select_related = ("user", )
|
||||
list_display = ("id", "code", "user", "kind", "value")
|
||||
list_display = ("id", "code", "user", "formatted_kind", "value")
|
||||
|
||||
def formatted_kind(self, obj):
|
||||
if obj.kind == "pd":
|
||||
return "PagerDuty"
|
||||
elif obj.kind == "webhook":
|
||||
return "Webhook"
|
||||
elif obj.kind == "email" and obj.email_verified:
|
||||
return "Email"
|
||||
elif obj.kind == "email" and not obj.email_verified:
|
||||
return "Email (unverified)"
|
||||
else:
|
||||
raise NotImplementedError("Bad channel kind: %s" % obj.kind)
|
||||
|
||||
|
||||
@admin.register(Notification)
|
||||
class NotificationsAdmin(admin.ModelAdmin):
|
||||
list_select_related = ("owner", "channel")
|
||||
list_display = ("id", "created", "check_status", "check_name",
|
||||
"channel_kind", "channel_value", "status")
|
||||
|
||||
def check_name(self, obj):
|
||||
return obj.owner.name_then_code()
|
||||
|
||||
def channel_kind(self, obj):
|
||||
return obj.channel.kind
|
||||
|
||||
def channel_value(self, obj):
|
||||
return obj.channel.value
|
||||
|
32
hc/api/management/commands/makechannels.py
Normal file
32
hc/api/management/commands/makechannels.py
Normal file
@ -0,0 +1,32 @@
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from hc.api.models import Channel, Check
|
||||
|
||||
|
||||
def _log(message):
|
||||
sys.stdout.write(message)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Sends UP/DOWN email alerts'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
for user in User.objects.all():
|
||||
q = Channel.objects.filter(user=user)
|
||||
q = q.filter(kind="email", email_verified=True, value=user.email)
|
||||
if q.count() > 0:
|
||||
continue
|
||||
|
||||
print("Creating default channel for %s" % user.email)
|
||||
channel = Channel(user=user)
|
||||
channel.kind = "email"
|
||||
channel.value = user.email
|
||||
channel.email_verified = True
|
||||
channel.save()
|
||||
|
||||
channel.checks.add(*Check.objects.filter(user=user))
|
@ -27,7 +27,7 @@ class Command(BaseCommand):
|
||||
for check in query:
|
||||
check.status = "down"
|
||||
|
||||
_log("\nSending email about going down for %s\n" % check.code)
|
||||
_log("\nSending notification(s) about going down for %s\n" % check.code)
|
||||
check.send_alert()
|
||||
ticks = 0
|
||||
|
||||
@ -42,7 +42,7 @@ class Command(BaseCommand):
|
||||
for check in query:
|
||||
check.status = "up"
|
||||
|
||||
_log("\nSending email about going up for %s\n" % check.code)
|
||||
_log("\nSending notification(s) about going up for %s\n" % check.code)
|
||||
check.send_alert()
|
||||
ticks = 0
|
||||
|
||||
|
25
hc/api/migrations/0011_notification.py
Normal file
25
hc/api/migrations/0011_notification.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0010_channel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
|
||||
('check_status', models.CharField(max_length=6)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('status', models.IntegerField(default=0)),
|
||||
('channel', models.ForeignKey(to='api.Channel')),
|
||||
('owner', models.ForeignKey(to='api.Check')),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,14 +1,19 @@
|
||||
# coding: utf-8
|
||||
|
||||
from datetime import timedelta as td
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
import requests
|
||||
|
||||
from hc.lib import emails
|
||||
|
||||
from hc.lib.emails import send
|
||||
|
||||
STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"))
|
||||
DEFAULT_TIMEOUT = td(days=1)
|
||||
@ -31,6 +36,12 @@ class Check(models.Model):
|
||||
def __str__(self):
|
||||
return "Check(%s)" % self.code
|
||||
|
||||
def name_then_code(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
|
||||
return str(self.code)
|
||||
|
||||
def url(self):
|
||||
return settings.PING_ENDPOINT + str(self.code)
|
||||
|
||||
@ -38,18 +49,12 @@ class Check(models.Model):
|
||||
return "%s@%s" % (self.code, settings.PING_EMAIL_DOMAIN)
|
||||
|
||||
def send_alert(self):
|
||||
ctx = {
|
||||
"check": self,
|
||||
"checks": self.user.check_set.order_by("created"),
|
||||
"now": timezone.now()
|
||||
|
||||
}
|
||||
|
||||
if self.status in ("up", "down"):
|
||||
send(self.user.email, "emails/alert", ctx)
|
||||
else:
|
||||
if self.status not in ("up", "down"):
|
||||
raise NotImplemented("Unexpected status: %s" % self.status)
|
||||
|
||||
for channel in self.channel_set.all():
|
||||
channel.notify(self)
|
||||
|
||||
def get_status(self):
|
||||
if self.status == "new":
|
||||
return "new"
|
||||
@ -65,8 +70,9 @@ class Check(models.Model):
|
||||
return "down"
|
||||
|
||||
def assign_all_channels(self):
|
||||
channels = Channel.objects.filter(user=self.user)
|
||||
self.channel_set.add(*channels)
|
||||
if self.user:
|
||||
channels = Channel.objects.filter(user=self.user)
|
||||
self.channel_set.add(*channels)
|
||||
|
||||
|
||||
class Ping(models.Model):
|
||||
@ -87,3 +93,62 @@ class Channel(models.Model):
|
||||
value = models.CharField(max_length=200, blank=True)
|
||||
email_verified = models.BooleanField(default=False)
|
||||
checks = models.ManyToManyField(Check)
|
||||
|
||||
def make_token(self):
|
||||
seed = "%s%s" % (self.code, settings.SECRET_KEY)
|
||||
seed = seed.encode("utf8")
|
||||
return hashlib.sha1(seed).hexdigest()
|
||||
|
||||
def send_verify_link(self):
|
||||
args = [self.code, self.make_token()]
|
||||
verify_link = reverse("hc-verify-email", args=args)
|
||||
verify_link = settings.SITE_ROOT + verify_link
|
||||
emails.verify_email(self.value, {"verify_link": verify_link})
|
||||
|
||||
def notify(self, check):
|
||||
n = Notification(owner=check, channel=self)
|
||||
n.check_status = check.status
|
||||
|
||||
if self.kind == "email" and self.email_verified:
|
||||
ctx = {
|
||||
"check": check,
|
||||
"checks": self.user.check_set.order_by("created"),
|
||||
"now": timezone.now()
|
||||
}
|
||||
emails.alert(self.value, ctx)
|
||||
n.save()
|
||||
elif self.kind == "webhook" and self.status == "down":
|
||||
r = requests.get(self.value)
|
||||
n.status = r.status_code
|
||||
n.save()
|
||||
elif self.kind == "pd":
|
||||
if check.status == "down":
|
||||
event_type = "trigger"
|
||||
description = "%s is DOWN" % check.name_then_code()
|
||||
else:
|
||||
event_type = "resolve"
|
||||
description = "%s received a ping and is now UP" % \
|
||||
check.name_then_code()
|
||||
|
||||
payload = {
|
||||
"service_key": self.value,
|
||||
"incident_key": str(check.code),
|
||||
"event_type": event_type,
|
||||
"description": description,
|
||||
"client": "healthchecks.io",
|
||||
"client_url": settings.SITE_ROOT
|
||||
}
|
||||
|
||||
url = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
|
||||
r = requests.post(url, data=json.dumps(payload))
|
||||
|
||||
n.status = r.status_code
|
||||
n.save()
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
owner = models.ForeignKey(Check)
|
||||
check_status = models.CharField(max_length=6)
|
||||
channel = models.ForeignKey(Channel)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
status = models.IntegerField(default=0)
|
||||
|
@ -16,4 +16,7 @@ urlpatterns = [
|
||||
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"),
|
||||
url(r'^channels/([\w-]+)/verify/([\w-]+)/$',
|
||||
views.verify_email, name="hc-verify-email"),
|
||||
|
||||
]
|
||||
|
@ -217,6 +217,9 @@ def add_channel(request):
|
||||
checks = Check.objects.filter(user=request.user)
|
||||
channel.checks.add(*checks)
|
||||
|
||||
if channel.kind == "email":
|
||||
channel.send_verify_link()
|
||||
|
||||
return redirect("hc-channels")
|
||||
|
||||
|
||||
@ -235,3 +238,14 @@ def channel_checks(request, code):
|
||||
}
|
||||
|
||||
return render(request, "front/channel_checks.html", ctx)
|
||||
|
||||
|
||||
@uuid_or_400
|
||||
def verify_email(request, code, token):
|
||||
channel = Channel.objects.get(code=code)
|
||||
if channel.make_token() == token:
|
||||
channel.email_verified = True
|
||||
channel.save()
|
||||
return render(request, "front/verify_email_success.html")
|
||||
|
||||
return render(request, "bad_link.html")
|
||||
|
@ -1,18 +1,16 @@
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from djmail.template_mail import InlineCSSTemplateMail
|
||||
|
||||
|
||||
def send(to, template_directory, ctx):
|
||||
""" Send HTML email using Mandrill.
|
||||
def login(to, ctx):
|
||||
o = InlineCSSTemplateMail("login")
|
||||
o.send(to, ctx)
|
||||
|
||||
Expect template_directory to be a path containing
|
||||
- subject.txt
|
||||
- body.html
|
||||
|
||||
"""
|
||||
def alert(to, ctx):
|
||||
o = InlineCSSTemplateMail("alert")
|
||||
o.send(to, ctx)
|
||||
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
subject = render_to_string("%s/subject.txt" % template_directory, ctx)
|
||||
body = render_to_string("%s/body.html" % template_directory, ctx)
|
||||
send_mail(subject, "", from_email, [to], html_message=body)
|
||||
|
||||
def verify_email(to, ctx):
|
||||
o = InlineCSSTemplateMail("verify-email")
|
||||
o.send(to, ctx)
|
||||
|
@ -31,7 +31,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'compressor',
|
||||
'djrill',
|
||||
'djmail',
|
||||
|
||||
'hc.accounts',
|
||||
'hc.api',
|
||||
@ -123,12 +123,9 @@ STATICFILES_FINDERS = (
|
||||
)
|
||||
COMPRESS_OFFLINE = True
|
||||
|
||||
|
||||
EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend"
|
||||
EMAIL_BACKEND = "djmail.backends.default.EmailBackend"
|
||||
|
||||
try:
|
||||
from local_settings import *
|
||||
except ImportError as e:
|
||||
warnings.warn("local_settings.py not found, using defaults")
|
||||
|
||||
print ("db engine: %s" % DATABASES["default"]["ENGINE"])
|
@ -1,5 +1,7 @@
|
||||
Django==1.8.2
|
||||
django_compressor
|
||||
psycopg2==2.6
|
||||
djrill
|
||||
pygments
|
||||
djmail
|
||||
premailer
|
||||
pygments
|
||||
requests
|
11
templates/emails/verify-email-body-html.html
Normal file
11
templates/emails/verify-email-body-html.html
Normal file
@ -0,0 +1,11 @@
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>To start receiving healthchecks.io notification to this address,
|
||||
please click the link below:</p>
|
||||
<p><a href="{{ verify_link }}">{{ verify_link }}</a></p>
|
||||
|
||||
<p>
|
||||
--<br />
|
||||
Regards,<br />
|
||||
healthchecks.io
|
||||
</p>
|
1
templates/emails/verify-email-subject.html
Normal file
1
templates/emails/verify-email-subject.html
Normal file
@ -0,0 +1 @@
|
||||
Verify email address on healthchecks.io
|
19
templates/front/verify_email_success.html
Normal file
19
templates/front/verify_email_success.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<div id="login_dialog">
|
||||
<h1>Email Address Verified!</h1>
|
||||
<br />
|
||||
<p>
|
||||
Success! You've verified this email
|
||||
address, and it will now receive
|
||||
healthchecks.io notifications.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user