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:
parent
2b5345cf67
commit
488add5004
45
ietf/bin/send-milestone-reminders
Executable file
45
ietf/bin/send-milestone-reminders
Executable 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))
|
7
ietf/templates/wginfo/reminder_milestones_due.txt
Normal file
7
ietf/templates/wginfo/reminder_milestones_due.txt
Normal 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 %}
|
10
ietf/templates/wginfo/reminder_milestones_need_review.txt
Normal file
10
ietf/templates/wginfo/reminder_milestones_need_review.txt
Normal 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 %}
|
7
ietf/templates/wginfo/reminder_milestones_overdue.txt
Normal file
7
ietf/templates/wginfo/reminder_milestones_overdue.txt
Normal 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 %}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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]))
|
||||
|
|
Loading…
Reference in a new issue