Add a utility function for extracting information about review
requests for a given set of teams/reviewers (making it trivial to compute statistics), revamp the related doc event code to support this by referencing the review request directly, add a reviewer overview page with recent performance for each reviewer as well as settings/unavailable periods. Fix some bugs and shuffle some of the review code a bit around. Finish the importer from the previous Perl-based review tool, importing log entries, figuring out whether a given review is early/telechat/last call and fixing corner cases. - Legacy-Id: 12080
This commit is contained in:
parent
c586feb579
commit
4c7b2847ba
|
@ -98,6 +98,9 @@ def notify_events(sender, instance, **kwargs):
|
|||
if instance.doc.type_id != 'draft':
|
||||
return
|
||||
|
||||
if getattr(instance, "skip_community_list_notification", False):
|
||||
return
|
||||
|
||||
from ietf.community.utils import notify_event_to_subscribers
|
||||
notify_event_to_subscribers(instance)
|
||||
|
||||
|
|
33
ietf/doc/migrations/0015_auto_20160927_0713.py
Normal file
33
ietf/doc/migrations/0015_auto_20160927_0713.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0015_insert_review_name_data'),
|
||||
('review', '0001_initial'),
|
||||
('doc', '0014_auto_20160824_2218'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReviewRequestDocEvent',
|
||||
fields=[
|
||||
('docevent_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='doc.DocEvent')),
|
||||
('review_request', models.ForeignKey(to='review.ReviewRequest')),
|
||||
('state', models.ForeignKey(blank=True, to='name.ReviewRequestStateName', null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=('doc.docevent',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='docevent',
|
||||
name='type',
|
||||
field=models.CharField(max_length=50, choices=[(b'new_revision', b'Added new revision'), (b'changed_document', b'Changed document metadata'), (b'added_comment', b'Added comment'), (b'deleted', b'Deleted document'), (b'changed_state', b'Changed state'), (b'changed_stream', b'Changed document stream'), (b'expired_document', b'Expired document'), (b'extended_expiry', b'Extended expiry of document'), (b'requested_resurrect', b'Requested resurrect'), (b'completed_resurrect', b'Completed resurrect'), (b'changed_consensus', b'Changed consensus'), (b'published_rfc', b'Published RFC'), (b'added_suggested_replaces', b'Added suggested replacement relationships'), (b'reviewed_suggested_replaces', b'Reviewed suggested replacement relationships'), (b'changed_group', b'Changed group'), (b'changed_protocol_writeup', b'Changed protocol writeup'), (b'changed_charter_milestone', b'Changed charter milestone'), (b'initial_review', b'Set initial review time'), (b'changed_review_announcement', b'Changed WG Review text'), (b'changed_action_announcement', b'Changed WG Action text'), (b'started_iesg_process', b'Started IESG process on document'), (b'created_ballot', b'Created ballot'), (b'closed_ballot', b'Closed ballot'), (b'sent_ballot_announcement', b'Sent ballot announcement'), (b'changed_ballot_position', b'Changed ballot position'), (b'changed_ballot_approval_text', b'Changed ballot approval text'), (b'changed_ballot_writeup_text', b'Changed ballot writeup text'), (b'changed_rfc_editor_note_text', b'Changed RFC Editor Note text'), (b'changed_last_call_text', b'Changed last call text'), (b'requested_last_call', b'Requested last call'), (b'sent_last_call', b'Sent last call'), (b'scheduled_for_telechat', b'Scheduled for telechat'), (b'iesg_approved', b'IESG approved document (no problem)'), (b'iesg_disapproved', b'IESG disapproved document (do not publish)'), (b'approved_in_minute', b'Approved in minute'), (b'iana_review', b'IANA review comment'), (b'rfc_in_iana_registry', b'RFC is in IANA registry'), (b'rfc_editor_received_announcement', b'Announcement was received by RFC Editor'), (b'requested_publication', b'Publication at RFC Editor requested'), (b'sync_from_rfc_editor', b'Received updated information from RFC Editor'), (b'requested_review', b'Requested review'), (b'assigned_review_request', b'Assigned review request'), (b'closed_review_request', b'Closed review request')]),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -14,7 +14,7 @@ import debug # pyflakes:ignore
|
|||
|
||||
from ietf.group.models import Group
|
||||
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
|
||||
DocRelationshipName, DocReminderTypeName, BallotPositionName )
|
||||
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName )
|
||||
from ietf.person.models import Email, Person
|
||||
from ietf.utils.admin import admin_link
|
||||
|
||||
|
@ -685,7 +685,8 @@ EVENT_TYPES = [
|
|||
|
||||
# review
|
||||
("requested_review", "Requested review"),
|
||||
("changed_review_request", "Changed review request"),
|
||||
("assigned_review_request", "Assigned review request"),
|
||||
("closed_review_request", "Closed review request"),
|
||||
]
|
||||
|
||||
class DocEvent(models.Model):
|
||||
|
@ -816,11 +817,14 @@ class TelechatDocEvent(DocEvent):
|
|||
telechat_date = models.DateField(blank=True, null=True)
|
||||
returning_item = models.BooleanField(default=False)
|
||||
|
||||
class ReviewRequestDocEvent(DocEvent):
|
||||
review_request = models.ForeignKey('review.ReviewRequest')
|
||||
state = models.ForeignKey(ReviewRequestStateName, blank=True, null=True)
|
||||
|
||||
# charter events
|
||||
class InitialReviewDocEvent(DocEvent):
|
||||
expires = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
|
||||
# dumping store for removed events
|
||||
class DeletedEvent(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
|
|
|
@ -131,7 +131,7 @@ class ReviewTests(TestCase):
|
|||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "withdrawn")
|
||||
e = doc.latest_event()
|
||||
self.assertEqual(e.type, "changed_review_request")
|
||||
self.assertEqual(e.type, "closed_review_request")
|
||||
self.assertTrue("closed" in e.desc.lower())
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue("closed" in unicode(outbox[0]).lower())
|
||||
|
@ -255,7 +255,7 @@ class ReviewTests(TestCase):
|
|||
reviewer=plain_email,
|
||||
)
|
||||
|
||||
reviewer_settings = ReviewerSettings.objects.get(person__email=plain_email)
|
||||
reviewer_settings = ReviewerSettings.objects.get(person__email=plain_email, team=review_req.team)
|
||||
reviewer_settings.filter_re = doc.name
|
||||
reviewer_settings.skip_next = 1
|
||||
reviewer_settings.save()
|
||||
|
@ -383,7 +383,7 @@ class ReviewTests(TestCase):
|
|||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "rejected")
|
||||
e = doc.latest_event()
|
||||
self.assertEqual(e.type, "changed_review_request")
|
||||
self.assertEqual(e.type, "closed_review_request")
|
||||
self.assertTrue("rejected" in e.desc)
|
||||
self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="requested").count(), 1)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
|
|
|
@ -9,7 +9,8 @@ from django.core.exceptions import ValidationError
|
|||
from django.template.loader import render_to_string
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent, State, DocAlias, LastCallDocEvent
|
||||
from ietf.doc.models import (Document, NewRevisionDocEvent, State, DocAlias,
|
||||
LastCallDocEvent, ReviewRequestDocEvent)
|
||||
from ietf.name.models import ReviewRequestStateName, ReviewResultName, DocTypeName
|
||||
from ietf.review.models import ReviewRequest
|
||||
from ietf.group.models import Group
|
||||
|
@ -103,12 +104,14 @@ def request_review(request, name):
|
|||
review_req.team = team
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
ReviewRequestDocEvent.objects.create(
|
||||
type="requested_review",
|
||||
doc=doc,
|
||||
by=request.user.person,
|
||||
desc="Requested {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
|
||||
time=review_req.time,
|
||||
review_request=review_req,
|
||||
state=None,
|
||||
)
|
||||
|
||||
return redirect('doc_view', name=doc.name)
|
||||
|
@ -272,8 +275,8 @@ def reject_reviewer_assignment(request, name, request_id):
|
|||
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
ReviewRequestDocEvent.objects.create(
|
||||
type="closed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Assignment of request for {} review by {} to {} was rejected".format(
|
||||
|
@ -281,13 +284,15 @@ def reject_reviewer_assignment(request, name, request_id):
|
|||
review_req.team.acronym.upper(),
|
||||
review_req.reviewer.person,
|
||||
),
|
||||
review_request=review_req,
|
||||
state=review_req.state,
|
||||
)
|
||||
|
||||
# make a new unassigned review request
|
||||
new_review_req = make_new_review_request_from_existing(review_req)
|
||||
new_review_req.save()
|
||||
|
||||
msg = render_to_string("doc/mail/reviewer_assignment_rejected.txt", {
|
||||
msg = render_to_string("review/reviewer_assignment_rejected.txt", {
|
||||
"by": request.user.person,
|
||||
"message_to_secretary": form.cleaned_data.get("message_to_secretary")
|
||||
})
|
||||
|
@ -393,7 +398,7 @@ def complete_review(request, name, request_id):
|
|||
strip_prefix(review_req.doc.name, "draft-"),
|
||||
form.cleaned_data["reviewed_rev"],
|
||||
review_req.team.acronym,
|
||||
review_req.type.slug if review_req.type.slug != "unknown" else "",
|
||||
review_req.type.slug,
|
||||
review_req.reviewer.person.ascii_parts()[3],
|
||||
datetime.date.today().isoformat(),
|
||||
]
|
||||
|
@ -403,8 +408,16 @@ def complete_review(request, name, request_id):
|
|||
name = "-".join(c for c in name_components if c).lower()
|
||||
if not Document.objects.filter(name=name).exists():
|
||||
review = Document.objects.create(name=name)
|
||||
DocAlias.objects.create(document=review, name=review.name)
|
||||
break
|
||||
|
||||
review.type = DocTypeName.objects.get(slug="review")
|
||||
review.rev = "00"
|
||||
review.title = "{} Review of {}-{}".format(review_req.type.name, review_req.doc.name, form.cleaned_data["reviewed_rev"])
|
||||
review.group = review_req.team
|
||||
if review_submission == "link":
|
||||
review.external_url = form.cleaned_data['review_url']
|
||||
|
||||
e = NewRevisionDocEvent.objects.create(
|
||||
type="new_revision",
|
||||
doc=review,
|
||||
|
@ -414,15 +427,9 @@ def complete_review(request, name, request_id):
|
|||
time=review.time,
|
||||
)
|
||||
|
||||
review.type = DocTypeName.objects.get(slug="review")
|
||||
review.rev = "00"
|
||||
review.title = "{} Review of {}-{}".format(review_req.type.name, review_req.doc.name, form.cleaned_data["reviewed_rev"])
|
||||
review.group = review_req.team
|
||||
if review_submission == "link":
|
||||
review.external_url = form.cleaned_data['review_url']
|
||||
review.save_with_history([e])
|
||||
review.set_state(State.objects.get(type="review", slug="active"))
|
||||
DocAlias.objects.create(document=review, name=review.name)
|
||||
|
||||
review.save_with_history([e])
|
||||
|
||||
# save file on disk
|
||||
if review_submission == "upload":
|
||||
|
@ -441,17 +448,25 @@ def complete_review(request, name, request_id):
|
|||
review_req.review = review
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
need_to_email_review = review_submission != "link" and review_req.team.list_email
|
||||
|
||||
desc = "Request for {} review by {} {}: {}. Reviewer: {}.".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.state.name,
|
||||
review_req.result.name,
|
||||
review_req.reviewer.person,
|
||||
)
|
||||
if need_to_email_review:
|
||||
desc += " " + "Sent review to list."
|
||||
|
||||
close_event = ReviewRequestDocEvent.objects.create(
|
||||
type="closed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Request for {} review by {} {}: {}. Reviewer: {}".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.state.name,
|
||||
review_req.result.name,
|
||||
review_req.reviewer,
|
||||
),
|
||||
desc=desc,
|
||||
review_request=review_req,
|
||||
state=review_req.state,
|
||||
)
|
||||
|
||||
if review_req.state_id == "part-completed":
|
||||
|
@ -463,7 +478,7 @@ def complete_review(request, name, request_id):
|
|||
url = urlreverse("ietf.doc.views_review.review_request", kwargs={ "name": new_review_req.doc.name, "request_id": new_review_req.pk })
|
||||
url = request.build_absolute_uri(url)
|
||||
|
||||
msg = render_to_string("doc/mail/partially_completed_review.txt", {
|
||||
msg = render_to_string("review/partially_completed_review.txt", {
|
||||
'new_review_req_url': url,
|
||||
"by": request.user.person,
|
||||
"new_review_req": new_review_req,
|
||||
|
@ -471,28 +486,21 @@ def complete_review(request, name, request_id):
|
|||
|
||||
email_review_request_change(request, review_req, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False, notify_requested_by=False)
|
||||
|
||||
if review_submission != "link" and review_req.team.list_email:
|
||||
if need_to_email_review:
|
||||
# email the review
|
||||
subject = "{} of {}-{}".format("Partial review" if review_req.state_id == "part-completed" else "Review", review_req.doc.name, review_req.reviewed_rev)
|
||||
msg = send_mail(request, [(review_req.team.name, review_req.team.list_email)], None,
|
||||
subject,
|
||||
"doc/mail/completed_review.txt", {
|
||||
"review/completed_review.txt", {
|
||||
"review_req": review_req,
|
||||
"content": encoded_content.decode("utf-8"),
|
||||
},
|
||||
cc=form.cleaned_data["cc"])
|
||||
|
||||
e = DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Sent review to list.",
|
||||
)
|
||||
|
||||
list_name = mailarch.list_name_from_email(review_req.team.list_email)
|
||||
if list_name:
|
||||
review.external_url = mailarch.construct_message_url(list_name, email.utils.unquote(msg["Message-ID"]))
|
||||
review.save_with_history([e])
|
||||
review.save_with_history([close_event])
|
||||
|
||||
return redirect("doc_view", name=review_req.review.name)
|
||||
else:
|
||||
|
|
|
@ -31,7 +31,7 @@ class GroupFeatures(object):
|
|||
if group in active_review_teams():
|
||||
self.has_reviews = True
|
||||
import ietf.group.views
|
||||
self.default_tab = ietf.group.views.review_requests
|
||||
self.default_tab = ietf.group.views_review.review_requests
|
||||
|
||||
if group.type_id == "dir":
|
||||
self.admin_roles = ["chair", "secr"]
|
||||
|
|
|
@ -31,7 +31,6 @@ from ietf.person.models import Person, Email
|
|||
from ietf.utils.mail import outbox, empty_outbox
|
||||
from ietf.utils.test_data import make_test_data, create_person, make_review_data
|
||||
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects
|
||||
import ietf.group.views
|
||||
|
||||
def group_urlreverse_list(group, viewname):
|
||||
return [
|
||||
|
@ -336,31 +335,6 @@ class GroupPagesTests(TestCase):
|
|||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(doc.title not in unicontent(r))
|
||||
|
||||
def test_review_requests(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
|
||||
group = review_req.team
|
||||
|
||||
for url in [ urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym }),
|
||||
urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id}),
|
||||
]:
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(review_req.doc.name in unicontent(r))
|
||||
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
|
||||
|
||||
url = urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym })
|
||||
|
||||
# close request, listed under closed
|
||||
review_req.state_id = "completed"
|
||||
review_req.result_id = "ready"
|
||||
review_req.save()
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(review_req.doc.name in unicontent(r))
|
||||
|
||||
def test_history(self):
|
||||
draft = make_test_data()
|
||||
group = draft.group
|
||||
|
|
|
@ -15,6 +15,31 @@ import ietf.group.views_review
|
|||
from ietf.utils.mail import outbox, empty_outbox
|
||||
|
||||
class ReviewTests(TestCase):
|
||||
def test_review_requests(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
|
||||
group = review_req.team
|
||||
|
||||
for url in [ urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym }),
|
||||
urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id}),
|
||||
]:
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(review_req.doc.name in unicontent(r))
|
||||
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
|
||||
|
||||
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.save()
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(review_req.doc.name in unicontent(r))
|
||||
|
||||
def test_suggested_review_requests(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
|
|
|
@ -30,7 +30,7 @@ urlpatterns = patterns('',
|
|||
(r'^materials/new/(?P<doc_type>[\w-]+)/$', 'ietf.doc.views_material.edit_material', { 'action': "new" }, "group_new_material"),
|
||||
(r'^archives/$', 'ietf.group.views.derived_archives'),
|
||||
(r'^photos/$', views.group_photos),
|
||||
(r'^reviews/$', views.review_requests),
|
||||
(r'^reviews/$', views_review.review_requests),
|
||||
(r'^reviews/manage/$', views_review.manage_review_requests),
|
||||
(r'^reviews/email-assignments/$', views_review.email_open_review_assignments),
|
||||
(r'^reviewers/$', views_review.reviewer_overview),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
@ -9,8 +11,8 @@ from ietf.person.models import Email
|
|||
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.community.models import CommunityList, SearchRule
|
||||
from ietf.community.utils import reset_name_contains_index_for_rule
|
||||
from ietf.doc.models import State
|
||||
from ietf.community.utils import reset_name_contains_index_for_rule, can_manage_community_list
|
||||
from ietf.doc.models import Document, State
|
||||
|
||||
|
||||
def save_group_in_history(group):
|
||||
|
@ -149,3 +151,86 @@ def setup_default_community_list_for_group(group):
|
|||
state=State.objects.get(slug="active", type="draft"),
|
||||
)
|
||||
reset_name_contains_index_for_rule(related_docs_rule)
|
||||
|
||||
def get_group_materials(group):
|
||||
return Document.objects.filter(
|
||||
group=group,
|
||||
type__in=group.features.material_types
|
||||
).exclude(states__slug__in=['deleted','archived'])
|
||||
|
||||
def construct_group_menu_context(request, group, selected, group_type, others):
|
||||
"""Return context with info for the group menu filled in."""
|
||||
kwargs = dict(acronym=group.acronym)
|
||||
if group_type:
|
||||
kwargs["group_type"] = group_type
|
||||
|
||||
# menu entries
|
||||
entries = []
|
||||
if group.features.has_documents:
|
||||
entries.append(("Documents", urlreverse("ietf.group.views.group_documents", kwargs=kwargs)))
|
||||
if group.features.has_chartering_process:
|
||||
entries.append(("Charter", urlreverse("group_charter", kwargs=kwargs)))
|
||||
else:
|
||||
entries.append(("About", urlreverse("group_about", kwargs=kwargs)))
|
||||
if group.features.has_materials and get_group_materials(group).exists():
|
||||
entries.append(("Materials", urlreverse("ietf.group.views.materials", kwargs=kwargs)))
|
||||
if group.features.has_reviews:
|
||||
import ietf.group.views_review
|
||||
entries.append(("Review requests", urlreverse(ietf.group.views_review.review_requests, kwargs=kwargs)))
|
||||
entries.append(("Reviewers", urlreverse(ietf.group.views_review.reviewer_overview, kwargs=kwargs)))
|
||||
if group.type_id in ('rg','wg','team'):
|
||||
entries.append(("Meetings", urlreverse("ietf.group.views.meetings", kwargs=kwargs)))
|
||||
entries.append(("History", urlreverse("ietf.group.views.history", kwargs=kwargs)))
|
||||
entries.append(("Photos", urlreverse("ietf.group.views.group_photos", kwargs=kwargs)))
|
||||
entries.append(("Email expansions", urlreverse("ietf.group.views.email", kwargs=kwargs)))
|
||||
if group.list_archive.startswith("http:") or group.list_archive.startswith("https:") or group.list_archive.startswith("ftp:"):
|
||||
if 'mailarchive.ietf.org' in group.list_archive:
|
||||
entries.append(("List archive", urlreverse("ietf.group.views.derived_archives", kwargs=kwargs)))
|
||||
else:
|
||||
entries.append((mark_safe("List archive »"), group.list_archive))
|
||||
if group.has_tools_page():
|
||||
entries.append((mark_safe("Tools »"), "https://tools.ietf.org/%s/%s/" % (group.type_id, group.acronym)))
|
||||
|
||||
# actions
|
||||
actions = []
|
||||
|
||||
is_admin = group.has_role(request.user, group.features.admin_roles)
|
||||
can_manage = can_manage_group(request.user, group)
|
||||
|
||||
if group.features.has_milestones:
|
||||
if group.state_id != "proposed" and (is_admin or can_manage):
|
||||
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
|
||||
|
||||
if group.features.has_documents:
|
||||
clist = CommunityList.objects.filter(group=group).first()
|
||||
if clist and can_manage_community_list(request.user, clist):
|
||||
import ietf.community.views
|
||||
actions.append((u'Manage document list', urlreverse(ietf.community.views.manage_list, kwargs=kwargs)))
|
||||
|
||||
if group.features.has_materials and can_manage_materials(request.user, group):
|
||||
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
|
||||
|
||||
if group.features.has_reviews:
|
||||
import ietf.group.views_review
|
||||
actions.append((u"Manage review requests", urlreverse(ietf.group.views_review.manage_review_requests, kwargs=kwargs)))
|
||||
|
||||
if group.state_id != "conclude" and (is_admin or can_manage):
|
||||
actions.append((u"Edit group", urlreverse("group_edit", kwargs=kwargs)))
|
||||
|
||||
if group.features.customize_workflow and (is_admin or can_manage):
|
||||
actions.append((u"Customize workflow", urlreverse("ietf.group.views_edit.customize_workflow", kwargs=kwargs)))
|
||||
|
||||
if group.state_id in ("active", "dormant") and not group.type_id in ["sdo", "rfcedtyp", "isoc", ] and can_manage:
|
||||
actions.append((u"Request closing group", urlreverse("ietf.group.views_edit.conclude", kwargs=kwargs)))
|
||||
|
||||
d = {
|
||||
"group": group,
|
||||
"selected_menu_entry": selected,
|
||||
"menu_entries": entries,
|
||||
"menu_actions": actions,
|
||||
"group_type": group_type,
|
||||
}
|
||||
|
||||
d.update(others)
|
||||
|
||||
return d
|
||||
|
|
|
@ -49,18 +49,19 @@ from django.conf import settings
|
|||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.db.models import Q
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from ietf.doc.models import Document, State, DocAlias, RelatedDocument
|
||||
from ietf.doc.models import State, DocAlias, RelatedDocument
|
||||
from ietf.doc.utils import get_chartering_type
|
||||
from ietf.doc.templatetags.ietf_filters import clean_whitespace
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
from ietf.doc.utils_charter import charter_name_for_group
|
||||
from ietf.group.models import Group, Role, ChangeStateGroupEvent
|
||||
from ietf.name.models import GroupTypeName
|
||||
from ietf.group.utils import get_charter_text, can_manage_group_type, can_manage_group, milestone_reviewer_for_group_type, can_provide_status_update
|
||||
from ietf.group.utils import can_manage_materials, get_group_or_404
|
||||
from ietf.community.utils import docs_tracked_by_community_list, can_manage_community_list
|
||||
from ietf.group.utils import (get_charter_text, can_manage_group_type, can_manage_group,
|
||||
milestone_reviewer_for_group_type, can_provide_status_update,
|
||||
can_manage_materials, get_group_or_404,
|
||||
construct_group_menu_context, get_group_materials)
|
||||
from ietf.community.utils import docs_tracked_by_community_list
|
||||
from ietf.community.models import CommunityList, EmailSubscription
|
||||
from ietf.utils.pipe import pipe
|
||||
from ietf.utils.textupload import get_cleaned_text_file_content
|
||||
|
@ -69,10 +70,7 @@ from ietf.mailtrigger.utils import gather_relevant_expansions
|
|||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.meeting.utils import group_sessions
|
||||
from ietf.meeting.helpers import get_meeting
|
||||
from ietf.review.models import ReviewRequest
|
||||
from ietf.review.utils import (can_manage_review_requests_for_team,
|
||||
suggested_review_requests_for_team,
|
||||
current_unavailable_periods_for_reviewers)
|
||||
|
||||
|
||||
def roles(group, role_name):
|
||||
return Role.objects.filter(group=group, name=role_name).select_related("email", "person")
|
||||
|
@ -331,85 +329,6 @@ def concluded_groups(request):
|
|||
return render(request, 'group/concluded_groups.html',
|
||||
dict(sections=sections))
|
||||
|
||||
def get_group_materials(group):
|
||||
# return Document.objects.filter(group=group, type__in=group.features.material_types, session=None).exclude(states__slug="deleted")
|
||||
return Document.objects.filter(group=group, type__in=group.features.material_types).exclude(states__slug__in=['deleted','archived'])
|
||||
|
||||
def construct_group_menu_context(request, group, selected, group_type, others):
|
||||
"""Return context with info for the group menu filled in."""
|
||||
kwargs = dict(acronym=group.acronym)
|
||||
if group_type:
|
||||
kwargs["group_type"] = group_type
|
||||
|
||||
# menu entries
|
||||
entries = []
|
||||
if group.features.has_documents:
|
||||
entries.append(("Documents", urlreverse("ietf.group.views.group_documents", kwargs=kwargs)))
|
||||
if group.features.has_chartering_process:
|
||||
entries.append(("Charter", urlreverse("group_charter", kwargs=kwargs)))
|
||||
else:
|
||||
entries.append(("About", urlreverse("group_about", kwargs=kwargs)))
|
||||
if group.features.has_materials and get_group_materials(group).exists():
|
||||
entries.append(("Materials", urlreverse("ietf.group.views.materials", kwargs=kwargs)))
|
||||
if group.features.has_reviews:
|
||||
entries.append(("Review requests", urlreverse(review_requests, kwargs=kwargs)))
|
||||
if group.type_id in ('rg','wg','team'):
|
||||
entries.append(("Meetings", urlreverse("ietf.group.views.meetings", kwargs=kwargs)))
|
||||
entries.append(("History", urlreverse("ietf.group.views.history", kwargs=kwargs)))
|
||||
entries.append(("Photos", urlreverse("ietf.group.views.group_photos", kwargs=kwargs)))
|
||||
entries.append(("Email expansions", urlreverse("ietf.group.views.email", kwargs=kwargs)))
|
||||
if group.list_archive.startswith("http:") or group.list_archive.startswith("https:") or group.list_archive.startswith("ftp:"):
|
||||
if 'mailarchive.ietf.org' in group.list_archive:
|
||||
entries.append(("List archive", urlreverse("ietf.group.views.derived_archives", kwargs=kwargs)))
|
||||
else:
|
||||
entries.append((mark_safe("List archive »"), group.list_archive))
|
||||
if group.has_tools_page():
|
||||
entries.append((mark_safe("Tools »"), "https://tools.ietf.org/%s/%s/" % (group.type_id, group.acronym)))
|
||||
|
||||
# actions
|
||||
actions = []
|
||||
|
||||
is_admin = group.has_role(request.user, group.features.admin_roles)
|
||||
can_manage = can_manage_group(request.user, group)
|
||||
|
||||
if group.features.has_milestones:
|
||||
if group.state_id != "proposed" and (is_admin or can_manage):
|
||||
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
|
||||
|
||||
if group.features.has_documents:
|
||||
clist = CommunityList.objects.filter(group=group).first()
|
||||
if clist and can_manage_community_list(request.user, clist):
|
||||
import ietf.community.views
|
||||
actions.append((u'Manage document list', urlreverse(ietf.community.views.manage_list, kwargs=kwargs)))
|
||||
|
||||
if group.features.has_materials and can_manage_materials(request.user, group):
|
||||
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
|
||||
|
||||
if group.features.has_reviews:
|
||||
import ietf.group.views_review
|
||||
actions.append((u"Manage review requests", urlreverse(ietf.group.views_review.manage_review_requests, kwargs=kwargs)))
|
||||
|
||||
if group.state_id != "conclude" and (is_admin or can_manage):
|
||||
actions.append((u"Edit group", urlreverse("group_edit", kwargs=kwargs)))
|
||||
|
||||
if group.features.customize_workflow and (is_admin or can_manage):
|
||||
actions.append((u"Customize workflow", urlreverse("ietf.group.views_edit.customize_workflow", kwargs=kwargs)))
|
||||
|
||||
if group.state_id in ("active", "dormant") and not group.type_id in ["sdo", "rfcedtyp", "isoc", ] and can_manage:
|
||||
actions.append((u"Request closing group", urlreverse("ietf.group.views_edit.conclude", kwargs=kwargs)))
|
||||
|
||||
d = {
|
||||
"group": group,
|
||||
"selected_menu_entry": selected,
|
||||
"menu_entries": entries,
|
||||
"menu_actions": actions,
|
||||
"group_type": group_type,
|
||||
}
|
||||
|
||||
d.update(others)
|
||||
|
||||
return d
|
||||
|
||||
def prepare_group_documents(request, group, clist):
|
||||
found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET)
|
||||
|
||||
|
@ -662,65 +581,6 @@ def history(request, acronym, group_type=None):
|
|||
"events": events,
|
||||
}))
|
||||
|
||||
def review_requests(request, acronym, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_reviews:
|
||||
raise Http404
|
||||
|
||||
open_review_requests = list(ReviewRequest.objects.filter(
|
||||
team=group, state__in=("requested", "accepted")
|
||||
).prefetch_related("reviewer", "type", "state").order_by("-time", "-id"))
|
||||
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(group)
|
||||
for review_req in open_review_requests:
|
||||
if review_req.reviewer:
|
||||
review_req.reviewer_unavailable = any(p.availability == "unavailable"
|
||||
for p in unavailable_periods.get(review_req.reviewer.person_id, []))
|
||||
|
||||
open_review_requests = suggested_review_requests_for_team(group) + open_review_requests
|
||||
|
||||
today = datetime.date.today()
|
||||
for r in open_review_requests:
|
||||
r.due = max(0, (today - r.deadline).days)
|
||||
|
||||
closed_review_requests = ReviewRequest.objects.filter(
|
||||
team=group,
|
||||
).exclude(
|
||||
state__in=("requested", "accepted")
|
||||
).prefetch_related("reviewer", "type", "state", "doc").order_by("-time", "-id")
|
||||
|
||||
since_choices = [
|
||||
(None, "1 month"),
|
||||
("3m", "3 months"),
|
||||
("6m", "6 months"),
|
||||
("1y", "1 year"),
|
||||
("2y", "2 years"),
|
||||
("all", "All"),
|
||||
]
|
||||
since = request.GET.get("since", None)
|
||||
if since not in [key for key, label in since_choices]:
|
||||
since = None
|
||||
|
||||
if since != "all":
|
||||
date_limit = {
|
||||
None: datetime.timedelta(days=31),
|
||||
"3m": datetime.timedelta(days=31 * 3),
|
||||
"6m": datetime.timedelta(days=180),
|
||||
"1y": datetime.timedelta(days=365),
|
||||
"2y": datetime.timedelta(days=2 * 365),
|
||||
}[since]
|
||||
|
||||
closed_review_requests = closed_review_requests.filter(time__gte=datetime.date.today() - date_limit)
|
||||
|
||||
return render(request, 'group/review_requests.html',
|
||||
construct_group_menu_context(request, group, "review requests", group_type, {
|
||||
"open_review_requests": open_review_requests,
|
||||
"closed_review_requests": closed_review_requests,
|
||||
"since_choices": since_choices,
|
||||
"since": since,
|
||||
"can_manage_review_requests": can_manage_review_requests_for_team(request.user, group)
|
||||
}))
|
||||
|
||||
def materials(request, acronym, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_materials:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect
|
||||
|
@ -14,18 +15,126 @@ from ietf.review.utils import (can_manage_review_requests_for_team, close_review
|
|||
close_review_request,
|
||||
setup_reviewer_field,
|
||||
suggested_review_requests_for_team,
|
||||
unavailability_periods_to_list,
|
||||
unavailable_periods_to_list,
|
||||
current_unavailable_periods_for_reviewers,
|
||||
email_reviewer_availability_change,
|
||||
reviewer_rotation_list)
|
||||
reviewer_rotation_list,
|
||||
extract_review_request_data)
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.utils import get_group_or_404
|
||||
from ietf.group.utils import get_group_or_404, construct_group_menu_context
|
||||
from ietf.person.fields import PersonEmailChoiceField
|
||||
from ietf.name.models import ReviewRequestStateName
|
||||
from ietf.utils.mail import send_mail_text
|
||||
from ietf.utils.fields import DatepickerDateField
|
||||
from ietf.ietfauth.utils import user_is_person
|
||||
|
||||
|
||||
def review_requests(request, acronym, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_reviews:
|
||||
raise Http404
|
||||
|
||||
open_review_requests = list(ReviewRequest.objects.filter(
|
||||
team=group, state__in=("requested", "accepted")
|
||||
).prefetch_related("reviewer", "type", "state").order_by("-time", "-id"))
|
||||
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(group)
|
||||
for review_req in open_review_requests:
|
||||
if review_req.reviewer:
|
||||
review_req.reviewer_unavailable = any(p.availability == "unavailable"
|
||||
for p in unavailable_periods.get(review_req.reviewer.person_id, []))
|
||||
|
||||
open_review_requests = suggested_review_requests_for_team(group) + open_review_requests
|
||||
|
||||
today = datetime.date.today()
|
||||
for r in open_review_requests:
|
||||
r.due = max(0, (today - r.deadline).days)
|
||||
|
||||
closed_review_requests = ReviewRequest.objects.filter(
|
||||
team=group,
|
||||
).exclude(
|
||||
state__in=("requested", "accepted")
|
||||
).prefetch_related("reviewer", "type", "state", "doc").order_by("-time", "-id")
|
||||
|
||||
since_choices = [
|
||||
(None, "1 month"),
|
||||
("3m", "3 months"),
|
||||
("6m", "6 months"),
|
||||
("1y", "1 year"),
|
||||
("2y", "2 years"),
|
||||
("all", "All"),
|
||||
]
|
||||
since = request.GET.get("since", None)
|
||||
if since not in [key for key, label in since_choices]:
|
||||
since = None
|
||||
|
||||
if since != "all":
|
||||
date_limit = {
|
||||
None: datetime.timedelta(days=31),
|
||||
"3m": datetime.timedelta(days=31 * 3),
|
||||
"6m": datetime.timedelta(days=180),
|
||||
"1y": datetime.timedelta(days=365),
|
||||
"2y": datetime.timedelta(days=2 * 365),
|
||||
}[since]
|
||||
|
||||
closed_review_requests = closed_review_requests.filter(time__gte=datetime.date.today() - date_limit)
|
||||
|
||||
return render(request, 'group/review_requests.html',
|
||||
construct_group_menu_context(request, group, "review requests", group_type, {
|
||||
"open_review_requests": open_review_requests,
|
||||
"closed_review_requests": closed_review_requests,
|
||||
"since_choices": since_choices,
|
||||
"since": since,
|
||||
"can_manage_review_requests": can_manage_review_requests_for_team(request.user, group)
|
||||
}))
|
||||
|
||||
def reviewer_overview(request, acronym, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_reviews:
|
||||
raise Http404
|
||||
|
||||
can_manage = can_manage_review_requests_for_team(request.user, group)
|
||||
|
||||
reviewers = reviewer_rotation_list(group)
|
||||
|
||||
reviewer_settings = { s.person_id: s for s in ReviewerSettings.objects.filter(team=group) }
|
||||
unavailable_periods = defaultdict(list)
|
||||
for p in unavailable_periods_to_list().filter(team=group):
|
||||
unavailable_periods[p.person_id].append(p)
|
||||
reviewer_roles = { r.person_id: r for r in Role.objects.filter(group=group, name="reviewer").select_related("email") }
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
all_req_data = extract_review_request_data(teams=[group], time_from=today - datetime.timedelta(days=365))
|
||||
review_state_by_slug = { n.slug: n for n in ReviewRequestStateName.objects.all() }
|
||||
|
||||
for person in reviewers:
|
||||
person.settings = reviewer_settings.get(person.pk) or ReviewerSettings(team=group, person=person)
|
||||
person.settings_url = None
|
||||
person.role = reviewer_roles.get(person.pk)
|
||||
if person.role and (can_manage or user_is_person(request.user, person)):
|
||||
person.settings_url = urlreverse("ietf.group.views_review.change_reviewer_settings", kwargs={ "group_type": group_type, "acronym": group.acronym, "reviewer_email": person.role.email.address })
|
||||
person.unavailable_periods = unavailable_periods.get(person.pk, [])
|
||||
person.completely_unavailable = any(p.availability == "unavailable"
|
||||
and p.start_date <= today and (p.end_date is None or today <= p.end_date)
|
||||
for p in person.unavailable_periods)
|
||||
|
||||
MAX_REQS = 5
|
||||
req_data = all_req_data.get((group.pk, person.pk), [])
|
||||
open_reqs = sum(1 for _, _, _, _, state, _, _, _, _, _ in req_data if state in ("requested", "accepted"))
|
||||
latest_reqs = []
|
||||
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
|
||||
latest_reqs.append((req_pk, doc, deadline, review_state_by_slug.get(state), assignment_to_closure_days))
|
||||
person.latest_reqs = latest_reqs
|
||||
|
||||
return render(request, 'group/reviewer_overview.html',
|
||||
construct_group_menu_context(request, group, "reviewers", group_type, {
|
||||
"reviewers": reviewers,
|
||||
}))
|
||||
|
||||
class ManageReviewRequestForm(forms.Form):
|
||||
ACTIONS = [
|
||||
("assign", "Assign"),
|
||||
|
@ -45,12 +154,12 @@ class ManageReviewRequestForm(forms.Form):
|
|||
|
||||
super(ManageReviewRequestForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if review_req.pk is None:
|
||||
self.fields["close"].queryset = self.fields["close"].queryset.filter(slug__in=["no-review-version", "no-review-document"])
|
||||
|
||||
close_initial = None
|
||||
if review_req.pk is None:
|
||||
if review_req.latest_reqs:
|
||||
close_initial = "no-review-version"
|
||||
else:
|
||||
close_initial = "no-review-document"
|
||||
close_initial = "no-review-version"
|
||||
elif review_req.reviewer:
|
||||
close_initial = "no-response"
|
||||
else:
|
||||
|
@ -59,9 +168,6 @@ class ManageReviewRequestForm(forms.Form):
|
|||
if close_initial:
|
||||
self.fields["close"].initial = close_initial
|
||||
|
||||
if review_req.pk is None:
|
||||
self.fields["close"].queryset = self.fields["close"].queryset.filter(slug__in=["noreviewversion", "noreviewdocument"])
|
||||
|
||||
self.fields["close"].widget.attrs["class"] = "form-control input-sm"
|
||||
|
||||
setup_reviewer_field(self.fields["reviewer"], review_req)
|
||||
|
@ -176,8 +282,8 @@ def manage_review_requests(request, acronym, group_type=None):
|
|||
if form_action == "save-continue":
|
||||
return redirect(manage_review_requests, **kwargs)
|
||||
else:
|
||||
import ietf.group.views
|
||||
return redirect(ietf.group.views.review_requests, **kwargs)
|
||||
import ietf.group.views_review
|
||||
return redirect(ietf.group.views_review.review_requests, **kwargs)
|
||||
|
||||
return render(request, 'group/manage_review_requests.html', {
|
||||
'group': group,
|
||||
|
@ -241,19 +347,6 @@ def email_open_review_assignments(request, acronym, group_type=None):
|
|||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def reviewer_overview(request, acronym, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_reviews:
|
||||
raise Http404
|
||||
|
||||
reviewer_roles = Role.objects.filter(name="reviewer", group=group)
|
||||
|
||||
if not (any(user_is_person(request.user, r) for r in reviewer_roles)
|
||||
or can_manage_review_requests_for_team(request.user, group)):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
|
||||
class ReviewerSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ReviewerSettings
|
||||
|
@ -312,8 +405,8 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
|
|||
|
||||
back_url = request.GET.get("next")
|
||||
if not back_url:
|
||||
import ietf.group.views
|
||||
back_url = urlreverse(ietf.group.views.review_requests, kwargs={ "group_type": group.type_id, "acronym": group.acronym})
|
||||
import ietf.group.views_review
|
||||
back_url = urlreverse(ietf.group.views_review.review_requests, kwargs={ "group_type": group.type_id, "acronym": group.acronym})
|
||||
|
||||
# settings
|
||||
if request.method == "POST" and request.POST.get("action") == "change_settings":
|
||||
|
@ -337,7 +430,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
|
|||
settings_form = ReviewerSettingsForm(instance=settings)
|
||||
|
||||
# periods
|
||||
unavailable_periods = unavailability_periods_to_list().filter(person=reviewer, team=group)
|
||||
unavailable_periods = unavailable_periods_to_list().filter(person=reviewer, team=group)
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "add_period":
|
||||
period_form = AddUnavailablePeriodForm(request.POST)
|
||||
|
|
|
@ -56,7 +56,7 @@ from ietf.ietfauth.utils import role_required
|
|||
from ietf.mailinglists.models import Subscribed, Whitelisted
|
||||
from ietf.person.models import Person, Email, Alias
|
||||
from ietf.review.models import ReviewRequest, ReviewerSettings, ReviewWish
|
||||
from ietf.review.utils import unavailability_periods_to_list
|
||||
from ietf.review.utils import unavailable_periods_to_list
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.doc.fields import SearchableDocumentField
|
||||
|
||||
|
@ -427,13 +427,13 @@ def review_overview(request):
|
|||
settings = { o.team_id: o for o in ReviewerSettings.objects.filter(person__user=request.user, team__in=teams) }
|
||||
|
||||
unavailable_periods = defaultdict(list)
|
||||
for o in unavailability_periods_to_list().filter(person__user=request.user, team__in=teams):
|
||||
for o in unavailable_periods_to_list().filter(person__user=request.user, team__in=teams):
|
||||
unavailable_periods[o.team_id].append(o)
|
||||
|
||||
roles = { o.group_id: o for o in Role.objects.filter(name="reviewer", person__user=request.user, group__in=teams) }
|
||||
|
||||
for t in teams:
|
||||
t.reviewer_settings = settings.get(t.pk) or ReviewerSettings()
|
||||
t.reviewer_settings = settings.get(t.pk) or ReviewerSettings(team=t)
|
||||
t.unavailable_periods = unavailable_periods.get(t.pk, [])
|
||||
t.role = roles.get(t.pk)
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ def insert_initial_review_data(apps, schema_editor):
|
|||
ReviewTypeName.objects.get_or_create(slug="early", name="Early", order=1)
|
||||
ReviewTypeName.objects.get_or_create(slug="lc", name="Last Call", order=2)
|
||||
ReviewTypeName.objects.get_or_create(slug="telechat", name="Telechat", order=3)
|
||||
ReviewTypeName.objects.get_or_create(slug="unknown", name="Unknown", order=4, used=False)
|
||||
|
||||
ReviewResultName = apps.get_model("name", "ReviewResultName")
|
||||
ReviewResultName.objects.get_or_create(slug="serious-issues", name="Serious Issues", order=1)
|
||||
|
|
|
@ -13,7 +13,7 @@ django.setup()
|
|||
|
||||
# script
|
||||
|
||||
import datetime
|
||||
import datetime, re, itertools
|
||||
from collections import namedtuple
|
||||
from django.db import connections
|
||||
from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultName,
|
||||
|
@ -21,9 +21,10 @@ from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultNam
|
|||
UnavailablePeriod, NextReviewerInTeam)
|
||||
from ietf.group.models import Group, Role, RoleName
|
||||
from ietf.person.models import Person, Email, Alias
|
||||
from ietf.doc.models import Document, DocAlias, ReviewRequestDocEvent, NewRevisionDocEvent, DocTypeName, State
|
||||
from ietf.utils.text import strip_prefix
|
||||
import argparse
|
||||
from unidecode import unidecode
|
||||
from collections import defaultdict
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("database", help="database must be included in settings")
|
||||
|
@ -110,7 +111,7 @@ with db_con.cursor() as c:
|
|||
team=team,
|
||||
person=email.person,
|
||||
)
|
||||
if reviewer:
|
||||
if created:
|
||||
print "created reviewer", reviewer.pk, unicode(reviewer).encode("utf-8")
|
||||
|
||||
if autopolicy_days.get(row.autopolicy):
|
||||
|
@ -181,12 +182,138 @@ type_names = { n.slug: n for n in ReviewTypeName.objects.all() }
|
|||
|
||||
# extract relevant log entries
|
||||
|
||||
request_assigned = defaultdict(list)
|
||||
document_history = {}
|
||||
|
||||
requested_re = re.compile("(?:ADDED docname =>|Created: remote=|AUTO UPDATED status TO new|CHANGED status FROM [^ ]+ => new|CHANGE status done => working)")
|
||||
|
||||
add_docstatus_re = re.compile('([a-zA-Z-_]+) ADD docstatus => (\w+)')
|
||||
update_docstatus_re = re.compile('([a-zA-Z-_]+) (?:UPDATE|CHANGE) docstatus \w+ => (\w+)')
|
||||
iesgstatus_re = re.compile('(?:ADD|ADDED|CHANGED) iesgstatus (?:FROM )?(?:[^ ]+ )?=> ([a-zA-Z-_]+)?')
|
||||
|
||||
deadline_re = re.compile('(?:ADD|ADDED|CHANGED) deadline (?:FROM )?(?:[^ ]+ )?=> ([1-9][0-9]+)')
|
||||
telechat_re = re.compile('(?:ADD|ADDED|CHANGED) telechat (?:FROM )?(?:[^ ]+ )?=> ([1-9][0-9]+)')
|
||||
lcend_re = re.compile('(?:ADD|ADDED|CHANGED) lcend (?:FROM )?(?:[^ ]+ )?=> ([1-9][0-9]+)')
|
||||
|
||||
close_states = ["done", "rejected", "withdrawn", "noresponse"]
|
||||
|
||||
with db_con.cursor() as c:
|
||||
c.execute("select docname, time, who from doclog where text = 'AUTO UPDATED status TO working' order by time desc;")
|
||||
for row in namedtuplefetchall(c):
|
||||
request_assigned[row.docname].append((row.time, row.who))
|
||||
c.execute("""select docname, time, who, text from doclog where
|
||||
text like 'Created: remote=%'
|
||||
or text like '%ADDED docname => %'
|
||||
or text like '%AUTO UPDATED status TO new%'
|
||||
or text like '%CHANGED status FROM % => new%'
|
||||
or text like '%CHANGE status done => working%'
|
||||
|
||||
or text like '% ADD docstatus => %'
|
||||
or text like '% UPDATE docstatus % => %'
|
||||
or text like '% CHANGE docstatus % => %'
|
||||
or text like '%CHANGE status working => done%'
|
||||
|
||||
or text like '%ADDED iesgstatus => %'
|
||||
or text like '%ADD iesgstatus => %'
|
||||
or text like '%CHANGED iesgstatus % => %'
|
||||
order by docname, time asc;""")
|
||||
for docname, rows in itertools.groupby(namedtuplefetchall(c), lambda row: row.docname):
|
||||
branches = {}
|
||||
|
||||
latest_requested = None
|
||||
non_specific_close = None
|
||||
latest_iesg_status = None
|
||||
|
||||
for row in rows:
|
||||
state = None
|
||||
used = False
|
||||
|
||||
if requested_re.search(row.text):
|
||||
if "Created: remote" in row.text:
|
||||
latest_iesg_status = "early"
|
||||
state = "requested"
|
||||
membername = None
|
||||
used = True
|
||||
else:
|
||||
if "ADD docstatus" in row.text:
|
||||
m = add_docstatus_re.match(row.text)
|
||||
assert m, 'row.text "{}" does not match add regexp'.format(row.text)
|
||||
membername, state = m.groups()
|
||||
used = True
|
||||
elif "UPDATE docstatus" in row.text or "CHANGE docstatus" in row.text:
|
||||
m = update_docstatus_re.match(row.text)
|
||||
assert m, 'row.text "{}" does not match update regexp'.format(row.text)
|
||||
membername, state = m.groups()
|
||||
used = True
|
||||
|
||||
if telechat_re.search(row.text) and not lcend_re.search(row.text):
|
||||
latest_iesg_status = "telechat"
|
||||
used = True
|
||||
elif ((not telechat_re.search(row.text) and lcend_re.search(row.text))
|
||||
or (deadline_re.search(row.text) and not telechat_re.search(row.text) and not lcend_re.search(row.text))):
|
||||
latest_iesg_status = "lc"
|
||||
used = True
|
||||
elif (deadline_re.search(row.text) and telechat_re.search(row.text) and lcend_re.search(row.text)):
|
||||
# if we got both, assume it was a Last Call
|
||||
latest_iesg_status = "lc"
|
||||
used = True
|
||||
|
||||
if iesgstatus_re.search(row.text):
|
||||
m = iesgstatus_re.search(row.text)
|
||||
if m.groups():
|
||||
literal_iesg_status = m.groups()[0]
|
||||
if literal_iesg_status == "IESG_Evaluation":
|
||||
latest_iesg_status = "telechat"
|
||||
elif literal_iesg_status == "In_Last_Call":
|
||||
latest_iesg_status = "lc"
|
||||
used = True
|
||||
|
||||
if "CHANGE status working => done" in row.text:
|
||||
non_specific_close = (row, latest_iesg_status)
|
||||
used = True
|
||||
|
||||
if not used:
|
||||
raise Exception("Unknown text {}".format(row.text))
|
||||
|
||||
if not state:
|
||||
continue
|
||||
|
||||
if state == "working":
|
||||
state = "assigned"
|
||||
|
||||
if state == "requested":
|
||||
latest_requested = (row.time, row.who, membername, state, latest_iesg_status)
|
||||
else:
|
||||
if membername not in branches:
|
||||
branches[membername] = []
|
||||
|
||||
latest = branches[membername][-1] if branches[membername] else None
|
||||
|
||||
if not latest or ((state == "assigned" and ("assigned" in latest or "closed" in latest))
|
||||
or (state not in ("assigned", "accepted") and "closed" in latest)):
|
||||
# open new
|
||||
branches[membername].append({})
|
||||
latest = branches[membername][-1]
|
||||
if latest_requested:
|
||||
latest["requested"] = latest_requested
|
||||
latest_requested = None
|
||||
|
||||
if state in ("assigned", 'accepted'):
|
||||
latest[state] = (row.time, row.who, membername, state, latest_iesg_status)
|
||||
else:
|
||||
latest["closed"] = (row.time, row.who, membername, state, latest_iesg_status)
|
||||
|
||||
|
||||
if branches:
|
||||
if non_specific_close:
|
||||
# find any open branches
|
||||
for m, hs in branches.iteritems():
|
||||
latest = hs[-1]
|
||||
if "assigned" in latest and "closed" not in latest:
|
||||
#print "closing with non specific", docname
|
||||
close_row, iesg_status = non_specific_close
|
||||
latest["closed"] = (close_row.time, close_row.who, m, "done", iesg_status)
|
||||
|
||||
document_history[docname] = branches
|
||||
|
||||
# if docname in document_history:
|
||||
# print docname, document_history[docname]
|
||||
|
||||
# extract document request metadata
|
||||
|
||||
|
@ -214,33 +341,97 @@ with db_con.cursor() as c:
|
|||
if not deadline:
|
||||
deadline = parse_timestamp(row.timeout)
|
||||
|
||||
type_name = type_names["unknown"]
|
||||
# FIXME: use lcend and telechat to try to deduce type
|
||||
|
||||
reviewed_rev = row.version if row.version and row.version != "99" else ""
|
||||
if row.summary == "noresponse":
|
||||
reviewed_rev = ""
|
||||
|
||||
assignment_logs = request_assigned.get(row.docname, [])
|
||||
if assignment_logs:
|
||||
time, who = assignment_logs.pop()
|
||||
|
||||
time = parse_timestamp(time)
|
||||
event_collection = None
|
||||
branches = document_history.get(row.docname)
|
||||
if not branches:
|
||||
print "WARNING: no history for", row.docname
|
||||
else:
|
||||
time = deadline
|
||||
history = branches.get(row.reviewer)
|
||||
if not history:
|
||||
print "WARNING: reviewer {} not found in history for".format(row.reviewer), row.docname
|
||||
else:
|
||||
event_collection = history.pop(0)
|
||||
if "requested" not in event_collection:
|
||||
print "WARNING: no requested log entry for", row.docname, [event_collection] + history
|
||||
|
||||
if "assigned" not in event_collection:
|
||||
print "WARNING: no assigned log entry for", row.docname, [event_collection] + history
|
||||
|
||||
if "closed" not in event_collection and row.docstatus in close_states:
|
||||
print "WARNING: no {} log entry for".format("/".join(close_states)), row.docname, [event_collection] + history
|
||||
|
||||
def day_delta(time_from, time_to):
|
||||
if time_from is None or time_to is None:
|
||||
return None
|
||||
|
||||
return float(time_to[0] - time_from[0]) / (24 * 60 * 60)
|
||||
|
||||
requested_assigned_days = day_delta(event_collection.get("requested"), event_collection.get("assigned"))
|
||||
if requested_assigned_days is not None and requested_assigned_days < 0:
|
||||
print "WARNING: assignment before request", requested_assigned_days, row.docname
|
||||
at_most_days = 20
|
||||
if requested_assigned_days is not None and requested_assigned_days > at_most_days:
|
||||
print "WARNING: more than {} days between request and assignment".format(at_most_days), round(requested_assigned_days), event_collection, row.docname
|
||||
|
||||
if "closed" in event_collection:
|
||||
assigned_closed_days = day_delta(event_collection.get("assigned"), event_collection.get("closed"))
|
||||
if assigned_closed_days is not None and assigned_closed_days < 0:
|
||||
print "WARNING: closure before assignment", assigned_closed_days, row.docname
|
||||
at_most_days = 60
|
||||
if assigned_closed_days is not None and assigned_closed_days > at_most_days and event_collection.get("closed")[3] not in ("noresponse", "withdrawn"):
|
||||
print "WARNING: more than {} days between assignment and completion".format(at_most_days), round(assigned_closed_days), event_collection, row.docname
|
||||
|
||||
if event_collection:
|
||||
time = None
|
||||
if "requested" in event_collection:
|
||||
time = parse_timestamp(event_collection["requested"][0])
|
||||
elif "assigned" in event_collection:
|
||||
time = parse_timestamp(event_collection["assigned"][0])
|
||||
elif "closed" in event_collection:
|
||||
time = parse_timestamp(event_collection["closed"][0])
|
||||
else:
|
||||
time = deadline
|
||||
|
||||
if not deadline and "closed" in event_collection:
|
||||
deadline = parse_timestamp(event_collection["closed"][0])
|
||||
|
||||
if not deadline:
|
||||
# bogus row
|
||||
print "SKIPPING WITH NO DEADLINE", time, row, meta
|
||||
print "SKIPPING WITH NO DEADLINE", row.reviewid, row.docname, meta, event_collection
|
||||
continue
|
||||
|
||||
if status == "done" and row.docstatus in ("assigned", "accepted"):
|
||||
# filter out some apparently dead requests
|
||||
print "SKIPPING MARKED DONE even if assigned/accepted", time, row
|
||||
continue
|
||||
type_slug = None
|
||||
if "assigned" in event_collection:
|
||||
type_slug = event_collection["assigned"][4]
|
||||
if type_slug:
|
||||
print "deduced type slug at assignment", type_slug, row.docname
|
||||
|
||||
req, _ = ReviewRequest.objects.get_or_create(
|
||||
doc_id=row.docname,
|
||||
if not type_slug and "requested" in event_collection:
|
||||
type_slug = event_collection["requested"][4]
|
||||
if type_slug:
|
||||
print "deduced type slug at request", type_slug, row.docname
|
||||
|
||||
if not type_slug and "closed" in event_collection and "assigned" not in event_collection:
|
||||
type_slug = event_collection["closed"][4]
|
||||
if type_slug:
|
||||
print "deduced type slug at closure", type_slug, row.docname
|
||||
|
||||
if not type_slug:
|
||||
print "did not deduce type slug, assuming early", row.docname, deadline
|
||||
type_slug = "early"
|
||||
|
||||
type_name = type_names[type_slug]
|
||||
|
||||
def fix_docname(docname):
|
||||
if docname == "draft-fenner-obsolete":
|
||||
docname = "draft-fenner-obsolete-1264"
|
||||
return docname
|
||||
|
||||
review_req, _ = ReviewRequest.objects.get_or_create(
|
||||
doc_id=fix_docname(row.docname),
|
||||
team=team,
|
||||
old_id=row.reviewid,
|
||||
defaults={
|
||||
|
@ -251,28 +442,142 @@ with db_con.cursor() as c:
|
|||
}
|
||||
)
|
||||
|
||||
req.reviewer = known_personnel[row.reviewer] if row.reviewer else None
|
||||
req.result = results.get(row.summary.lower()) if row.summary else None
|
||||
req.state = states.get(row.docstatus) if row.docstatus else None
|
||||
req.type = type_name
|
||||
req.time = time
|
||||
req.reviewed_rev = reviewed_rev
|
||||
req.deadline = deadline.date()
|
||||
req.save()
|
||||
review_req.reviewer = known_personnel[row.reviewer] if row.reviewer else None
|
||||
review_req.result = results.get(row.summary.lower()) if row.summary else None
|
||||
review_req.state = states.get(row.docstatus) if row.docstatus else None
|
||||
review_req.type = type_name
|
||||
review_req.time = time
|
||||
review_req.reviewed_rev = reviewed_rev if review_req.state_id not in ("requested", "accepted") else ""
|
||||
review_req.deadline = deadline.date()
|
||||
review_req.save()
|
||||
|
||||
# FIXME: add log entries
|
||||
# FIXME: add review from reviewurl
|
||||
# FIXME: do something about missing result
|
||||
completion_event = None
|
||||
|
||||
# adcomments IGNORED
|
||||
# lccomments IGNORED
|
||||
# nits IGNORED
|
||||
# reviewurl review.external_url
|
||||
# review request events
|
||||
for key, data in event_collection.iteritems():
|
||||
timestamp, who_did_it, reviewer, state, latest_iesg_status = data
|
||||
|
||||
#print meta and meta[0], telechat, lcend, req.type
|
||||
if who_did_it in known_personnel:
|
||||
by = known_personnel[who_did_it].person
|
||||
else:
|
||||
by = system_person
|
||||
|
||||
if req.state_id == "requested" and req.doc.get_state_slug("draft-iesg") in ["approved", "ann", "rfcqueue", "pub"]:
|
||||
req.state = states["overtaken"]
|
||||
req.save()
|
||||
if key == "requested":
|
||||
if "assigned" in event_collection:
|
||||
continue # skip requested unless there's no assigned event
|
||||
|
||||
print "imported review", row.reviewid, "as", req.pk, req.time, req.deadline, req.type, req.doc_id, req.state, req.doc.get_state_slug("draft-iesg")
|
||||
e = ReviewRequestDocEvent.objects.filter(type="requested_review", doc=review_req.doc).first() or ReviewRequestDocEvent(type="requested_review", doc=review_req.doc)
|
||||
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.time = parse_timestamp(timestamp)
|
||||
e.by = by
|
||||
e.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)",
|
||||
)
|
||||
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.time = parse_timestamp(timestamp)
|
||||
e.by = by
|
||||
e.state = states.get(state) if state else None
|
||||
if e.state_id == "rejected":
|
||||
e.desc = "Assignment of request for {} review by {} to {} was rejected".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.reviewer.person,
|
||||
)
|
||||
elif e.state_id == "completed":
|
||||
e.desc = "Request for {} review by {} {}{}. Reviewer: {}.".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.state.name,
|
||||
": {}".format(review_req.result.name) if review_req.result else "",
|
||||
review_req.reviewer.person,
|
||||
)
|
||||
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
|
||||
print "imported event closed_review_request", e.desc, e.doc_id
|
||||
|
||||
if review_req.state_id == "completed":
|
||||
if not row.reviewurl: # don't have anything to store, so skip
|
||||
continue
|
||||
|
||||
if completion_event:
|
||||
completion_time = completion_event.time
|
||||
completion_by = completion_event.by
|
||||
else:
|
||||
completion_time = deadline
|
||||
completion_by = system_person
|
||||
|
||||
# create the review document
|
||||
if review_req.review:
|
||||
review = review_req.review
|
||||
else:
|
||||
for i in range(1, 100):
|
||||
name_components = [
|
||||
"review",
|
||||
strip_prefix(review_req.doc_id, "draft-"),
|
||||
review_req.reviewed_rev,
|
||||
review_req.team.acronym,
|
||||
review_req.type_id,
|
||||
review_req.reviewer.person.ascii_parts()[3],
|
||||
completion_time.date().isoformat(),
|
||||
]
|
||||
if i > 1:
|
||||
name_components.append(str(i))
|
||||
|
||||
name = "-".join(c for c in name_components if c).lower()
|
||||
if not Document.objects.filter(name=name).exists():
|
||||
review = Document.objects.create(name=name)
|
||||
DocAlias.objects.create(document=review, name=review.name)
|
||||
break
|
||||
|
||||
review.set_state(State.objects.get(type="review", slug="active"))
|
||||
|
||||
review.time = completion_time
|
||||
review.type = DocTypeName.objects.get(slug="review")
|
||||
review.rev = "00"
|
||||
review.title = "{} Review of {}-{}".format(review_req.type.name, review_req.doc.name, review_req.reviewed_rev)
|
||||
review.group = review_req.team
|
||||
review.external_url = row.reviewurl
|
||||
|
||||
existing = NewRevisionDocEvent.objects.filter(doc=review).first() or NewRevisionDocEvent(doc=review)
|
||||
e.type = "new_revision"
|
||||
e.by = completion_by
|
||||
e.rev = review.rev
|
||||
e.desc = 'New revision available'
|
||||
e.time = completion_time
|
||||
e.skip_community_list_notification = True
|
||||
|
||||
review.save_with_history([e])
|
||||
|
||||
review_req.review = review
|
||||
review_req.save()
|
||||
|
||||
print "imported review document", review_req.doc, review.name
|
||||
|
||||
if review_req.state_id in ("requested", "accepted") and review_req.doc.get_state_slug("draft-iesg") in ["approved", "ann", "rfcqueue", "pub"]:
|
||||
review_req.state = states["overtaken"]
|
||||
review_req.save()
|
||||
|
||||
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")
|
||||
|
|
|
@ -49,7 +49,7 @@ class Migration(migrations.Migration):
|
|||
('deadline', models.DateField()),
|
||||
('requested_rev', models.CharField(help_text=b'Fill in if a specific revision is to be reviewed, e.g. 02', max_length=16, verbose_name=b'requested revision', blank=True)),
|
||||
('reviewed_rev', models.CharField(max_length=16, verbose_name=b'reviewed revision', blank=True)),
|
||||
('doc', models.ForeignKey(related_name='review_request_set', to='doc.Document')),
|
||||
('doc', models.ForeignKey(related_name='reviewrequest_set', to='doc.Document')),
|
||||
('requested_by', models.ForeignKey(to='person.Person')),
|
||||
('result', models.ForeignKey(blank=True, to='name.ReviewResultName', null=True)),
|
||||
('review', models.OneToOneField(null=True, blank=True, to='doc.Document')),
|
||||
|
|
|
@ -96,7 +96,7 @@ class ReviewRequest(models.Model):
|
|||
# constitute the request part.
|
||||
time = models.DateTimeField(default=datetime.datetime.now)
|
||||
type = models.ForeignKey(ReviewTypeName)
|
||||
doc = models.ForeignKey(Document, related_name='review_request_set')
|
||||
doc = models.ForeignKey(Document, related_name='reviewrequest_set')
|
||||
team = models.ForeignKey(Group, limit_choices_to=~models.Q(reviewteamresult=None))
|
||||
deadline = models.DateField()
|
||||
requested_by = models.ForeignKey(Person)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import datetime, re
|
||||
import datetime, re, itertools
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models import Q, Max
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.doc.models import Document, DocEvent, State, LastCallDocEvent, DocumentAuthor, DocAlias
|
||||
from ietf.doc.models import (Document, ReviewRequestDocEvent, State,
|
||||
LastCallDocEvent, DocumentAuthor, DocAlias)
|
||||
from ietf.iesg.models import TelechatDate
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
|
||||
|
@ -50,7 +51,7 @@ def no_review_from_teams_on_doc(doc, rev):
|
|||
reviewrequest__state="no-review-version",
|
||||
).distinct()
|
||||
|
||||
def unavailability_periods_to_list(past_days=30):
|
||||
def unavailable_periods_to_list(past_days=14):
|
||||
return UnavailablePeriod.objects.filter(
|
||||
Q(end_date=None) | Q(end_date__gte=datetime.date.today() - datetime.timedelta(days=past_days)),
|
||||
).order_by("start_date")
|
||||
|
@ -71,7 +72,7 @@ def current_unavailable_periods_for_reviewers(team):
|
|||
|
||||
return res
|
||||
|
||||
def reviewer_rotation_list(team):
|
||||
def reviewer_rotation_list(team, skip_unavailable=False, dont_skip=[]):
|
||||
"""Returns person id -> index in rotation (next reviewer has index 0)."""
|
||||
reviewers = list(Person.objects.filter(role__name="reviewer", role__group=team))
|
||||
reviewers.sort(key=lambda p: p.last_name())
|
||||
|
@ -94,7 +95,26 @@ def reviewer_rotation_list(team):
|
|||
else:
|
||||
next_reviewer_index = reviewers.index(n)
|
||||
|
||||
return reviewers[next_reviewer_index:] + reviewers[:next_reviewer_index]
|
||||
rotation_list = reviewers[next_reviewer_index:] + reviewers[:next_reviewer_index]
|
||||
|
||||
if skip_unavailable:
|
||||
# prune reviewers not in the rotation (but not the assigned
|
||||
# reviewer who must have been available for assignment anyway)
|
||||
reviewers_to_skip = set()
|
||||
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(team)
|
||||
for person_id, periods in unavailable_periods.iteritems():
|
||||
if periods and person_id not in dont_skip:
|
||||
reviewers_to_skip.add(person_id)
|
||||
|
||||
days_needed_for_reviewers = days_needed_to_fulfill_min_interval_for_reviewers(team)
|
||||
for person_id, days_needed in days_needed_for_reviewers.iteritems():
|
||||
if days_needed > 0 and person_id not in dont_skip:
|
||||
reviewers_to_skip.add(person_id)
|
||||
|
||||
rotation_list = [p.pk for p in rotation_list if p.pk not in reviewers_to_skip]
|
||||
|
||||
return rotation_list
|
||||
|
||||
def days_needed_to_fulfill_min_interval_for_reviewers(team):
|
||||
"""Returns person_id -> days needed until min_interval is fulfilled for
|
||||
|
@ -121,6 +141,66 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
|
|||
|
||||
return res
|
||||
|
||||
def extract_review_request_data(teams=None, reviewers=None, time_from=None, time_to=None):
|
||||
"""Returns a dict keyed on (team.pk, reviewer_person.pk) which lists data on each review request."""
|
||||
|
||||
filters = Q()
|
||||
|
||||
if teams:
|
||||
filters &= Q(team__in=teams)
|
||||
|
||||
if reviewers:
|
||||
filters &= Q(reviewer__person__in=reviewers)
|
||||
|
||||
if time_from:
|
||||
filters &= Q(time__gte=time_from)
|
||||
|
||||
if time_to:
|
||||
filters &= Q(time__lte=time_to)
|
||||
|
||||
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")
|
||||
|
||||
# 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")
|
||||
|
||||
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
|
||||
|
||||
for _, events in itertools.groupby(event_qs.iterator(), lambda t: t[0]):
|
||||
requested_time = assigned_time = closed_time = None
|
||||
|
||||
for e in events:
|
||||
req_pk, doc, req_time, state, deadline, result, team, reviewer, event_time, event_type = e
|
||||
|
||||
if event_type == "requested_review" and requested_time is None:
|
||||
requested_time = event_time
|
||||
elif event_type == "assigned_review_request" and assigned_time is None:
|
||||
assigned_time = event_time
|
||||
elif event_type == "closed_review_request" and closed_time is None:
|
||||
closed_time = event_time
|
||||
|
||||
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)
|
||||
|
||||
res[(team, reviewer)].append((req_pk, doc, req_time, state, deadline, result,
|
||||
late_days, request_to_assignment_days, assignment_to_closure_days,
|
||||
request_to_closure_days))
|
||||
|
||||
return res
|
||||
|
||||
def make_new_review_request_from_existing(review_req):
|
||||
obj = ReviewRequest()
|
||||
obj.time = review_req.time
|
||||
|
@ -233,10 +313,11 @@ def assign_review_request_to_reviewer(request, review_req, reviewer):
|
|||
review_req.reviewer = reviewer
|
||||
review_req.save()
|
||||
|
||||
possibly_advance_next_reviewer_for_team(review_req.team, review_req.reviewer.person_id)
|
||||
if review_req.reviewer:
|
||||
possibly_advance_next_reviewer_for_team(review_req.team, review_req.reviewer.person_id)
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
ReviewRequestDocEvent.objects.create(
|
||||
type="assigned_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Request for {} review by {} is assigned to {}".format(
|
||||
|
@ -244,6 +325,8 @@ def assign_review_request_to_reviewer(request, review_req, reviewer):
|
|||
review_req.team.acronym.upper(),
|
||||
review_req.reviewer.person if review_req.reviewer else "(None)",
|
||||
),
|
||||
review_request=review_req,
|
||||
state=None,
|
||||
)
|
||||
|
||||
email_review_request_change(
|
||||
|
@ -253,26 +336,14 @@ def assign_review_request_to_reviewer(request, review_req, reviewer):
|
|||
by=request.user.person, notify_secretary=False, notify_reviewer=True, notify_requested_by=False)
|
||||
|
||||
def possibly_advance_next_reviewer_for_team(team, assigned_review_to_person_id):
|
||||
# prune reviewers not in the rotation (but not the assigned
|
||||
# reviewer who must have been available for assignment anyway)
|
||||
reviewers_to_skip = set()
|
||||
assert assigned_review_to_person_id is not None
|
||||
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(team)
|
||||
for person_id, periods in unavailable_periods.iteritems():
|
||||
if periods and person_id != assigned_review_to_person_id:
|
||||
reviewers_to_skip.add(person_id)
|
||||
|
||||
days_needed_for_reviewers = days_needed_to_fulfill_min_interval_for_reviewers(team)
|
||||
for person_id, days_needed in days_needed_for_reviewers.iteritems():
|
||||
if days_needed > 0 and person_id != assigned_review_to_person_id:
|
||||
reviewers_to_skip.add(person_id)
|
||||
|
||||
rotation_list = [p.pk for p in reviewer_rotation_list(team) if p.pk not in reviewers_to_skip]
|
||||
|
||||
if not rotation_list:
|
||||
return
|
||||
rotation_list = reviewer_rotation_list(team, skip_unavailable=True, dont_skip=[assigned_review_to_person_id])
|
||||
|
||||
def reviewer_at_index(i):
|
||||
if not rotation_list:
|
||||
return None
|
||||
|
||||
return rotation_list[i % len(rotation_list)]
|
||||
|
||||
def reviewer_settings_for(person_id):
|
||||
|
@ -281,7 +352,7 @@ def possibly_advance_next_reviewer_for_team(team, assigned_review_to_person_id):
|
|||
|
||||
current_i = 0
|
||||
|
||||
if reviewer_at_index(current_i) == assigned_review_to_person_id:
|
||||
if assigned_review_to_person_id == reviewer_at_index(current_i):
|
||||
# move 1 ahead
|
||||
current_i += 1
|
||||
else:
|
||||
|
@ -289,6 +360,9 @@ def possibly_advance_next_reviewer_for_team(team, assigned_review_to_person_id):
|
|||
settings.skip_next += 1
|
||||
settings.save()
|
||||
|
||||
if not rotation_list:
|
||||
return
|
||||
|
||||
while True:
|
||||
# as a clean-up step go through any with a skip next > 0
|
||||
current_reviewer_person_id = reviewer_at_index(current_i)
|
||||
|
@ -315,12 +389,14 @@ def close_review_request(request, review_req, close_state):
|
|||
review_req.save()
|
||||
|
||||
if not suggested_req:
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
ReviewRequestDocEvent.objects.create(
|
||||
type="closed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Closed request for {} review by {} with state '{}'".format(
|
||||
review_req.type.name, review_req.team.acronym.upper(), close_state.name),
|
||||
review_request=review_req,
|
||||
state=review_req.state,
|
||||
)
|
||||
|
||||
if prev_state.slug != "requested":
|
||||
|
@ -337,6 +413,8 @@ def suggested_review_requests_for_team(team):
|
|||
|
||||
requests = {}
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
requested_state = ReviewRequestStateName.objects.get(slug="requested", used=True)
|
||||
|
||||
if True: # FIXME
|
||||
|
@ -345,9 +423,9 @@ def suggested_review_requests_for_team(team):
|
|||
last_call_docs = Document.objects.filter(states=State.objects.get(type="draft-iesg", slug="lc", used=True))
|
||||
last_call_expires = { e.doc_id: e.expires for e in LastCallDocEvent.objects.order_by("time", "id") }
|
||||
for doc in last_call_docs:
|
||||
deadline = last_call_expires[doc.pk].date() if doc.pk in last_call_expires else datetime.date.today()
|
||||
deadline = last_call_expires[doc.pk].date() if doc.pk in last_call_expires else today
|
||||
|
||||
if deadline > seen_deadlines.get(doc.pk, datetime.date.max):
|
||||
if deadline > seen_deadlines.get(doc.pk, datetime.date.max) or deadline < today:
|
||||
continue
|
||||
|
||||
requests[doc.pk] = ReviewRequest(
|
||||
|
@ -486,7 +564,7 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
|
||||
for p in possible_person_ids:
|
||||
if p not in reviewer_settings:
|
||||
reviewer_settings[p] = ReviewerSettings()
|
||||
reviewer_settings[p] = ReviewerSettings(team=team)
|
||||
|
||||
# frequency
|
||||
days_needed_for_reviewers = days_needed_to_fulfill_min_interval_for_reviewers(team)
|
||||
|
|
|
@ -519,6 +519,10 @@ table.unavailable-periods td {
|
|||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
table.unavailable-periods td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.unavailable-period-past {
|
||||
color: #777;
|
||||
}
|
||||
|
@ -527,6 +531,10 @@ table.unavailable-periods td {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reviewer-overview .completely-unavailable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* === Photo pages ========================================================== */
|
||||
|
||||
.photo-name {
|
||||
|
|
|
@ -69,12 +69,14 @@
|
|||
<td>{{ review_req.reviewer.person }}</td>
|
||||
</tr>
|
||||
|
||||
{% if review_req.result %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Review result</th>
|
||||
<td class="edit"></td>
|
||||
<td>{{ review_req.result.name }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if doc.external_url %}
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<tr>
|
||||
<th></th>
|
||||
<th>Team</th>
|
||||
<td><a href="{% url "ietf.group.views.review_requests" group_type=review_req.team.type_id acronym=review_req.team.acronym %}">{{ review_req.team.acronym|upper }}</a></td>
|
||||
<td><a href="{% url "ietf.group.views_review.review_requests" group_type=review_req.team.type_id acronym=review_req.team.acronym %}">{{ review_req.team.acronym|upper }}</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<div class="review-request-summary">
|
||||
{% if review_request.state_id == "completed" or review_request.state_id == "part-completed" %}
|
||||
<a href="{% if review_request.review %}{% url "doc_view" review_request.review.name %}{% else %}{% url "ietf.doc.views_review.review_request" review_request.doc_id review_request.pk %}{% endif %}">
|
||||
{{ review_request.team.acronym|upper }} {{ review_request.type.name }} Review{% if review_request.reviewed_rev and review_request.reviewed_rev != current_rev or review_request.doc_id != current_doc_name %} (of {% if review_request.doc_id != current_doc_name %}{{ review_request.doc_id }}{% endif %}-{{ review_request.reviewed_rev }}){% endif %}:
|
||||
{{ review_request.result.name }} {% if review_request.state_id == "part-completed" %}(partially completed){% endif %}
|
||||
{{ review_request.team.acronym|upper }} {{ review_request.type.name }} Review{% if review_request.reviewed_rev and review_request.reviewed_rev != current_rev or review_request.doc_id != current_doc_name %} (of {% if review_request.doc_id != current_doc_name %}{{ review_request.doc_id }}{% endif %}-{{ review_request.reviewed_rev }}){% endif %}{% if review_request.result %}:
|
||||
{{ review_request.result.name }}{% endif %} {% if review_request.state_id == "part-completed" %}(partially completed){% endif %}
|
||||
- reviewer: {{ review_request.reviewer.person }}</a>
|
||||
{% else %}
|
||||
<i>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<h1>Manage open review requests for {{ group.acronym }}</h1>
|
||||
|
||||
<p>Other options:
|
||||
<a href="{% url "ietf.group.views.review_requests" group_type=group.type_id acronym=group.acronym %}#closed-review-requests">Closed review requests</a>
|
||||
<a href="{% url "ietf.group.views_review.reviewer_overview" group_type=group.type_id acronym=group.acronym %}">Reviewers in team</a>
|
||||
- <a href="{% url "ietf.group.views_review.email_open_review_assignments" group_type=group.type_id acronym=group.acronym %}">Email open assignments summary</a>
|
||||
</p>
|
||||
|
||||
|
@ -59,7 +59,7 @@
|
|||
{% for rlatest in r.latest_reqs %}
|
||||
<div>
|
||||
<small>- prev. review of {% if rlatest.doc_id != r.doc_id %}{{ rlatest.doc_id }}{% endif %}-{{ rlatest.reviewed_rev }}:
|
||||
<a href="{% url "ietf.doc.views_review.review_request" name=rlatest.doc_id request_id=rlatest.pk %}">{{ rlatest.result.name }}</a>
|
||||
<a href="{% url "ietf.doc.views_review.review_request" name=rlatest.doc_id request_id=rlatest.pk %}">{% if rlatest.result %}{{ rlatest.result.name }}{% else %}result unavail.{% endif %}</a>
|
||||
(<a href="{{ rfcdiff_base_url }}?url1={{ rlatest.doc.name }}-{{ rlatest.reviewed_rev }}&url2={{ r.doc.name }}-{{ r.doc.rev }}">diff</a>){% if not forloop.last %},{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
|
@ -129,7 +129,7 @@
|
|||
</table>
|
||||
|
||||
{% buttons %}
|
||||
<a href="{% url "ietf.group.views.review_requests" group_type=group.type_id acronym=group.acronym %}" class="btn btn-default pull-right">Cancel</a>
|
||||
<a href="{% url "ietf.group.views_review.review_requests" group_type=group.type_id acronym=group.acronym %}" class="btn btn-default pull-right">Cancel</a>
|
||||
<button class="btn btn-primary" type="submit" name="action" value="save">Save changes</button>
|
||||
<button class="btn btn-primary" type="submit" name="action" value="save-continue">Save and continue editing</button>
|
||||
<button class="btn btn-default" type="submit" name="action" value="refresh">Refresh (keeping changes)</button>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<td>{% if r.pk %}{{ r.time|date:"Y-m-d" }}{% else %}<em>auto-suggested</em>{% endif %}</td>
|
||||
<td>
|
||||
{{ r.deadline|date:"Y-m-d" }}
|
||||
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
|
||||
{% if r.due %}<span class="label label-warning" title="{{ r.due }} day{{ r.due|pluralize }} past deadline">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if r.reviewer %}
|
||||
|
|
49
ietf/templates/group/reviewer_overview.html
Normal file
49
ietf/templates/group/reviewer_overview.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% extends "group/group_base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}{% origin %}
|
||||
|
||||
{% load ietf_filters staticfiles bootstrap3 %}
|
||||
|
||||
{% block group_subtitle %}Reviewers{% endblock %}
|
||||
|
||||
{% block group_content %}
|
||||
{% origin %}
|
||||
|
||||
<h2>Reviewers</h2>
|
||||
|
||||
{% if reviewers %}
|
||||
<table class="table reviewer-overview">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reviewer</th>
|
||||
<th>Latest assignments (deadline, state)</th>
|
||||
<th>Settings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for person in reviewers %}
|
||||
<tr {% if person.completely_unavailable %}class="completely-unavailable"{% endif %}>
|
||||
<td><a {% if person.settings_url %}href="{{ person.settings_url }}"{% endif %}>{{ person }}</a></td>
|
||||
<td>
|
||||
{% for req_pk, doc_name, deadline, state, assignment_to_closure_days in person.latest_reqs %}
|
||||
<div><a href="{% url "ietf.doc.views_review.review_request" name=doc_name request_id=req_pk %}">{{ deadline|date }} {{ doc_name }}: {{ state.name }} {% if assignment_to_closure_days != None %}{% if state.slug == "completed" or state.slug == "part-completed" %}(in {{ assignment_to_closure_days|floatformat }} day{{ assignment_to_closure_days|pluralize }}){% endif %}{% endif %}</a></div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{{ person.settings.get_min_interval_display }} {% if person.settings.skip_next %}(skip: {{ person.settings.skip_next }}){% endif %}<br>
|
||||
{% if person.settings.filter_re %}Filter: <code title="{{ person.settings.filter_re }}">{{ person.settings.filter_re|truncatechars:15 }}</code><br>{% endif %}
|
||||
|
||||
{% if person.unavailable_periods %}
|
||||
{% include "review/unavailable_table.html" with unavailable_periods=person.unavailable_periods %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<p>No reviewers found.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -140,14 +140,7 @@
|
|||
<th>Unavailable periods</th>
|
||||
<td>
|
||||
{% if t.unavailable_periods %}
|
||||
<table class="unavailable-periods">
|
||||
{% for o in t.unavailable_periods %}
|
||||
<tr class="unavailable-period-{{ o.state }}">
|
||||
<td>{{ o.start_date }} - {{ o.end_date|default:"" }}</td>
|
||||
<td>{{ o.get_availability_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% include "review/unavailable_table.html" with unavailable_periods=t.unavailable_periods %}
|
||||
{% else %}
|
||||
(No periods)
|
||||
{% endif %}
|
||||
|
|
8
ietf/templates/review/unavailable_table.html
Normal file
8
ietf/templates/review/unavailable_table.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<table class="unavailable-periods">
|
||||
{% for p in unavailable_periods %}
|
||||
<tr class="unavailable-period-{{ p.state }}">
|
||||
<td>{{ p.start_date }} - {{ p.end_date|default:"" }}</td>
|
||||
<td>{{ p.get_availability_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
Loading…
Reference in a new issue