From 5c8be91b08ab85f2c386f733bc8e18f3cac9ffaa Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 7 Jul 2016 12:17:55 +0000 Subject: [PATCH] Improve review assignment slightly by sorting reviewers by latest review - still missing a bunch of factors, and unassignment is now temporarily gone - Legacy-Id: 11531 --- ietf/doc/views_review.py | 5 +++- ietf/group/views_review.py | 5 ++-- ietf/review/models.py | 9 +++++- ietf/review/utils.py | 60 +++++++++++++++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index e6400516f..b80219d49 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -17,7 +17,8 @@ from ietf.person.fields import PersonEmailChoiceField, SearchablePersonField from ietf.review.utils import (active_review_teams, assign_review_request_to_reviewer, can_request_review_of_doc, can_manage_review_requests_for_team, email_review_request_change, make_new_review_request_from_existing, - close_review_request_states, close_review_request) + close_review_request_states, close_review_request, + construct_review_request_assignment_choices) from ietf.review import mailarch from ietf.utils.fields import DatepickerDateField from ietf.utils.text import skip_prefix @@ -212,6 +213,8 @@ class AssignReviewerForm(forms.Form): f.queryset = f.queryset.filter(role__name="reviewer", role__group=review_req.team) if review_req.reviewer: f.initial = review_req.reviewer_id + f.choices = construct_review_request_assignment_choices(f.queryset, review_req.team, review_req) + @login_required def assign_reviewer(request, name, request_id): diff --git a/ietf/group/views_review.py b/ietf/group/views_review.py index e5e324e03..4bb51d387 100644 --- a/ietf/group/views_review.py +++ b/ietf/group/views_review.py @@ -8,7 +8,8 @@ from ietf.review.utils import (can_manage_review_requests_for_team, close_review extract_revision_ordered_review_requests_for_documents, assign_review_request_to_reviewer, close_review_request, -# email_review_request_change, make_new_review_request_from_existing, + construct_review_request_assignment_choices, +# make_new_review_request_from_existing, suggested_review_requests_for_team) from ietf.group.utils import get_group_or_404 from ietf.person.fields import PersonEmailChoiceField @@ -58,7 +59,7 @@ class ManageReviewRequestForm(forms.Form): role__name="reviewer", role__group=review_req.team, ) - + self.fields["reviewer"].choices = construct_review_request_assignment_choices(self.fields["reviewer"].queryset, review_req.team, review_req) self.fields["reviewer"].widget.attrs["class"] = "form-control input-sm" if self.is_bound: diff --git a/ietf/review/models.py b/ietf/review/models.py index 7f39f96c3..f6fbc632e 100644 --- a/ietf/review/models.py +++ b/ietf/review/models.py @@ -11,7 +11,14 @@ class Reviewer(models.Model): reviewer and team.""" team = models.ForeignKey(Group) person = models.ForeignKey(Person) - frequency = models.IntegerField(default=30, help_text="Can review every N days") + FREQUENCIES = [ + (7, "Once per week"), + (14, "Once per fortnight"), + (30, "Once per month"), + (60, "Once per two months"), + (90, "Once per quarter"), + ] + frequency = models.IntegerField(default=30, help_text="Can review every N days", choices=FREQUENCIES) unavailable_until = models.DateTimeField(blank=True, null=True, help_text="When will this reviewer be available again") filter_re = models.CharField(max_length=255, blank=True) skip_next = models.IntegerField(default=0, help_text="Skip the next N review assignments") diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 91df63d8f..b27a13cf8 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -2,13 +2,14 @@ import datetime from collections import defaultdict from django.contrib.sites.models import Site +from django.db import models from ietf.group.models import Group, Role from ietf.doc.models import Document, DocEvent, State, LastCallDocEvent from ietf.iesg.models import TelechatDate from ietf.person.models import Person from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream -from ietf.review.models import ReviewRequest, ReviewRequestStateName, ReviewTypeName +from ietf.review.models import ReviewRequest, ReviewRequestStateName, ReviewTypeName, Reviewer from ietf.utils.mail import send_mail from ietf.doc.utils import extract_complete_replaces_ancestor_mapping_for_docs @@ -273,3 +274,60 @@ def extract_revision_ordered_review_requests_for_documents(queryset, names): front = next_front return res + +def construct_review_request_assignment_choices(possible_emails, team, review_req=None): + possible_emails = list(possible_emails) + + reviewers = { r.person_id: r for r in Reviewer.objects.filter(team=team, person__in=[e.person_id for e in possible_emails]) } + + latest_assignment_for_reviewer = dict(ReviewRequest.objects.filter( + reviewer__in=possible_emails, + ).values_list("reviewer").annotate(models.Max("time"))) + + now = datetime.datetime.now() + + rankings = [] + for e in possible_emails: + reviewer = reviewers.get(e.person_id) + if not reviewer: + reviewer = Reviewer() + + days_past = None + latest = latest_assignment_for_reviewer.get(e.pk) + if latest is not None: + days_past = (now - latest).days - reviewer.frequency + + # FIXME: + # positive: (Perhaps do these separately? As initial values?) + # has done review of previous rev + # would like to review + + # blocks: + # connections to doc + filter_re + # has rejected same request/completed partial review + # is unavailable_until + + if days_past is None: + ready_for = "first time" + else: + d = int(round(days_past)) + if d > 0: + ready_for = "ready for {} {}".format(d, "day" if d == 1 else "days") + else: + d = -d + ready_for = "frequency exceeded - ready in {} {}".format(d, "day" if d == 1 else "days") + + label = "{}: {}".format(e.person, ready_for) + + rank = (-100000 if days_past is None else -days_past,) + + rankings.append({ + "email": e, + "rank": rank, + "label": label, + }) + + rankings.sort(key=lambda r: r["rank"]) + + # FIXME: empty choices + return [(r["email"].pk, r["label"]) for r in rankings]