Skip to content

Commit c8812c7

Browse files
author
Sasha Romijn
committed
Account for 'canfinish' unavailabilities.
- Legacy-Id: 16999
1 parent 1c84e3c commit c8812c7

2 files changed

Lines changed: 55 additions & 8 deletions

File tree

ietf/review/policies.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def recommended_assignment_order(self, email_queryset, review_req):
7373
resolver = AssignmentOrderResolver(email_queryset, review_req, self.default_reviewer_rotation_list())
7474
return [(r['email'].pk, r['label']) for r in resolver.determine_ranking()]
7575

76-
def _unavailable_reviewers(self, dont_skip):
76+
def _entirely_unavailable_reviewers(self, dont_skip):
7777
# prune reviewers not in the rotation (but not the assigned
7878
# reviewer who must have been available for assignment anyway)
7979
reviewers_to_skip = set()
@@ -119,7 +119,8 @@ def _collect_context(self):
119119
self.connections = self._connections_with_doc(self.doc, self.possible_person_ids)
120120
self.unavailable_periods = current_unavailable_periods_for_reviewers(self.team)
121121
self.assignment_data_for_reviewers = latest_review_assignments_for_reviewers(self.team)
122-
122+
self.unavailable_periods = current_unavailable_periods_for_reviewers(self.team)
123+
123124
def determine_ranking(self):
124125
"""
125126
Determine the ranking of reviewers.
@@ -139,7 +140,7 @@ def _ranking_for_email(self, email):
139140
Determine the ranking for a specific Email.
140141
Returns a dict with an email object, the scores and an explanation label.
141142
The scores are a list of individual scores, i.e. they are prioritised, not
142-
cumulative.
143+
cumulative.
143144
"""
144145
settings = self.reviewer_settings.get(email.person_id)
145146
scores = []
@@ -152,7 +153,25 @@ def add_boolean_score(direction, expr, explanation=None):
152153

153154
if email.person_id not in self.rotation_index:
154155
return
156+
157+
# If a reviewer is unavailable, they are ignored.
158+
periods = self.unavailable_periods.get(email.person_id, [])
159+
unavailable_at_the_moment = periods and not (
160+
email.person_id in self.has_reviewed_previous and
161+
all(p.availability == "canfinish" for p in periods)
162+
)
163+
if unavailable_at_the_moment:
164+
return
155165

166+
def format_period(p):
167+
if p.end_date:
168+
res = "unavailable until {}".format(p.end_date.isoformat())
169+
else:
170+
res = "unavailable indefinitely"
171+
return "{} ({})".format(res, p.get_availability_display())
172+
if periods:
173+
explanations.append(", ".join(format_period(p) for p in periods))
174+
156175
# misc
157176
add_boolean_score(+1, email.person_id in self.has_reviewed_previous, "reviewed document before")
158177
add_boolean_score(+1, email.person_id in self.wish_to_review, "wishes to review document")
@@ -334,7 +353,7 @@ def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip=[]
334353
rotation_list = reviewers[next_reviewer_index:] + reviewers[:next_reviewer_index]
335354

336355
if not include_unavailable:
337-
reviewers_to_skip = self._unavailable_reviewers(dont_skip)
356+
reviewers_to_skip = self._entirely_unavailable_reviewers(dont_skip)
338357
rotation_list = [p for p in rotation_list if p.pk not in reviewers_to_skip]
339358

340359
return rotation_list

ietf/review/test_policies.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ def test_default_reviewer_rotation_list(self):
3131
team=team,
3232
person=unavailable_reviewer,
3333
start_date='2000-01-01',
34-
availability=UnavailablePeriod.AVAILABILITY_CHOICES[0],
34+
availability='unavailable',
35+
)
36+
# This should not have any impact. Canfinish unavailable reviewers are included in
37+
# the default rotation, and filtered further when making assignment choices.
38+
UnavailablePeriod.objects.create(
39+
team=team,
40+
person=reviewers[1],
41+
start_date='2000-01-01',
42+
availability='canfinish',
3543
)
36-
3744
# Default policy without a NextReviewerInTeam
3845
rotation = policy.default_reviewer_rotation_list()
3946
self.assertNotIn(unavailable_reviewer, rotation)
@@ -185,13 +192,16 @@ def test_determine_ranking(self):
185192
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
186193
reviewer_high = create_person(team, "reviewer", name="Test Reviewer-high", username="testreviewerhigh")
187194
reviewer_low = create_person(team, "reviewer", name="Test Reviewer-low", username="testreviewerlow")
195+
reviewer_unavailable = create_person(team, "reviewer", name="Test Reviewer-unavailable", username="testreviewerunavailable")
196+
# This reviewer should be ignored because it is not in the rotation list.
197+
create_person(team, "reviewer", name="Test Reviewer-out-of-rotation", username="testreviewer-out-of-rotation")
188198

189199
# Trigger author check, AD check and group check
190200
doc = WgDraftFactory(group__acronym='mars', rev='01', authors=[reviewer_low], ad=reviewer_low, shepherd=reviewer_low.email())
191201
Role.objects.create(group=doc.group, person=reviewer_low, email=reviewer_low.email(), name_id='advisor')
192202

193203
review_req = ReviewRequestFactory(doc=doc, team=team, type_id='early')
194-
rotation_list = [reviewer_low, reviewer_high]
204+
rotation_list = [reviewer_low, reviewer_high, reviewer_unavailable]
195205

196206
# Trigger previous review check and completed review stats - TODO: something something related documents
197207
ReviewAssignmentFactory(review_request__team=team, review_request__doc=doc, reviewer=reviewer_high.email(), state_id='completed')
@@ -201,6 +211,23 @@ def test_determine_ranking(self):
201211
# Trigger review wish check
202212
ReviewWish.objects.create(team=team, doc=doc, person=reviewer_high)
203213

214+
# This period should not have an impact, because it is the canfinish type,
215+
# and this reviewer has reviewed previously.
216+
UnavailablePeriod.objects.create(
217+
team=team,
218+
person=reviewer_high,
219+
start_date='2000-01-01',
220+
availability='canfinish',
221+
)
222+
# This period should exclude this reviewer entirely, as it is 'canfinish',
223+
# but this reviewer has not reviewed previously.
224+
UnavailablePeriod.objects.create(
225+
team=team,
226+
person=reviewer_unavailable,
227+
start_date='2000-01-01',
228+
availability='canfinish',
229+
)
230+
204231
# Trigger max frequency and open review stats
205232
ReviewAssignmentFactory(review_request__team=team, reviewer=reviewer_low.email(), state_id='assigned', review_request__doc__pages=10)
206233
# Trigger skip_next, max frequency and filter_re
@@ -214,9 +241,10 @@ def test_determine_ranking(self):
214241

215242
order = AssignmentOrderResolver(Email.objects.all(), review_req, rotation_list)
216243
ranking = order.determine_ranking()
244+
self.assertEqual(len(ranking), 2)
217245
self.assertEqual(ranking[0]['email'], reviewer_high.email())
218246
self.assertEqual(ranking[1]['email'], reviewer_low.email())
219247
self.assertEqual(ranking[0]['scores'], [ 1, 1, 1, 1, 0, 0, -1])
220248
self.assertEqual(ranking[1]['scores'], [-1, -1, -1, -1, -91, -2, 0])
221-
self.assertEqual(ranking[0]['label'], 'Test Reviewer-high: reviewed document before; wishes to review document; #2; 1 no response, 1 partially complete, 1 fully completed')
249+
self.assertEqual(ranking[0]['label'], 'Test Reviewer-high: unavailable indefinitely (Can do follow-ups); reviewed document before; wishes to review document; #2; 1 no response, 1 partially complete, 1 fully completed')
222250
self.assertEqual(ranking[1]['label'], 'Test Reviewer-low: is author of document; filter regexp matches; max frequency exceeded, ready in 91 days; skip next 2; #1; currently 1 open, 10 pages')

0 commit comments

Comments
 (0)