Browse Source

Add an option for weekly reports (in addition to monthly)

pull/522/head
Pēteris Caune 4 years ago
parent
commit
df44ee58c0
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
13 changed files with 120 additions and 71 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +1
    -0
      hc/accounts/admin.py
  3. +2
    -0
      hc/accounts/forms.py
  4. +18
    -0
      hc/accounts/migrations/0037_profile_tz.py
  5. +28
    -1
      hc/accounts/models.py
  6. +28
    -11
      hc/accounts/tests/test_notifications.py
  7. +4
    -8
      hc/accounts/views.py
  8. +4
    -5
      hc/api/management/commands/sendreports.py
  9. +4
    -2
      hc/api/tests/test_sendreports.py
  10. +0
    -19
      hc/lib/date.py
  11. +2
    -21
      hc/lib/tests/test_date.py
  12. +3
    -0
      static/js/notifications.js
  13. +25
    -4
      templates/accounts/notifications.html

+ 1
- 0
CHANGELOG.md View File

@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
- Django 3.2.2
- Improve the handling of unknown email addresses in the Sign In form
- Add support for "... is UP" SMS notifications
- Add an option for weekly reports (in addition to monthly)
## v1.20.0 - 2020-04-22


+ 1
- 0
hc/accounts/admin.py View File

@ -44,6 +44,7 @@ class ProfileFieldset(Fieldset):
fields = (
"email",
"reports",
"tz",
"next_report_date",
"nag_period",
"next_nag_date",


+ 2
- 0
hc/accounts/forms.py View File

@ -8,6 +8,7 @@ from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from hc.accounts.models import REPORT_CHOICES
from hc.api.models import TokenBucket
from hc.front.validators import TimezoneValidator
class LowercaseEmailField(forms.EmailField):
@ -87,6 +88,7 @@ class PasswordLoginForm(forms.Form):
class ReportSettingsForm(forms.Form):
reports = forms.ChoiceField(choices=REPORT_CHOICES)
nag_period = forms.IntegerField(min_value=0, max_value=86400)
tz = forms.CharField(max_length=36, validators=[TimezoneValidator()])
def clean_nag_period(self):
seconds = self.cleaned_data["nag_period"]


+ 18
- 0
hc/accounts/migrations/0037_profile_tz.py View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2021-05-24 09:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0036_fill_profile_reports'),
]
operations = [
migrations.AddField(
model_name='profile',
name='tz',
field=models.CharField(default='UTC', max_length=36),
),
]

+ 28
- 1
hc/accounts/models.py View File

@ -1,4 +1,5 @@
from datetime import timedelta
import random
from secrets import token_urlsafe
from urllib.parse import quote, urlencode
import uuid
@ -14,7 +15,7 @@ from django.utils import timezone
from fido2.ctap2 import AttestedCredentialData
from hc.lib import emails
from hc.lib.date import month_boundaries
import pytz
NO_NAG = timedelta()
NAG_PERIODS = (
@ -71,6 +72,7 @@ class Profile(models.Model):
sort = models.CharField(max_length=20, default="created")
deletion_notice_date = models.DateTimeField(null=True, blank=True)
last_active_date = models.DateTimeField(null=True, blank=True)
tz = models.CharField(max_length=36, default="UTC")
objects = ProfileManager()
@ -283,6 +285,31 @@ class Profile(models.Model):
self.next_nag_date = None
self.save(update_fields=["next_nag_date"])
def choose_next_report_date(self):
""" Calculate the target date for the next monthly/weekly report.
Monthly reports should get sent on 1st of each month, between
9AM and 10AM in user's timezone.
Weekly reports should get sent on Mondays, between
9AM and 10AM in user's timezone.
"""
if self.reports == "off":
return None
tz = pytz.timezone(self.tz)
dt = timezone.now().astimezone(tz)
dt = dt.replace(hour=9, minute=random.randrange(0, 60))
while True:
dt += timedelta(days=1)
if self.reports == "monthly" and dt.day == 1:
return dt
elif self.reports == "weekly" and dt.weekday() == 0:
return dt
class Project(models.Model):
code = models.UUIDField(default=uuid.uuid4, unique=True)


+ 28
- 11
hc/accounts/tests/test_notifications.py View File

@ -6,6 +6,13 @@ from hc.test import BaseTestCase
class NotificationsTestCase(BaseTestCase):
url = "/accounts/profile/notifications/"
def _payload(self, **kwargs):
result = {"reports": "monthly", "nag_period": "0", "tz": "Europe/Riga"}
result.update(kwargs)
return result
def test_it_saves_reports_monthly(self):
self.profile.reports = "off"
self.profile.reports_allowed = False
@ -13,14 +20,28 @@ class NotificationsTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password")
form = {"reports": "monthly", "nag_period": "0"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload())
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertTrue(self.profile.reports_allowed)
self.assertEqual(self.profile.reports, "monthly")
self.assertIsNotNone(self.profile.next_report_date)
self.assertEqual(self.profile.next_report_date.day, 1)
def test_it_saves_reports_weekly(self):
self.profile.reports = "off"
self.profile.reports_allowed = False
self.profile.save()
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, self._payload(reports="weekly"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertTrue(self.profile.reports_allowed)
self.assertEqual(self.profile.reports, "weekly")
self.assertEqual(self.profile.next_report_date.weekday(), 0)
def test_it_saves_reports_off(self):
self.profile.reports_allowed = True
@ -30,8 +51,7 @@ class NotificationsTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password")
form = {"reports": "off", "nag_period": "0"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(reports="off"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
@ -44,8 +64,7 @@ class NotificationsTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password")
form = {"reports": "off", "nag_period": "3600"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(nag_period="3600"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
@ -58,8 +77,7 @@ class NotificationsTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password")
form = {"reports": "off", "nag_period": "3600"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(nag_period="3600"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
@ -72,8 +90,7 @@ class NotificationsTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password")
form = {"reports": "off", "nag_period": "1234"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(nag_period="1234"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()


+ 4
- 8
hc/accounts/views.py View File

@ -29,7 +29,6 @@ from hc.accounts import forms
from hc.accounts.decorators import require_sudo_mode
from hc.accounts.models import Credential, 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
POST_LOGIN_ROUTES = (
@ -447,13 +446,10 @@ def notifications(request):
if request.method == "POST":
form = forms.ReportSettingsForm(request.POST)
if form.is_valid():
if profile.reports != form.cleaned_data["reports"]:
profile.reports = form.cleaned_data["reports"]
profile.reports_allowed = profile.reports == "monthly"
if profile.reports_allowed:
profile.next_report_date = choose_next_report_date()
else:
profile.next_report_date = None
profile.reports = form.cleaned_data["reports"]
profile.tz = form.cleaned_data["tz"]
profile.next_report_date = profile.choose_next_report_date()
profile.reports_allowed = profile.reports != "off"
if profile.nag_period != form.cleaned_data["nag_period"]:
# Set the new nag period


+ 4
- 5
hc/api/management/commands/sendreports.py View File

@ -5,7 +5,6 @@ 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):
@ -19,7 +18,7 @@ class Command(BaseCommand):
tmpl = "Sent monthly report to %s"
def pause(self):
time.sleep(1)
time.sleep(3)
def add_arguments(self, parser):
parser.add_argument(
@ -35,7 +34,7 @@ class Command(BaseCommand):
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.exclude(reports="off")
profile = q.first()
if profile is None:
@ -50,10 +49,10 @@ class Command(BaseCommand):
# 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())
qq.update(next_report_date=profile.choose_next_report_date())
return True
num_updated = qq.update(next_report_date=choose_next_report_date())
num_updated = qq.update(next_report_date=profile.choose_next_report_date())
if num_updated != 1:
# next_report_date was already updated elsewhere, skipping
return True


+ 4
- 2
hc/api/tests/test_sendreports.py View File

@ -20,9 +20,11 @@ class SendReportsTestCase(BaseTestCase):
self.profile.save()
# Disable bob's and charlie's monthly reports so they don't interfere
self.bobs_profile.reports = "off"
self.bobs_profile.reports_allowed = False
self.bobs_profile.save()
self.charlies_profile.reports = "off"
self.charlies_profile.reports_allowed = False
self.charlies_profile.save()
@ -66,8 +68,8 @@ class SendReportsTestCase(BaseTestCase):
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
def test_it_obeys_reports_off(self):
self.profile.reports = "off"
self.profile.save()
found = Command().handle_one_monthly_report()


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

@ -85,22 +85,3 @@ 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)

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

@ -1,7 +1,7 @@
from datetime import datetime as dt, timedelta as td
from datetime import timedelta as td
from django.test import TestCase
from hc.lib.date import format_hms, choose_next_report_date
from hc.lib.date import format_hms
class DateFormattingTestCase(TestCase):
@ -28,22 +28,3 @@ 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)

+ 3
- 0
static/js/notifications.js View File

@ -0,0 +1,3 @@
$(function () {
$("#tz").val(Intl.DateTimeFormat().resolvedOptions().timeZone);
});

+ 25
- 4
templates/accounts/notifications.html View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load hc_extras %}
{% load compress hc_extras static tz %}
{% block title %}Account Settings - {{ site_name }}{% endblock %}
@ -31,7 +31,9 @@
{% csrf_token %}
<h2>Email Reports</h2>
<p>Send me periodic emails reports:</p>
<input id="tz" type="hidden" name="tz" value="{{ profile.tz }}" />
<p>Send me periodic email reports:</p>
<label class="radio-container">
<input
type="radio"
@ -41,6 +43,15 @@
<span class="radiomark"></span>
Do not send me email reports
</label>
<label class="radio-container">
<input
type="radio"
name="reports"
value="weekly"
{% if profile.reports == "weekly" %} checked {% endif %}>
<span class="radiomark"></span>
Weekly on Mondays
</label>
<label class="radio-container">
<input
type="radio"
@ -86,8 +97,10 @@
<p class="text-muted">
Reports will be delivered to {{ profile.user.email }}. <br />
{% if profile.next_report_date %}
Next monthly report date is
{{ profile.next_report_date.date }}.
{% timezone profile.tz %}
Next {{ profile.reports }} report date is
{{ profile.next_report_date|date:"F j, Y" }}.
{% endtimezone %}
{% endif %}
</p>
<br />
@ -105,3 +118,11 @@
</div>
</div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/notifications.js' %}"></script>
{% endcompress %}
{% endblock %}

Loading…
Cancel
Save