Browse Source

Send monthly reports on 1st of every month, not randomly during the month

pull/307/head
Pēteris Caune 5 years ago
parent
commit
2bb769f7bb
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
6 changed files with 79 additions and 19 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +2
    -1
      hc/accounts/views.py
  3. +10
    -8
      hc/api/management/commands/sendreports.py
  4. +25
    -8
      hc/api/tests/test_sendreports.py
  5. +20
    -0
      hc/lib/date.py
  6. +21
    -2
      hc/lib/tests/test_date.py

+ 1
- 0
CHANGELOG.md View File

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


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

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


+ 10
- 8
hc/api/management/commands/sendreports.py View File

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


+ 25
- 8
hc/api/tests/test_sendreports.py View File

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


+ 20
- 0
hc/lib/date.py View File

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

+ 21
- 2
hc/lib/tests/test_date.py View File

@ -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…
Cancel
Save