Checkpoint. ReviewTests pass, but others still fail of course. Next step is to address stats.

- Legacy-Id: 16021
This commit is contained in:
Robert Sparks 2019-03-12 17:48:19 +00:00
parent b85052fe63
commit 073ce55f6c
11 changed files with 255 additions and 87 deletions

View file

@ -152,6 +152,7 @@ class ReviewTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
content = unicontent(r)
#debug.show('unicontent(r)')
self.assertTrue("{} Review".format(review_req.type.name) in content)
def test_review_request(self):
@ -181,8 +182,8 @@ class ReviewTests(TestCase):
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first())
close_url = urlreverse('ietf.doc.views_review.close_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
@ -290,7 +291,8 @@ class ReviewTests(TestCase):
settings, _ = ReviewerSettings.objects.get_or_create(team=team, person=reviewers[2])
settings.min_interval = 30
settings.save()
ReviewRequest.objects.create(team=team, doc=doc, type_id="early", state_id="accepted", deadline=today, requested_by=reviewers[0], reviewer=reviewers[2].email_set.first())
req = ReviewRequest.objects.create(team=team, doc=doc, type_id="early", state_id="assigned", deadline=today, requested_by=reviewers[0])
ReviewAssignmentFactory(review_request=req, state_id="accepted", reviewer = reviewers[2].email_set.first(),assigned_on = req.time)
possibly_advance_next_reviewer_for_team(team, assigned_review_to_person_id=reviewers[3].pk)
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[4])
self.assertEqual(get_skip_next(reviewers[0]), 0)
@ -317,17 +319,24 @@ class ReviewTests(TestCase):
doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
# previous review
ReviewRequest.objects.create(
req = ReviewRequestFactory(
time=datetime.datetime.now() - datetime.timedelta(days=100),
requested_by=Person.objects.get(name="(System)"),
doc=doc,
type=ReviewTypeName.objects.get(slug="early"),
type_id='early',
team=review_req.team,
state=ReviewRequestStateName.objects.get(slug="completed"),
result_id='serious-issues',
reviewed_rev="01",
state_id='assigned',
requested_rev="01",
deadline=datetime.date.today() - datetime.timedelta(days=80),
)
ReviewAssignmentFactory(
review_request = req,
state_id='completed',
result_id='serious-issues',
reviewer=reviewer_email,
reviewed_rev="01",
assigned_on=req.time,
completed_on=req.time + datetime.timedelta(days=10),
)
reviewer_settings = ReviewerSettings.objects.get(person__email=reviewer_email, team=review_req.team)
@ -390,37 +399,25 @@ class ReviewTests(TestCase):
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "requested")
self.assertEqual(review_req.reviewer, reviewer)
self.assertEqual(review_req.state_id, "assigned")
self.assertEqual(review_req.reviewassignment_set.count(),1)
assignment = review_req.reviewassignment_set.first()
self.assertEqual(assignment.reviewer, reviewer)
self.assertEqual(assignment.state_id, "requested")
self.assertEqual(len(outbox), 1)
self.assertTrue("assigned" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertEqual(NextReviewerInTeam.objects.get(team=review_req.team).next_reviewer, rotation_list[1])
# re-assign
empty_outbox()
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
review_req.save()
reviewer = Email.objects.filter(role__name="reviewer", role__group=review_req.team, person=rotation_list[1]).first()
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk, "add_skip": 1 })
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "requested") # check that state is reset
self.assertEqual(review_req.reviewer, reviewer)
self.assertEqual(len(outbox), 2)
self.assertTrue("cancelled your assignment" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertTrue("assigned" in outbox[1].get_payload(decode=True).decode("utf-8"))
self.assertEqual(ReviewerSettings.objects.get(person=reviewer.person).skip_next, 1)
def test_accept_reviewer_assignment(self):
doc = WgDraftFactory(group__acronym='mars',rev='01')
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='requested',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
assignment = ReviewAssignmentFactory(review_request=review_req, state_id='requested', reviewer=rev_role.person.email_set.first())
url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
username = review_req.reviewer.person.user.username
username = assignment.reviewer.person.user.username
self.client.login(username=username, password=username + "+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@ -431,8 +428,8 @@ class ReviewTests(TestCase):
r = self.client.post(url, { "action": "accept" })
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "accepted")
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "accepted")
def test_reject_reviewer_assignment(self):
doc = WgDraftFactory(group__acronym='mars',rev='01')
@ -682,6 +679,7 @@ class ReviewTests(TestCase):
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "completed")
self.assertNotEqual(assignment.completed_on, None)
with open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
self.assertEqual(f.read(), "This is a review\nwith two lines")

View file

@ -64,8 +64,8 @@ from ietf.doc.mails import email_comment
from ietf.mailtrigger.utils import gather_relevant_expansions
from ietf.meeting.models import Session
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions
from ietf.review.models import ReviewRequest
from ietf.review.utils import can_request_review_of_doc, review_requests_to_list_for_docs
from ietf.review.models import ReviewRequest, ReviewAssignment
from ietf.review.utils import can_request_review_of_doc, review_requests_to_list_for_docs, review_assignments_to_list_for_docs
from ietf.review.utils import no_review_from_teams_on_doc
from ietf.utils import markup_txt, log
from ietf.utils.text import maybe_split
@ -382,7 +382,7 @@ def document_main(request, name, rev=None):
published = doc.latest_event(type="published_rfc")
started_iesg_process = doc.latest_event(type="started_iesg_process")
review_requests = review_requests_to_list_for_docs([doc]).get(doc.pk, [])
review_assignments = review_assignments_to_list_for_docs([doc]).get(doc.pk, [])
no_review_from_teams = no_review_from_teams_on_doc(doc, rev or doc.rev)
return render(request, "doc/document_draft.html",
@ -446,7 +446,7 @@ def document_main(request, name, rev=None):
search_archive=search_archive,
actions=actions,
presentations=presentations,
review_requests=review_requests,
review_assignments=review_assignments,
no_review_from_teams=no_review_from_teams,
))
@ -598,12 +598,11 @@ def document_main(request, name, rev=None):
# If we want to go back to using markup_txt.markup_unicode, call it explicitly here like this:
# content = markup_txt.markup_unicode(content, split=False, width=80)
# TODO - verify if this shows other reviews from the same team - I bet it doesn't
review_req = ReviewRequest.objects.filter(reviewassignment__review=doc.name).first()
review_assignment = ReviewAssignment.objects.filter(review=doc.name).first()
other_reviews = []
if review_req:
other_reviews = [r for r in review_requests_to_list_for_docs([review_req.doc]).get(doc.pk, []) if r != review_req]
if review_assignment:
other_reviews = [r for r in review_assignments_to_list_for_docs([review_assignment.review_request.doc]).get(doc.pk, []) if r != review_assignment]
return render(request, "doc/document_review.html",
dict(doc=doc,
@ -612,7 +611,7 @@ def document_main(request, name, rev=None):
revisions=revisions,
latest_rev=latest_rev,
snapshot=snapshot,
review_req=review_req,
review_req=review_assignment.review_request,
other_reviews=other_reviews,
))

View file

@ -186,11 +186,11 @@ def review_request(request, name, request_id):
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
can_close_request = (review_req.state_id in ["requested", "accepted"]
can_close_request = (review_req.state_id in ["requested", "assigned"]
and (is_authorized_in_doc_stream(request.user, doc)
or can_manage_request))
can_assign_reviewer = (review_req.state_id in ["requested", "accepted"]
can_assign_reviewer = (review_req.state_id in ["requested", "assigned"]
and can_manage_request)
can_edit_comment = can_request_review_of_doc(request.user, doc)
@ -210,6 +210,9 @@ def review_request(request, name, request_id):
assignment.can_complete_review = (assignment.state_id in ["requested", "accepted", "overtaken", "no-response", "part-completed", "completed"]
and (assignment.is_reviewer or can_manage_request))
# This implementation means if a reviewer accepts one assignment for a review_request, he accepts all assigned to him (for that request)
# This problematic - it's a bug (probably) for the same person to have more than one assignment for the same request.
# It is, however unintuitive, and acceptance should be refactored to be something that works on assignments, not requests
if request.method == "POST" and request.POST.get("action") == "accept":
for assignment in assignments:
if assignment.can_accept_reviewer_assignment:
@ -245,7 +248,7 @@ class CloseReviewRequestForm(forms.Form):
@login_required
def close_request(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "assigned"])
can_request = is_authorized_in_doc_stream(request.user, doc)
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
@ -265,6 +268,7 @@ def close_request(request, name, request_id):
return render(request, 'doc/review/close_request.html', {
'doc': doc,
'review_req': review_req,
'assignments': review_req.reviewassignment_set.all(),
'form': form,
})
@ -745,7 +749,7 @@ def edit_deadline(request, name, request_id):
if form.is_valid():
if form.cleaned_data['deadline'] != old_deadline:
form.save()
subject = "Deadline changed: {} {} review of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(), review_req.doc.name, review_req.reviewed_rev)
subject = "Deadline changed: {} {} review of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(), review_req.doc.name, review_req.requested_rev)
msg = render_to_string("review/deadline_changed.txt", {
"review_req": review_req,
"old_deadline": old_deadline,

View file

@ -156,7 +156,7 @@ def fill_in_agenda_docs(date, sections, docs=None):
docs = docs.select_related("stream", "group").distinct()
fill_in_telechat_date(docs)
review_requests_for_docs = review_requests_to_list_for_docs(docs)
review_assignments_for_docs = review_assignments_to_list_for_docs(docs)
for doc in docs:
if doc.telechat_date() != date:
@ -182,7 +182,7 @@ def fill_in_agenda_docs(date, sections, docs=None):
if e and (e.consensus != None):
doc.consensus = "Yes" if e.consensus else "No"
doc.review_requests = review_requests_for_docs.get(doc.pk, [])
doc.review_assignments = review_assignments_for_docs.get(doc.pk, [])
elif doc.type_id == "conflrev":
doc.conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document
elif doc.type_id == "charter":

View file

@ -46,6 +46,7 @@ class ReviewAssignmentFactory(factory.DjangoModelFactory):
review_request = factory.SubFactory('ietf.review.factories.ReviewRequestFactory')
state_id = 'requested'
reviewer = factory.SubFactory('ietf.person.factories.EmailFactory')
assigned_on = datetime.datetime.now()
class ReviewerSettingsFactory(factory.DjangoModelFactory):
class Meta:

View file

@ -8,7 +8,7 @@ from django.contrib.sites.models import Site
import debug # pyflakes:ignore
from ietf.group.models import Group, Role
from ietf.doc.models import (Document, ReviewRequestDocEvent, State,
from ietf.doc.models import (Document, ReviewRequestDocEvent, ReviewAssignmentDocEvent, State,
LastCallDocEvent, TelechatDocEvent,
DocumentAuthor, DocAlias)
from ietf.iesg.models import TelechatDate
@ -24,7 +24,7 @@ def active_review_teams():
return Group.objects.filter(reviewteamsettings__isnull=False,state="active")
def close_review_request_states():
return ReviewRequestStateName.objects.filter(used=True).exclude(slug__in=["requested", "accepted", "rejected", "part-completed", "completed"])
return ReviewRequestStateName.objects.filter(used=True).exclude(slug__in=["requested", "assigned"])
def can_request_review_of_doc(user, doc):
if not user.is_authenticated:
@ -50,6 +50,16 @@ def can_access_review_stats_for_team(user, team):
return (Role.objects.filter(name__in=("secr", "reviewer"), person__user=user, group=team).exists()
or has_role(user, ["Secretariat", "Area Director"]))
def review_assignments_to_list_for_docs(docs):
assignment_qs = ReviewAssignment.objects.filter(
state__in=["requested", "accepted", "part-completed", "completed"],
).prefetch_related("result")
doc_names = [d.name for d in docs]
return extract_revision_ordered_review_assignments_for_documents_and_replaced(assignment_qs, doc_names)
# TODO : remove this when there are no callers
def review_requests_to_list_for_docs(docs):
request_qs = ReviewRequest.objects.filter(
state__in=["requested", "accepted", "part-completed", "completed"],
@ -67,7 +77,7 @@ def augment_review_requests_with_events(review_reqs):
def no_review_from_teams_on_doc(doc, rev):
return Group.objects.filter(
reviewrequest__doc__name=doc.name,
reviewrequest__reviewed_rev=rev,
reviewrequest__requested_rev=rev,
reviewrequest__state__slug="no-review-version",
).distinct()
@ -140,9 +150,9 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
"""Returns person_id -> days needed until min_interval is fulfilled
for reviewer (in case it is necessary to wait, otherwise reviewer
is absent in result)."""
latest_assignments = dict(ReviewRequest.objects.filter(
team=team,
).values_list("reviewer__person").annotate(Max("time")))
latest_assignments = dict(ReviewAssignment.objects.filter(
review_request__team=team,
).values_list("reviewer__person").annotate(Max("assigned_on")))
min_intervals = dict(ReviewerSettings.objects.filter(team=team).values_list("person_id", "min_interval"))
@ -161,12 +171,78 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
return res
ReviewAssignmentData = namedtuple("ReviewAssignmentData", [
"assignment_pk", "doc", "doc_pages", "req_time", "state", "assigned_time", "deadline", "reviewed_rev", "result", "team", "reviewer",
"late_days",
"request_to_assignment_days", "assignment_to_closure_days", "request_to_closure_days"])
# TODO - see if this becomes dead
ReviewRequestData = namedtuple("ReviewRequestData", [
"req_pk", "doc", "doc_pages", "req_time", "state", "assigned_time", "deadline", "reviewed_rev", "result", "team", "reviewer",
"late_days",
"request_to_assignment_days", "assignment_to_closure_days", "request_to_closure_days"])
def extract_review_assignment_data(teams=None, reviewers=None, time_from=None, time_to=None, ordering=[]):
"""Yield data on each review assignment, sorted by (*ordering, assigned_on)
for easy use with itertools.groupby. Valid entries in *ordering are "team" and "reviewer"."""
filters = Q()
if teams:
filters &= Q(review_request__team__in=teams)
if reviewers:
filters &= Q(reviewer__person__in=reviewers)
if time_from:
filters &= Q(assigned_on__gte=time_from)
if time_to:
filters &= Q(assigned_on__lte=time_to)
# This doesn't do the left-outer join on docevent that the previous code did. These variables could be renamed
event_qs = ReviewAssignment.objects.filter(filters)
event_qs = event_qs.values_list(
"pk", "review_request__doc", "review_request__doc__pages", "review_request__time", "state", "review_request__deadline", "reviewed_rev", "result", "review_request__team",
"reviewer__person", "assigned_on", "completed_on"
)
event_qs = event_qs.order_by(*[o.replace("reviewer", "reviewer__person") for o in ordering] + ["assigned_on", "pk", "completed_on"])
def positive_days(time_from, time_to):
if time_from is None or time_to is None:
return None
delta = time_to - time_from
seconds = delta.total_seconds()
if seconds > 0:
return seconds / float(24 * 60 * 60)
else:
return 0.0
requested_time = assigned_time = closed_time = None
for assignment in event_qs:
assignment_pk, doc, doc_pages, req_time, state, deadline, reviewed_rev, result, team, reviewer, assigned_on, completed_on = assignment
requested_time = req_time
assigned_time = assigned_on
closed_time = completed_on
late_days = positive_days(datetime.datetime.combine(deadline, datetime.time.max), closed_time)
request_to_assignment_days = positive_days(requested_time, assigned_time)
assignment_to_closure_days = positive_days(assigned_time, closed_time)
request_to_closure_days = positive_days(requested_time, closed_time)
d = ReviewAssignmentData(assignment_pk, doc, doc_pages, req_time, state, assigned_time, deadline, reviewed_rev, result, team, reviewer,
late_days, request_to_assignment_days, assignment_to_closure_days,
request_to_closure_days)
yield d
# TODO - see if this is dead code
def extract_review_request_data(teams=None, reviewers=None, time_from=None, time_to=None, ordering=[]):
"""Yield data on each review request, sorted by (*ordering, time)
for easy use with itertools.groupby. Valid entries in *ordering are "team" and "reviewer"."""
@ -190,11 +266,11 @@ def extract_review_request_data(teams=None, reviewers=None, time_from=None, time
# left outer join with RequestRequestDocEvent for request/assign/close time
event_qs = event_qs.values_list(
"pk", "doc", "doc__pages", "time", "state", "deadline", "reviewed_rev", "result", "team",
"reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type"
"pk", "doc", "doc__pages", "time", "state", "deadline", "reviewassignment__reviewed_rev", "reviewassignment__result", "team",
"reviewassignment__reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type"
)
event_qs = event_qs.order_by(*[o.replace("reviewer", "reviewer__person") for o in ordering] + ["time", "pk", "-reviewrequestdocevent__time"])
event_qs = event_qs.order_by(*[o.replace("reviewer", "reviewassignment__reviewer__person") for o in ordering] + ["time", "pk", "-reviewrequestdocevent__time"])
def positive_days(time_from, time_to):
if time_from is None or time_to is None:
@ -304,6 +380,24 @@ def sum_raw_review_request_aggregations(raw_aggregations):
return state_dict, late_state_dict, result_dict, assignment_to_closure_days_list, assignment_to_closure_days_count
def latest_review_assignments_for_reviewers(team, days_back=365):
"""Collect and return stats for reviewers on latest assignments, in
extract_review_assignment_data format."""
extracted_data = extract_review_assignment_data(
teams=[team],
time_from=datetime.date.today() - datetime.timedelta(days=days_back),
ordering=["reviewer"],
)
assignment_data_for_reviewers = {
reviewer: list(reversed(list(req_data_items)))
for reviewer, req_data_items in itertools.groupby(extracted_data, key=lambda data: data.reviewer)
}
return assignment_data_for_reviewers
# TODO - see if this is dead code
def latest_review_requests_for_reviewers(team, days_back=365):
"""Collect and return stats for reviewers on latest requests, in
extract_review_request_data format."""
@ -397,7 +491,8 @@ def email_review_request_change(request, review_req, subject, msg, by, notify_se
else:
extract_email_addresses(Role.objects.filter(name="secr", group=review_req.team).distinct())
if notify_reviewer:
extract_email_addresses([review_req.reviewer])
for assignment in review_req.reviewassignment_set.all():
extract_email_addresses([assignment.reviewer])
if notify_requested_by:
extract_email_addresses([review_req.requested_by.email()])
@ -462,19 +557,17 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa
if review_req.reviewassignment_set.filter(reviewer=reviewer).exists():
return
# Unassignment now has to be explicit
#if review_req.reviewer:
# email_review_request_change(
# request, review_req,
# "Unassigned from review of %s" % review_req.doc.name,
# "%s has cancelled your assignment to the review." % request.user.person,
# by=request.user.person, notify_secretary=False, notify_reviewer=True, notify_requested_by=False)
# Note that assigning a review no longer unassigns other reviews
review_req.reviewassignment_set.create(state_id='requested', reviewer = reviewer, assigned_on = datetime.datetime.now())
if review_req.state_id != 'assigned':
review_req.state_id = 'assigned'
review_req.save()
if reviewer:
possibly_advance_next_reviewer_for_team(review_req.team, reviewer.person_id, add_skip)
# TODO - should these be ReviewAssignmentDocEvents?
ReviewRequestDocEvent.objects.create(
type="assigned_review_request",
doc=review_req.doc,
@ -483,7 +576,7 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa
desc="Request for {} review by {} is assigned to {}".format(
review_req.type.name,
review_req.team.acronym.upper(),
review_req.reviewer.person if review_req.reviewer else "(None)",
reviewer.person if reviewer else "(None)",
),
review_request=review_req,
state=None,
@ -491,9 +584,9 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa
msg = "%s has assigned you as a reviewer for this document." % request.user.person.ascii
prev_team_reviews = ReviewAssignment.objects.filter(
doc=review_req.doc,
review_request__doc=review_req.doc,
state="completed",
team=review_req.team,
review_request__team=review_req.team,
)
if prev_team_reviews.exists():
msg = msg + '\n\nThis team has completed other reviews of this document:\n'
@ -575,6 +668,19 @@ def close_review_request(request, review_req, close_state):
state=review_req.state,
)
for assignment in review_req.reviewassignment_set.filter(state_id__in=['requested','accepted']):
assignment.state_id = 'withdrawn'
assignment.save()
ReviewAssignmentDocEvent.objects.create(
type='closed_review_request',
doc=review_req.doc,
rev=review_req.doc.rev,
by=request.user.person,
desc="Request closed, assignment withdrawn: {} {} {} review".format(assignment.reviewer.person.plain_name, assignment.review_request.type.name, assignment.review_request.team.acronym.upper()),
review_assignment=assignment,
state=assignment.state,
)
email_review_request_change(
request, review_req,
"Closed review request for {}: {}".format(review_req.doc.name, close_state.name),
@ -693,6 +799,53 @@ def suggested_review_requests_for_team(team):
res.sort(key=lambda r: (r.deadline, r.doc_id), reverse=True)
return res
def extract_revision_ordered_review_assignments_for_documents_and_replaced(review_assignment_queryset, names):
"""Extracts all review assignments for document names (including replaced ancestors), return them neatly sorted."""
names = set(names)
replaces = extract_complete_replaces_ancestor_mapping_for_docs(names)
assignments_for_each_doc = defaultdict(list)
for r in review_assignment_queryset.filter(review_request__doc__in=set(e for l in replaces.itervalues() for e in l) | names).order_by("-reviewed_rev","-assigned_on", "-id").iterator():
assignments_for_each_doc[r.review_request.doc_id].append(r)
# now collect in breadth-first order to keep the revision order intact
res = defaultdict(list)
for name in names:
front = replaces.get(name, [])
res[name].extend(assignments_for_each_doc.get(name, []))
seen = set()
while front:
replaces_assignments = []
next_front = []
for replaces_name in front:
if replaces_name in seen:
continue
seen.add(replaces_name)
assignments = assignments_for_each_doc.get(replaces_name, [])
if assignments:
replaces_assignments.append(assignments)
next_front.extend(replaces.get(replaces_name, []))
# in case there are multiple replaces, move the ones with
# the latest reviews up front
replaces_assignments.sort(key=lambda l: l[0].assigned_on, reverse=True)
for assignments in replaces_assignments:
res[name].extend(assignments)
# move one level down
front = next_front
return res
# TODO - remove when there are no remaining callers
def extract_revision_ordered_review_requests_for_documents_and_replaced(review_request_queryset, names):
"""Extracts all review requests for document names (including replaced ancestors), return them neatly sorted."""
@ -740,10 +893,12 @@ def extract_revision_ordered_review_requests_for_documents_and_replaced(review_r
return res
# TODO : Change this field to deal with multiple already assigned reviewers
def setup_reviewer_field(field, review_req):
field.queryset = field.queryset.filter(role__name="reviewer", role__group=review_req.team)
if review_req.reviewer:
field.initial = review_req.reviewer_id
one_assignment = review_req.reviewassignment_set.first()
if one_assignment:
field.initial = one_assignment.reviewer_id
choices = make_assignment_choices(field.queryset, review_req)
if not field.required:
@ -788,15 +943,15 @@ def make_assignment_choices(email_queryset, review_req):
# previous review of document
has_reviewed_previous = ReviewRequest.objects.filter(
doc=doc,
reviewer__person__in=possible_person_ids,
state="completed",
reviewassignment__reviewer__person__in=possible_person_ids,
reviewassignment__state="completed",
team=team,
)
).distinct()
if review_req.pk is not None:
has_reviewed_previous = has_reviewed_previous.exclude(pk=review_req.pk)
has_reviewed_previous = set(has_reviewed_previous.values_list("reviewer__person", flat=True))
has_reviewed_previous = set(has_reviewed_previous.values_list("reviewassignment__reviewer__person", flat=True))
# review wishes
wish_to_review = set(ReviewWish.objects.filter(team=team, person__in=possible_person_ids, doc=doc).values_list("person", flat=True))
@ -816,7 +971,7 @@ def make_assignment_choices(email_queryset, review_req):
unavailable_periods = current_unavailable_periods_for_reviewers(team)
# reviewers statistics
req_data_for_reviewers = latest_review_requests_for_reviewers(team)
assignment_data_for_reviewers = latest_review_assignments_for_reviewers(team)
ranking = []
for e in possible_emails:
@ -871,21 +1026,21 @@ def make_assignment_choices(email_queryset, review_req):
# stats
stats = []
req_data = req_data_for_reviewers.get(e.person_id, [])
assignment_data = assignment_data_for_reviewers.get(e.person_id, [])
currently_open = sum(1 for d in req_data if d.state in ["requested", "accepted"])
pages = sum(rd.doc_pages for rd in req_data if rd.state in ["requested", "accepted"])
currently_open = sum(1 for d in assignment_data if d.state in ["requested", "accepted"])
pages = sum(rd.doc_pages for rd in assignment_data if rd.state in ["requested", "accepted"])
if currently_open > 0:
stats.append("currently {count} open, {pages} pages".format(count=currently_open, pages=pages))
could_have_completed = [d for d in req_data if d.state in ["part-completed", "completed", "no-response"]]
could_have_completed = [d for d in assignment_data if d.state in ["part-completed", "completed", "no-response"]]
if could_have_completed:
no_response = len([d for d in req_data if d.state == 'no-response'])
no_response = len([d for d in assignment_data if d.state == 'no-response'])
if no_response:
stats.append("%s no response" % no_response)
part_completed = len([d for d in req_data if d.state == 'part-completed'])
part_completed = len([d for d in assignment_data if d.state == 'part-completed'])
if part_completed:
stats.append("%s partially complete" % part_completed)
completed = len([d for d in req_data if d.state == 'completed'])
completed = len([d for d in assignment_data if d.state == 'completed'])
if completed:
stats.append("%s fully completed" % completed)

View file

@ -204,14 +204,14 @@
{% endif %}
{% endfor %}
{% if review_requests or can_request_review %}
{% if review_assignments or can_request_review %}
<tr>
<th></th>
<th>Reviews</th>
<td class="edit"></td>
<td>
{% for review_request in review_requests %}
{% include "doc/review_request_summary.html" with current_doc_name=doc.name current_rev=doc.rev %}
{% for review_assignment in review_assignments %}
{% include "doc/review_assignment_summary.html" with current_doc_name=doc.name current_rev=doc.rev %}
{% endfor %}
{% if no_review_from_teams %}

View file

@ -43,8 +43,8 @@
<th>Other reviews</th>
<td class="edit"></td>
<td>
{% for review_request in other_reviews %}
{% include "doc/review_request_summary.html" with current_doc_name=review_req.doc_id current_rev=review_req.reviewed_rev %}
{% for review_assignment in other_reviews %}
{% include "doc/review_assignment_summary.html" with current_doc_name=review_assignemnt.review_request.doc_id current_rev=review_assignment.reviewed_rev %}
{% endfor %}
</td>
</tr>

View file

@ -1,5 +1,4 @@
{# Copyright The IETF Trust 2017, All Rights Reserved #}{% load origin bootstrap3 %}{% origin %}
<p>I got included</p>
<table class="table table-condensed">
<tbody class="meta">
<tr>

View file

@ -0,0 +1,12 @@
<div class="review-assignment-summary">
{% if review_assignment.state_id == "completed" or review_assignment.state_id == "part-completed" %}
<a href="{% if review_assignment.review %}{% url "ietf.doc.views_doc.document_main" review_assignment.review.name %}{% else %}{% url "ietf.doc.views_review.review_request" review_assignment.review_request.doc_id review_assignment.review_request.pk %}{% endif %}">
{{ review_assignment.review_request.team.acronym|upper }} {{ review_assignment.review_request.type.name }} Review{% if review_assignment.reviewed_rev and review_assignment.reviewed_rev != current_rev or review_assignment.review_request.doc_id != current_doc_name %} (of {% if review_assignment.review_request.doc_id != current_doc_name %}{{ review_assignment.review_request.doc_id }}{% endif %}-{{ review_assignment.reviewed_rev }}){% endif %}{% if review_assignment.result %}:
{{ review_assignment.result.name }}{% endif %} {% if review_assignment.state_id == "part-completed" %}(partially completed){% endif %}
</a>
{% else %}
<i>
<a href="{% url "ietf.doc.views_review.review_request" review_assignment.review_request.doc_id review_assignment.review_request.pk %}">{{ review_assignment.review_request.team.acronym|upper }} {{ review_assignment.review_request.type.name }} Review
- due: {{ review_assignment.review_request.deadline|date:"Y-m-d" }}</a></i>
{% endif %}
</div>

View file

@ -50,8 +50,8 @@
{% if doc.review_requests %}
<dt>Reviews</dt>
<dd>
{% for review_request in doc.review_requests %}
{% include "doc/review_request_summary.html" with current_doc_name=doc.name current_rev=doc.rev %}
{% for review_assignment in doc.review_assignments %}
{% include "doc/review_assignment_summary.html" with current_doc_name=doc.name current_rev=doc.rev %}
{% endfor %}
</dd>
{% endif %}