diff --git a/ietf/dbtemplate/fixtures/nomcom_templates.xml b/ietf/dbtemplate/fixtures/nomcom_templates.xml index bd15def85..1059d7073 100644 --- a/ietf/dbtemplate/fixtures/nomcom_templates.xml +++ b/ietf/dbtemplate/fixtures/nomcom_templates.xml @@ -96,8 +96,8 @@ $position: Position - - /nomcom/defaults/email/nomination_reminder.txt + + /nomcom/defaults/email/nomination_accept_reminder.txt Email sent to nominees asking them to accept (or decline) the nominations. $positions: Nomination positions plain @@ -114,7 +114,7 @@ If you accept, you will need to fill out a questionnaire. Best regards, - + /nomcom/defaults/email/nomination_receipt.txt Email sent to nominator to get a confirmation mail containing feedback in cleartext $nominee: Full name of the nominee @@ -137,7 +137,7 @@ $comments Thank you, - + /nomcom/defaults/email/feedback_receipt.txt Email sent to feedback author to get a confirmation mail containing feedback in cleartext $nominee: Full name of the nominee @@ -158,4 +158,18 @@ $comments Thank you, + + /nomcom/defaults/email/questionnaire_reminder.txt + Email sent to nominees reminding them to complete a questionnaire + $positions: Nomination positions + plain + +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. + +-------- + + diff --git a/ietf/nomcom/management/commands/send_reminders.py b/ietf/nomcom/management/commands/send_reminders.py index 8a0d55529..7d1350bc5 100644 --- a/ietf/nomcom/management/commands/send_reminders.py +++ b/ietf/nomcom/management/commands/send_reminders.py @@ -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 ' - - 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 diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index f5f9f4428..3a10ad102 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -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']) + + diff --git a/ietf/nomcom/urls.py b/ietf/nomcom/urls.py index 8e3ccd308..4bfdbfca0 100644 --- a/ietf/nomcom/urls.py +++ b/ietf/nomcom/urls.py @@ -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\d{4})/private/view-feedback/nominee/(?P\d+)$', 'view_feedback_nominee', name='nomcom_view_feedback_nominee'), url(r'^(?P\d{4})/private/edit/nominee/(?P\d+)$', 'edit_nominee', name='nomcom_edit_nominee'), url(r'^(?P\d{4})/private/merge/$', 'private_merge', name='nomcom_private_merge'), - url(r'^(?P\d{4})/private/send-reminder-mail/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'), +# url(r'^(?P\d{4})/private/send-reminder-mail/$', redirect_to, { 'url': reverse_lazy('nomcom_send_reminder_mail',kwargs={'year':year,'type':'accept'})}), + url(r'^(?P\d{4})/private/send-reminder-mail/(?P\w+)/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'), url(r'^(?P\d{4})/private/edit-members/$', EditMembersFormPreview(EditMembersForm), name='nomcom_edit_members'), url(r'^(?P\d{4})/private/edit-chair/$', EditChairFormPreview(EditChairForm), name='nomcom_edit_chair'), url(r'^(?P\d{4})/private/edit-nomcom/$', 'edit_nomcom', name='nomcom_edit_nomcom'), diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 869925b27..4361cacd6 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -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): diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 8b4ec220c..bb095ee87 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -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)) diff --git a/ietf/templates/nomcom/nomcom_private_base.html b/ietf/templates/nomcom/nomcom_private_base.html index 8573fa715..6b5a20451 100644 --- a/ietf/templates/nomcom/nomcom_private_base.html +++ b/ietf/templates/nomcom/nomcom_private_base.html @@ -19,7 +19,8 @@ {% if user|is_chair:year %} | {% if selected == "feedback_pending" %}Pending Feedback{% else %}Pending Feedback{% endif %} | {% if selected == "feedback_email" %}Enter Email Feedback{% else %}Enter Email Feedback{% endif %} | - {% if selected == "send_reminder_mail" %}Send Reminder{% else %}Send Reminder{% endif %} + {% if selected == "send_accept_reminder" %}Send Accept Reminder{% else %}Send Accept Reminder{% endif %} | + {% if selected == "send_questionnaire_reminder" %}Send Questionnaire Reminder{% else %}Send Questionnaire Reminder{% endif %} {% endif %} {% if user|is_chair:year %} diff --git a/ietf/templates/nomcom/private_index.html b/ietf/templates/nomcom/private_index.html index 65b6973c1..5815ec168 100644 --- a/ietf/templates/nomcom/private_index.html +++ b/ietf/templates/nomcom/private_index.html @@ -33,8 +33,6 @@

List of nominees by position

The following is a list of registered nominees. -{% if is_chair %}(You can request confirmation from nominees if they haven't -replied to the nomination notification they have received.){% endif %}

diff --git a/ietf/templates/nomcom/send_reminder_mail.html b/ietf/templates/nomcom/send_reminder_mail.html index 456a32b6c..eea18e111 100644 --- a/ietf/templates/nomcom/send_reminder_mail.html +++ b/ietf/templates/nomcom/send_reminder_mail.html @@ -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 %} -

Request nomination acceptance from the listed nominees

+

Send a message to the selected nominees reminding them to {{reminder_description}}

-

-An query will be sent to each person, asking them to accept (or decline) the nomination. +

These are the nominees that are in the '{{state_description}}' state for the listed positions.

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

+

The message that will be sent is shown below the list of nominees.

Nominees who have not responded

@@ -31,7 +28,7 @@ The message that will be sent is shown below the address entry form. {{ nominee }} - {% for nominee_position in nominee.nomineeposition_set.all %}{% ifequal nominee_position.state.slug "pending" %} {{ nominee_position.position.name }}, {% endifequal %}{% endfor %} + {{nominee.interesting_positions|join:", "}} {% endfor %} @@ -46,6 +43,6 @@ The message that will be sent is shown below the address entry form.

The message that will be sent is as follows: {% if mail_template %}(Edit the message){% endif %}

-{% include mail_template.path %} +
{{ mail_template.content|wrap_text:80|escape }}
-{% endblock %} \ No newline at end of file +{% endblock %}