Add script and code (and tests) to send milestone reminders to ADs

(for reviews) and group chairs.
 - Legacy-Id: 4557
This commit is contained in:
Ole Laursen 2012-06-30 16:46:31 +00:00
parent 2b5345cf67
commit 488add5004
7 changed files with 293 additions and 3 deletions

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
#
# This script will send various milestone reminders. It's supposed to
# be run daily, and will then send reminders weekly/monthly as
# appropriate.
import datetime, os
import syslog
from ietf import settings
from django.core import management
management.setup_environ(settings)
syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0)
from ietf.wginfo.mails import *
today = datetime.date.today()
MONDAY = 1
FIRST_DAY_OF_MONTH = 1
if today.isoweekday() == MONDAY:
# send milestone review reminders - ideally we'd keep track of
# exactly when we sent one last time for a group, but it's a bit
# complicated because people can change the milestones in the mean
# time, so dodge all of this by simply sending once a week only
for g in groups_with_milestones_needing_review():
mail_sent = email_milestone_review_reminder(g, grace_period=7)
if mail_sent:
syslog.syslog("Sent milestone review reminder for %s %s" % (g.acronym, g.type.name))
early_warning_days = 30
# send any milestones due reminders
for g in groups_needing_milestones_due_reminder(early_warning_days):
email_milestones_due(g, early_warning_days)
syslog.syslog("Sent milestones due reminder for %s %s" % (g.acronym, g.type.name))
if today.day == FIRST_DAY_OF_MONTH:
# send milestone overdue reminders - once a month
for g in groups_needing_milestones_overdue_reminder(grace_period=30):
email_milestones_overdue(g)
syslog.syslog("Sent milestones overdue reminder for %s %s" % (g.acronym, g.type.name))

View file

@ -0,0 +1,7 @@
{% autoescape off %}{% filter wordwrap:73 %}This is a reminder that milestones in "{{ group.name }}" are soon due.
{% for m in milestones %}"{{ m.desc }}" is due {% if m.due == today %}today!{% else %}in {{ early_warning_days }} days.{% endif %}
{% endfor %}
URL: {{ url }}
{% endfilter %}{% endautoescape %}

View file

@ -0,0 +1,10 @@
{% autoescape off %}{% filter wordwrap:73 %}{{ milestones|length }} new milestone{{ milestones|pluralize }} in "{{ group.name }}" {% if milestones|length > 1 %}need{% else %}needs{%endif %} an AD review:
{% for m in milestones %}"{{ m.desc }}"{% if m.days_ready != None %}
Waiting for {{ m.days_ready }} day{{ m.days_ready|pluralize }}.{% endif %}
{% endfor %}
Go here to either accept or reject the new milestones:
{{ url }}
{% endfilter %}{% endautoescape %}

View file

@ -0,0 +1,7 @@
{% autoescape off %}{% filter wordwrap:73 %}This is a reminder that milestones in "{{ group.name }}" are overdue.
{% for m in milestones %}"{{ m.desc }}" is overdue{% if m.months_overdue > 0 %} with {{ m.months_overdue }} month{{ m.months_overdue|pluralize }}{% endif %}!
{% endfor %}
URL: {{ url }}
{% endfilter %}{% endautoescape %}

View file

@ -147,6 +147,9 @@ def make_test_data():
person=p,
email=email)
mars_wg.ad = ad
mars_wg.save()
# create a bunch of ads for swarm tests
for i in range(1, 10):
u = User.objects.create(username="ad%s" % i)

View file

@ -10,6 +10,8 @@ from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import send_mail, send_mail_text
from ietf.group.models import *
def email_milestones_changed(request, group, text):
to = []
if group.ad:
@ -20,8 +22,96 @@ def email_milestones_changed(request, group, text):
text = wrap(strip_tags(text), 70)
text += "\n\n"
text += "URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym)))
text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym)))
send_mail_text(request, to, None,
"Milestones changed for %s %s" % (group.acronym, group.type.name),
u"Milestones changed for %s %s" % (group.acronym, group.type.name),
text)
def email_milestone_review_reminder(group, grace_period=7):
"""Email reminders about milestones needing review to AD."""
if not group.ad:
return False
to = [group.ad.role_email("ad").formatted_email()]
cc = [r.formatted_email() for r in group.role_set.filter(name="chair")]
now = datetime.datetime.now()
too_early = True
milestones = group.groupmilestone_set.filter(state="review")
for m in milestones:
e = m.milestonegroupevent_set.filter(type="changed_milestone").order_by("-time")[:1]
m.days_ready = (now - e[0].time).days if e else None
if m.days_ready == None or m.days_ready >= grace_period:
too_early = False
if too_early:
return False
subject = u"Reminder: Milestone%s needing review in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name)
send_mail(None, to, None,
subject,
"wginfo/reminder_milestones_need_review.txt",
dict(group=group,
milestones=milestones,
url=settings.IDTRACKER_BASE_URL + urlreverse("wg_edit_milestones", kwargs=dict(acronym=group.acronym))
))
return True
def groups_with_milestones_needing_review():
return Group.objects.filter(groupmilestone__state="review").distinct()
def email_milestones_due(group, early_warning_days):
to = [r.formatted_email() for r in group.role_set.filter(name="chair")]
today = datetime.date.today()
early_warning = today + datetime.timedelta(days=early_warning_days)
milestones = group.groupmilestone_set.filter(due__in=[today, early_warning],
resolved="", state="active")
subject = u"Reminder: Milestone%s are soon due in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name)
send_mail(None, to, None,
subject,
"wginfo/reminder_milestones_due.txt",
dict(group=group,
milestones=milestones,
today=today,
early_warning_days=early_warning_days,
url=settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))
))
def groups_needing_milestones_due_reminder(early_warning_days):
"""Return groups having milestones that are either
early_warning_days from being due or are due today."""
today = datetime.date.today()
return Group.objects.filter(state="active", groupmilestone__due__in=[today, today + datetime.timedelta(days=early_warning_days)], groupmilestone__resolved="", groupmilestone__state="active").distinct()
def email_milestones_overdue(group):
to = [r.formatted_email() for r in group.role_set.filter(name="chair")]
today = datetime.date.today()
milestones = group.groupmilestone_set.filter(due__lt=today, resolved="", state="active")
for m in milestones:
m.months_overdue = (today - m.due).days // 30
subject = u"Reminder: Milestone%s overdue in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name)
send_mail(None, to, None,
subject,
"wginfo/reminder_milestones_overdue.txt",
dict(group=group,
milestones=milestones,
url=settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))
))
def groups_needing_milestones_overdue_reminder(grace_period=30):
cut_off = datetime.date.today() - datetime.timedelta(days=grace_period)
return Group.objects.filter(state="active", groupmilestone__due__lt=cut_off, groupmilestone__resolved="", groupmilestone__state="active").distinct()

View file

@ -47,6 +47,7 @@ from ietf.group.models import *
from ietf.group.utils import *
from ietf.name.models import *
from ietf.person.models import *
from ietf.wginfo.mails import *
class WgInfoUrlTestCase(SimpleUrlTestCase):
@ -404,7 +405,7 @@ class MilestoneTestCase(django.test.TestCase):
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEquals(m.state_id, "active")
self.assertEquals(group.groupevent_set.count(), events_before + 1)
self.assertTrue("from review to active" in m.milestonegroupevent_set.all()[0].desc)
self.assertTrue("to active from review" in m.milestonegroupevent_set.all()[0].desc)
def test_delete_milestone(self):
m1, m2, group = self.create_test_milestones()
@ -508,3 +509,130 @@ class MilestoneTestCase(django.test.TestCase):
self.assertEquals(GroupMilestone.objects.filter(due=m1.due, desc=m1.desc, state="charter").count(), 1)
self.assertEquals(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add
def test_send_review_needed_reminders(self):
draft = make_test_data()
group = Group.objects.get(acronym="mars")
person = Person.objects.get(user__username="marschairman")
m1 = GroupMilestone.objects.create(group=group,
desc="Test 1",
due=datetime.date.today(),
resolved="",
state_id="review")
MilestoneGroupEvent.objects.create(
group=group, type="changed_milestone",
by=person, desc='Added milestone "%s"' % m1.desc, milestone=m1,
time=datetime.datetime.now() - datetime.timedelta(seconds=60))
# send
mailbox_before = len(outbox)
for g in groups_with_milestones_needing_review():
email_milestone_review_reminder(g)
self.assertEquals(len(outbox), mailbox_before) # too early to send reminder
# add earlier added milestone
m2 = GroupMilestone.objects.create(group=group,
desc="Test 2",
due=datetime.date.today(),
resolved="",
state_id="review")
MilestoneGroupEvent.objects.create(
group=group, type="changed_milestone",
by=person, desc='Added milestone "%s"' % m2.desc, milestone=m2,
time=datetime.datetime.now() - datetime.timedelta(days=10))
# send
mailbox_before = len(outbox)
for g in groups_with_milestones_needing_review():
email_milestone_review_reminder(g)
self.assertEquals(len(outbox), mailbox_before + 1)
self.assertTrue(group.acronym in outbox[-1]["Subject"])
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
def test_send_milestones_due_reminders(self):
draft = make_test_data()
group = Group.objects.get(acronym="mars")
person = Person.objects.get(user__username="marschairman")
early_warning_days = 30
m1 = GroupMilestone.objects.create(group=group,
desc="Test 1",
due=datetime.date.today(),
resolved="Done",
state_id="active")
m2 = GroupMilestone.objects.create(group=group,
desc="Test 2",
due=datetime.date.today() + datetime.timedelta(days=early_warning_days - 10),
resolved="",
state_id="active")
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_due_reminder(early_warning_days):
email_milestones_due(g, early_warning_days)
self.assertEquals(len(outbox), mailbox_before) # none found
m1.resolved = ""
m1.save()
m2.due = datetime.date.today() + datetime.timedelta(days=early_warning_days)
m2.save()
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_due_reminder(early_warning_days):
email_milestones_due(g, early_warning_days)
self.assertEquals(len(outbox), mailbox_before + 1)
self.assertTrue(group.acronym in outbox[-1]["Subject"])
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
def test_send_milestones_overdue_reminders(self):
draft = make_test_data()
group = Group.objects.get(acronym="mars")
person = Person.objects.get(user__username="marschairman")
m1 = GroupMilestone.objects.create(group=group,
desc="Test 1",
due=datetime.date.today() - datetime.timedelta(days=200),
resolved="Done",
state_id="active")
m2 = GroupMilestone.objects.create(group=group,
desc="Test 2",
due=datetime.date.today() - datetime.timedelta(days=10),
resolved="",
state_id="active")
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_overdue_reminder(grace_period=30):
email_milestones_overdue(g)
self.assertEquals(len(outbox), mailbox_before) # none found
m1.resolved = ""
m1.save()
m2.due = datetime.date.today() - datetime.timedelta(days=300)
m2.save()
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_overdue_reminder(grace_period=30):
email_milestones_overdue(g)
self.assertEquals(len(outbox), mailbox_before + 1)
self.assertTrue(group.acronym in outbox[-1]["Subject"])
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))