mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-11 07:41:19 +00:00
Code to send monthly reports (but no management command yet to actually send them)
This commit is contained in:
parent
cf84c02fc8
commit
91e6f80d9a
18 changed files with 353 additions and 13 deletions
|
@ -1,10 +1,22 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from hc.accounts.models import Profile
|
||||||
from hc.api.models import Channel, Check
|
from hc.api.models import Channel, Check
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Profile)
|
||||||
|
class ProfileAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ("id", "email", "reports_allowed", "next_report_date")
|
||||||
|
search_fields = ["user__email"]
|
||||||
|
|
||||||
|
def email(self, obj):
|
||||||
|
return obj.user.email
|
||||||
|
|
||||||
|
|
||||||
class HcUserAdmin(UserAdmin):
|
class HcUserAdmin(UserAdmin):
|
||||||
|
actions = ["send_report"]
|
||||||
list_display = ('id', 'username', 'email', 'date_joined', 'involvement',
|
list_display = ('id', 'username', 'email', 'date_joined', 'involvement',
|
||||||
'is_staff')
|
'is_staff')
|
||||||
|
|
||||||
|
@ -33,6 +45,13 @@ class HcUserAdmin(UserAdmin):
|
||||||
|
|
||||||
involvement.allow_tags = True
|
involvement.allow_tags = True
|
||||||
|
|
||||||
|
def send_report(self, request, qs):
|
||||||
|
for user in qs:
|
||||||
|
profile = Profile.objects.for_user(user)
|
||||||
|
profile.send_report()
|
||||||
|
|
||||||
|
self.message_user(request, "%d email(s) sent" % qs.count())
|
||||||
|
|
||||||
|
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
admin.site.register(User, HcUserAdmin)
|
admin.site.register(User, HcUserAdmin)
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django import forms
|
||||||
|
|
||||||
|
|
||||||
class LowercaseEmailField(forms.EmailField):
|
class LowercaseEmailField(forms.EmailField):
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
value = super(LowercaseEmailField, self).clean(value)
|
value = super(LowercaseEmailField, self).clean(value)
|
||||||
return value.lower()
|
return value.lower()
|
||||||
|
@ -9,3 +10,7 @@ class LowercaseEmailField(forms.EmailField):
|
||||||
|
|
||||||
class EmailForm(forms.Form):
|
class EmailForm(forms.Form):
|
||||||
email = LowercaseEmailField()
|
email = LowercaseEmailField()
|
||||||
|
|
||||||
|
|
||||||
|
class ReportSettingsForm(forms.Form):
|
||||||
|
reports_allowed = forms.BooleanField(required=False)
|
||||||
|
|
0
hc/accounts/management/__init__.py
Normal file
0
hc/accounts/management/__init__.py
Normal file
0
hc/accounts/management/commands/__init__.py
Normal file
0
hc/accounts/management/commands/__init__.py
Normal file
14
hc/accounts/management/commands/createprofiles.py
Normal file
14
hc/accounts/management/commands/createprofiles.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from hc.accounts.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Make sure all users have profiles'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
for user in User.objects.all():
|
||||||
|
# this should create profile object if it does not exist
|
||||||
|
Profile.objects.for_user(user)
|
||||||
|
|
||||||
|
print("Done.")
|
24
hc/accounts/migrations/0001_initial.py
Normal file
24
hc/accounts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Profile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)),
|
||||||
|
('next_report_date', models.DateTimeField(null=True, blank=True)),
|
||||||
|
('reports_allowed', models.BooleanField(default=True)),
|
||||||
|
('user', models.OneToOneField(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,47 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core import signing
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from hc.lib import emails
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
class ProfileManager(models.Manager):
|
||||||
|
|
||||||
|
def for_user(self, user):
|
||||||
|
try:
|
||||||
|
profile = self.get(user_id=user.id)
|
||||||
|
except Profile.DoesNotExist:
|
||||||
|
profile = Profile(user=user)
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
class Profile(models.Model):
|
||||||
|
user = models.OneToOneField(User, blank=True, null=True)
|
||||||
|
next_report_date = models.DateTimeField(null=True, blank=True)
|
||||||
|
reports_allowed = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
objects = ProfileManager()
|
||||||
|
|
||||||
|
def send_report(self):
|
||||||
|
# reset next report date first:
|
||||||
|
now = timezone.now()
|
||||||
|
self.next_report_date = now + timedelta(days=30)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
token = signing.Signer().sign(uuid.uuid4())
|
||||||
|
path = reverse("hc-unsubscribe-reports", args=[self.user.username])
|
||||||
|
unsub_link = "%s%s?token=%s" % (settings.SITE_ROOT, path, token)
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"checks": self.user.check_set.order_by("created"),
|
||||||
|
"now": now,
|
||||||
|
"unsub_link": unsub_link
|
||||||
|
}
|
||||||
|
|
||||||
|
emails.report(self.user.email, ctx)
|
||||||
|
|
|
@ -2,8 +2,17 @@ from django.conf.urls import url
|
||||||
from hc.accounts import views
|
from hc.accounts import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^login/$', views.login, name="hc-login"),
|
url(r'^login/$', views.login, name="hc-login"),
|
||||||
url(r'^logout/$', views.logout, name="hc-logout"),
|
url(r'^logout/$', views.logout, name="hc-logout"),
|
||||||
url(r'^login_link_sent/$', views.login_link_sent, name="hc-login-link-sent"),
|
url(r'^login_link_sent/$',
|
||||||
url(r'^check_token/([\w-]+)/([\w-]+)/$', views.check_token, name="hc-check-token"),
|
views.login_link_sent, name="hc-login-link-sent"),
|
||||||
|
|
||||||
|
url(r'^check_token/([\w-]+)/([\w-]+)/$',
|
||||||
|
views.check_token, name="hc-check-token"),
|
||||||
|
|
||||||
|
url(r'^profile/$', views.profile, name="hc-profile"),
|
||||||
|
|
||||||
|
url(r'^unsubscribe_reports/([\w-]+)/$',
|
||||||
|
views.unsubscribe_reports, name="hc-unsubscribe-reports"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login as auth_login
|
from django.contrib.auth import login as auth_login
|
||||||
from django.contrib.auth import logout as auth_logout
|
from django.contrib.auth import logout as auth_logout
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core import signing
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponseBadRequest
|
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, ReportSettingsForm
|
||||||
|
from hc.accounts.models import Profile
|
||||||
from hc.api.models import Channel, Check
|
from hc.api.models import Channel, Check
|
||||||
from hc.lib import emails
|
from hc.lib import emails
|
||||||
|
|
||||||
|
@ -108,3 +112,35 @@ def check_token(request, username, token):
|
||||||
|
|
||||||
ctx = {"bad_link": True}
|
ctx = {"bad_link": True}
|
||||||
return render(request, "accounts/login.html", ctx)
|
return render(request, "accounts/login.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile(request):
|
||||||
|
profile = Profile.objects.for_user(request.user)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = ReportSettingsForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
profile.reports_allowed = form.cleaned_data["reports_allowed"]
|
||||||
|
profile.save()
|
||||||
|
messages.info(request, "Your settings have been updated!")
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"profile": profile
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "accounts/profile.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def unsubscribe_reports(request, username):
|
||||||
|
try:
|
||||||
|
signing.Signer().unsign(request.GET.get("token"))
|
||||||
|
except signing.BadSignature:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
profile = Profile.objects.for_user(user)
|
||||||
|
profile.reports_allowed = False
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
return render(request, "accounts/unsubscribed.html")
|
||||||
|
|
|
@ -14,3 +14,8 @@ def alert(to, ctx):
|
||||||
def verify_email(to, ctx):
|
def verify_email(to, ctx):
|
||||||
o = InlineCSSTemplateMail("verify-email")
|
o = InlineCSSTemplateMail("verify-email")
|
||||||
o.send(to, ctx)
|
o.send(to, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def report(to, ctx):
|
||||||
|
o = InlineCSSTemplateMail("report")
|
||||||
|
o.send(to, ctx)
|
||||||
|
|
|
@ -24,14 +24,13 @@ body {
|
||||||
margin-top: -16px;
|
margin-top: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-default .navbar-nav > li > a {
|
#nav-main-sections > li > a {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.navbar-default .navbar-nav > li.active > a, .navbar-default .navbar-nav > li > a:hover {
|
.navbar-default .navbar-nav > li.active > a, .navbar-default .navbar-nav > li > a:hover {
|
||||||
background: none;
|
|
||||||
border-bottom: 5px solid #eee;
|
border-bottom: 5px solid #eee;
|
||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
12
static/css/settings.css
Normal file
12
static/css/settings.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#settings-title {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-block {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-block h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
44
templates/accounts/profile.html
Normal file
44
templates/accounts/profile.html
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load compress staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}Account Settings - healthchecks.io{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<h1 id="settings-title">Settings</h1>
|
||||||
|
</div>
|
||||||
|
{% if messages %}
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{% for message in messages %}
|
||||||
|
<p class="alert alert-success">{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body settings-block">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<h2>Monthly Reports</h2>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
name="reports_allowed"
|
||||||
|
type="checkbox"
|
||||||
|
{% if profile.reports_allowed %} checked {% endif %}>
|
||||||
|
Each month send me a summary of my checks
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-default pull-right">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
12
templates/accounts/unsubscribed.html
Normal file
12
templates/accounts/unsubscribed.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
|
<div class="hc-dialog">
|
||||||
|
<h1>You have been unsubscribed</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -26,6 +26,7 @@
|
||||||
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/log.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/log.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/settings.css' %}" type="text/css">
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
</head>
|
</head>
|
||||||
<body class="page-{{ page }}">
|
<body class="page-{{ page }}">
|
||||||
|
@ -68,7 +69,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div id="navbar" class="navbar-collapse collapse">
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul id="nav-main-sections" class="nav navbar-nav">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li {% if page == 'checks' %} class="active" {% endif %}>
|
<li {% if page == 'checks' %} class="active" {% endif %}>
|
||||||
<a href="{% url 'hc-checks' %}">Checks</a>
|
<a href="{% url 'hc-checks' %}">Checks</a>
|
||||||
|
@ -105,10 +106,18 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<p class="navbar-text navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
{{ request.user.email }}
|
<li class="dropdown">
|
||||||
<a href="{% url 'hc-logout' %}">Log Out</a>
|
<a id="nav-email" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">
|
||||||
</p>
|
{{ request.user.email }} <span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{% url 'hc-profile' %}">Settings</a></li>
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
<li><a href="{% url 'hc-logout' %}">Log Out</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,10 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="unnamed">unnamed</span>
|
<span class="unnamed">unnamed</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if check.tags %}
|
||||||
|
<br />
|
||||||
|
<small>{{ check.tags }}</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="url-cell">
|
<td class="url-cell">
|
||||||
<code>{{ check.url }}</code>
|
<code>{{ check.url }}</code>
|
||||||
|
|
102
templates/emails/report-body-html.html
Normal file
102
templates/emails/report-body-html.html
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
{% load humanize hc_extras %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 10px;
|
||||||
|
color: white;
|
||||||
|
padding: 4px;
|
||||||
|
font-family: sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new { background: #AAA; }
|
||||||
|
.paused { background: #AAA; }
|
||||||
|
.up { background: #5cb85c; }
|
||||||
|
.grace { background: #f0ad4e; }
|
||||||
|
.down { background: #d9534f; }
|
||||||
|
|
||||||
|
|
||||||
|
.unnamed {
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>This is a monthly report sent by <a href="https://healthchecks.io">healthchecks.io</a>.</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Period</th>
|
||||||
|
<th>Last Ping</th>
|
||||||
|
</tr>
|
||||||
|
{% for check in checks %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if check.get_status == "new" %}
|
||||||
|
<span class="badge new">NEW</span>
|
||||||
|
{% elif check.get_status == "up" %}
|
||||||
|
<span class="badge up">UP</span>
|
||||||
|
{% elif check.get_status == "grace" %}
|
||||||
|
<span class="badge grace">LATE</span>
|
||||||
|
{% elif check.get_status == "down" %}
|
||||||
|
<span class="badge down">DOWN</span>
|
||||||
|
{% elif check.get_status == "paused" %}
|
||||||
|
<span class="badge paused">PAUSED</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if check.name %}
|
||||||
|
{{ check.name }}
|
||||||
|
{% else %}
|
||||||
|
<span class="unnamed">unnamed</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if check.tags %}
|
||||||
|
<br />
|
||||||
|
<small>{{ check.tags }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="url-cell">
|
||||||
|
<code>{{ check.url }}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ check.timeout|hc_duration }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if check.last_ping %}
|
||||||
|
{{ check.last_ping|naturaltime }}
|
||||||
|
{% else %}
|
||||||
|
Never
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p><strong>Just one more thing to check:</strong>
|
||||||
|
Do you have more cron jobs,
|
||||||
|
not yet on this list, that would benefit from monitoring?
|
||||||
|
Get the ball rolling by adding one more!</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
--<br />
|
||||||
|
Regards,<br />
|
||||||
|
healthchecks.io
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{ unsub_link }}">Unsubscribe from future monthly reports</a>
|
||||||
|
</p>
|
2
templates/emails/report-subject.html
Normal file
2
templates/emails/report-subject.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Monthly Report
|
||||||
|
|
Loading…
Add table
Reference in a new issue