Browse Source

Send emails using djmail, can verify email addresses in /channels/

pull/7/head
Pēteris Caune 9 years ago
parent
commit
f0089e2cd2
18 changed files with 234 additions and 39 deletions
  1. +2
    -2
      hc/accounts/views.py
  2. +30
    -2
      hc/api/admin.py
  3. +32
    -0
      hc/api/management/commands/makechannels.py
  4. +2
    -2
      hc/api/management/commands/sendalerts.py
  5. +25
    -0
      hc/api/migrations/0011_notification.py
  6. +78
    -13
      hc/api/models.py
  7. +3
    -0
      hc/front/urls.py
  8. +14
    -0
      hc/front/views.py
  9. +11
    -13
      hc/lib/emails.py
  10. +2
    -5
      hc/settings.py
  11. +4
    -2
      requirements.txt
  12. +0
    -0
      templates/emails/alert-body-html.html
  13. +0
    -0
      templates/emails/alert-subject.html
  14. +0
    -0
      templates/emails/login-body-html.html
  15. +0
    -0
      templates/emails/login-subject.html
  16. +11
    -0
      templates/emails/verify-email-body-html.html
  17. +1
    -0
      templates/emails/verify-email-subject.html
  18. +19
    -0
      templates/front/verify_email_success.html

+ 2
- 2
hc/accounts/views.py View File

@ -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):


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

@ -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
- 0
hc/api/management/commands/makechannels.py View 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))

+ 2
- 2
hc/api/management/commands/sendalerts.py View File

@ -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
- 0
hc/api/migrations/0011_notification.py View 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')),
],
),
]

+ 78
- 13
hc/api/models.py View File

@ -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)

+ 3
- 0
hc/front/urls.py View File

@ -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"),
]

+ 14
- 0
hc/front/views.py View File

@ -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")

+ 11
- 13
hc/lib/emails.py View File

@ -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)

+ 2
- 5
hc/settings.py View File

@ -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"])

+ 4
- 2
requirements.txt View File

@ -1,5 +1,7 @@
Django==1.8.2
django_compressor
psycopg2==2.6
djrill
pygments
djmail
premailer
pygments
requests

templates/emails/alert/body.html → templates/emails/alert-body-html.html View File


templates/emails/alert/subject.txt → templates/emails/alert-subject.html View File


templates/emails/login/body.html → templates/emails/login-body-html.html View File


templates/emails/login/subject.txt → templates/emails/login-subject.html View File


+ 11
- 0
templates/emails/verify-email-body-html.html View 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
- 0
templates/emails/verify-email-subject.html View File

@ -0,0 +1 @@
Verify email address on healthchecks.io

+ 19
- 0
templates/front/verify_email_success.html View 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…
Cancel
Save