diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py
index de2c8df9f..951c72a72 100644
--- a/ietf/doc/resources.py
+++ b/ietf/doc/resources.py
@@ -11,7 +11,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
DocumentAuthor, DocEvent, StateDocEvent, DocHistory, ConsensusDocEvent, DocAlias,
TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent,
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
- RelatedDocHistory, BallotPositionDocEvent)
+ RelatedDocHistory, BallotPositionDocEvent, ReviewRequestDocEvent)
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
@@ -513,3 +513,32 @@ class BallotPositionDocEventResource(ModelResource):
}
api.doc.register(BallotPositionDocEventResource())
+
+
+from ietf.person.resources import PersonResource
+from ietf.review.resources import ReviewRequestResource
+from ietf.name.resources import ReviewRequestStateNameResource
+class ReviewRequestDocEventResource(ModelResource):
+ by = ToOneField(PersonResource, 'by')
+ doc = ToOneField(DocumentResource, 'doc')
+ docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr')
+ review_request = ToOneField(ReviewRequestResource, 'review_request')
+ state = ToOneField(ReviewRequestStateNameResource, 'state', null=True)
+ class Meta:
+ queryset = ReviewRequestDocEvent.objects.all()
+ serializer = api.Serializer()
+ cache = SimpleCache()
+ #resource_name = 'reviewrequestdocevent'
+ filtering = {
+ "id": ALL,
+ "time": ALL,
+ "type": ALL,
+ "desc": ALL,
+ "by": ALL_WITH_RELATIONS,
+ "doc": ALL_WITH_RELATIONS,
+ "docevent_ptr": ALL_WITH_RELATIONS,
+ "review_request": ALL_WITH_RELATIONS,
+ "state": ALL_WITH_RELATIONS,
+ }
+api.doc.register(ReviewRequestDocEventResource())
+
diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py
index 6be685ea9..b93e60f11 100644
--- a/ietf/group/tests_review.py
+++ b/ietf/group/tests_review.py
@@ -9,8 +9,9 @@ from ietf.utils.test_utils import login_testing_unauthorized, TestCase, uniconte
from ietf.doc.models import TelechatDocEvent
from ietf.iesg.models import TelechatDate
from ietf.person.models import Email, Person
-from ietf.review.models import ReviewRequest, ReviewRequestStateName, ReviewerSettings, UnavailablePeriod
+from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod
from ietf.review.utils import suggested_review_requests_for_team
+from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName
import ietf.group.views_review
from ietf.utils.mail import outbox, empty_outbox
@@ -32,8 +33,8 @@ class ReviewTests(TestCase):
url = urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym })
# close request, listed under closed
- review_req.state_id = "completed"
- review_req.result_id = "ready"
+ review_req.state = ReviewRequestStateName.objects.get(slug="completed")
+ review_req.result = ReviewResultName.objects.get(slug="ready")
review_req.save()
r = self.client.get(url)
@@ -97,8 +98,50 @@ class ReviewTests(TestCase):
review_req.save()
self.assertEqual(len(suggested_review_requests_for_team(team)), 1)
-
+ def test_reviewer_overview(self):
+ doc = make_test_data()
+ review_req1 = make_review_data(doc)
+ review_req1.state = ReviewRequestStateName.objects.get(slug="completed")
+ review_req1.save()
+
+ reviewer = review_req1.reviewer.person
+
+ ReviewRequest.objects.create(
+ doc=review_req1.doc,
+ team=review_req1.team,
+ type_id="early",
+ deadline=datetime.date.today() + datetime.timedelta(days=30),
+ state_id="accepted",
+ reviewer=review_req1.reviewer,
+ requested_by=Person.objects.get(user__username="plain"),
+ )
+
+ UnavailablePeriod.objects.create(
+ team=review_req1.team,
+ person=reviewer,
+ start_date=datetime.date.today() - datetime.timedelta(days=10),
+ availability="unavailable",
+ )
+
+ settings = ReviewerSettings.objects.get(person=reviewer)
+ settings.skip_next = 1
+ settings.save()
+
+ group = review_req1.team
+
+ url = urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
+
+ # get
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(unicode(reviewer) in unicontent(r))
+ self.assertTrue(review_req1.doc.name in unicontent(r))
+
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+
def test_manage_review_requests(self):
doc = make_test_data()
review_req1 = make_review_data(doc)
@@ -128,6 +171,33 @@ class ReviewTests(TestCase):
requested_by=Person.objects.get(user__username="plain"),
)
+ # previous reviews
+ ReviewRequest.objects.create(
+ time=datetime.datetime.now() - datetime.timedelta(days=100),
+ requested_by=Person.objects.get(name="(System)"),
+ doc=doc,
+ type=ReviewTypeName.objects.get(slug="early"),
+ team=review_req1.team,
+ state=ReviewRequestStateName.objects.get(slug="completed"),
+ result=ReviewResultName.objects.get(slug="ready-nits"),
+ reviewed_rev="01",
+ deadline=datetime.date.today() - datetime.timedelta(days=80),
+ reviewer=review_req1.reviewer,
+ )
+
+ ReviewRequest.objects.create(
+ time=datetime.datetime.now() - datetime.timedelta(days=100),
+ requested_by=Person.objects.get(name="(System)"),
+ doc=doc,
+ type=ReviewTypeName.objects.get(slug="early"),
+ team=review_req1.team,
+ state=ReviewRequestStateName.objects.get(slug="completed"),
+ result=ReviewResultName.objects.get(slug="ready"),
+ reviewed_rev="01",
+ deadline=datetime.date.today() - datetime.timedelta(days=80),
+ reviewer=review_req1.reviewer,
+ )
+
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
diff --git a/ietf/group/views_review.py b/ietf/group/views_review.py
index 072872a43..dc792a8ac 100644
--- a/ietf/group/views_review.py
+++ b/ietf/group/views_review.py
@@ -1,4 +1,4 @@
-import datetime
+import datetime, math
from collections import defaultdict
from django.shortcuts import render, redirect, get_object_or_404
@@ -126,7 +126,8 @@ def reviewer_overview(request, acronym, group_type=None):
for req_pk, doc, req_time, state, deadline, result, late_days, request_to_assignment_days, assignment_to_closure_days, request_to_closure_days in req_data:
# any open requests pushes the others out
if ((state in ("requested", "accepted") and len(latest_reqs) < MAX_REQS) or (len(latest_reqs) + open_reqs < MAX_REQS)):
- print review_state_by_slug.get(state), assignment_to_closure_days
+ if assignment_to_closure_days is not None:
+ assignment_to_closure_days = int(math.ceil(assignment_to_closure_days))
latest_reqs.append((req_pk, doc, deadline, review_state_by_slug.get(state), assignment_to_closure_days))
person.latest_reqs = latest_reqs
@@ -205,14 +206,13 @@ def manage_review_requests(request, acronym, group_type=None):
set(r.doc_id for r in review_requests),
)
-
# we need a mutable query dict for resetting upon saving with
# conflicts
query_dict = request.POST.copy() if request.method == "POST" else None
for req in review_requests:
l = []
# take all on the latest reviewed rev
- for r in document_requests[req.doc_id]:
+ for r in document_requests.get(req.doc_id, []):
if l and l[0].reviewed_rev:
if r.doc_id == l[0].doc_id and r.reviewed_rev:
if int(r.reviewed_rev) > int(l[0].reviewed_rev):
diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py
index 309318054..b3ede910c 100644
--- a/ietf/ietfauth/tests.py
+++ b/ietf/ietfauth/tests.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-import os, shutil, time
+import os, shutil, time, datetime
from urlparse import urlsplit
from pyquery import PyQuery
from unittest import skipIf
@@ -18,7 +18,7 @@ from ietf.person.models import Person, Email
from ietf.group.models import Group, Role, RoleName
from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.mailinglists.models import Subscribed
-from ietf.review.models import ReviewWish
+from ietf.review.models import ReviewWish, UnavailablePeriod
from ietf.utils.decorators import skip_coverage
import ietf.ietfauth.views
@@ -348,6 +348,13 @@ class IetfAuthTests(TestCase):
review_req.reviewer = reviewer.email_set.first()
review_req.save()
+ UnavailablePeriod.objects.create(
+ team=review_req.team,
+ person=reviewer,
+ start_date=datetime.date.today() - datetime.timedelta(days=10),
+ availability="unavailable",
+ )
+
url = urlreverse(ietf.ietfauth.views.review_overview)
login_testing_unauthorized(self, reviewer.user.username, url)
diff --git a/ietf/review/import_from_review_tool.py b/ietf/review/import_from_review_tool.py
index 8a86acf49..71e358698 100755
--- a/ietf/review/import_from_review_tool.py
+++ b/ietf/review/import_from_review_tool.py
@@ -466,18 +466,21 @@ with db_con.cursor() as c:
if "assigned" in event_collection:
continue # skip requested unless there's no assigned event
- e = ReviewRequestDocEvent.objects.filter(type="requested_review", doc=review_req.doc).first() or ReviewRequestDocEvent(type="requested_review", doc=review_req.doc)
+ e = ReviewRequestDocEvent.objects.filter(type="requested_review", doc=review_req.doc, review_request=review_req).first()
+ if not e:
+ e = ReviewRequestDocEvent(type="requested_review", doc=review_req.doc, review_request=review_req)
e.time = time
e.by = by
e.desc = "Requested {} review by {}".format(review_req.type.name, review_req.team.acronym.upper())
- e.review_request = review_req
e.state = None
e.skip_community_list_notification = True
e.save()
print "imported event requested_review", e.desc, e.doc_id
elif key == "assigned":
- e = ReviewRequestDocEvent.objects.filter(type="assigned_review_request", doc=review_req.doc).first() or ReviewRequestDocEvent(type="assigned_review_request", doc=review_req.doc)
+ e = ReviewRequestDocEvent.objects.filter(type="assigned_review_request", doc=review_req.doc, review_request=review_req).first()
+ if not e:
+ e = ReviewRequestDocEvent(type="assigned_review_request", doc=review_req.doc, review_request=review_req)
e.time = parse_timestamp(timestamp)
e.by = by
e.desc = "Request for {} review by {} is assigned to {}".format(
@@ -485,14 +488,15 @@ with db_con.cursor() as c:
review_req.team.acronym.upper(),
review_req.reviewer.person if review_req.reviewer else "(None)",
)
- e.review_request = review_req
e.state = None
e.skip_community_list_notification = True
e.save()
print "imported event assigned_review_request", e.pk, e.desc, e.doc_id
elif key == "closed" and review_req.state_id not in ("requested", "accepted"):
- e = ReviewRequestDocEvent.objects.filter(type="closed_review_request", doc=review_req.doc).first() or ReviewRequestDocEvent(type="closed_review_request", doc=review_req.doc)
+ e = ReviewRequestDocEvent.objects.filter(type="closed_review_request", doc=review_req.doc, review_request=review_req).first()
+ if not e:
+ e = ReviewRequestDocEvent(type="closed_review_request", doc=review_req.doc, review_request=review_req)
e.time = parse_timestamp(timestamp)
e.by = by
e.state = states.get(state) if state else None
@@ -512,7 +516,6 @@ with db_con.cursor() as c:
)
else:
e.desc = "Closed request for {} review by {} with state '{}'".format(review_req.type.name, review_req.team.acronym.upper(), e.state.name)
- e.review_request = review_req
e.skip_community_list_notification = True
e.save()
completion_event = e
@@ -580,4 +583,18 @@ with db_con.cursor() as c:
review_req.state = states["overtaken"]
review_req.save()
+ if "closed" not in event_collection and "assigned" in event_collection:
+ e = ReviewRequestDocEvent.objects.filter(type="closed_review_request", doc=review_req.doc, review_request=review_req).first()
+ if not e:
+ e = ReviewRequestDocEvent(type="closed_review_request", doc=review_req.doc, review_request=review_req)
+ e.time = datetime.datetime.now()
+ e.by = by
+ e.state = review_req.state
+ e.desc = "Closed request for {} review by {} with state '{}'".format(review_req.type.name, review_req.team.acronym.upper(), e.state.name)
+ e.skip_community_list_notification = True
+ e.save()
+ completion_event = e
+ print "imported event closed_review_request (generated upon closing)", e.desc, e.doc_id
+
+
print "imported review request", row.reviewid, "as", review_req.pk, review_req.time, review_req.deadline, review_req.type, review_req.doc_id, review_req.state, review_req.doc.get_state_slug("draft-iesg")
diff --git a/ietf/review/utils.py b/ietf/review/utils.py
index 4813cac1d..29dddf192 100644
--- a/ietf/review/utils.py
+++ b/ietf/review/utils.py
@@ -161,10 +161,10 @@ def extract_review_request_data(teams=None, reviewers=None, time_from=None, time
res = defaultdict(list)
# we may be dealing with a big bunch of data, so treat it carefully
- event_qs = ReviewRequest.objects.filter(filters).order_by("-time", "-id")
+ event_qs = ReviewRequest.objects.filter(filters)
# left outer join with RequestRequestDocEvent for request/assign/close time
- event_qs = event_qs.values_list("pk", "doc", "time", "state", "deadline", "result", "team", "reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type")
+ event_qs = event_qs.values_list("pk", "doc", "time", "state", "deadline", "result", "team", "reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type").order_by("-time", "-pk", "-reviewrequestdocevent__time")
def positive_days(time_from, time_to):
if time_from is None or time_to is None:
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index 97b12bf0b..2697bcae0 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -515,11 +515,11 @@ form.email-open-review-assignments [name=body] {
font-family: monospace;
}
-table.unavailable-periods td {
+table.simple-table td {
padding-right: 0.5em;
}
-table.unavailable-periods td:last-child {
+table.simple-table td:last-child {
padding-right: 0;
}
diff --git a/ietf/templates/group/reviewer_overview.html b/ietf/templates/group/reviewer_overview.html
index c9b290f48..6b4107e3d 100644
--- a/ietf/templates/group/reviewer_overview.html
+++ b/ietf/templates/group/reviewer_overview.html
@@ -16,7 +16,7 @@
Reviewer |
- Latest assignments (deadline, state) |
+ Deadline/state/time between assignment and closure for latest assignments |
Settings |
@@ -25,9 +25,20 @@
{{ person }} |
- {% for req_pk, doc_name, deadline, state, assignment_to_closure_days in person.latest_reqs %}
-
+
+ {% for req_pk, doc_name, deadline, state, assignment_to_closure_days in person.latest_reqs %}
+
+ {{ deadline|date }} |
+
+ {{ state.name }}
+ |
+
+ {% if assignment_to_closure_days != None %}{{ assignment_to_closure_days }} day{{ assignment_to_closure_days|pluralize }}{% endif %}
+ |
+ {{ doc_name }} |
+
{% endfor %}
+
|
{{ person.settings.get_min_interval_display }} {% if person.settings.skip_next %}(skip: {{ person.settings.skip_next }}){% endif %}
diff --git a/ietf/templates/review/unavailable_table.html b/ietf/templates/review/unavailable_table.html
index 7fc1d5e35..dcb5d2836 100644
--- a/ietf/templates/review/unavailable_table.html
+++ b/ietf/templates/review/unavailable_table.html
@@ -1,4 +1,4 @@
-
+
{% for p in unavailable_periods %}
{{ p.start_date }} - {{ p.end_date|default:"" }} |
|