Merged in [16792] from sasha@dashcare.nl:

Fix #2475 - Send opt-in reminders for unconfirmed review assignments.
If enabled for a team, reminders will be sent every X days to reviewers
for review assignments they have not accepted or rejected.
 - Legacy-Id: 16846
Note: SVN reference [16792] has been migrated to Git commit 486b6daa29
This commit is contained in:
Henrik Levkowetz 2019-10-08 15:57:28 +00:00
commit be641ac22a
6 changed files with 119 additions and 7 deletions

View file

@ -25,7 +25,7 @@ from ietf.review.utils import (
review_assignments_needing_reviewer_reminder, email_reviewer_reminder,
review_assignments_needing_secretary_reminder, email_secretary_reminder,
send_unavaibility_period_ending_reminder, send_reminder_all_open_reviews,
send_review_reminder_overdue_assignment)
send_review_reminder_overdue_assignment, send_reminder_unconfirmed_assignments)
today = datetime.date.today()
@ -47,3 +47,6 @@ print('\n'.join(overdue_reviews_reminders_sent)
open_reviews_reminders_sent = send_reminder_all_open_reviews(today)
print('\n'.join(open_reviews_reminders_sent))
unconfirmed_assignment_reminders_sent = send_reminder_unconfirmed_assignments(today)
print('\n'.join(unconfirmed_assignment_reminders_sent))

View file

@ -17,14 +17,15 @@ from ietf.doc.models import TelechatDocEvent
from ietf.group.models import Role
from ietf.iesg.models import TelechatDate
from ietf.person.models import Person
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.models import ( ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings,
ReviewTeamSettings )
from ietf.review.utils import (
suggested_review_requests_for_team,
review_assignments_needing_reviewer_reminder, email_reviewer_reminder,
review_assignments_needing_secretary_reminder, email_secretary_reminder,
reviewer_rotation_list,
send_unavaibility_period_ending_reminder, send_reminder_all_open_reviews,
send_review_reminder_overdue_assignment)
send_review_reminder_overdue_assignment, send_reminder_unconfirmed_assignments)
from ietf.name.models import ReviewResultName, ReviewRequestStateName, ReviewAssignmentStateName
import ietf.group.views
from ietf.utils.mail import outbox, empty_outbox
@ -592,6 +593,34 @@ class ReviewTests(TestCase):
self.assertTrue(reviewer.email_address() in log[0])
self.assertTrue('1 open review' in log[0])
def test_send_reminder_unconfirmed_assignments(self):
review_req = ReviewRequestFactory(state_id='assigned')
reviewer = RoleFactory(name_id='reviewer', group=review_req.team, person__user__username='reviewer').person
ReviewAssignmentFactory(review_request=review_req, state_id='assigned', assigned_on=review_req.time, reviewer=reviewer.email_set.first())
RoleFactory(name_id='secr', group=review_req.team, person__user__username='reviewsecretary')
today = datetime.date.today()
# By default, these reminders are disabled for all teams.
empty_outbox()
log = send_reminder_unconfirmed_assignments(today)
self.assertEqual(len(outbox), 0)
self.assertFalse(log)
ReviewTeamSettings.objects.update(remind_days_unconfirmed_assignments=1)
empty_outbox()
log = send_reminder_unconfirmed_assignments(today)
self.assertEqual(len(outbox), 1)
self.assertIn(reviewer.email_address(), outbox[0]["To"])
self.assertEqual(outbox[0]["Subject"], "Reminder: you have not responded to a review assignment")
message = outbox[0].get_payload(decode=True).decode("utf-8")
self.assertIn(review_req.team.acronym, message)
self.assertIn('accept or reject the assignment on', message)
self.assertIn(review_req.doc.name, message)
self.assertEqual(len(log), 1)
self.assertIn(reviewer.email_address(), log[0])
self.assertIn('not accepted/rejected review assignment', log[0])
class BulkAssignmentTests(TestCase):

View file

@ -0,0 +1,22 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-10-01 04:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('review', '0015_add_remind_days_open_reviews'),
]
operations = [
migrations.AddField(
model_name='reviewteamsettings',
name='remind_days_unconfirmed_assignments',
field=models.PositiveIntegerField(blank=True, help_text="To send a periodic email reminder to reviewers of review assignments that are not accepted yet, enter the number of days between these reminders. Clear the field if you don't want these reminders to be sent.", null=True, verbose_name='Periodic reminder of not yet accepted or rejected review assignments to reviewer every X days'),
),
]

View file

@ -174,6 +174,11 @@ class ReviewTeamSettings(models.Model):
review_results = models.ManyToManyField(ReviewResultName, default=get_default_review_results, related_name='reviewteamsettings_review_results_set')
notify_ad_when = models.ManyToManyField(ReviewResultName, related_name='reviewteamsettings_notify_ad_set', blank=True)
secr_mail_alias = models.CharField(verbose_name="Email alias for all of the review team secretaries", max_length=255, blank=True, help_text="Email alias for all of the review team secretaries")
remind_days_unconfirmed_assignments = models.PositiveIntegerField(null=True, blank=True,
verbose_name="Periodic reminder of not yet accepted or rejected review assignments to reviewer every X days",
help_text="To send a periodic email reminder to reviewers of review assignments they have neither accepted"
" nor rejected, enter the number of days between these reminders. Clear the field if you don't"
" want these reminders to be sent.")
def __str__(self):
return "%s" % (self.group.acronym,)

View file

@ -28,10 +28,15 @@ from ietf.person.models import Person
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
from ietf.review.models import (ReviewRequest, ReviewAssignment, ReviewRequestStateName, ReviewTypeName,
ReviewerSettings, UnavailablePeriod, ReviewWish, NextReviewerInTeam,
ReviewSecretarySettings)
ReviewSecretarySettings, ReviewTeamSettings)
from ietf.utils.mail import send_mail
from ietf.doc.utils import extract_complete_replaces_ancestor_mapping_for_docs
# The origin date is used to have a single reference date for "every X days".
# This date is arbitrarily chosen and has no special meaning, but should be consistent.
ORIGIN_DATE_PERIODIC_REMINDERS = datetime.date(2019, 1, 1)
def active_review_teams():
return Group.objects.filter(reviewteamsettings__isnull=False,state="active")
@ -992,9 +997,7 @@ def send_review_reminder_overdue_assignment(remind_date):
def send_reminder_all_open_reviews(remind_date):
log = []
# The origin date is arbitrarily chosen, to have a single reference date for "every X days"
origin_date = datetime.date(2019, 1, 1)
days_since_origin = (remind_date - origin_date).days
days_since_origin = (remind_date - ORIGIN_DATE_PERIODIC_REMINDERS).days
relevant_reviewer_settings = ReviewerSettings.objects.filter(remind_days_open_reviews__isnull=False)
for reviewer_settings in relevant_reviewer_settings:
@ -1027,6 +1030,47 @@ def send_reminder_all_open_reviews(remind_date):
return log
def send_reminder_unconfirmed_assignments(remind_date):
"""
Remind reviewers of any assigned ReviewAssignments which they have not
accepted or rejected, if enabled in ReviewTeamSettings.
"""
log = []
days_since_origin = (remind_date - ORIGIN_DATE_PERIODIC_REMINDERS).days
relevant_review_team_settings = ReviewTeamSettings.objects.filter(
remind_days_unconfirmed_assignments__isnull=False)
for review_team_settings in relevant_review_team_settings:
if days_since_origin % review_team_settings.remind_days_unconfirmed_assignments != 0:
continue
assignments = ReviewAssignment.objects.filter(
state='assigned',
review_request__team=review_team_settings.group,
)
if not assignments:
continue
for assignment in assignments:
to = assignment.reviewer.formatted_email()
subject = "Reminder: you have not responded to a review assignment"
domain = Site.objects.get_current().domain
review_request_url = urlreverse("ietf.doc.views_review.review_request", kwargs={
"name": assignment.review_request.doc.name,
"request_id": assignment.review_request.pk
})
send_mail(None, to, None, subject, "review/reviewer_reminder_unconfirmed_assignments.txt", {
"review_request_url": "https://{}{}".format(domain, review_request_url),
"assignment": assignment,
"team": assignment.review_request.team,
"remind_days": review_team_settings.remind_days_unconfirmed_assignments,
})
log.append("Emailed reminder to {} about not accepted/rejected review assignment {}".format(to, assignment.pk))
return log
def review_assignments_needing_reviewer_reminder(remind_date):
assignment_qs = ReviewAssignment.objects.filter(
state__in=("assigned", "accepted"),

View file

@ -0,0 +1,9 @@
{% load ietf_filters %}{% autoescape off %}{% filter wordwrap:78 %}This is just a friendly reminder that you have a review assignment which you have neither accepted nor rejected.
The review assignment is for {{ assignment.review_request.doc.name }} in team {{ team.acronym }}.
You can accept or reject the assignment on:
{{ review_request_url }}
You are receiving this reminder because your team secretary has configured the Datatracker to remind you every {{ remind_days }} day{{ remind_days|pluralize }}.
{% endfilter %}{% endautoescape %}