|
3 | 3 | from __future__ import absolute_import, print_function, unicode_literals |
4 | 4 |
|
5 | 5 | import re |
| 6 | +from collections import OrderedDict |
6 | 7 |
|
7 | 8 | import six |
8 | 9 |
|
|
11 | 12 | from ietf.group.models import Role |
12 | 13 | from ietf.person.models import Person |
13 | 14 | import debug # pyflakes:ignore |
14 | | -from ietf.review.models import NextReviewerInTeam, ReviewerSettings, ReviewWish, ReviewRequest |
| 15 | +from ietf.review.models import NextReviewerInTeam, ReviewerSettings, ReviewWish, ReviewRequest, \ |
| 16 | + ReviewAssignment |
15 | 17 | from ietf.review.utils import (current_unavailable_periods_for_reviewers, |
16 | 18 | days_needed_to_fulfill_min_interval_for_reviewers, |
17 | 19 | get_default_filter_re, |
@@ -41,12 +43,58 @@ def default_reviewer_rotation_list(self, dont_skip_person_ids=None): |
41 | 43 |
|
42 | 44 | def update_policy_state_for_assignment(self, assignee_person, add_skip=False): |
43 | 45 | """ |
44 | | - Update the internal state of a policy to reflect an assignment. |
| 46 | + Update the skip_count if the assignment was in order, and |
| 47 | + update NextReviewerInTeam. Note that NextReviewerInTeam is |
| 48 | + not used by all policies. |
45 | 49 | """ |
46 | 50 | settings = self._reviewer_settings_for(assignee_person) |
47 | 51 | settings.request_assignment_next = False |
48 | 52 | settings.save() |
| 53 | + rotation_list = self.default_reviewer_rotation_list( |
| 54 | + dont_skip_person_ids=[assignee_person.pk]) |
49 | 55 |
|
| 56 | + def reviewer_at_index(i): |
| 57 | + if not rotation_list: |
| 58 | + return None |
| 59 | + return rotation_list[i % len(rotation_list)] |
| 60 | + |
| 61 | + if not rotation_list: |
| 62 | + return |
| 63 | + |
| 64 | + rotation_list_without_skip = [r for r in rotation_list if |
| 65 | + not self._reviewer_settings_for(r).skip_next] |
| 66 | + # In order means: assigned to the first person in the rotation list with skip_next=0 |
| 67 | + # If the assignment is not in order, skip_next and NextReviewerInTeam are not modified. |
| 68 | + in_order_assignment = rotation_list_without_skip[0] == assignee_person |
| 69 | + |
| 70 | + # Loop through the list until finding the first person with skip_next=0, |
| 71 | + # who is not the current assignee. Anyone with skip_next>0 encountered before |
| 72 | + # has their skip_next decreased. |
| 73 | + current_idx = 0 |
| 74 | + if in_order_assignment: |
| 75 | + while True: |
| 76 | + current_idx_person = reviewer_at_index(current_idx) |
| 77 | + settings = self._reviewer_settings_for(current_idx_person) |
| 78 | + if settings.skip_next > 0: |
| 79 | + settings.skip_next -= 1 |
| 80 | + settings.save() |
| 81 | + elif current_idx_person != assignee_person: |
| 82 | + # NextReviewerInTeam is not used by all policies to determine |
| 83 | + # default rotation order, but updated regardless. |
| 84 | + nr = NextReviewerInTeam.objects.filter( |
| 85 | + team=self.team).first() or NextReviewerInTeam( |
| 86 | + team=self.team) |
| 87 | + nr.next_reviewer = current_idx_person |
| 88 | + nr.save() |
| 89 | + |
| 90 | + break |
| 91 | + current_idx += 1 |
| 92 | + |
| 93 | + if add_skip: |
| 94 | + settings = self._reviewer_settings_for(assignee_person) |
| 95 | + settings.skip_next += 1 |
| 96 | + settings.save() |
| 97 | + |
50 | 98 | # TODO : Change this field to deal with multiple already assigned reviewers??? |
51 | 99 | def setup_reviewer_field(self, field, review_req): |
52 | 100 | """ |
@@ -298,50 +346,11 @@ def _reviewer_settings_for_person_ids(self, person_ids): |
298 | 346 |
|
299 | 347 |
|
300 | 348 | class RotateAlphabeticallyReviewerQueuePolicy(AbstractReviewerQueuePolicy): |
301 | | - |
302 | | - def update_policy_state_for_assignment(self, assignee_person, add_skip=False): |
303 | | - super(RotateAlphabeticallyReviewerQueuePolicy, self).update_policy_state_for_assignment(assignee_person, add_skip) |
304 | | - assert assignee_person is not None |
305 | | - |
306 | | - rotation_list = self.default_reviewer_rotation_list(dont_skip_person_ids=[assignee_person.pk]) |
307 | | - |
308 | | - def reviewer_at_index(i): |
309 | | - if not rotation_list: |
310 | | - return None |
311 | | - return rotation_list[i % len(rotation_list)] |
312 | | - |
313 | | - if not rotation_list: |
314 | | - return |
315 | | - |
316 | | - rotation_list_without_skip = [r for r in rotation_list if not self._reviewer_settings_for(r).skip_next] |
317 | | - # In order means: assigned to the first person in the rotation list with skip_next=0 |
318 | | - # If the assignment is not in order, skip_next and NextReviewerInTeam are not modified. |
319 | | - in_order_assignment = rotation_list_without_skip[0] == assignee_person |
320 | | - |
321 | | - # Loop through the list until finding the first person with skip_next=0, |
322 | | - # who is not the current assignee. Anyone with skip_next>0 encountered before |
323 | | - # has their skip_next decreased. |
324 | | - current_idx = 0 |
325 | | - if in_order_assignment: |
326 | | - while True: |
327 | | - current_idx_person = reviewer_at_index(current_idx) |
328 | | - settings = self._reviewer_settings_for(current_idx_person) |
329 | | - if settings.skip_next > 0: |
330 | | - settings.skip_next -= 1 |
331 | | - settings.save() |
332 | | - elif current_idx_person != assignee_person: |
333 | | - nr = NextReviewerInTeam.objects.filter(team=self.team).first() or NextReviewerInTeam( |
334 | | - team=self.team) |
335 | | - nr.next_reviewer = current_idx_person |
336 | | - nr.save() |
337 | | - |
338 | | - break |
339 | | - current_idx += 1 |
340 | | - |
341 | | - if add_skip: |
342 | | - settings = self._reviewer_settings_for(assignee_person) |
343 | | - settings.skip_next += 1 |
344 | | - settings.save() |
| 349 | + """ |
| 350 | + A policy in which the default rotation list is based on last name, alphabetically. |
| 351 | + NextReviewerInTeam is used to store a pointer to where the queue is currently |
| 352 | + positioned. |
| 353 | + """ |
345 | 354 |
|
346 | 355 | def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip_person_ids=None): |
347 | 356 | reviewers = list(Person.objects.filter(role__name="reviewer", role__group=self.team)) |
@@ -372,3 +381,28 @@ def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip_pe |
372 | 381 |
|
373 | 382 | return rotation_list |
374 | 383 |
|
| 384 | + |
| 385 | +class LeastRecentlyUsedReviewerQueuePolicy(AbstractReviewerQueuePolicy): |
| 386 | + """ |
| 387 | + A policy where the default rotation list is based on the most recent |
| 388 | + assigned, accepted or completed review assignment. |
| 389 | + """ |
| 390 | + def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip_person_ids=None): |
| 391 | + reviewers = list(Person.objects.filter(role__name="reviewer", role__group=self.team)) |
| 392 | + assignments = ReviewAssignment.objects.filter( |
| 393 | + review_request__team=self.team, |
| 394 | + state__in=['accepted', 'assigned', 'completed'], |
| 395 | + ).order_by('assigned_on') |
| 396 | + |
| 397 | + reviewers_with_assignment = [assignment.reviewer.person for assignment in assignments] |
| 398 | + reviewers_without_assignment = set(reviewers) - set(reviewers_with_assignment) |
| 399 | + |
| 400 | + rotation_list = sorted(list(reviewers_without_assignment), key=lambda r: r.pk) |
| 401 | + rotation_list += list(OrderedDict.fromkeys(reviewers_with_assignment)) |
| 402 | + |
| 403 | + if not include_unavailable: |
| 404 | + reviewers_to_skip = self._entirely_unavailable_reviewers(dont_skip_person_ids) |
| 405 | + rotation_list = [p for p in rotation_list if p.pk not in reviewers_to_skip] |
| 406 | + |
| 407 | + return rotation_list |
| 408 | + |
0 commit comments