Skip to content

Commit c36fcdc

Browse files
author
Sasha Romijn
committed
Update update_policy_state_for_assignment for new policies, fix tests,
fix some other minor things. - Legacy-Id: 17023
1 parent c8812c7 commit c36fcdc

4 files changed

Lines changed: 91 additions & 97 deletions

File tree

ietf/doc/tests_review.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ def test_assign_reviewer(self):
239239
RoleFactory(group=review_team,person__user__username='marschairman',person__name='WG Cháir Man',name_id='reviewer')
240240
secretary = RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
241241
ReviewerSettings.objects.create(team=review_team, person=rev_role.person, min_interval=14, skip_next=0)
242+
queue_policy = get_reviewer_queue_policy(review_team)
242243

243244
# review to assign to
244245
review_req = ReviewRequestFactory(team=review_team,doc=doc,state_id='requested')
@@ -282,8 +283,6 @@ def test_assign_reviewer(self):
282283

283284
ReviewWish.objects.create(person=reviewer_email.person, team=review_req.team, doc=doc)
284285

285-
# pick a non-existing reviewer as next to see that we can
286-
# handle reviewers who have left
287286
NextReviewerInTeam.objects.filter(team=review_req.team).delete()
288287
NextReviewerInTeam.objects.create(
289288
team=review_req.team,
@@ -317,7 +316,7 @@ def test_assign_reviewer(self):
317316

318317
# assign
319318
empty_outbox()
320-
rotation_list = get_reviewer_queue_policy(review_req.team).default_reviewer_rotation_list()
319+
rotation_list = queue_policy.default_reviewer_rotation_list()
321320
reviewer = Email.objects.filter(role__name="reviewer", role__group=review_req.team, person=rotation_list[0]).first()
322321
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
323322
self.assertEqual(r.status_code, 302)
@@ -328,7 +327,6 @@ def test_assign_reviewer(self):
328327
assignment = review_req.reviewassignment_set.first()
329328
self.assertEqual(assignment.reviewer, reviewer)
330329
self.assertEqual(assignment.state_id, "assigned")
331-
self.assertEqual(NextReviewerInTeam.objects.get(team=review_req.team).next_reviewer, rotation_list[1])
332330

333331
self.assertEqual(len(outbox), 1)
334332
self.assertEqual('"Some Reviewer" <reviewer@example.com>', outbox[0]["To"])

ietf/review/policies.py

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ class AbstractReviewerQueuePolicy:
3232
def __init__(self, team):
3333
self.team = team
3434

35-
def default_reviewer_rotation_list(self, dont_skip=[]):
35+
def default_reviewer_rotation_list(self, dont_skip_person_ids=None):
3636
"""
37-
Return a list of reviewers in the default reviewer rotation for a policy.
37+
Return a list of reviewers (Person objects) in the default reviewer rotation for a policy.
3838
"""
3939
raise NotImplementedError # pragma: no cover
4040

@@ -73,24 +73,29 @@ 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 _entirely_unavailable_reviewers(self, dont_skip):
77-
# prune reviewers not in the rotation (but not the assigned
78-
# reviewer who must have been available for assignment anyway)
76+
def _entirely_unavailable_reviewers(self, dont_skip_person_ids=None):
77+
"""
78+
Return a set of PKs of Persons that should not be considered
79+
to be in the rotation list at all.
80+
"""
7981
reviewers_to_skip = set()
82+
if not dont_skip_person_ids:
83+
dont_skip_person_ids = []
8084

8185
unavailable_periods = current_unavailable_periods_for_reviewers(self.team)
8286
for person_id, periods in unavailable_periods.items():
83-
if periods and person_id not in dont_skip:
87+
if periods and person_id not in dont_skip_person_ids and not any(p.availability == "canfinish" for p in periods):
8488
reviewers_to_skip.add(person_id)
8589

86-
days_needed_for_reviewers = days_needed_to_fulfill_min_interval_for_reviewers(self.team)
87-
for person_id, days_needed in days_needed_for_reviewers.items():
88-
if person_id not in dont_skip:
89-
reviewers_to_skip.add(person_id)
9090
return reviewers_to_skip
91-
91+
9292

9393
class AssignmentOrderResolver:
94+
"""
95+
The AssignmentOrderResolver resolves the "recommended assignment order",
96+
for a set of possible reviewers (email_queryset), a review request, and a
97+
rotation list.
98+
"""
9499
def __init__(self, email_queryset, review_req, rotation_list):
95100
self.review_req = review_req
96101
self.doc = review_req.doc
@@ -285,51 +290,61 @@ def _reviewer_settings_for_person_ids(self, person_ids):
285290

286291
class RotateWithSkipReviewerQueuePolicy(AbstractReviewerQueuePolicy):
287292

288-
def update_policy_state_for_assignment(self, assignee_person_id, add_skip=False):
289-
assert assignee_person_id is not None
293+
def update_policy_state_for_assignment(self, assignee_person, add_skip=False):
294+
print('====================')
295+
assert assignee_person is not None
290296

291-
rotation_list = [p.id for p in self.default_reviewer_rotation_list(
292-
dont_skip=[assignee_person_id])]
297+
rotation_list = self.default_reviewer_rotation_list(dont_skip_person_ids=[assignee_person.pk])
293298

294299
def reviewer_at_index(i):
295300
if not rotation_list:
296301
return None
297302
return rotation_list[i % len(rotation_list)]
298303

299-
def reviewer_settings_for(person_id):
300-
return (ReviewerSettings.objects.filter(team=self.team, person=person_id).first()
301-
or ReviewerSettings(team=self.team, person_id=person_id))
302-
303-
if add_skip:
304-
settings = reviewer_settings_for(assignee_person_id)
305-
settings.skip_next += 1
306-
settings.save()
304+
def reviewer_settings_for(person):
305+
return (ReviewerSettings.objects.filter(team=self.team, person=person).first()
306+
or ReviewerSettings(team=self.team, person=person))
307307

308308
if not rotation_list:
309309
return
310310

311+
rotation_list_without_skip = [r for r in rotation_list if not reviewer_settings_for(r).skip_next]
312+
print('input: {} assigned'.format(assignee_person))
313+
print('with skipped {}'.format([r for r in rotation_list]))
314+
print('without skip {}'.format([r for r in rotation_list_without_skip]))
315+
print('skip counts {}'.format([(r, reviewer_settings_for(r).skip_next) for r in rotation_list]))
316+
in_order_assignment = rotation_list_without_skip[0] == assignee_person
317+
print('in order: {}'.format(in_order_assignment))
318+
319+
# Loop through the list until finding the first person with skip_next=0,
320+
# who is not the current assignee. Anyone with skip_next>0 encountered before
321+
# has their skip_next decreased.
311322
current_idx = 0
312-
313-
if assignee_person_id == reviewer_at_index(current_idx):
314-
# Skip the first reviewer in considering who is next.
315-
current_idx += 1
316-
317-
while True:
318-
current_reviewer_person_id = reviewer_at_index(current_idx)
319-
settings = reviewer_settings_for(current_reviewer_person_id)
323+
while in_order_assignment:
324+
current_idx_person = reviewer_at_index(current_idx)
325+
settings = reviewer_settings_for(current_idx_person)
326+
print('evaluating {} with skip_next {}, assignee {}'.format(current_idx_person, settings.skip_next, assignee_person))
320327
if settings.skip_next > 0:
328+
print('dropping skip_next')
321329
settings.skip_next -= 1
322330
settings.save()
323-
current_idx += 1
324-
else:
331+
elif current_idx_person != assignee_person:
332+
print('nr appointed')
325333
nr = NextReviewerInTeam.objects.filter(team=self.team).first() or NextReviewerInTeam(
326334
team=self.team)
327-
nr.next_reviewer_id = current_reviewer_person_id
335+
nr.next_reviewer = current_idx_person
328336
nr.save()
329337

330338
break
339+
current_idx += 1
340+
341+
if add_skip:
342+
print('raising skip count for assignee')
343+
settings = reviewer_settings_for(assignee_person)
344+
settings.skip_next += 1
345+
settings.save()
331346

332-
def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip=[]):
347+
def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip_person_ids=None):
333348
reviewers = list(Person.objects.filter(role__name="reviewer", role__group=self.team))
334349
reviewers.sort(key=lambda p: p.last_name())
335350
next_reviewer_index = 0
@@ -353,7 +368,7 @@ def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip=[]
353368
rotation_list = reviewers[next_reviewer_index:] + reviewers[:next_reviewer_index]
354369

355370
if not include_unavailable:
356-
reviewers_to_skip = self._entirely_unavailable_reviewers(dont_skip)
371+
reviewers_to_skip = self._entirely_unavailable_reviewers(dont_skip_person_ids)
357372
rotation_list = [p for p in rotation_list if p.pk not in reviewers_to_skip]
358373

359374
return rotation_list

ietf/review/test_policies.py

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
# Copyright The IETF Trust 2016-2019, All Rights Reserved
22

3-
import datetime
4-
53
from ietf.doc.factories import WgDraftFactory
64
from ietf.group.factories import ReviewTeamFactory
75
from ietf.group.models import Group, Role
86
from ietf.person.fields import PersonEmailChoiceField
97
from ietf.person.models import Email
108
from ietf.review.factories import ReviewAssignmentFactory, ReviewRequestFactory
11-
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, \
12-
ReviewRequest, ReviewWish
9+
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish
1310
from ietf.review.policies import get_reviewer_queue_policy, AssignmentOrderResolver
1411
from ietf.utils.test_data import create_person
1512
from ietf.utils.test_utils import TestCase
@@ -99,7 +96,6 @@ def test_recommended_assignment_order(self):
9996
def test_update_policy_state_for_assignment(self):
10097
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
10198
policy = get_reviewer_queue_policy(team)
102-
doc = WgDraftFactory()
10399

104100
# make a bunch of reviewers
105101
reviewers = [
@@ -109,81 +105,66 @@ def test_update_policy_state_for_assignment(self):
109105

110106
self.assertEqual(reviewers, policy.default_reviewer_rotation_list())
111107

108+
def reviewer_settings_for(person):
109+
return (ReviewerSettings.objects.filter(team=team, person=person).first()
110+
or ReviewerSettings(team=team, person=person))
111+
112112
def get_skip_next(person):
113-
settings = (ReviewerSettings.objects.filter(team=team, person=person).first()
114-
or ReviewerSettings(team=team))
115-
return settings.skip_next
113+
return reviewer_settings_for(person).skip_next
116114

117-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[0].pk, add_skip=False)
115+
# Regular in-order assignment without skips
116+
policy.update_policy_state_for_assignment(assignee_person=reviewers[0], add_skip=False)
118117
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[1])
119118
self.assertEqual(get_skip_next(reviewers[0]), 0)
120119
self.assertEqual(get_skip_next(reviewers[1]), 0)
121-
122-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[1].pk, add_skip=True)
123-
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[2])
124-
self.assertEqual(get_skip_next(reviewers[1]), 1)
125-
self.assertEqual(get_skip_next(reviewers[2]), 0)
126-
127-
# skip reviewer 2
128-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[3].pk, add_skip=True)
129-
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[2])
130-
self.assertEqual(get_skip_next(reviewers[0]), 0)
131-
self.assertEqual(get_skip_next(reviewers[1]), 1)
132-
self.assertEqual(get_skip_next(reviewers[2]), 0)
133-
self.assertEqual(get_skip_next(reviewers[3]), 1)
134-
135-
# pick reviewer 2, use up reviewer 3's skip_next
136-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[2].pk, add_skip=False)
137-
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[4])
138-
self.assertEqual(get_skip_next(reviewers[0]), 0)
139-
self.assertEqual(get_skip_next(reviewers[1]), 1)
140-
self.assertEqual(get_skip_next(reviewers[2]), 0)
141-
self.assertEqual(get_skip_next(reviewers[3]), 0)
142-
self.assertEqual(get_skip_next(reviewers[4]), 0)
143-
144-
# check wrap-around
145-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[4].pk)
146-
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[0])
147-
self.assertEqual(get_skip_next(reviewers[0]), 0)
148-
self.assertEqual(get_skip_next(reviewers[1]), 1)
149120
self.assertEqual(get_skip_next(reviewers[2]), 0)
150121
self.assertEqual(get_skip_next(reviewers[3]), 0)
151122
self.assertEqual(get_skip_next(reviewers[4]), 0)
152123

153-
# unavailable
154-
today = datetime.date.today()
155-
UnavailablePeriod.objects.create(team=team, person=reviewers[1], start_date=today, end_date=today, availability="unavailable")
156-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[0].pk)
124+
# In-order assignment with add_skip
125+
policy.update_policy_state_for_assignment(assignee_person=reviewers[1], add_skip=True)
157126
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[2])
158127
self.assertEqual(get_skip_next(reviewers[0]), 0)
159-
self.assertEqual(get_skip_next(reviewers[1]), 1) # don't consume that skip while the reviewer is unavailable
128+
self.assertEqual(get_skip_next(reviewers[1]), 1) # from current add_skip=True
160129
self.assertEqual(get_skip_next(reviewers[2]), 0)
161130
self.assertEqual(get_skip_next(reviewers[3]), 0)
162131
self.assertEqual(get_skip_next(reviewers[4]), 0)
163132

164-
# pick unavailable anyway
165-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[1].pk, add_skip=False)
166-
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[2])
133+
# In-order assignment to 2, but 3 has a skip_next, so 4 should be assigned.
134+
# 3 has skip_next decreased as it is skipped over, 1 retains its skip_next
135+
reviewer3_settings = reviewer_settings_for(reviewers[3])
136+
reviewer3_settings.skip_next = 2
137+
reviewer3_settings.save()
138+
policy.update_policy_state_for_assignment(assignee_person=reviewers[2], add_skip=False)
139+
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[4])
167140
self.assertEqual(get_skip_next(reviewers[0]), 0)
168-
self.assertEqual(get_skip_next(reviewers[1]), 1)
141+
self.assertEqual(get_skip_next(reviewers[1]), 1) # from previous add_skip=true
169142
self.assertEqual(get_skip_next(reviewers[2]), 0)
170-
self.assertEqual(get_skip_next(reviewers[3]), 0)
143+
self.assertEqual(get_skip_next(reviewers[3]), 1) # from manually set skip_next - 1
171144
self.assertEqual(get_skip_next(reviewers[4]), 0)
172145

173-
# not through min_interval so advance past reviewer[2]
174-
settings, _ = ReviewerSettings.objects.get_or_create(team=team, person=reviewers[2])
175-
settings.min_interval = 30
176-
settings.save()
177-
req = ReviewRequest.objects.create(team=team, doc=doc, type_id="early", state_id="assigned", deadline=today, requested_by=reviewers[0])
178-
ReviewAssignmentFactory(review_request=req, state_id="accepted", reviewer = reviewers[2].email_set.first(),assigned_on = req.time)
179-
policy.update_policy_state_for_assignment(assignee_person_id=reviewers[3].pk)
146+
# Out of order assignments, nothing should change,
147+
# except the add_skip=True should still apply
148+
policy.update_policy_state_for_assignment(assignee_person=reviewers[3], add_skip=False)
149+
policy.update_policy_state_for_assignment(assignee_person=reviewers[2], add_skip=False)
150+
policy.update_policy_state_for_assignment(assignee_person=reviewers[1], add_skip=False)
151+
policy.update_policy_state_for_assignment(assignee_person=reviewers[0], add_skip=True)
180152
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[4])
181-
self.assertEqual(get_skip_next(reviewers[0]), 0)
153+
self.assertEqual(get_skip_next(reviewers[0]), 1) # from current add_skip=True
182154
self.assertEqual(get_skip_next(reviewers[1]), 1)
183155
self.assertEqual(get_skip_next(reviewers[2]), 0)
184-
self.assertEqual(get_skip_next(reviewers[3]), 0)
156+
self.assertEqual(get_skip_next(reviewers[3]), 1)
185157
self.assertEqual(get_skip_next(reviewers[4]), 0)
186158

159+
# Regular assignment, testing wrap-around
160+
policy.update_policy_state_for_assignment(assignee_person=reviewers[4], add_skip=False)
161+
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[2])
162+
self.assertEqual(get_skip_next(reviewers[0]), 0) # skipped over with this assignment
163+
self.assertEqual(get_skip_next(reviewers[1]), 0) # skipped over with this assignment
164+
self.assertEqual(get_skip_next(reviewers[2]), 0)
165+
self.assertEqual(get_skip_next(reviewers[3]), 1)
166+
self.assertEqual(get_skip_next(reviewers[4]), 0)
167+
187168

188169
class AssignmentOrderResolverTests(TestCase):
189170
def test_determine_ranking(self):

ietf/review/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa
381381
assignment = review_req.reviewassignment_set.create(state_id='assigned', reviewer = reviewer, assigned_on = datetime.datetime.now())
382382

383383
from ietf.review.policies import get_reviewer_queue_policy
384-
get_reviewer_queue_policy(review_req.team).update_policy_state_for_assignment(reviewer.person_id, add_skip)
384+
get_reviewer_queue_policy(review_req.team).update_policy_state_for_assignment(reviewer.person, add_skip)
385385

386386
ReviewRequestDocEvent.objects.create(
387387
type="assigned_review_request",

0 commit comments

Comments
 (0)