forked from GithubBackups/healthchecks
Adding an option to send daily or hourly reminders if any check is down. Fixes #48
This commit is contained in:
parent
ca2393d0a4
commit
d520706c27
@ -21,7 +21,8 @@ class Fieldset:
|
|||||||
class ProfileFieldset(Fieldset):
|
class ProfileFieldset(Fieldset):
|
||||||
name = "User Profile"
|
name = "User Profile"
|
||||||
fields = ("email", "api_key", "current_team", "reports_allowed",
|
fields = ("email", "api_key", "current_team", "reports_allowed",
|
||||||
"next_report_date", "token", "sort")
|
"next_report_date", "nag_period", "next_nag_date",
|
||||||
|
"token", "sort")
|
||||||
|
|
||||||
|
|
||||||
class TeamFieldset(Fieldset):
|
class TeamFieldset(Fieldset):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
@ -16,6 +17,15 @@ class EmailPasswordForm(forms.Form):
|
|||||||
|
|
||||||
class ReportSettingsForm(forms.Form):
|
class ReportSettingsForm(forms.Form):
|
||||||
reports_allowed = forms.BooleanField(required=False)
|
reports_allowed = forms.BooleanField(required=False)
|
||||||
|
nag_period = forms.IntegerField(min_value=0, max_value=86400)
|
||||||
|
|
||||||
|
def clean_nag_period(self):
|
||||||
|
seconds = self.cleaned_data["nag_period"]
|
||||||
|
|
||||||
|
if seconds not in (0, 3600, 86400):
|
||||||
|
raise forms.ValidationError("Bad nag_period: %d" % seconds)
|
||||||
|
|
||||||
|
return td(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordForm(forms.Form):
|
class SetPasswordForm(forms.Form):
|
||||||
|
26
hc/accounts/migrations/0012_auto_20171014_1002.py
Normal file
26
hc/accounts/migrations/0012_auto_20171014_1002.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2017-10-14 10:02
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0011_profile_sort'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='nag_period',
|
||||||
|
field=models.DurationField(choices=[(datetime.timedelta(0), 'Disabled'), (datetime.timedelta(0, 3600), 'Hourly'), (datetime.timedelta(1), 'Daily')], default=datetime.timedelta(0)),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='next_nag_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -13,6 +13,12 @@ from django.utils import timezone
|
|||||||
from hc.lib import emails
|
from hc.lib import emails
|
||||||
|
|
||||||
|
|
||||||
|
NO_NAG = timedelta()
|
||||||
|
NAG_PERIODS = ((NO_NAG, "Disabled"),
|
||||||
|
(timedelta(hours=1), "Hourly"),
|
||||||
|
(timedelta(days=1), "Daily"))
|
||||||
|
|
||||||
|
|
||||||
def month(dt):
|
def month(dt):
|
||||||
""" For a given datetime, return the matching first-day-of-month date. """
|
""" For a given datetime, return the matching first-day-of-month date. """
|
||||||
return dt.date().replace(day=1)
|
return dt.date().replace(day=1)
|
||||||
@ -23,7 +29,7 @@ class ProfileManager(models.Manager):
|
|||||||
try:
|
try:
|
||||||
return user.profile
|
return user.profile
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
profile = Profile(user=user, team_access_allowed=user.is_superuser)
|
profile = Profile(user=user)
|
||||||
if not settings.USE_PAYMENTS:
|
if not settings.USE_PAYMENTS:
|
||||||
# If not using payments, set high limits
|
# If not using payments, set high limits
|
||||||
profile.check_limit = 500
|
profile.check_limit = 500
|
||||||
@ -41,6 +47,8 @@ class Profile(models.Model):
|
|||||||
team_access_allowed = models.BooleanField(default=False)
|
team_access_allowed = models.BooleanField(default=False)
|
||||||
next_report_date = models.DateTimeField(null=True, blank=True)
|
next_report_date = models.DateTimeField(null=True, blank=True)
|
||||||
reports_allowed = models.BooleanField(default=True)
|
reports_allowed = models.BooleanField(default=True)
|
||||||
|
nag_period = models.DurationField(default=NO_NAG, choices=NAG_PERIODS)
|
||||||
|
next_nag_date = models.DateTimeField(null=True, blank=True)
|
||||||
ping_log_limit = models.IntegerField(default=100)
|
ping_log_limit = models.IntegerField(default=100)
|
||||||
check_limit = models.IntegerField(default=20)
|
check_limit = models.IntegerField(default=20)
|
||||||
token = models.CharField(max_length=128, blank=True)
|
token = models.CharField(max_length=128, blank=True)
|
||||||
@ -58,6 +66,9 @@ class Profile(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.team_name or self.user.email
|
return self.team_name or self.user.email
|
||||||
|
|
||||||
|
def notifications_url(self):
|
||||||
|
return settings.SITE_ROOT + reverse("hc-notifications")
|
||||||
|
|
||||||
def team(self):
|
def team(self):
|
||||||
# compare ids to avoid SQL queries
|
# compare ids to avoid SQL queries
|
||||||
if self.current_team_id and self.current_team_id != self.id:
|
if self.current_team_id and self.current_team_id != self.id:
|
||||||
@ -106,11 +117,15 @@ class Profile(models.Model):
|
|||||||
self.api_key = base64.urlsafe_b64encode(os.urandom(24))
|
self.api_key = base64.urlsafe_b64encode(os.urandom(24))
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def send_report(self):
|
def send_report(self, nag=False):
|
||||||
# reset next report date first:
|
# Are there any non-new checks in the account?
|
||||||
now = timezone.now()
|
q = self.user.check_set.filter(last_ping__isnull=False)
|
||||||
self.next_report_date = now + timedelta(days=30)
|
if not q.exists():
|
||||||
self.save()
|
return False
|
||||||
|
|
||||||
|
num_down = q.filter(status="down").count()
|
||||||
|
if nag and num_down == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
token = signing.Signer().sign(uuid.uuid4())
|
token = signing.Signer().sign(uuid.uuid4())
|
||||||
path = reverse("hc-unsubscribe-reports", args=[self.user.username])
|
path = reverse("hc-unsubscribe-reports", args=[self.user.username])
|
||||||
@ -118,11 +133,16 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"checks": self.user.check_set.order_by("created"),
|
"checks": self.user.check_set.order_by("created"),
|
||||||
"now": now,
|
"now": timezone.now(),
|
||||||
"unsub_link": unsub_link
|
"unsub_link": unsub_link,
|
||||||
|
"notifications_url": self.notifications_url,
|
||||||
|
"nag": nag,
|
||||||
|
"nag_period": self.nag_period.total_seconds(),
|
||||||
|
"num_down": num_down
|
||||||
}
|
}
|
||||||
|
|
||||||
emails.report(self.user.email, ctx)
|
emails.report(self.user.email, ctx)
|
||||||
|
return True
|
||||||
|
|
||||||
def can_invite(self):
|
def can_invite(self):
|
||||||
return self.member_set.count() < self.team_limit
|
return self.member_set.count() < self.team_limit
|
||||||
@ -161,6 +181,16 @@ class Profile(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_next_nag_date(self):
|
||||||
|
""" Set next_nag_date for all members of this team. """
|
||||||
|
|
||||||
|
is_owner = models.Q(id=self.id)
|
||||||
|
is_member = models.Q(user__member__team=self)
|
||||||
|
q = Profile.objects.filter(is_owner | is_member)
|
||||||
|
q = q.exclude(nag_period=NO_NAG)
|
||||||
|
|
||||||
|
q.update(next_nag_date=timezone.now() + models.F("nag_period"))
|
||||||
|
|
||||||
|
|
||||||
class Member(models.Model):
|
class Member(models.Model):
|
||||||
team = models.ForeignKey(Profile, models.CASCADE)
|
team = models.ForeignKey(Profile, models.CASCADE)
|
||||||
|
@ -1,24 +1,60 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
class NotificationsTestCase(BaseTestCase):
|
class NotificationsTestCase(BaseTestCase):
|
||||||
|
|
||||||
def test_it_saves_reports_allowed_true(self):
|
def test_it_saves_reports_allowed_true(self):
|
||||||
|
self.profile.reports_allowed = False
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
form = {"reports_allowed": "on"}
|
form = {"reports_allowed": "on", "nag_period": "0"}
|
||||||
r = self.client.post("/accounts/profile/notifications/", form)
|
r = self.client.post("/accounts/profile/notifications/", form)
|
||||||
assert r.status_code == 200
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertTrue(self.alice.profile.reports_allowed)
|
self.assertTrue(self.profile.reports_allowed)
|
||||||
|
self.assertIsNotNone(self.profile.next_report_date)
|
||||||
|
|
||||||
def test_it_saves_reports_allowed_false(self):
|
def test_it_saves_reports_allowed_false(self):
|
||||||
|
self.profile.reports_allowed = True
|
||||||
|
self.profile.next_report_date = now()
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
form = {}
|
form = {"nag_period": "0"}
|
||||||
r = self.client.post("/accounts/profile/notifications/", form)
|
r = self.client.post("/accounts/profile/notifications/", form)
|
||||||
assert r.status_code == 200
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertFalse(self.alice.profile.reports_allowed)
|
self.assertFalse(self.profile.reports_allowed)
|
||||||
|
self.assertIsNone(self.profile.next_report_date)
|
||||||
|
|
||||||
|
def test_it_saves_hourly_nag_period(self):
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
|
form = {"nag_period": "3600"}
|
||||||
|
r = self.client.post("/accounts/profile/notifications/", form)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertEqual(self.profile.nag_period.total_seconds(), 3600)
|
||||||
|
self.assertIsNotNone(self.profile.next_nag_date)
|
||||||
|
|
||||||
|
def test_it_does_not_save_nonstandard_nag_period(self):
|
||||||
|
self.profile.nag_period = td(seconds=3600)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
|
form = {"nag_period": "1234"}
|
||||||
|
r = self.client.post("/accounts/profile/notifications/", form)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertEqual(self.profile.nag_period.total_seconds(), 3600)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.timezone import now
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
from hc.accounts.models import Member
|
from hc.accounts.models import Member
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileTestCase(BaseTestCase):
|
class ProfileTestCase(BaseTestCase):
|
||||||
@ -16,8 +18,8 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
|
|
||||||
# profile.token should be set now
|
# profile.token should be set now
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
token = self.alice.profile.token
|
token = self.profile.token
|
||||||
self.assertTrue(len(token) > 10)
|
self.assertTrue(len(token) > 10)
|
||||||
|
|
||||||
# And an email should have been sent
|
# And an email should have been sent
|
||||||
@ -32,8 +34,8 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
r = self.client.post("/accounts/profile/", form)
|
r = self.client.post("/accounts/profile/", form)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
api_key = self.alice.profile.api_key
|
api_key = self.profile.api_key
|
||||||
self.assertTrue(len(api_key) > 10)
|
self.assertTrue(len(api_key) > 10)
|
||||||
|
|
||||||
def test_it_revokes_api_key(self):
|
def test_it_revokes_api_key(self):
|
||||||
@ -43,14 +45,16 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
r = self.client.post("/accounts/profile/", form)
|
r = self.client.post("/accounts/profile/", form)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertEqual(self.alice.profile.api_key, "")
|
self.assertEqual(self.profile.api_key, "")
|
||||||
|
|
||||||
def test_it_sends_report(self):
|
def test_it_sends_report(self):
|
||||||
check = Check(name="Test Check", user=self.alice)
|
check = Check(name="Test Check", user=self.alice)
|
||||||
|
check.last_ping = now()
|
||||||
check.save()
|
check.save()
|
||||||
|
|
||||||
self.alice.profile.send_report()
|
sent = self.profile.send_report()
|
||||||
|
self.assertTrue(sent)
|
||||||
|
|
||||||
# And an email should have been sent
|
# And an email should have been sent
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
@ -59,6 +63,38 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
self.assertEqual(message.subject, 'Monthly Report')
|
self.assertEqual(message.subject, 'Monthly Report')
|
||||||
self.assertIn("Test Check", message.body)
|
self.assertIn("Test Check", message.body)
|
||||||
|
|
||||||
|
def test_it_sends_nag(self):
|
||||||
|
check = Check(name="Test Check", user=self.alice)
|
||||||
|
check.status = "down"
|
||||||
|
check.last_ping = now()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
sent = self.profile.send_report(nag=True)
|
||||||
|
self.assertTrue(sent)
|
||||||
|
|
||||||
|
# And an email should have been sent
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
message = mail.outbox[0]
|
||||||
|
|
||||||
|
self.assertEqual(message.subject, 'Reminder: 1 check still down')
|
||||||
|
self.assertIn("Test Check", message.body)
|
||||||
|
|
||||||
|
def test_it_skips_nag_if_none_down(self):
|
||||||
|
check = Check(name="Test Check", user=self.alice)
|
||||||
|
check.last_ping = now()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
sent = self.profile.send_report(nag=True)
|
||||||
|
self.assertFalse(sent)
|
||||||
|
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
def test_it_adds_team_member(self):
|
def test_it_adds_team_member(self):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
@ -67,7 +103,7 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
member_emails = set()
|
member_emails = set()
|
||||||
for member in self.alice.profile.member_set.all():
|
for member in self.profile.member_set.all():
|
||||||
member_emails.add(member.user.email)
|
member_emails.add(member.user.email)
|
||||||
|
|
||||||
self.assertEqual(len(member_emails), 2)
|
self.assertEqual(len(member_emails), 2)
|
||||||
@ -107,8 +143,8 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
r = self.client.post("/accounts/profile/", form)
|
r = self.client.post("/accounts/profile/", form)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertEqual(self.alice.profile.team_name, "Alpha Team")
|
self.assertEqual(self.profile.team_name, "Alpha Team")
|
||||||
|
|
||||||
def test_it_switches_to_own_team(self):
|
def test_it_switches_to_own_team(self):
|
||||||
self.client.login(username="bob@example.org", password="password")
|
self.client.login(username="bob@example.org", password="password")
|
||||||
@ -128,8 +164,8 @@ class ProfileTestCase(BaseTestCase):
|
|||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
|
|
||||||
# profile.token should be set now
|
# profile.token should be set now
|
||||||
self.alice.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
token = self.alice.profile.token
|
token = self.profile.token
|
||||||
self.assertTrue(len(token) > 10)
|
self.assertTrue(len(token) > 10)
|
||||||
|
|
||||||
# And an email should have been sent
|
# And an email should have been sent
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
|
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
@ -5,6 +7,9 @@ from hc.test import BaseTestCase
|
|||||||
class UnsubscribeReportsTestCase(BaseTestCase):
|
class UnsubscribeReportsTestCase(BaseTestCase):
|
||||||
|
|
||||||
def test_it_works(self):
|
def test_it_works(self):
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
token = signing.Signer().sign("foo")
|
token = signing.Signer().sign("foo")
|
||||||
url = "/accounts/unsubscribe_reports/alice/?token=%s" % token
|
url = "/accounts/unsubscribe_reports/alice/?token=%s" % token
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
@ -12,3 +17,4 @@ class UnsubscribeReportsTestCase(BaseTestCase):
|
|||||||
|
|
||||||
self.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertFalse(self.profile.reports_allowed)
|
self.assertFalse(self.profile.reports_allowed)
|
||||||
|
self.assertEqual(self.profile.nag_period.total_seconds(), 0)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ from django.contrib.auth.models import User
|
|||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.http import HttpResponseForbidden, HttpResponseBadRequest
|
from django.http import HttpResponseForbidden, HttpResponseBadRequest
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
|
from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm,
|
||||||
InviteTeamMemberForm, RemoveTeamMemberForm,
|
InviteTeamMemberForm, RemoveTeamMemberForm,
|
||||||
@ -238,7 +240,22 @@ def notifications(request):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = ReportSettingsForm(request.POST)
|
form = ReportSettingsForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
profile.reports_allowed = form.cleaned_data["reports_allowed"]
|
if profile.reports_allowed != form.cleaned_data["reports_allowed"]:
|
||||||
|
profile.reports_allowed = form.cleaned_data["reports_allowed"]
|
||||||
|
if profile.reports_allowed:
|
||||||
|
profile.next_report_date = now() + td(days=30)
|
||||||
|
else:
|
||||||
|
profile.next_report_date = None
|
||||||
|
|
||||||
|
if profile.nag_period != form.cleaned_data["nag_period"]:
|
||||||
|
# Set the new nag period
|
||||||
|
profile.nag_period = form.cleaned_data["nag_period"]
|
||||||
|
# and schedule next_nag_date:
|
||||||
|
if profile.nag_period:
|
||||||
|
profile.next_nag_date = now() + profile.nag_period
|
||||||
|
else:
|
||||||
|
profile.next_nag_date = None
|
||||||
|
|
||||||
profile.save()
|
profile.save()
|
||||||
messages.success(request, "Your settings have been updated!")
|
messages.success(request, "Your settings have been updated!")
|
||||||
|
|
||||||
@ -338,6 +355,7 @@ def unsubscribe_reports(request, username):
|
|||||||
user = User.objects.get(username=username)
|
user = User.objects.get(username=username)
|
||||||
profile = Profile.objects.for_user(user)
|
profile = Profile.objects.for_user(user)
|
||||||
profile.reports_allowed = False
|
profile.reports_allowed = False
|
||||||
|
profile.nag_period = td()
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
return render(request, "accounts/unsubscribed.html")
|
return render(request, "accounts/unsubscribed.html")
|
||||||
|
@ -8,9 +8,14 @@ from hc.api.models import Check
|
|||||||
|
|
||||||
def notify(check_id, stdout):
|
def notify(check_id, stdout):
|
||||||
check = Check.objects.get(id=check_id)
|
check = Check.objects.get(id=check_id)
|
||||||
|
|
||||||
tmpl = "Sending alert, status=%s, code=%s\n"
|
tmpl = "Sending alert, status=%s, code=%s\n"
|
||||||
stdout.write(tmpl % (check.status, check.code))
|
stdout.write(tmpl % (check.status, check.code))
|
||||||
|
|
||||||
|
# Set dates for followup nags
|
||||||
|
if check.status == "down" and check.user.profile:
|
||||||
|
check.user.profile.set_next_nag_date()
|
||||||
|
|
||||||
|
# Send notifications
|
||||||
errors = check.send_alert()
|
errors = check.send_alert()
|
||||||
for ch, error in errors:
|
for ch, error in errors:
|
||||||
stdout.write("ERROR: %s %s %s\n" % (ch.kind, ch.value, error))
|
stdout.write("ERROR: %s %s %s\n" % (ch.kind, ch.value, error))
|
||||||
|
@ -15,8 +15,8 @@ def num_pinged_checks(profile):
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Send due monthly reports'
|
help = 'Send due monthly reports and nags'
|
||||||
tmpl = "Sending monthly report to %s"
|
tmpl = "Sent monthly report to %s"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -27,7 +27,7 @@ class Command(BaseCommand):
|
|||||||
help='Keep running indefinitely in a 300 second wait loop',
|
help='Keep running indefinitely in a 300 second wait loop',
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_one_run(self):
|
def handle_one_monthly_report(self):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
month_before = now - timedelta(days=30)
|
month_before = now - timedelta(days=30)
|
||||||
month_after = now + timedelta(days=30)
|
month_after = now + timedelta(days=30)
|
||||||
@ -38,39 +38,70 @@ class Command(BaseCommand):
|
|||||||
q = Profile.objects.filter(report_due | report_not_scheduled)
|
q = Profile.objects.filter(report_due | report_not_scheduled)
|
||||||
q = q.filter(reports_allowed=True)
|
q = q.filter(reports_allowed=True)
|
||||||
q = q.filter(user__date_joined__lt=month_before)
|
q = q.filter(user__date_joined__lt=month_before)
|
||||||
profiles = list(q)
|
profile = q.first()
|
||||||
|
|
||||||
sent = 0
|
if profile is None:
|
||||||
for profile in profiles:
|
return False
|
||||||
qq = Profile.objects
|
|
||||||
qq = qq.filter(id=profile.id,
|
|
||||||
next_report_date=profile.next_report_date)
|
|
||||||
|
|
||||||
num_updated = qq.update(next_report_date=month_after)
|
# A sort of optimistic lock. Try to update next_report_date,
|
||||||
if num_updated != 1:
|
# and if does get modified, we're in drivers seat:
|
||||||
# Was updated elsewhere, skipping
|
qq = Profile.objects.filter(id=profile.id,
|
||||||
continue
|
next_report_date=profile.next_report_date)
|
||||||
|
|
||||||
if num_pinged_checks(profile) == 0:
|
num_updated = qq.update(next_report_date=month_after)
|
||||||
continue
|
if num_updated != 1:
|
||||||
|
# next_report_date was already updated elsewhere, skipping
|
||||||
|
return True
|
||||||
|
|
||||||
|
if profile.send_report():
|
||||||
self.stdout.write(self.tmpl % profile.user.email)
|
self.stdout.write(self.tmpl % profile.user.email)
|
||||||
profile.send_report()
|
|
||||||
# Pause before next report to avoid hitting sending quota
|
# Pause before next report to avoid hitting sending quota
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
sent += 1
|
|
||||||
|
|
||||||
return sent
|
return True
|
||||||
|
|
||||||
|
def handle_one_nag(self):
|
||||||
|
now = timezone.now()
|
||||||
|
q = Profile.objects.filter(next_nag_date__lt=now)
|
||||||
|
profile = q.first()
|
||||||
|
|
||||||
|
if profile is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
qq = Profile.objects.filter(id=profile.id,
|
||||||
|
next_nag_date=profile.next_nag_date)
|
||||||
|
|
||||||
|
num_updated = qq.update(next_nag_date=now + profile.nag_period)
|
||||||
|
if num_updated != 1:
|
||||||
|
# next_rag_date was already updated elsewhere, skipping
|
||||||
|
return True
|
||||||
|
|
||||||
|
if profile.send_report(nag=True):
|
||||||
|
self.stdout.write("Sent nag to %s" % profile.user.email)
|
||||||
|
# Pause before next report to avoid hitting sending quota
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
profile.next_nag_date = None
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if not options["loop"]:
|
|
||||||
return "Sent %d reports" % self.handle_one_run()
|
|
||||||
|
|
||||||
self.stdout.write("sendreports is now running")
|
self.stdout.write("sendreports is now running")
|
||||||
while True:
|
while True:
|
||||||
self.handle_one_run()
|
# Monthly reports
|
||||||
|
while self.handle_one_monthly_report():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Daily and hourly nags
|
||||||
|
while self.handle_one_nag():
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not options["loop"]:
|
||||||
|
break
|
||||||
|
|
||||||
formatted = timezone.now().isoformat()
|
formatted = timezone.now().isoformat()
|
||||||
self.stdout.write("-- MARK %s --" % formatted)
|
self.stdout.write("-- MARK %s --" % formatted)
|
||||||
|
|
||||||
time.sleep(300)
|
# Sleep for 1 minute before looking for more work
|
||||||
|
time.sleep(60)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from mock import patch
|
from mock import Mock, patch
|
||||||
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from hc.api.management.commands.sendalerts import Command
|
from hc.api.management.commands.sendalerts import Command, notify
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
@ -93,3 +93,31 @@ class SendAlertsTestCase(BaseTestCase):
|
|||||||
|
|
||||||
# It should call `notify` instead of `notify_on_thread`
|
# It should call `notify` instead of `notify_on_thread`
|
||||||
self.assertTrue(mock_notify.called)
|
self.assertTrue(mock_notify.called)
|
||||||
|
|
||||||
|
def test_it_updates_owners_next_nag_date(self):
|
||||||
|
self.profile.nag_period = timedelta(hours=1)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
check = Check(user=self.alice, status="down")
|
||||||
|
check.last_ping = timezone.now() - timedelta(days=2)
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
notify(check.id, Mock())
|
||||||
|
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertIsNotNone(self.profile.next_nag_date)
|
||||||
|
|
||||||
|
def test_it_updates_members_next_nag_date(self):
|
||||||
|
self.bobs_profile.nag_period = timedelta(hours=1)
|
||||||
|
self.bobs_profile.save()
|
||||||
|
|
||||||
|
check = Check(user=self.alice, status="down")
|
||||||
|
check.last_ping = timezone.now() - timedelta(days=2)
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
notify(check.id, Mock())
|
||||||
|
|
||||||
|
self.bobs_profile.refresh_from_db()
|
||||||
|
self.assertIsNotNone(self.bobs_profile.next_nag_date)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
|
|
||||||
|
from django.core import mail
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from hc.api.management.commands.sendreports import Command
|
from hc.api.management.commands.sendreports import Command
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check
|
||||||
@ -16,34 +17,72 @@ class SendAlertsTestCase(BaseTestCase):
|
|||||||
self.alice.date_joined = now() - td(days=365)
|
self.alice.date_joined = now() - td(days=365)
|
||||||
self.alice.save()
|
self.alice.save()
|
||||||
|
|
||||||
|
# Make alice eligible for nags:
|
||||||
|
self.profile.nag_period = td(hours=1)
|
||||||
|
self.profile.next_nag_date = now() - td(seconds=10)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
# And it needs at least one check that has been pinged.
|
# And it needs at least one check that has been pinged.
|
||||||
self.check = Check(user=self.alice, last_ping=now())
|
self.check = Check(user=self.alice, last_ping=now())
|
||||||
|
self.check.status = "down"
|
||||||
self.check.save()
|
self.check.save()
|
||||||
|
|
||||||
def test_it_sends_report(self):
|
def test_it_sends_report(self):
|
||||||
sent = Command().handle_one_run()
|
found = Command().handle_one_monthly_report()
|
||||||
self.assertEqual(sent, 1)
|
self.assertTrue(found)
|
||||||
|
|
||||||
# Alice's profile should have been updated
|
|
||||||
self.profile.refresh_from_db()
|
self.profile.refresh_from_db()
|
||||||
self.assertTrue(self.profile.next_report_date > now())
|
self.assertTrue(self.profile.next_report_date > now())
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
def test_it_obeys_next_report_date(self):
|
def test_it_obeys_next_report_date(self):
|
||||||
self.profile.next_report_date = now() + td(days=1)
|
self.profile.next_report_date = now() + td(days=1)
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
sent = Command().handle_one_run()
|
found = Command().handle_one_monthly_report()
|
||||||
self.assertEqual(sent, 0)
|
self.assertFalse(found)
|
||||||
|
|
||||||
def test_it_obeys_reports_allowed_flag(self):
|
def test_it_obeys_reports_allowed_flag(self):
|
||||||
self.profile.reports_allowed = False
|
self.profile.reports_allowed = False
|
||||||
self.profile.save()
|
self.profile.save()
|
||||||
|
|
||||||
sent = Command().handle_one_run()
|
found = Command().handle_one_monthly_report()
|
||||||
self.assertEqual(sent, 0)
|
self.assertFalse(found)
|
||||||
|
|
||||||
def test_it_requires_pinged_checks(self):
|
def test_it_requires_pinged_checks(self):
|
||||||
self.check.delete()
|
self.check.delete()
|
||||||
|
|
||||||
sent = Command().handle_one_run()
|
found = Command().handle_one_monthly_report()
|
||||||
self.assertEqual(sent, 0)
|
self.assertTrue(found)
|
||||||
|
|
||||||
|
# No email should have been sent:
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
def test_it_sends_nag(self):
|
||||||
|
found = Command().handle_one_nag()
|
||||||
|
self.assertTrue(found)
|
||||||
|
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertTrue(self.profile.next_nag_date > now())
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
def test_it_obeys_next_nag_date(self):
|
||||||
|
self.profile.next_nag_date = now() + td(days=1)
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
found = Command().handle_one_nag()
|
||||||
|
self.assertFalse(found)
|
||||||
|
|
||||||
|
def test_nags_require_down_checks(self):
|
||||||
|
self.check.status = "up"
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
found = Command().handle_one_nag()
|
||||||
|
self.assertTrue(found)
|
||||||
|
|
||||||
|
# No email should have been sent:
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
# next_nag_date should now be unset
|
||||||
|
self.profile.refresh_from_db()
|
||||||
|
self.assertIsNone(self.profile.next_nag_date)
|
||||||
|
67
static/css/checkbox.css
Normal file
67
static/css/checkbox.css
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/* Customize the label (the container) */
|
||||||
|
.checkbox-container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 30px;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the browser's default checkbox */
|
||||||
|
.checkbox-container input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a custom checkbox */
|
||||||
|
.checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mouse-over tint the border */
|
||||||
|
.checkmark:hover {
|
||||||
|
border-color: #5db4ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the checkbox is checked, add a colored background */
|
||||||
|
.checkbox-container input:checked ~ .checkmark {
|
||||||
|
border-color: #0091EA;
|
||||||
|
background-color: #0091EA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create the checkmark/indicator (hidden when not checked) */
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the checkmark when checked */
|
||||||
|
.checkbox-container input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the checkmark/indicator */
|
||||||
|
.checkbox-container .checkmark:after {
|
||||||
|
left: 7px;
|
||||||
|
top: 3px;
|
||||||
|
width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
64
static/css/radio.css
Normal file
64
static/css/radio.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* Customize the label (the container) */
|
||||||
|
.radio-container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 30px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-left: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the browser's default radio button */
|
||||||
|
.radio-container input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a custom radio button */
|
||||||
|
.radiomark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mouse-over, tint the border */
|
||||||
|
.radiomark:hover {
|
||||||
|
border-color: #5db4ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the radio button is checked, add a colored background */
|
||||||
|
.radio-container input:checked ~ .radiomark {
|
||||||
|
border-color: #0091EA;
|
||||||
|
background-color: #0091EA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create the indicator (the dot/circle - hidden when not checked) */
|
||||||
|
.radiomark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the indicator (dot/circle) when checked */
|
||||||
|
.radio-container input:checked ~ .radiomark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the indicator (dot/circle) */
|
||||||
|
.radio-container .radiomark:after {
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
<div class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li><a href="{% url 'hc-profile' %}">Security</a></li>
|
<li><a href="{% url 'hc-profile' %}">Security</a></li>
|
||||||
<li><a href="{% url 'hc-notifications' %}">Notifications</a></li>
|
<li><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
|
||||||
<li class="active"><a href="{% url 'hc-badges' %}">Badges</a></li>
|
<li class="active"><a href="{% url 'hc-badges' %}">Badges</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li><a href="{% url 'hc-profile' %}">Account</a></li>
|
<li><a href="{% url 'hc-profile' %}">Account</a></li>
|
||||||
<li class="active"><a href="{% url 'hc-notifications' %}">Notifications</a></li>
|
<li class="active"><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
|
||||||
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
|
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -29,20 +29,66 @@
|
|||||||
<div class="col-sm-9 col-md-6">
|
<div class="col-sm-9 col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
<h2>Monthly Reports</h2>
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label>
|
<h2>Email Reports</h2>
|
||||||
|
|
||||||
|
<p>Send me monthly emails about:</p>
|
||||||
|
<label class="checkbox-container">
|
||||||
<input
|
<input
|
||||||
name="reports_allowed"
|
name="reports_allowed"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{% if profile.reports_allowed %} checked {% endif %}>
|
{% if profile.reports_allowed %} checked {% endif %}>
|
||||||
Each month send me a summary of my checks
|
<span class="checkmark"></span>
|
||||||
|
The status of checks my checks
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<p>If any checks are down:</p>
|
||||||
|
|
||||||
|
<label class="radio-container">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="nag_period"
|
||||||
|
value="0"
|
||||||
|
{% if profile.nag_period.total_seconds == 0 %} checked {% endif %}>
|
||||||
|
<span class="radiomark"></span>
|
||||||
|
Do not remind me
|
||||||
|
</label>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="nag_period"
|
||||||
|
value="86400"
|
||||||
|
{% if profile.nag_period.total_seconds == 86400 %} checked {% endif %}>
|
||||||
|
<span class="radiomark"></span>
|
||||||
|
Remind me daily
|
||||||
|
</label>
|
||||||
|
<label class="radio-container">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="nag_period"
|
||||||
|
value="3600"
|
||||||
|
{% if profile.nag_period.total_seconds == 3600 %} checked {% endif %}>
|
||||||
|
<span class="radiomark"></span>
|
||||||
|
Remind me hourly
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<p style="color: #888">
|
||||||
|
Reports will be delivered to {{ profile.user.email }}. <br />
|
||||||
|
{% if profile.next_report_date %}
|
||||||
|
Next monthly report date is
|
||||||
|
{{ profile.next_report_date.date }}.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
name="update_reports_allowed"
|
name="update_reports_allowed"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-default pull-right">Save</button>
|
class="btn btn-default pull-right">Save Changes</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active"><a href="{% url 'hc-profile' %}">Account</a></li>
|
<li class="active"><a href="{% url 'hc-profile' %}">Account</a></li>
|
||||||
<li><a href="{% url 'hc-notifications' %}">Notifications</a></li>
|
<li><a href="{% url 'hc-notifications' %}">Email Reports</a></li>
|
||||||
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
|
<li><a href="{% url 'hc-badges' %}">Badges</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +37,8 @@
|
|||||||
<link rel="stylesheet" href="{% static 'css/settings.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/settings.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/last_ping.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/last_ping.css' %}" type="text/css">
|
||||||
<link rel="stylesheet" href="{% static 'css/profile.css' %}" type="text/css">
|
<link rel="stylesheet" href="{% static 'css/profile.css' %}" type="text/css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/checkbox.css' %}" type="text/css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/radio.css' %}" type="text/css">
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
</head>
|
</head>
|
||||||
<body class="page-{{ page }}">
|
<body class="page-{{ page }}">
|
||||||
|
@ -3,15 +3,35 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
Hello,<br />
|
Hello,<br />
|
||||||
This is a monthly report sent by <a href="{% site_root %}">{% site_name %}</a>.
|
|
||||||
|
{% if nag %}
|
||||||
|
This is a
|
||||||
|
{% if nag_period == 3600 %}hourly{% endif %}
|
||||||
|
{% if nag_period == 86400 %}daily{% endif %}
|
||||||
|
reminder sent by <a href="{% site_root %}">{% site_name %}</a>.<br />
|
||||||
|
|
||||||
|
{% if num_down == 1%}
|
||||||
|
One check is currently <strong>DOWN</strong>.
|
||||||
|
{% else %}
|
||||||
|
{{ num_down }} checks are currently <strong>DOWN</strong>.
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
This is a monthly report sent by <a href="{% site_root %}">{% site_name %}</a>.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
{% include "emails/summary-html.html" %}
|
{% include "emails/summary-html.html" %}
|
||||||
|
|
||||||
|
{% if nag %}
|
||||||
|
<strong>Too many notifications?</strong>
|
||||||
|
Visit the <a href="{{ notifications_url }}">Email Reports</a>
|
||||||
|
page on {% site_name %} to set your notification preferences.
|
||||||
|
{% else %}
|
||||||
<strong>Just one more thing to check:</strong>
|
<strong>Just one more thing to check:</strong>
|
||||||
Do you have more cron jobs,
|
Do you have more cron jobs,
|
||||||
not yet on this list, that would benefit from monitoring?
|
not yet on this list, that would benefit from monitoring?
|
||||||
Get the ball rolling by adding one more!
|
Get the ball rolling by adding one more!
|
||||||
|
{% endif %}
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
Cheers,<br>
|
Cheers,<br>
|
||||||
@ -22,6 +42,6 @@ The {% escaped_site_name %} Team
|
|||||||
{% block unsub %}
|
{% block unsub %}
|
||||||
<br>
|
<br>
|
||||||
<a href="{{ unsub_link }}" target="_blank" style="color: #666666; text-decoration: underline;">
|
<a href="{{ unsub_link }}" target="_blank" style="color: #666666; text-decoration: underline;">
|
||||||
Unsubscribe from Monthly Reports
|
Unsubscribe
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{% load hc_extras %}
|
{% load hc_extras %}
|
||||||
Hello,
|
Hello,
|
||||||
|
|
||||||
This is a monthly report sent by {% site_name %}.
|
{% if nag %}This is a {% if nag_period == 3600 %}hourly {% endif %}{% if nag_period == 86400 %}daily {% endif %}reminder sent by {% site_name %}.
|
||||||
|
|
||||||
|
{% if num_down == 1%}One check is currently DOWN.{% else %}{{ num_down }} checks are currently DOWN.{% endif %}{% else %}This is a monthly report sent by {% site_name %}.{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% include 'emails/summary-text.html' %}
|
{% include 'emails/summary-text.html' %}
|
||||||
|
|
||||||
|
@ -1,2 +1,6 @@
|
|||||||
Monthly Report
|
{% if nag %}
|
||||||
|
Reminder: {{ num_down }} check{{ num_down|pluralize }} still down
|
||||||
|
{% else %}
|
||||||
|
Monthly Report
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user