forked from GithubBackups/healthchecks
Send monthly reports on 1st of every month, not randomly during the month
This commit is contained in:
parent
391921d8af
commit
2bb769f7bb
@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Add "last_duration" attribute to the Check API resource (#257)
|
||||
- Upgrade to psycopg2 2.8.3
|
||||
- Add Go usage example
|
||||
- Send monthly reports on 1st of every month, not randomly during the month
|
||||
|
||||
### Bug Fixes
|
||||
- Prevent double-clicking the submit button in signup form
|
||||
|
@ -32,6 +32,7 @@ from hc.accounts.forms import (
|
||||
)
|
||||
from hc.accounts.models import Profile, Project, Member
|
||||
from hc.api.models import Channel, Check, TokenBucket
|
||||
from hc.lib.date import choose_next_report_date
|
||||
from hc.payments.models import Subscription
|
||||
|
||||
NEXT_WHITELIST = (
|
||||
@ -355,7 +356,7 @@ def notifications(request):
|
||||
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)
|
||||
profile.next_report_date = choose_next_report_date()
|
||||
else:
|
||||
profile.next_report_date = None
|
||||
|
||||
|
@ -6,6 +6,7 @@ from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from hc.accounts.models import NO_NAG, Profile
|
||||
from hc.api.models import Check
|
||||
from hc.lib.date import choose_next_report_date
|
||||
|
||||
|
||||
def num_pinged_checks(profile):
|
||||
@ -31,28 +32,29 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle_one_monthly_report(self):
|
||||
now = timezone.now()
|
||||
month_before = now - timedelta(days=30)
|
||||
month_after = now + timedelta(days=30)
|
||||
|
||||
report_due = Q(next_report_date__lt=now)
|
||||
report_due = Q(next_report_date__lt=timezone.now())
|
||||
report_not_scheduled = Q(next_report_date__isnull=True)
|
||||
|
||||
q = Profile.objects.filter(report_due | report_not_scheduled)
|
||||
q = q.filter(reports_allowed=True)
|
||||
q = q.filter(user__date_joined__lt=month_before)
|
||||
profile = q.first()
|
||||
|
||||
if profile is None:
|
||||
# No matching profiles found – nothing to do right now.
|
||||
return False
|
||||
|
||||
# A sort of optimistic lock. Try to update next_report_date,
|
||||
# A sort of optimistic lock. Will 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
|
||||
)
|
||||
|
||||
num_updated = qq.update(next_report_date=month_after)
|
||||
# Next report date is currently not scheduled: schedule it and move on.
|
||||
if profile.next_report_date is None:
|
||||
qq.update(next_report_date=choose_next_report_date())
|
||||
return True
|
||||
|
||||
num_updated = qq.update(next_report_date=choose_next_report_date())
|
||||
if num_updated != 1:
|
||||
# next_report_date was already updated elsewhere, skipping
|
||||
return True
|
||||
|
@ -8,20 +8,24 @@ from hc.test import BaseTestCase
|
||||
from mock import Mock
|
||||
|
||||
|
||||
class SendAlertsTestCase(BaseTestCase):
|
||||
class SendReportsTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(SendAlertsTestCase, self).setUp()
|
||||
super(SendReportsTestCase, self).setUp()
|
||||
|
||||
# Make alice eligible for reports:
|
||||
# account needs to be more than one month old
|
||||
self.alice.date_joined = now() - td(days=365)
|
||||
self.alice.save()
|
||||
|
||||
# Make alice eligible for nags:
|
||||
# Make alice eligible for a monthly report:
|
||||
self.profile.next_report_date = now() - td(hours=1)
|
||||
# and for a nag
|
||||
self.profile.nag_period = td(hours=1)
|
||||
self.profile.next_nag_date = now() - td(seconds=10)
|
||||
self.profile.save()
|
||||
|
||||
# Disable bob's and charlie's monthly reports so they don't interfere
|
||||
self.bobs_profile.reports_allowed = False
|
||||
self.bobs_profile.save()
|
||||
|
||||
self.charlies_profile.reports_allowed = False
|
||||
self.charlies_profile.save()
|
||||
|
||||
# And it needs at least one check that has been pinged.
|
||||
self.check = Check(project=self.project, last_ping=now())
|
||||
self.check.status = "down"
|
||||
@ -37,6 +41,7 @@ class SendAlertsTestCase(BaseTestCase):
|
||||
|
||||
self.profile.refresh_from_db()
|
||||
self.assertTrue(self.profile.next_report_date > now())
|
||||
self.assertEqual(self.profile.next_report_date.day, 1)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
|
||||
email = mail.outbox[0]
|
||||
@ -49,6 +54,18 @@ class SendAlertsTestCase(BaseTestCase):
|
||||
found = Command().handle_one_monthly_report()
|
||||
self.assertFalse(found)
|
||||
|
||||
def test_it_fills_blank_next_report_date(self):
|
||||
self.profile.next_report_date = None
|
||||
self.profile.save()
|
||||
|
||||
found = Command().handle_one_monthly_report()
|
||||
self.assertTrue(found)
|
||||
|
||||
self.profile.refresh_from_db()
|
||||
self.assertTrue(self.profile.next_report_date)
|
||||
self.assertEqual(self.profile.next_report_date.day, 1)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_it_obeys_reports_allowed_flag(self):
|
||||
self.profile.reports_allowed = False
|
||||
self.profile.save()
|
||||
|
@ -1,4 +1,5 @@
|
||||
from datetime import datetime as dt
|
||||
from random import randint
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@ -81,3 +82,22 @@ def month_boundaries(months=2):
|
||||
y = y - 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def choose_next_report_date(now=None):
|
||||
""" Calculate the target date for the next monthly report.
|
||||
|
||||
Monthly reports should get sent on 1st of each month, at a random
|
||||
time after 12PM UTC (so it's over the month boundary even in UTC-12).
|
||||
|
||||
"""
|
||||
|
||||
if now is None:
|
||||
now = timezone.now()
|
||||
|
||||
h, m, s = randint(12, 23), randint(0, 59), randint(0, 59)
|
||||
|
||||
if now.month == 12:
|
||||
return now.replace(now.year + 1, 1, 1, h, m, s)
|
||||
else:
|
||||
return now.replace(now.year, now.month + 1, 1, h, m, s)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from datetime import timedelta as td
|
||||
from datetime import datetime as dt, timedelta as td
|
||||
from django.test import TestCase
|
||||
|
||||
from hc.lib.date import format_hms
|
||||
from hc.lib.date import format_hms, choose_next_report_date
|
||||
|
||||
|
||||
class DateFormattingTestCase(TestCase):
|
||||
@ -24,3 +24,22 @@ class DateFormattingTestCase(TestCase):
|
||||
|
||||
s = format_hms(td(seconds=60 * 60))
|
||||
self.assertEqual(s, "1 h 0 min 0 sec")
|
||||
|
||||
|
||||
class NextReportDateTestCase(TestCase):
|
||||
def test_it_works(self):
|
||||
# October
|
||||
nao = dt(year=2019, month=10, day=15, hour=6)
|
||||
result = choose_next_report_date(nao)
|
||||
self.assertEqual(result.year, 2019)
|
||||
self.assertEqual(result.month, 11)
|
||||
self.assertEqual(result.day, 1)
|
||||
self.assertTrue(result.hour >= 12)
|
||||
|
||||
# December
|
||||
nao = dt(year=2019, month=12, day=15, hour=6)
|
||||
result = choose_next_report_date(nao)
|
||||
self.assertEqual(result.year, 2020)
|
||||
self.assertEqual(result.month, 1)
|
||||
self.assertEqual(result.day, 1)
|
||||
self.assertTrue(result.hour >= 12)
|
||||
|
Loading…
x
Reference in New Issue
Block a user