From d520706c27e68de747b0151c044604272d422d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Sat, 14 Oct 2017 16:03:56 +0300 Subject: [PATCH] Adding an option to send daily or hourly reminders if any check is down. Fixes #48 --- hc/accounts/admin.py | 3 +- hc/accounts/forms.py | 10 +++ .../migrations/0012_auto_20171014_1002.py | 26 +++++++ hc/accounts/models.py | 46 +++++++++-- hc/accounts/tests/test_notifications.py | 52 +++++++++++-- hc/accounts/tests/test_profile.py | 62 +++++++++++---- hc/accounts/tests/test_unsubscribe_reports.py | 6 ++ hc/accounts/views.py | 20 ++++- hc/api/management/commands/sendalerts.py | 7 +- hc/api/management/commands/sendreports.py | 77 +++++++++++++------ hc/api/tests/test_sendalerts.py | 32 +++++++- hc/api/tests/test_sendreports.py | 57 +++++++++++--- static/css/checkbox.css | 67 ++++++++++++++++ static/css/radio.css | 64 +++++++++++++++ templates/accounts/badges.html | 2 +- templates/accounts/notifications.html | 56 ++++++++++++-- templates/accounts/profile.html | 2 +- templates/base.html | 2 + templates/emails/report-body-html.html | 24 +++++- templates/emails/report-body-text.html | 5 +- templates/emails/report-subject.html | 6 +- 21 files changed, 549 insertions(+), 77 deletions(-) create mode 100644 hc/accounts/migrations/0012_auto_20171014_1002.py create mode 100644 static/css/checkbox.css create mode 100644 static/css/radio.css diff --git a/hc/accounts/admin.py b/hc/accounts/admin.py index deabc73f..2713fcbe 100644 --- a/hc/accounts/admin.py +++ b/hc/accounts/admin.py @@ -21,7 +21,8 @@ class Fieldset: class ProfileFieldset(Fieldset): name = "User Profile" 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): diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index 54a3d85c..97a714a1 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -1,3 +1,4 @@ +from datetime import timedelta as td from django import forms from django.contrib.auth.models import User @@ -16,6 +17,15 @@ class EmailPasswordForm(forms.Form): class ReportSettingsForm(forms.Form): 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): diff --git a/hc/accounts/migrations/0012_auto_20171014_1002.py b/hc/accounts/migrations/0012_auto_20171014_1002.py new file mode 100644 index 00000000..22174fc6 --- /dev/null +++ b/hc/accounts/migrations/0012_auto_20171014_1002.py @@ -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), + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 8d8e2001..f50de424 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -13,6 +13,12 @@ from django.utils import timezone from hc.lib import emails +NO_NAG = timedelta() +NAG_PERIODS = ((NO_NAG, "Disabled"), + (timedelta(hours=1), "Hourly"), + (timedelta(days=1), "Daily")) + + def month(dt): """ For a given datetime, return the matching first-day-of-month date. """ return dt.date().replace(day=1) @@ -23,7 +29,7 @@ class ProfileManager(models.Manager): try: return user.profile except Profile.DoesNotExist: - profile = Profile(user=user, team_access_allowed=user.is_superuser) + profile = Profile(user=user) if not settings.USE_PAYMENTS: # If not using payments, set high limits profile.check_limit = 500 @@ -41,6 +47,8 @@ class Profile(models.Model): team_access_allowed = models.BooleanField(default=False) next_report_date = models.DateTimeField(null=True, blank=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) check_limit = models.IntegerField(default=20) token = models.CharField(max_length=128, blank=True) @@ -58,6 +66,9 @@ class Profile(models.Model): def __str__(self): return self.team_name or self.user.email + def notifications_url(self): + return settings.SITE_ROOT + reverse("hc-notifications") + def team(self): # compare ids to avoid SQL queries 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.save() - def send_report(self): - # reset next report date first: - now = timezone.now() - self.next_report_date = now + timedelta(days=30) - self.save() + def send_report(self, nag=False): + # Are there any non-new checks in the account? + q = self.user.check_set.filter(last_ping__isnull=False) + if not q.exists(): + return False + + num_down = q.filter(status="down").count() + if nag and num_down == 0: + return False token = signing.Signer().sign(uuid.uuid4()) path = reverse("hc-unsubscribe-reports", args=[self.user.username]) @@ -118,11 +133,16 @@ class Profile(models.Model): ctx = { "checks": self.user.check_set.order_by("created"), - "now": now, - "unsub_link": unsub_link + "now": timezone.now(), + "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) + return True def can_invite(self): return self.member_set.count() < self.team_limit @@ -161,6 +181,16 @@ class Profile(models.Model): self.save() 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): team = models.ForeignKey(Profile, models.CASCADE) diff --git a/hc/accounts/tests/test_notifications.py b/hc/accounts/tests/test_notifications.py index 5ed0c8e1..33744bc4 100644 --- a/hc/accounts/tests/test_notifications.py +++ b/hc/accounts/tests/test_notifications.py @@ -1,24 +1,60 @@ +from datetime import timedelta as td + +from django.utils.timezone import now from hc.test import BaseTestCase class NotificationsTestCase(BaseTestCase): 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") - form = {"reports_allowed": "on"} + form = {"reports_allowed": "on", "nag_period": "0"} 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.assertTrue(self.alice.profile.reports_allowed) + self.profile.refresh_from_db() + self.assertTrue(self.profile.reports_allowed) + self.assertIsNotNone(self.profile.next_report_date) 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") + + form = {"nag_period": "0"} + r = self.client.post("/accounts/profile/notifications/", form) + self.assertEqual(r.status_code, 200) + + self.profile.refresh_from_db() + 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 = {} + form = {"nag_period": "1234"} 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.assertFalse(self.alice.profile.reports_allowed) + self.profile.refresh_from_db() + self.assertEqual(self.profile.nag_period.total_seconds(), 3600) diff --git a/hc/accounts/tests/test_profile.py b/hc/accounts/tests/test_profile.py index ceadde46..30f9d2fd 100644 --- a/hc/accounts/tests/test_profile.py +++ b/hc/accounts/tests/test_profile.py @@ -1,9 +1,11 @@ +from datetime import timedelta as td from django.core import mail +from django.conf import settings +from django.utils.timezone import now from hc.test import BaseTestCase from hc.accounts.models import Member from hc.api.models import Check -from django.conf import settings class ProfileTestCase(BaseTestCase): @@ -16,8 +18,8 @@ class ProfileTestCase(BaseTestCase): assert r.status_code == 302 # profile.token should be set now - self.alice.profile.refresh_from_db() - token = self.alice.profile.token + self.profile.refresh_from_db() + token = self.profile.token self.assertTrue(len(token) > 10) # And an email should have been sent @@ -32,8 +34,8 @@ class ProfileTestCase(BaseTestCase): r = self.client.post("/accounts/profile/", form) self.assertEqual(r.status_code, 200) - self.alice.profile.refresh_from_db() - api_key = self.alice.profile.api_key + self.profile.refresh_from_db() + api_key = self.profile.api_key self.assertTrue(len(api_key) > 10) def test_it_revokes_api_key(self): @@ -43,14 +45,16 @@ class ProfileTestCase(BaseTestCase): r = self.client.post("/accounts/profile/", form) assert r.status_code == 200 - self.alice.profile.refresh_from_db() - self.assertEqual(self.alice.profile.api_key, "") + self.profile.refresh_from_db() + self.assertEqual(self.profile.api_key, "") def test_it_sends_report(self): check = Check(name="Test Check", user=self.alice) + check.last_ping = now() check.save() - self.alice.profile.send_report() + sent = self.profile.send_report() + self.assertTrue(sent) # And an email should have been sent self.assertEqual(len(mail.outbox), 1) @@ -59,6 +63,38 @@ class ProfileTestCase(BaseTestCase): self.assertEqual(message.subject, 'Monthly Report') 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): self.client.login(username="alice@example.org", password="password") @@ -67,7 +103,7 @@ class ProfileTestCase(BaseTestCase): self.assertEqual(r.status_code, 200) 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) self.assertEqual(len(member_emails), 2) @@ -107,8 +143,8 @@ class ProfileTestCase(BaseTestCase): r = self.client.post("/accounts/profile/", form) self.assertEqual(r.status_code, 200) - self.alice.profile.refresh_from_db() - self.assertEqual(self.alice.profile.team_name, "Alpha Team") + self.profile.refresh_from_db() + self.assertEqual(self.profile.team_name, "Alpha Team") def test_it_switches_to_own_team(self): self.client.login(username="bob@example.org", password="password") @@ -128,8 +164,8 @@ class ProfileTestCase(BaseTestCase): assert r.status_code == 302 # profile.token should be set now - self.alice.profile.refresh_from_db() - token = self.alice.profile.token + self.profile.refresh_from_db() + token = self.profile.token self.assertTrue(len(token) > 10) # And an email should have been sent diff --git a/hc/accounts/tests/test_unsubscribe_reports.py b/hc/accounts/tests/test_unsubscribe_reports.py index 8b707028..ef232dd6 100644 --- a/hc/accounts/tests/test_unsubscribe_reports.py +++ b/hc/accounts/tests/test_unsubscribe_reports.py @@ -1,3 +1,5 @@ +from datetime import timedelta as td + from django.core import signing from hc.test import BaseTestCase @@ -5,6 +7,9 @@ from hc.test import BaseTestCase class UnsubscribeReportsTestCase(BaseTestCase): def test_it_works(self): + self.profile.nag_period = td(hours=1) + self.profile.save() + token = signing.Signer().sign("foo") url = "/accounts/unsubscribe_reports/alice/?token=%s" % token r = self.client.get(url) @@ -12,3 +17,4 @@ class UnsubscribeReportsTestCase(BaseTestCase): self.profile.refresh_from_db() self.assertFalse(self.profile.reports_allowed) + self.assertEqual(self.profile.nag_period.total_seconds(), 0) diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 23753452..ec4d9c56 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -1,3 +1,4 @@ +from datetime import timedelta as td import uuid import re @@ -11,6 +12,7 @@ from django.contrib.auth.models import User from django.core import signing from django.http import HttpResponseForbidden, HttpResponseBadRequest from django.shortcuts import redirect, render +from django.utils.timezone import now from django.views.decorators.http import require_POST from hc.accounts.forms import (ChangeEmailForm, EmailPasswordForm, InviteTeamMemberForm, RemoveTeamMemberForm, @@ -238,7 +240,22 @@ def notifications(request): if request.method == "POST": form = ReportSettingsForm(request.POST) 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() messages.success(request, "Your settings have been updated!") @@ -338,6 +355,7 @@ def unsubscribe_reports(request, username): user = User.objects.get(username=username) profile = Profile.objects.for_user(user) profile.reports_allowed = False + profile.nag_period = td() profile.save() return render(request, "accounts/unsubscribed.html") diff --git a/hc/api/management/commands/sendalerts.py b/hc/api/management/commands/sendalerts.py index 316353b2..a1a25104 100644 --- a/hc/api/management/commands/sendalerts.py +++ b/hc/api/management/commands/sendalerts.py @@ -8,9 +8,14 @@ from hc.api.models import Check def notify(check_id, stdout): check = Check.objects.get(id=check_id) - tmpl = "Sending alert, status=%s, code=%s\n" 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() for ch, error in errors: stdout.write("ERROR: %s %s %s\n" % (ch.kind, ch.value, error)) diff --git a/hc/api/management/commands/sendreports.py b/hc/api/management/commands/sendreports.py index 994cdee9..59fb8413 100644 --- a/hc/api/management/commands/sendreports.py +++ b/hc/api/management/commands/sendreports.py @@ -15,8 +15,8 @@ def num_pinged_checks(profile): class Command(BaseCommand): - help = 'Send due monthly reports' - tmpl = "Sending monthly report to %s" + help = 'Send due monthly reports and nags' + tmpl = "Sent monthly report to %s" def add_arguments(self, parser): parser.add_argument( @@ -27,7 +27,7 @@ class Command(BaseCommand): help='Keep running indefinitely in a 300 second wait loop', ) - def handle_one_run(self): + def handle_one_monthly_report(self): now = timezone.now() month_before = 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 = q.filter(reports_allowed=True) q = q.filter(user__date_joined__lt=month_before) - profiles = list(q) + profile = q.first() - sent = 0 - for profile in profiles: - qq = Profile.objects - qq = qq.filter(id=profile.id, - next_report_date=profile.next_report_date) + if profile is None: + return False - num_updated = qq.update(next_report_date=month_after) - if num_updated != 1: - # Was updated elsewhere, skipping - continue + # A sort of optimistic lock. Try to update next_report_date, + # and if does get modified, we're in drivers seat: + qq = Profile.objects.filter(id=profile.id, + next_report_date=profile.next_report_date) - if num_pinged_checks(profile) == 0: - continue + num_updated = qq.update(next_report_date=month_after) + 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) - profile.send_report() # Pause before next report to avoid hitting sending quota time.sleep(1) - sent += 1 - return sent + return True - def handle(self, *args, **options): - if not options["loop"]: - return "Sent %d reports" % self.handle_one_run() + 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): self.stdout.write("sendreports is now running") 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() self.stdout.write("-- MARK %s --" % formatted) - time.sleep(300) + # Sleep for 1 minute before looking for more work + time.sleep(60) diff --git a/hc/api/tests/test_sendalerts.py b/hc/api/tests/test_sendalerts.py index fc64bb7e..7ed585e9 100644 --- a/hc/api/tests/test_sendalerts.py +++ b/hc/api/tests/test_sendalerts.py @@ -1,9 +1,9 @@ from datetime import timedelta -from mock import patch +from mock import Mock, patch from django.core.management import call_command 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.test import BaseTestCase @@ -93,3 +93,31 @@ class SendAlertsTestCase(BaseTestCase): # It should call `notify` instead of `notify_on_thread` 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) diff --git a/hc/api/tests/test_sendreports.py b/hc/api/tests/test_sendreports.py index 09ed4c95..60169036 100644 --- a/hc/api/tests/test_sendreports.py +++ b/hc/api/tests/test_sendreports.py @@ -1,5 +1,6 @@ from datetime import timedelta as td +from django.core import mail from django.utils.timezone import now from hc.api.management.commands.sendreports import Command from hc.api.models import Check @@ -16,34 +17,72 @@ class SendAlertsTestCase(BaseTestCase): self.alice.date_joined = now() - td(days=365) 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. self.check = Check(user=self.alice, last_ping=now()) + self.check.status = "down" self.check.save() def test_it_sends_report(self): - sent = Command().handle_one_run() - self.assertEqual(sent, 1) + found = Command().handle_one_monthly_report() + self.assertTrue(found) - # Alice's profile should have been updated self.profile.refresh_from_db() self.assertTrue(self.profile.next_report_date > now()) + self.assertEqual(len(mail.outbox), 1) def test_it_obeys_next_report_date(self): self.profile.next_report_date = now() + td(days=1) self.profile.save() - sent = Command().handle_one_run() - self.assertEqual(sent, 0) + found = Command().handle_one_monthly_report() + self.assertFalse(found) def test_it_obeys_reports_allowed_flag(self): self.profile.reports_allowed = False self.profile.save() - sent = Command().handle_one_run() - self.assertEqual(sent, 0) + found = Command().handle_one_monthly_report() + self.assertFalse(found) def test_it_requires_pinged_checks(self): self.check.delete() - sent = Command().handle_one_run() - self.assertEqual(sent, 0) + found = Command().handle_one_monthly_report() + 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) diff --git a/static/css/checkbox.css b/static/css/checkbox.css new file mode 100644 index 00000000..48277283 --- /dev/null +++ b/static/css/checkbox.css @@ -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); +} \ No newline at end of file diff --git a/static/css/radio.css b/static/css/radio.css new file mode 100644 index 00000000..8d4be112 --- /dev/null +++ b/static/css/radio.css @@ -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; +} \ No newline at end of file diff --git a/templates/accounts/badges.html b/templates/accounts/badges.html index 1d074db0..f0019dd0 100644 --- a/templates/accounts/badges.html +++ b/templates/accounts/badges.html @@ -14,7 +14,7 @@
diff --git a/templates/accounts/notifications.html b/templates/accounts/notifications.html index 67e4300f..a39aa8e4 100644 --- a/templates/accounts/notifications.html +++ b/templates/accounts/notifications.html @@ -21,7 +21,7 @@
@@ -29,20 +29,66 @@
-

Monthly Reports

{% csrf_token %} -
diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index dac09ab2..d79be8d7 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -22,7 +22,7 @@ diff --git a/templates/base.html b/templates/base.html index b8014b1b..e2d15bcc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -37,6 +37,8 @@ + + {% endcompress %} diff --git a/templates/emails/report-body-html.html b/templates/emails/report-body-html.html index 7e10c1ce..862fc420 100644 --- a/templates/emails/report-body-html.html +++ b/templates/emails/report-body-html.html @@ -3,15 +3,35 @@ {% block content %} 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-html.html" %} +{% if nag %} + Too many notifications? + Visit the Email Reports + page on {% site_name %} to set your notification preferences. +{% else %} Just one more thing to check: Do you have more cron jobs, not yet on this list, that would benefit from monitoring? Get the ball rolling by adding one more! +{% endif %}

Cheers,
@@ -22,6 +42,6 @@ The {% escaped_site_name %} Team {% block unsub %}
- Unsubscribe from Monthly Reports + Unsubscribe {% endblock %} diff --git a/templates/emails/report-body-text.html b/templates/emails/report-body-text.html index db51f1d8..8bfc9f7c 100644 --- a/templates/emails/report-body-text.html +++ b/templates/emails/report-body-text.html @@ -1,7 +1,10 @@ {% load hc_extras %} 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' %} diff --git a/templates/emails/report-subject.html b/templates/emails/report-subject.html index 743aaf34..9671b5ee 100644 --- a/templates/emails/report-subject.html +++ b/templates/emails/report-subject.html @@ -1,2 +1,6 @@ -Monthly Report +{% if nag %} + Reminder: {{ num_down }} check{{ num_down|pluralize }} still down +{% else %} + Monthly Report +{% endif %}