datatracker/ietf/review/tests.py

510 lines
24 KiB
Python

# Copyright The IETF Trust 2019-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
from ietf.group.factories import RoleFactory
from ietf.utils.mail import empty_outbox, get_payload_text, outbox
from ietf.utils.test_utils import TestCase, reload_db_objects
from .factories import ReviewAssignmentFactory, ReviewRequestFactory, ReviewerSettingsFactory
from .mailarch import hash_list_message_id
from .models import ReviewerSettings, ReviewSecretarySettings, ReviewTeamSettings, UnavailablePeriod
from .utils import (email_secretary_reminder, review_assignments_needing_secretary_reminder,
email_reviewer_reminder, review_assignments_needing_reviewer_reminder,
send_reminder_unconfirmed_assignments, send_review_reminder_overdue_assignment,
send_reminder_all_open_reviews, send_unavailability_period_ending_reminder,
ORIGIN_DATE_PERIODIC_REMINDERS)
class HashTest(TestCase):
def test_hash_list_message_id(self):
for list, msgid, hash in (
('ietf', '156182196167.12901.11966487185176024571@ietfa.amsl.com', 'lr6RtZ4TiVMZn1fZbykhkXeKhEk'),
('codesprints', 'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', 'N1nFHHUXiFWYtdzBgjtqzzILFHI'),
('xml2rfc', '3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org', 'g6DN4SxJGDrlSuKsubwb6rRSePU'),
(u'ietf', u'156182196167.12901.11966487185176024571@ietfa.amsl.com','lr6RtZ4TiVMZn1fZbykhkXeKhEk'),
(u'codesprints', u'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', 'N1nFHHUXiFWYtdzBgjtqzzILFHI'),
(u'xml2rfc', u'3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org','g6DN4SxJGDrlSuKsubwb6rRSePU'),
(b'ietf', b'156182196167.12901.11966487185176024571@ietfa.amsl.com','lr6RtZ4TiVMZn1fZbykhkXeKhEk'),
(b'codesprints', b'E1hNffl-0004RM-Dh@zinfandel.tools.ietf.org', 'N1nFHHUXiFWYtdzBgjtqzzILFHI'),
(b'xml2rfc', b'3A0F4CD6-451F-44E2-9DA4-28235C638588@rfc-editor.org','g6DN4SxJGDrlSuKsubwb6rRSePU'),
):
self.assertEqual(hash, hash_list_message_id(list, msgid))
class ReviewAssignmentTest(TestCase):
def do_test_update_review_req_status(self, assignment_state, expected_state):
review_req = ReviewRequestFactory(state_id='assigned')
ReviewAssignmentFactory(review_request=review_req, state_id='part-completed')
assignment = ReviewAssignmentFactory(review_request=review_req)
assignment.state_id = assignment_state
assignment.save()
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, expected_state)
def test_update_review_req_status(self):
# Test change
for assignment_state in ['no-response', 'rejected', 'withdrawn', 'overtaken']:
self.do_test_update_review_req_status(assignment_state, 'requested')
# Test no-change
for assignment_state in ['accepted', 'assigned', 'completed', 'part-completed', 'unknown', ]:
self.do_test_update_review_req_status(assignment_state, 'assigned')
def test_no_update_review_req_status_when_other_active_assignment(self):
# If there is another still active assignment, do not update review_req state
review_req = ReviewRequestFactory(state_id='assigned')
ReviewAssignmentFactory(review_request=review_req, state_id='assigned')
assignment = ReviewAssignmentFactory(review_request=review_req)
assignment.state_id = 'no-response'
assignment.save()
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, 'assigned')
def test_no_update_review_req_status_when_review_req_withdrawn(self):
# review_req state must only be changed to "requested", if old state was "assigned",
# to prevent reviving dead review requests
review_req = ReviewRequestFactory(state_id='withdrawn')
assignment = ReviewAssignmentFactory(review_request=review_req)
assignment.state_id = 'no-response'
assignment.save()
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, 'withdrawn')
class ReviewAssignmentReminderTests(TestCase):
today = datetime.date.today()
deadline = today + datetime.timedelta(days=6)
def setUp(self):
super().setUp()
self.review_req = ReviewRequestFactory(
state_id='assigned',
deadline=self.deadline,
)
self.team = self.review_req.team
self.reviewer = RoleFactory(
name_id='reviewer',
group=self.team,
person__user__username='reviewer',
).person
self.assignment = ReviewAssignmentFactory(
review_request=self.review_req,
state_id='assigned',
assigned_on=self.review_req.time,
reviewer=self.reviewer.email_set.first(),
)
def make_secretary(self, username, remind_days=None):
secretary_role = RoleFactory(
name_id='secr',
group=self.team,
person__user__username=username,
)
ReviewSecretarySettings.objects.create(
team=self.team,
person=secretary_role.person,
remind_days_before_deadline=remind_days,
)
return secretary_role
def make_non_secretary(self, username, remind_days=None):
"""Make a non-secretary role that has a ReviewSecretarySettings
This is a little odd, but might come up if an ex-secretary takes on another role and still
has a ReviewSecretarySettings record.
"""
role = RoleFactory(
name_id='reviewer',
group=self.team,
person__user__username=username,
)
ReviewSecretarySettings.objects.create(
team=self.team,
person=role.person,
remind_days_before_deadline=remind_days,
)
return role
def test_review_assignments_needing_secretary_reminder(self):
"""Notification sent to multiple secretaries"""
# Set up two secretaries with the same remind_days one with a different, and one with None.
secretary_roles = [
self.make_secretary(username='reviewsecretary0', remind_days=6),
self.make_secretary(username='reviewsecretary1', remind_days=6),
self.make_secretary(username='reviewsecretary2', remind_days=5),
self.make_secretary(username='reviewsecretary3', remind_days=None), # never notified
]
self.make_non_secretary(username='nonsecretary', remind_days=6) # never notified
# Check from more than remind_days before the deadline all the way through the day before.
# Should only get reminders on the expected days.
self.assertCountEqual(
review_assignments_needing_secretary_reminder(self.deadline - datetime.timedelta(days=7)),
[],
'No reminder needed when deadline is more than remind_days away',
)
self.assertCountEqual(
review_assignments_needing_secretary_reminder(self.deadline - datetime.timedelta(days=6)),
[(self.assignment, secretary_roles[0]), (self.assignment, secretary_roles[1])],
'Reminders needed for all secretaries when deadline is exactly remind_days away',
)
self.assertCountEqual(
review_assignments_needing_secretary_reminder(self.deadline - datetime.timedelta(days=5)),
[(self.assignment, secretary_roles[2])],
'Reminder needed when deadline is exactly remind_days away',
)
for days in range(1, 5):
self.assertCountEqual(
review_assignments_needing_secretary_reminder(self.deadline - datetime.timedelta(days=days)),
[],
f'No reminder needed when deadline is less than remind_days away (tried {days})',
)
def test_email_secretary_reminder_emails_secretaries(self):
"""Secretary review assignment reminders are sent to secretaries"""
secretary_role = self.make_secretary(username='reviewsecretary')
# create a couple other roles for the team to check that only the requested secretary is reminded
self.make_secretary(username='ignoredsecretary')
self.make_non_secretary(username='nonsecretary')
empty_outbox()
email_secretary_reminder(self.assignment, secretary_role)
self.assertEqual(len(outbox), 1)
msg = outbox[0]
text = get_payload_text(msg)
self.assertIn(secretary_role.email.address, msg['to'])
self.assertIn(self.review_req.doc.name, msg['subject'])
self.assertIn(self.review_req.doc.name, text)
self.assertIn(self.team.acronym, msg['subject'])
self.assertIn(self.team.acronym, text)
def test_review_assignments_needing_reviewer_reminder(self):
# method should find lists of assignments
reviewer_settings = ReviewerSettings.objects.create(
team=self.team,
person=self.reviewer,
remind_days_before_deadline=6,
)
# Give this reviewer another team with a review to be sure
# we don't have cross-talk between teams.
second_req = ReviewRequestFactory(state_id='assigned', deadline=self.deadline)
second_team = second_req.team
second_assignment = ReviewAssignmentFactory(
review_request=second_req,
state_id='assigned',
assigned_on=second_req.time,
reviewer=self.reviewer.email(),
)
ReviewerSettingsFactory(
team=second_team,
person=self.reviewer,
remind_days_before_deadline=5,
)
self.assertCountEqual(
review_assignments_needing_reviewer_reminder(self.deadline - datetime.timedelta(days=7)),
[],
'No reminder needed when deadline is more than remind_days away'
)
self.assertCountEqual(
review_assignments_needing_reviewer_reminder(self.deadline - datetime.timedelta(days=6)),
[self.assignment],
'Reminder needed when deadline is exactly remind_days away',
)
self.assertCountEqual(
review_assignments_needing_reviewer_reminder(self.deadline - datetime.timedelta(days=5)),
[second_assignment],
'Reminder needed for other assignment'
)
self.assertCountEqual(
review_assignments_needing_reviewer_reminder(self.deadline - datetime.timedelta(days=4)),
[],
'No reminder needed when deadline is less than remind_days away'
)
# should never send a reminder when disabled
reviewer_settings.remind_days_before_deadline = None
reviewer_settings.save()
second_assignment.delete() # get rid of this one for the second test
# test over a range that includes when we *did* send a reminder above
for days in range(1, 8):
self.assertCountEqual(
review_assignments_needing_reviewer_reminder(self.deadline - datetime.timedelta(days=days)),
[],
f'No reminder should be sent when reminders are disabled (sent for days={days})',
)
def test_email_review_reminder_emails_reviewers(self):
"""Reviewer assignment reminders are sent to the reviewers"""
empty_outbox()
email_reviewer_reminder(self.assignment)
self.assertEqual(len(outbox), 1)
msg = outbox[0]
text = get_payload_text(msg)
self.assertIn(self.reviewer.email_address(), msg['to'])
self.assertIn(self.review_req.doc.name, msg['subject'])
self.assertIn(self.review_req.doc.name, text)
self.assertIn(self.team.acronym, msg['subject'])
def test_send_reminder_unconfirmed_assignments(self):
"""Unconfirmed assignment reminders are sent to reviewer and team secretary"""
assigned_on = self.assignment.assigned_on.date()
secretaries = [
self.make_secretary(username='reviewsecretary0').person,
self.make_secretary(username='reviewsecretary1').person,
]
# assignments that should be ignored (will result in extra emails being sent if not)
ReviewAssignmentFactory(
review_request=self.review_req,
state_id='accepted',
assigned_on=self.review_req.time,
)
ReviewAssignmentFactory(
review_request=self.review_req,
state_id='completed',
assigned_on=self.review_req.time,
)
ReviewAssignmentFactory(
review_request=self.review_req,
state_id='rejected',
assigned_on=self.review_req.time,
)
# Create a second review for a different team to test for cross-talk between teams.
ReviewAssignmentFactory(
state_id='completed', # something that does not need a reminder
reviewer=self.reviewer.email(),
)
# By default, these reminders are disabled for all teams.
ReviewTeamSettings.objects.update(remind_days_unconfirmed_assignments=1)
empty_outbox()
log = send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=1))
self.assertEqual(len(outbox), 1)
self.assertIn(self.reviewer.email_address(), outbox[0]["To"])
for secretary in secretaries:
self.assertIn(
secretary.email_address(),
outbox[0]["Cc"],
f'Secretary {secretary.user.username} was not copied on the reminder',
)
self.assertEqual(outbox[0]["Subject"], "Reminder: you have not responded to a review assignment")
message = get_payload_text(outbox[0])
self.assertIn(self.team.acronym, message)
self.assertIn('accept or reject the assignment on', message)
self.assertIn(self.review_req.doc.name, message)
self.assertEqual(len(log), 1)
self.assertIn(self.reviewer.email_address(), log[0])
self.assertIn('not accepted/rejected review assignment', log[0])
def test_send_reminder_unconfirmed_assignments_respects_remind_days(self):
"""Unconfirmed assignment reminders should respect the team settings"""
assigned_on = self.assignment.assigned_on.date()
# By default, these reminders are disabled for all teams.
empty_outbox()
for days in range(10):
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=days))
self.assertEqual(len(outbox), 0)
# expect a notification every day except the day of assignment
ReviewTeamSettings.objects.update(remind_days_unconfirmed_assignments=1)
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=0))
self.assertEqual(len(outbox), 0) # no message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=1))
self.assertEqual(len(outbox), 1) # one new message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=2))
self.assertEqual(len(outbox), 2) # one new message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=3))
self.assertEqual(len(outbox), 3) # one new message
# expect a notification every other day
empty_outbox()
ReviewTeamSettings.objects.update(remind_days_unconfirmed_assignments=2)
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=0))
self.assertEqual(len(outbox), 0) # no message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=1))
self.assertEqual(len(outbox), 0) # no message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=2))
self.assertEqual(len(outbox), 1) # one new message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=3))
self.assertEqual(len(outbox), 1) # no new message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=4))
self.assertEqual(len(outbox), 2) # one new message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=5))
self.assertEqual(len(outbox), 2) # no new message
send_reminder_unconfirmed_assignments(assigned_on + datetime.timedelta(days=6))
self.assertEqual(len(outbox), 3) # no new message
def test_send_unavailability_period_ending_reminder(self):
secretary = self.make_secretary(username='reviewsecretary')
empty_outbox()
today = datetime.date.today()
UnavailablePeriod.objects.create(
team=self.team,
person=self.reviewer,
start_date=today - datetime.timedelta(days=40),
end_date=today + datetime.timedelta(days=3),
availability="unavailable",
)
UnavailablePeriod.objects.create(
team=self.team,
person=self.reviewer,
# This object should be ignored, length is too short
start_date=today - datetime.timedelta(days=20),
end_date=today + datetime.timedelta(days=3),
availability="unavailable",
)
UnavailablePeriod.objects.create(
team=self.team,
person=self.reviewer,
start_date=today - datetime.timedelta(days=40),
# This object should be ignored, end date is too far away
end_date=today + datetime.timedelta(days=4),
availability="unavailable",
)
UnavailablePeriod.objects.create(
team=self.team,
person=self.reviewer,
# This object should be ignored, end date is too close
start_date=today - datetime.timedelta(days=40),
end_date=today + datetime.timedelta(days=2),
availability="unavailable",
)
log = send_unavailability_period_ending_reminder(today)
self.assertEqual(len(outbox), 1)
self.assertTrue(self.reviewer.email_address() in outbox[0]["To"])
self.assertTrue(secretary.person.email_address() in outbox[0]["To"])
message = get_payload_text(outbox[0])
self.assertTrue(self.reviewer.name in message)
self.assertTrue(self.team.acronym in message)
self.assertEqual(len(log), 1)
self.assertTrue(self.reviewer.name in log[0])
self.assertTrue(self.team.acronym in log[0])
def test_send_review_reminder_overdue_assignment(self):
"""An overdue assignment reminder should be sent to the secretary
This tests that a second set of assignments for the same reviewer but a different
review team does not cause cross-talk between teams. To do this, it removes the
ReviewTeamSettings instance for the second review team. At the moment, this has
the effect of disabling these reminders. This is a bit of a hack, because I'm not
sure that review teams without the ReviewTeamSettings should exist. It has the
needed effect but might require rethinking in the future.
"""
secretary = self.make_secretary(username='reviewsecretary')
# Set the remind_date to be exactly one grace period after self.deadline
remind_date = self.deadline + datetime.timedelta(days=5)
# Create a second request for a second team that will not be sent reminders
second_team = ReviewAssignmentFactory(
review_request__state_id='assigned',
review_request__deadline=self.deadline,
state_id='assigned',
assigned_on=self.deadline,
reviewer=self.reviewer.email_set.first(),
).review_request.team
second_team.reviewteamsettings.delete() # prevent it from being sent reminders
# An assignment that is not yet overdue
not_overdue = remind_date + datetime.timedelta(days=1)
ReviewAssignmentFactory(
review_request__team=self.team,
review_request__state_id='assigned',
review_request__deadline=not_overdue,
state_id='assigned',
assigned_on=not_overdue,
reviewer=self.reviewer.email_set.first(),
)
ReviewAssignmentFactory(
review_request__team=second_team,
review_request__state_id='assigned',
review_request__deadline=not_overdue,
state_id='assigned',
assigned_on=not_overdue,
reviewer=self.reviewer.email_set.first(),
)
# An assignment that is overdue but is not past the grace period
in_grace_period = remind_date - datetime.timedelta(days=1)
ReviewAssignmentFactory(
review_request__team=self.team,
review_request__state_id='assigned',
review_request__deadline=in_grace_period,
state_id='assigned',
assigned_on=in_grace_period,
reviewer=self.reviewer.email_set.first(),
)
ReviewAssignmentFactory(
review_request__team=second_team,
review_request__state_id='assigned',
review_request__deadline=in_grace_period,
state_id='assigned',
assigned_on=in_grace_period,
reviewer=self.reviewer.email_set.first(),
)
empty_outbox()
log = send_review_reminder_overdue_assignment(remind_date)
self.assertEqual(len(log), 1)
self.assertEqual(len(outbox), 1)
self.assertTrue(secretary.person.email_address() in outbox[0]["To"])
self.assertEqual(outbox[0]["Subject"], "1 Overdue review for team {}".format(self.team.acronym))
message = get_payload_text(outbox[0])
self.assertIn(
self.team.acronym + ' has 1 accepted or assigned review overdue by at least 5 days.',
message,
)
self.assertIn('Review of {} by {}'.format(self.review_req.doc.name, self.reviewer.plain_name()), message)
self.assertEqual(len(log), 1)
self.assertIn(secretary.person.email_address(), log[0])
self.assertIn('1 overdue review', log[0])
def test_send_reminder_all_open_reviews(self):
today = datetime.date.today()
self.make_secretary(username='reviewsecretary')
ReviewerSettingsFactory(team=self.team, person=self.reviewer, remind_days_open_reviews=1)
# Create another assignment for this reviewer in a different team.
# Configure so that a reminder should not be sent for the date we test. It should not
# be included in the reminder that's sent - only one open review assignment should be
# reported.
second_req = ReviewRequestFactory(state_id='assigned', deadline=self.deadline)
second_team = second_req.team
ReviewAssignmentFactory(
review_request=second_req,
state_id='assigned',
assigned_on=second_req.time,
reviewer=self.reviewer.email(),
)
ReviewerSettingsFactory(
team=second_team,
person=self.reviewer,
# set the reminder never to be due to be sent today for this team
remind_days_open_reviews=(today - ORIGIN_DATE_PERIODIC_REMINDERS).days + 1,
)
empty_outbox()
log = send_reminder_all_open_reviews(today)
self.assertEqual(len(outbox), 1)
self.assertTrue(self.reviewer.email_address() in outbox[0]["To"])
self.assertEqual(outbox[0]["Subject"], "Reminder: you have 1 open review assignment")
message = get_payload_text(outbox[0])
self.assertTrue(self.team.acronym in message)
self.assertTrue('you have 1 open review' in message)
self.assertTrue(self.review_req.doc.name in message)
self.assertTrue(self.review_req.deadline.strftime('%Y-%m-%d') in message)
self.assertEqual(len(log), 1)
self.assertTrue(self.reviewer.email_address() in log[0])
self.assertTrue('1 open review' in log[0])