Added the ability to send reminders to complete questionnaires to nominees in the accepted state.

Changed the send_reminders management command to not take any arguments and run against all active nomcoms.
Removed bugs from the send_reminders management command and added tests for it.
Adjusted several dbtemplate titles and content.

Fixes bug 1157

Commit ready for merge
 - Legacy-Id: 6465
This commit is contained in:
Robert Sparks 2013-10-18 16:08:37 +00:00
parent 3814b60c5d
commit 22f356434a
9 changed files with 195 additions and 81 deletions

View file

@ -96,8 +96,8 @@ $position: Position</field>
</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object>
<object p="8" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/email/nomination_reminder.txt</field>
<object pk="8" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/email/nomination_accept_reminder.txt</field>
<field type="CharField" name="title">Email sent to nominees asking them to accept (or decline) the nominations.</field>
<field type="TextField" name="variables">$positions: Nomination positions</field>
<field to="name.dbtemplatetypename" name="type" rel="ManyToOneRel">plain</field>
@ -114,7 +114,7 @@ If you accept, you will need to fill out a questionnaire.
Best regards,</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object>
<object p="9" model="dbtemplate.dbtemplate">
<object pk="9" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/email/nomination_receipt.txt</field>
<field type="CharField" name="title">Email sent to nominator to get a confirmation mail containing feedback in cleartext</field>
<field type="TextField" name="variables">$nominee: Full name of the nominee
@ -137,7 +137,7 @@ $comments
Thank you,</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object>
<object p="10" model="dbtemplate.dbtemplate">
<object pk="10" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/email/feedback_receipt.txt</field>
<field type="CharField" name="title">Email sent to feedback author to get a confirmation mail containing feedback in cleartext</field>
<field type="TextField" name="variables">$nominee: Full name of the nominee
@ -158,4 +158,18 @@ $comments
Thank you,</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object>
<object pk="11" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/email/questionnaire_reminder.txt</field>
<field type="CharField" name="title">Email sent to nominees reminding them to complete a questionnaire</field>
<field type="TextField" name="variables">$positions: Nomination positions</field>
<field to="name.dbtemplatetypename" name="type" rel="ManyToOneRel">plain</field>
<field type="TextField" name="content">
Thank you for accepting your nomination for the position of $position.
Please remember to complete and return the questionnaire for this position at your earliest opportunity.
The questionaire is repeated below for your convenience.
--------</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object>
</django-objects>

View file

@ -1,48 +1,35 @@
import datetime
import syslog
from optparse import make_option
from django.core.management.base import BaseCommand
from ietf.nomcom.models import NomCom, NomineePosition
from nomcom.utils import send_accept_reminder_to_nominee,send_questionnaire_reminder_to_nominee
from django.core.management.base import BaseCommand, CommandError
from ietf.nomcom.models import Nominee, NomCom
from nomcom.utils import send_reminder_to_nominee
def log(message):
syslog.syslog(message)
def is_time_to_send(nomcom,send_date,nomination_date):
if nomcom.reminder_interval:
days_passed = (send_date - nomination_date).days
return days_passed > 0 and days_passed % nomcom.reminder_interval == 0
else:
return bool(nomcom.reminderdates_set.filter(date=send_date))
class Command(BaseCommand):
help = (u"Send reminders to nominees")
option_list = BaseCommand.option_list + (
make_option('--nomcom-year', dest='year', help='NomCom year'),)
help = (u"Send acceptance and questionnaire reminders to nominees")
def handle(self, *args, **options):
year = options.get('year', None)
help_message = 'Usage: send_reminders --nomcom-year <nomcom-year>'
if not year:
raise CommandError(help_message)
try:
nomcom = NomCom.objects.get(group__acronym__icontains=year,
group__state__slug='active')
except NomCom.DoesNotExist:
raise CommandError("NomCom %s does not exist or it isn't active" % year)
today = datetime.date.today()
if nomcom.reminder_interval:
nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().filter(nomineeposition__state='pending').distinct()
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.all():
nomination_date = nominee_position.time.date()
if not (today - nomination_date).days <= 0:
if (today - nomination_date).days % nomcom.reminder_interval == 0:
send_reminder_to_nominee(nominee_position)
syslog.syslog(u"Sent reminder to %s" % nominee_position.nominee.email.address)
print u"Sent reminder to %s" % nominee_position.nominee.email.address
else:
if nomcom.reminderdates_set.filter(date=today):
nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().filter(nomineeposition__state='pending').distinct()
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.pending():
send_reminder_to_nominee(nominee_position)
syslog.syslog(u"Sent reminder to %s" % nominee_position.nominee.email.address)
print u"Sent reminder (by dates) to %s" % nominee_position.nominee.email.address
for nomcom in NomCom.objects.filter(group__state__slug='active'):
for state in ('pending','accepted'):
for nominee_position in NomineePosition.objects.filter(nominee__nomcom=nomcom,
state=state,
nominee__duplicated__isnull=True):
if is_time_to_send(nomcom, datetime.date.today(), nominee_position.time.date()):
if state=='pending':
send_accept_reminder_to_nominee(nominee_position)
log('Sent accept reminder to %s' % nominee_position.nominee.email.address)
elif state=='accepted':
send_questionnaire_reminder_to_nominee(nominee_position)
log('Sent questionnaire reminder to %s' % nominee_position.nominee.email.address)
else:
pass

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import tempfile
import datetime
from ietf.utils import TestCase
from django.db import IntegrityError
@ -9,6 +10,7 @@ from django.core.files import File
from django.contrib.formtools.preview import security_hash
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.utils.mail import outbox
from ietf.person.models import Email, Person
@ -20,7 +22,8 @@ from ietf.nomcom.models import NomineePosition, Position, Nominee, \
NomineePositionState, Feedback, FeedbackType, \
Nomination
from ietf.nomcom.forms import EditChairForm, EditMembersForm
from ietf.nomcom.utils import get_nomcom_by_year
from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee
from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send
class NomcomViewsTest(TestCase):
@ -653,3 +656,63 @@ class FeedbackTest(TestCase):
os.unlink(self.privatekey_file.name)
os.unlink(self.cert_file.name)
class ReminderCommandTest(TestCase):
perma_fixtures = ['names', 'nomcom_templates']
def setUp(self):
nomcom_test_data()
self.nomcom = get_nomcom_by_year(NOMCOM_YEAR)
gen = Position.objects.get(nomcom=self.nomcom,name='GEN')
rai = Position.objects.get(nomcom=self.nomcom,name='RAI')
iab = Position.objects.get(nomcom=self.nomcom,name='IAB')
today = datetime.date.today()
t_minus_3 = today - datetime.timedelta(days=3)
t_minus_4 = today - datetime.timedelta(days=4)
n = get_or_create_nominee(self.nomcom,"Nominee 1","nominee1@example.org",gen,None)
np = n.nomineeposition_set.get(position=gen)
np.time = t_minus_3
np.save()
n = get_or_create_nominee(self.nomcom,"Nominee 1","nominee1@example.org",iab,None)
np = n.nomineeposition_set.get(position=iab)
np.state = NomineePositionState.objects.get(slug='accepted')
np.time = t_minus_3
np.save()
n = get_or_create_nominee(self.nomcom,"Nominee 2","nominee2@example.org",rai,None)
np = n.nomineeposition_set.get(position=rai)
np.time = t_minus_4
np.save()
def test_is_time_to_send(self):
self.nomcom.reminder_interval = 4
today = datetime.date.today()
self.assertTrue(is_time_to_send(self.nomcom,today+datetime.timedelta(days=4),today))
for delta in range(4):
self.assertFalse(is_time_to_send(self.nomcom,today+datetime.timedelta(days=delta),today))
self.nomcom.reminder_interval = None
self.assertFalse(is_time_to_send(self.nomcom,today,today))
self.nomcom.reminderdates_set.create(date=today)
self.assertTrue(is_time_to_send(self.nomcom,today,today))
def test_command(self):
c = Command()
messages_before=len(outbox)
self.nomcom.reminder_interval = 3
self.nomcom.save()
c.handle(None,None)
self.assertEqual(len(outbox), messages_before + 2)
self.assertTrue('nominee1@example.org' in outbox[-1]['To'])
self.assertTrue('please complete' in outbox[-1]['Subject'])
self.assertTrue('nominee1@example.org' in outbox[-2]['To'])
self.assertTrue('please accept' in outbox[-2]['Subject'])
messages_before=len(outbox)
self.nomcom.reminder_interval = 4
self.nomcom.save()
c.handle(None,None)
self.assertEqual(len(outbox), messages_before + 1)
self.assertTrue('nominee2@example.org' in outbox[-1]['To'])
self.assertTrue('please accept' in outbox[-1]['Subject'])

View file

@ -1,5 +1,6 @@
from django.conf.urls.defaults import patterns, url
from django.views.generic.simple import direct_to_template
from django.views.generic.simple import direct_to_template, redirect_to
from ietf.utils.lazy import reverse_lazy
from ietf.nomcom.forms import EditChairForm, EditChairFormPreview, \
EditMembersForm, EditMembersFormPreview
@ -17,7 +18,8 @@ urlpatterns = patterns('ietf.nomcom.views',
url(r'^(?P<year>\d{4})/private/view-feedback/nominee/(?P<nominee_id>\d+)$', 'view_feedback_nominee', name='nomcom_view_feedback_nominee'),
url(r'^(?P<year>\d{4})/private/edit/nominee/(?P<nominee_id>\d+)$', 'edit_nominee', name='nomcom_edit_nominee'),
url(r'^(?P<year>\d{4})/private/merge/$', 'private_merge', name='nomcom_private_merge'),
url(r'^(?P<year>\d{4})/private/send-reminder-mail/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'),
# url(r'^(?P<year>\d{4})/private/send-reminder-mail/$', redirect_to, { 'url': reverse_lazy('nomcom_send_reminder_mail',kwargs={'year':year,'type':'accept'})}),
url(r'^(?P<year>\d{4})/private/send-reminder-mail/(?P<type>\w+)/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'),
url(r'^(?P<year>\d{4})/private/edit-members/$', EditMembersFormPreview(EditMembersForm), name='nomcom_edit_members'),
url(r'^(?P<year>\d{4})/private/edit-chair/$', EditChairFormPreview(EditChairForm), name='nomcom_edit_chair'),
url(r'^(?P<year>\d{4})/private/edit-nomcom/$', 'edit_nomcom', name='nomcom_edit_nomcom'),

View file

@ -32,7 +32,8 @@ HOME_TEMPLATE = 'home.rst'
INEXISTENT_PERSON_TEMPLATE = 'email/inexistent_person.txt'
NOMINEE_EMAIL_TEMPLATE = 'email/new_nominee.txt'
NOMINATION_EMAIL_TEMPLATE = 'email/new_nomination.txt'
NOMINEE_REMINDER_TEMPLATE = 'email/nomination_reminder.txt'
NOMINEE_ACCEPT_REMINDER_TEMPLATE = 'email/nomination_accept_reminder.txt'
NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE = 'email/questionnaire_reminder.txt'
NOMINATION_RECEIPT_TEMPLATE = 'email/nomination_receipt.txt'
FEEDBACK_RECEIPT_TEMPLATE = 'email/feedback_receipt.txt'
@ -40,7 +41,8 @@ DEFAULT_NOMCOM_TEMPLATES = [HOME_TEMPLATE,
INEXISTENT_PERSON_TEMPLATE,
NOMINEE_EMAIL_TEMPLATE,
NOMINATION_EMAIL_TEMPLATE,
NOMINEE_REMINDER_TEMPLATE,
NOMINEE_ACCEPT_REMINDER_TEMPLATE,
NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE,
NOMINATION_RECEIPT_TEMPLATE,
FEEDBACK_RECEIPT_TEMPLATE]
@ -107,7 +109,7 @@ def initialize_questionnaire_for_position(position):
content=header_template.content)
questionnaire = DBTemplate.objects.create(
group=position.nomcom.group,
title=template.title + '[%s]' % position.name,
title=template.title + ' [%s]' % position.name,
path='/nomcom/' + position.nomcom.group.acronym + '/' + str(position.id) + '/' + QUESTIONNAIRE_TEMPLATE,
variables=template.variables,
type_id=template.type_id,
@ -191,15 +193,15 @@ def validate_public_key(public_key):
return (not error, error)
def send_reminder_to_nominee(nominee_position):
def send_accept_reminder_to_nominee(nominee_position):
today = datetime.date.today().strftime('%Y%m%d')
subject = 'IETF Nomination Information'
subject = 'Reminder: please accept (or decline) your nomination.'
from_email = settings.NOMCOM_FROM_EMAIL
domain = Site.objects.get_current().domain
position = nominee_position.position
nomcom = position.nomcom
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_REMINDER_TEMPLATE
mail_path = nomcom_template_path + NOMINEE_ACCEPT_REMINDER_TEMPLATE
nominee = nominee_position.nominee
to_email = nominee.email.address
@ -229,11 +231,42 @@ def send_reminder_to_nominee(nominee_position):
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body)
def send_questionnaire_reminder_to_nominee(nominee_position):
today = datetime.date.today().strftime('%Y%m%d')
subject = 'Reminder: please complete the Nomcom questionnaires for your nomination.'
from_email = settings.NOMCOM_FROM_EMAIL
domain = Site.objects.get_current().domain
position = nominee_position.position
nomcom = position.nomcom
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE
nominee = nominee_position.nominee
to_email = nominee.email.address
def send_reminder_to_nominees(nominees):
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.pending():
send_reminder_to_nominee(nominee_position)
hash = get_hash_nominee_position(today, nominee_position.id)
context = {'nominee': nominee,
'position': position,
'domain': domain,
}
body = render_to_string(mail_path, context)
path = '%s%d/%s' % (nomcom_template_path, position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body)
def send_reminder_to_nominees(nominees,type):
addrs = []
if type=='accept':
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.pending():
send_accept_reminder_to_nominee(nominee_position)
addrs.append(nominee_position.nominee.email.address)
elif type=='questionnaire':
for nominee in nominees:
for nominee_position in nominee.nomineeposition_set.accepted():
send_questionnaire_reminder_to_nominee(nominee_position)
addrs.append(nominee_position.nominee.email.address)
return addrs
def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, author):

View file

@ -31,11 +31,9 @@ from ietf.nomcom.forms import (NominateForm, FeedbackForm, QuestionnaireForm,
from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates
from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key,
get_hash_nominee_position, send_reminder_to_nominees,
HOME_TEMPLATE, NOMINEE_REMINDER_TEMPLATE)
HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE)
from ietf.ietfauth.utils import role_required
import debug
def index(request):
nomcom_list = Group.objects.filter(type__slug='nomcom').order_by('acronym')
for nomcom in nomcom_list:
@ -166,11 +164,27 @@ def private_index(request, year):
@role_required("Nomcom Chair", "Nomcom Advisor")
def send_reminder_mail(request, year):
def send_reminder_mail(request, year, type):
nomcom = get_nomcom_by_year(year)
nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().filter(nomineeposition__state='pending').distinct()
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_REMINDER_TEMPLATE
if type=='accept':
interesting_state = 'pending'
mail_path = nomcom_template_path + NOMINEE_ACCEPT_REMINDER_TEMPLATE
reminder_description = 'accept (or decline) a nomination'
selected_tab = 'send_accept_reminder'
elif type=='questionnaire':
interesting_state = 'accepted'
mail_path = nomcom_template_path + NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE
reminder_description = 'complete the questionnaire for a nominated position'
selected_tab = 'send_questionnaire_reminder'
else:
raise Http404
nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().filter(nomineeposition__state=interesting_state).distinct()
annotated_nominees = list(nominees)
for nominee in annotated_nominees:
nominee.interesting_positions = [x.position.name for x in nominee.nomineeposition_set.all() if x.state.slug==interesting_state]
mail_template = DBTemplate.objects.filter(group=nomcom.group, path=mail_path)
mail_template = mail_template and mail_template[0] or None
message = None
@ -179,16 +193,21 @@ def send_reminder_mail(request, year):
selected_nominees = request.POST.getlist('selected')
selected_nominees = nominees.filter(id__in=selected_nominees)
if selected_nominees:
send_reminder_to_nominees(selected_nominees)
message = ('success', 'An query has been sent to each person, asking them to accept (or decline) the nominations')
addrs = send_reminder_to_nominees(selected_nominees,type)
if addrs:
message = ('success', 'A copy of "%s" has been sent to %s'%(mail_template.title,", ".join(addrs)))
else:
message = {'warning', 'No messages were sent.'}
else:
message = ('warning', "Please, select some nominee")
message = ('warning', "Please, select at least one nominee")
return render_to_response('nomcom/send_reminder_mail.html',
{'nomcom': nomcom,
'year': year,
'nominees': nominees,
'nominees': annotated_nominees,
'mail_template': mail_template,
'selected': 'send_reminder_mail',
'selected': selected_tab,
'reminder_description': reminder_description,
'state_description': NomineePositionState.objects.get(slug=interesting_state).name,
'message': message}, RequestContext(request))

View file

@ -19,7 +19,8 @@
{% if user|is_chair:year %} |
{% if selected == "feedback_pending" %}<span class="selected">Pending Feedback</span>{% else %}<a href="{% url nomcom_view_feedback_pending year %}">Pending Feedback</a>{% endif %} |
{% if selected == "feedback_email" %}<span class="selected">Enter Email Feedback</span>{% else %}<a href="{% url nomcom_private_feedback_email year %}">Enter Email Feedback</a>{% endif %} |
{% if selected == "send_reminder_mail" %}<span class="selected">Send Reminder</span>{% else %}<a href="{% url nomcom_send_reminder_mail year %}">Send Reminder</a>{% endif %}
{% if selected == "send_accept_reminder" %}<span class="selected">Send Accept Reminder</span>{% else %}<a href="{% url nomcom_send_reminder_mail year "accept" %}">Send Accept Reminder</a>{% endif %} |
{% if selected == "send_questionnaire_reminder" %}<span class="selected">Send Questionnaire Reminder</span>{% else %}<a href="{% url nomcom_send_reminder_mail year "questionnaire" %}">Send Questionnaire Reminder</a>{% endif %}
{% endif %}
</div>
{% if user|is_chair:year %}

View file

@ -33,8 +33,6 @@
<h2>List of nominees by position</h2>
<p>The following is a list of registered nominees.
{% if is_chair %}(You can <a href="{% url nomcom_send_reminder_mail year %}">request confirmation</a> from nominees if they haven't
replied to the nomination notification they have received.){% endif %}</p>
<div class="content">

View file

@ -1,18 +1,15 @@
{% extends "nomcom/nomcom_private_base.html" %}
{% load ietf_filters %}
{% block subtitle %} - Send reminder emails {% endblock %}
{% block subtitle %} - Send reminder messages {% endblock %}
{% block nomcom_content %}
<h2>Request nomination acceptance from the listed nominees</h2>
<h2>Send a message to the selected nominees reminding them to {{reminder_description}}</h2>
<p>
An query will be sent to each person, asking them to accept (or decline) the nomination.
<p>These are the nominees that are in the '{{state_description}}' state for the listed positions. </p>
The list has been pre-populated with the selected list of nominees
The message that will be sent is shown below the address entry form.
</p>
<p>The message that will be sent is shown below the list of nominees. </p>
<form method="post" action="">
<h3>Nominees who have not responded</h2>
@ -31,7 +28,7 @@ The message that will be sent is shown below the address entry form.
<tr class="{{ forloop.counter|divisibleby:2|yesno:"oddrow,evenrow" }}">
<td><input class="batch-select" type="checkbox" value="{{ nominee.id }}" name="selected" checked="checked"/></td>
<td>{{ nominee }}</td>
<td>{% for nominee_position in nominee.nomineeposition_set.all %}{% ifequal nominee_position.state.slug "pending" %} {{ nominee_position.position.name }}, {% endifequal %}{% endfor %}</td>
<td>{{nominee.interesting_positions|join:", "}}</td>
</tr>
{% endfor %}
</table>
@ -46,6 +43,6 @@ The message that will be sent is shown below the address entry form.
<p>The message that will be sent is as follows: {% if mail_template %}<a href="{% url nomcom_edit_template year mail_template.id %}">(Edit the message)</a>{% endif %}</p>
{% include mail_template.path %}
<pre>{{ mail_template.content|wrap_text:80|escape }}</pre>
{% endblock %}
{% endblock %}