Merged in [16144] from housley@vigilsec.com:

Remove some clutter from I-D upload page.  The information was mived to the Instructions tab, but a higligh to the Note Well and BCP 78 was added. Fixes #2057.
 - Legacy-Id: 16151
Note: SVN reference [16144] has been migrated to Git commit 37851b9742
This commit is contained in:
Henrik Levkowetz 2019-04-22 17:39:26 +00:00
commit cfcefc2e32
55 changed files with 2110 additions and 1044 deletions

View file

@ -19,17 +19,19 @@ django.setup()
import datetime
from ietf.review.utils import (
review_requests_needing_reviewer_reminder, email_reviewer_reminder,
review_requests_needing_secretary_reminder, email_secretary_reminder,
review_assignments_needing_reviewer_reminder, email_reviewer_reminder,
review_assignments_needing_secretary_reminder, email_secretary_reminder,
)
today = datetime.date.today()
for review_req in review_requests_needing_reviewer_reminder(today):
email_reviewer_reminder(review_req)
print("Emailed reminder to {} for review of {} in {} (req. id {})".format(review_req.reviewer.address, review_req.doc_id, review_req.team.acronym, review_req.pk))
for assignment in review_assignments_needing_reviewer_reminder(today):
email_reviewer_reminder(assignment.review_request)
for review_assignment in assignments.review_req.reviewassignment_set.all():
print("Emailed reminder to {} for review of {} in {} (req. id {})".format(review_assignment.reviewer.address, assignment.review_req.doc_id, assignment.review_req.team.acronym, assignment.review_req.pk))
for review_req, secretary_role in review_requests_needing_secretary_reminder(today):
email_secretary_reminder(review_req, secretary_role)
print("Emailed reminder to {} for review of {} in {} (req. id {})".format(review_req.secretary_role.email.address, review_req.doc_id, review_req.team.acronym, review_req.pk))
for assignment, secretary_role in review_assignments_needing_secretary_reminder(today):
email_secretary_reminder(assignment.review_request, secretary_role)
review_req = assignment.review_request
print("Emailed reminder to {} for review of {} in {} (req. id {})".format(secretary_role.email.address, review_req.doc_id, review_req.team.acronym, review_req.pk))

View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-05 11:39
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate', 'DBTemplate')
DBTemplate.objects.filter(id=186).update(content="""I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please treat these comments just
like any other last call comments.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document: {{ assignment.review_request.doc.name }}-??
Reviewer: {{ assignment.reviewer.person.plain_name }}
Review Date: {{ today }}
IETF LC End Date: {% if assignment.review_request.doc.most_recent_ietflc %}{{ assignment.review_request.doc.most_recent_ietflc.expires|date:"Y-m-d" }}{% else %}None{% endif %}
IESG Telechat date: {{ assignment.review_request.doc.telechat_date|default:'Not scheduled for a telechat' }}
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
""")
DBTemplate.objects.filter(id=187).update(content="""I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please wait for direction from your
document shepherd or AD before posting a new version of the draft.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document: {{ assignment.review_request.doc.name }}-??
Reviewer: {{ assignment.reviewer.person.plain_name }}
Review Date: {{ today }}
IETF LC End Date: {% if assignment.review_req.doc.most_recent_ietflc %}{{ assignment.review_request.doc.most_recent_ietflc.expires|date:"Y-m-d" }}{% else %}None{% endif %}
IESG Telechat date: {{ assignment.review_request.doc.telechat_date|default:'Not scheduled for a telechat' }}
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
""")
def reverse(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
DBTemplate.objects.filter(id=186).update(content="""I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please treat these comments just
like any other last call comments.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document: {{ review_req.doc.name }}-??
Reviewer: {{ review_req.reviewer.person.plain_name }}
Review Date: {{ today }}
IETF LC End Date: {% if review_req.doc.most_recent_ietflc %}{{ review_req.doc.most_recent_ietflc.expires|date:"Y-m-d" }}{% else %}None{% endif %}
IESG Telechat date: {{ review_req.doc.telechat_date|default:'Not scheduled for a telechat' }}
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
""")
DBTemplate.objects.filter(id=187).update(content="""I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please wait for direction from your
document shepherd or AD before posting a new version of the draft.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document: {{ review_req.doc.name }}-??
Reviewer: {{ review_req.reviewer.person.plain_name }}
Review Date: {{ today }}
IETF LC End Date: {% if review_req.doc.most_recent_ietflc %}{{ review_req.doc.most_recent_ietflc.expires|date:"Y-m-d" }}{% else %}None{% endif %}
IESG Telechat date: {{ review_req.doc.telechat_date|default:'Not scheduled for a telechat' }}
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
""")
class Migration(migrations.Migration):
dependencies = [
('dbtemplate', '0002_auto_20180220_1052'),
('doc','0011_reviewassignmentdocevent'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-03-13 13:41
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
DBTemplate.objects.filter(pk=182).update(content="""{% autoescape off %}Subject: Open review assignments in {{group.acronym}}
The following reviewers have assignments:{% for r in review_assignments %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.review_request.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.review_request.doc_id }}-{% if r.review_request..requested_rev %}{{ r.review_request.requested_rev }}{% else %}{{ r.review_request..doc.rev }}{% endif %} {{ r.earlier_review_mark }}{% endfor %}
* Other revision previously reviewed
** This revision already reviewed
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}{% endautoescape %}
""")
DBTemplate.objects.filter(pk=183).update(content="""{% autoescape off %}Subject: Review Assignments
Hi all,
The following reviewers have assignments:{% for r in review_assignments %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer Type LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.review_request.deadline|date:"Y-m-d" }}{% else %}{{r.review_request.type.name|ljust:"10"}}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.review_request.doc_id }}-{% if r.review_request.requested_rev %}{{ r.review_request.requested_rev }}{% else %}{{ r.review_request.doc.rev }}{% endif %}{% if r.earlier_review_mark %} {{ r.earlier_review_mark }}{% endif %}{% endfor %}
* Other revision previously reviewed
** This revision already reviewed
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}
The LC and Telechat review templates are included below:
-------------------------------------------------------
-- Begin LC Template --
I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please treat these comments just
like any other last call comments.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document:
Reviewer:
Review Date:
IETF LC End Date:
IESG Telechat date: (if known)
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
-- End LC Template --
-- Begin Telechat Template --
I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please wait for direction from your
document shepherd or AD before posting a new version of the draft.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document:
Reviewer:
Review Date:
IETF LC End Date:
IESG Telechat date: (if known)
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
-- End Telechat Template --
{% endautoescape %}
""")
DBTemplate.objects.filter(pk=184).update(content="""{% autoescape off %}Subject: Assignments
Review instructions and related resources are at:
http://tools.ietf.org/area/sec/trac/wiki/SecDirReview{% for r in review_assignments %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }}{{ r.earlier_review|yesno:'R, , ' }}{% if r.section == 'Early review requests:' %}{{ r.review_request.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.review_request.doc_id }}-{% if r.review_request.requested_rev %}{{ r.review_request.requested_rev }}{% else %}{{ r.review_request.doc.rev }}{% endif %}{% endfor %}
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}{% endautoescape %}
""")
DBTemplate.objects.filter(pk=185).update(content="""{% autoescape off %}Subject: Open review assignments in {{group.acronym}}
Review instructions and related resources are at:
<https://trac.ietf.org/trac/ops/wiki/Directorates>
The following reviewers have assignments:{% for r in review_assignments %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.review_request.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.review_request.doc_id }}-{% if r.review_request.requested_rev %}{{ r.review_request.requested_rev }}{% else %}{{ r.review_request.doc.rev }}{% endif %} {{ r.earlier_review_mark }}{% endfor %}
* Other revision previously reviewed
** This revision already reviewed
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}{% endautoescape %}
""")
def reverse(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
DBTemplate.objects.filter(pk=182).update(content="""{% autoescape off %}Subject: Open review assignments in {{group.acronym}}
The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %} {{ r.earlier_review_mark }}{% endfor %}
* Other revision previously reviewed
** This revision already reviewed
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}{% endautoescape %}
""")
DBTemplate.objects.filter(pk=183).update(content="""{% autoescape off %}Subject: Review Assignments
Hi all,
The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer Type LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{r.type.name|ljust:"10"}}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}{% if r.earlier_review_mark %} {{ r.earlier_review_mark }}{% endif %}{% endfor %}
* Other revision previously reviewed
** This revision already reviewed
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}
The LC and Telechat review templates are included below:
-------------------------------------------------------
-- Begin LC Template --
I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please treat these comments just
like any other last call comments.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document:
Reviewer:
Review Date:
IETF LC End Date:
IESG Telechat date: (if known)
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
-- End LC Template --
-- Begin Telechat Template --
I am the assigned Gen-ART reviewer for this draft. The General Area
Review Team (Gen-ART) reviews all IETF documents being processed
by the IESG for the IETF Chair. Please wait for direction from your
document shepherd or AD before posting a new version of the draft.
For more information, please see the FAQ at
<https://trac.ietf.org/trac/gen/wiki/GenArtfaq>.
Document:
Reviewer:
Review Date:
IETF LC End Date:
IESG Telechat date: (if known)
Summary:
Major issues:
Minor issues:
Nits/editorial comments:
-- End Telechat Template --
{% endautoescape %}
""")
DBTemplate.objects.filter(pk=184).update(content="""{% autoescape off %}Subject: Assignments
Review instructions and related resources are at:
http://tools.ietf.org/area/sec/trac/wiki/SecDirReview{% for r in review_requests %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }}{{ r.earlier_review|yesno:'R, , ' }}{% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}{% endfor %}
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}{% endautoescape %}
""")
DBTemplate.objects.filter(pk=185).update(content="""{% autoescape off %}Subject: Open review assignments in {{group.acronym}}
Review instructions and related resources are at:
<https://trac.ietf.org/trac/ops/wiki/Directorates>
The following reviewers have assignments:{% for r in review_requests %}{% ifchanged r.section %}
{{r.section}}
{% if r.section == 'Early review requests:' %}Reviewer Due Draft{% else %}Reviewer LC end Draft{% endif %}{% endifchanged %}
{{ r.reviewer.person.plain_name|ljust:"22" }} {% if r.section == 'Early review requests:' %}{{ r.deadline|date:"Y-m-d" }}{% else %}{{ r.lastcall_ends|default:"None " }}{% endif %} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %} {{ r.earlier_review_mark }}{% endfor %}
* Other revision previously reviewed
** This revision already reviewed
{% if rotation_list %}Next in the reviewer rotation:
{% for p in rotation_list %} {{ p }}
{% endfor %}{% endif %}{% endautoescape %}
""")
class Migration(migrations.Migration):
dependencies = [
('dbtemplate', '0003_adjust_review_templates'),
]
operations = [
migrations.RunPython(forward,reverse),
]

View file

@ -5,7 +5,8 @@ from models import (StateType, State, RelatedDocument, DocumentAuthor, Document,
DocHistoryAuthor, DocHistory, DocAlias, DocReminder, DocEvent, NewRevisionDocEvent,
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL)
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
ReviewAssignmentDocEvent )
class StateTypeAdmin(admin.ModelAdmin):
@ -143,6 +144,7 @@ admin.site.register(WriteupDocEvent, DocEventAdmin)
admin.site.register(LastCallDocEvent, DocEventAdmin)
admin.site.register(TelechatDocEvent, DocEventAdmin)
admin.site.register(ReviewRequestDocEvent, DocEventAdmin)
admin.site.register(ReviewAssignmentDocEvent, DocEventAdmin)
admin.site.register(InitialReviewDocEvent, DocEventAdmin)
admin.site.register(AddedMessageEvent, DocEventAdmin)
admin.site.register(SubmissionDocEvent, DocEventAdmin)

View file

@ -209,6 +209,12 @@ class ConflictReviewFactory(BaseDocumentFactory):
else:
obj.set_state(State.objects.get(type_id='conflrev',slug='iesgeval'))
# This is very skeletal. It is enough for the tests that use it now, but when it's needed, it will need to be improved with, at least, a group generator that backs the object with a review team.
class ReviewFactory(BaseDocumentFactory):
type_id = 'review'
name = factory.LazyAttribute(lambda o: 'review-doesnotexist-00-%s-%s'%(o.group.acronym,datetime.date.today().isoformat()))
group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review')
class DocAliasFactory(factory.DjangoModelFactory):
class Meta:
model = DocAlias

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-11 11:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('review', '0009_refactor_review_request'),
('name', '0005_reviewassignmentstatename'),
('doc', '0010_auto_20190225_1302'),
]
operations = [
migrations.CreateModel(
name='ReviewAssignmentDocEvent',
fields=[
('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')),
('review_assignment', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.ReviewAssignment')),
('state', ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.ReviewAssignmentStateName')),
],
bases=('doc.docevent',),
),
]

View file

@ -20,7 +20,7 @@ import debug # pyflakes:ignore
from ietf.group.models import Group
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, FormalLanguageName,
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName,
DocUrlTagName)
from ietf.person.models import Email, Person
from ietf.person.utils import get_active_ads
@ -1149,6 +1149,10 @@ class ReviewRequestDocEvent(DocEvent):
review_request = ForeignKey('review.ReviewRequest')
state = ForeignKey(ReviewRequestStateName, blank=True, null=True)
class ReviewAssignmentDocEvent(DocEvent):
review_assignment = ForeignKey('review.ReviewAssignment')
state = ForeignKey(ReviewAssignmentStateName, blank=True, null=True)
# charter events
class InitialReviewDocEvent(DocEvent):
expires = models.DateTimeField(blank=True, null=True)

View file

@ -12,7 +12,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent,
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
ReviewRequestDocEvent, EditedAuthorsDocEvent, DocumentURL)
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL)
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
class BallotTypeResource(ModelResource):
@ -652,3 +652,32 @@ class DocumentURLResource(ModelResource):
api.doc.register(DocumentURLResource())
from ietf.person.resources import PersonResource
from ietf.review.resources import ReviewAssignmentResource
from ietf.name.resources import ReviewAssignmentStateNameResource
class ReviewAssignmentDocEventResource(ModelResource):
by = ToOneField(PersonResource, 'by')
doc = ToOneField(DocumentResource, 'doc')
docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr')
review_assignment = ToOneField(ReviewAssignmentResource, 'review_assignment')
state = ToOneField(ReviewAssignmentStateNameResource, 'state', null=True)
class Meta:
queryset = ReviewAssignmentDocEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'reviewassignmentdocevent'
filtering = {
"id": ALL,
"time": ALL,
"type": ALL,
"rev": ALL,
"desc": ALL,
"by": ALL_WITH_RELATIONS,
"doc": ALL_WITH_RELATIONS,
"docevent_ptr": ALL_WITH_RELATIONS,
"review_assignment": ALL_WITH_RELATIONS,
"state": ALL_WITH_RELATIONS,
}
api.doc.register(ReviewAssignmentDocEventResource())

View file

@ -17,14 +17,14 @@ from pyquery import PyQuery
import debug # pyflakes:ignore
import ietf.review.mailarch
from ietf.doc.factories import NewRevisionDocEventFactory, WgDraftFactory, WgRfcFactory
from ietf.doc.models import DocumentAuthor, RelatedDocument, DocEvent, ReviewRequestDocEvent
from ietf.doc.factories import NewRevisionDocEventFactory, WgDraftFactory, WgRfcFactory, ReviewFactory
from ietf.doc.models import DocumentAuthor, RelatedDocument, DocEvent, ReviewAssignmentDocEvent
from ietf.group.factories import RoleFactory, ReviewTeamFactory
from ietf.group.models import Group
from ietf.message.models import Message
from ietf.name.models import ReviewResultName, ReviewRequestStateName, ReviewTypeName
from ietf.name.models import ReviewResultName, ReviewRequestStateName, ReviewAssignmentStateName
from ietf.person.models import Email, Person
from ietf.review.factories import ReviewRequestFactory
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
from ietf.review.models import (ReviewRequest, ReviewerSettings,
ReviewWish, UnavailablePeriod, NextReviewerInTeam)
from ietf.review.utils import reviewer_rotation_list, possibly_advance_next_reviewer_for_team
@ -60,7 +60,8 @@ class ReviewTests(TestCase):
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
RoleFactory(group=review_team3,person__user__username='reviewsecretary3',person__user__email='reviewsecretary3@example.com',name_id='secr')
ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request = req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
login_testing_unauthorized(self, "ad", url)
@ -135,7 +136,8 @@ class ReviewTests(TestCase):
doc = WgDraftFactory(group__acronym='mars',rev='01')
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request=review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted')
# move the review request to a doubly-replaced document to
# check we can fish it out
@ -150,13 +152,15 @@ class ReviewTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
content = unicontent(r)
#debug.show('unicontent(r)')
self.assertTrue("{} Review".format(review_req.type.name) in content)
def test_review_request(self):
doc = WgDraftFactory(group__acronym='mars',rev='01')
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
@ -178,8 +182,8 @@ class ReviewTests(TestCase):
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first())
close_url = urlreverse('ietf.doc.views_review.close_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
@ -287,7 +291,8 @@ class ReviewTests(TestCase):
settings, _ = ReviewerSettings.objects.get_or_create(team=team, person=reviewers[2])
settings.min_interval = 30
settings.save()
ReviewRequest.objects.create(team=team, doc=doc, type_id="early", state_id="accepted", deadline=today, requested_by=reviewers[0], reviewer=reviewers[2].email_set.first())
req = ReviewRequest.objects.create(team=team, doc=doc, type_id="early", state_id="assigned", deadline=today, requested_by=reviewers[0])
ReviewAssignmentFactory(review_request=req, state_id="accepted", reviewer = reviewers[2].email_set.first(),assigned_on = req.time)
possibly_advance_next_reviewer_for_team(team, assigned_review_to_person_id=reviewers[3].pk)
self.assertEqual(NextReviewerInTeam.objects.get(team=team).next_reviewer, reviewers[4])
self.assertEqual(get_skip_next(reviewers[0]), 0)
@ -314,17 +319,25 @@ class ReviewTests(TestCase):
doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
# previous review
ReviewRequest.objects.create(
req = ReviewRequestFactory(
time=datetime.datetime.now() - datetime.timedelta(days=100),
requested_by=Person.objects.get(name="(System)"),
doc=doc,
type=ReviewTypeName.objects.get(slug="early"),
type_id='early',
team=review_req.team,
state=ReviewRequestStateName.objects.get(slug="completed"),
result_id='serious-issues',
reviewed_rev="01",
state_id='assigned',
requested_rev="01",
deadline=datetime.date.today() - datetime.timedelta(days=80),
)
ReviewAssignmentFactory(
review_request = req,
state_id='completed',
result_id='serious-issues',
reviewer=reviewer_email,
reviewed_rev="01",
review = ReviewFactory(),
assigned_on=req.time,
completed_on=req.time + datetime.timedelta(days=10),
)
reviewer_settings = ReviewerSettings.objects.get(person__email=reviewer_email, team=review_req.team)
@ -387,37 +400,25 @@ class ReviewTests(TestCase):
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "requested")
self.assertEqual(review_req.reviewer, reviewer)
self.assertEqual(review_req.state_id, "assigned")
self.assertEqual(review_req.reviewassignment_set.count(),1)
assignment = review_req.reviewassignment_set.first()
self.assertEqual(assignment.reviewer, reviewer)
self.assertEqual(assignment.state_id, "assigned")
self.assertEqual(len(outbox), 1)
self.assertTrue("assigned" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertEqual(NextReviewerInTeam.objects.get(team=review_req.team).next_reviewer, rotation_list[1])
# re-assign
empty_outbox()
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
review_req.save()
reviewer = Email.objects.filter(role__name="reviewer", role__group=review_req.team, person=rotation_list[1]).first()
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk, "add_skip": 1 })
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "requested") # check that state is reset
self.assertEqual(review_req.reviewer, reviewer)
self.assertEqual(len(outbox), 2)
self.assertTrue("cancelled your assignment" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertTrue("assigned" in outbox[1].get_payload(decode=True).decode("utf-8"))
self.assertEqual(ReviewerSettings.objects.get(person=reviewer.person).skip_next, 1)
def test_accept_reviewer_assignment(self):
doc = WgDraftFactory(group__acronym='mars',rev='01')
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='requested',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned', reviewer=rev_role.person.email_set.first())
url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
username = review_req.reviewer.person.user.username
username = assignment.reviewer.person.user.username
self.client.login(username=username, password=username + "+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@ -428,21 +429,22 @@ class ReviewTests(TestCase):
r = self.client.post(url, { "action": "accept" })
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "accepted")
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "accepted")
def test_reject_reviewer_assignment(self):
doc = WgDraftFactory(group__acronym='mars',rev='01')
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
assignment = ReviewAssignmentFactory(review_request = review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted')
reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk })
reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "assignment_id": assignment.pk })
# follow link
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": assignment.review_request.pk })
self.client.login(username="reviewsecretary", password="reviewsecretary+password")
r = self.client.get(req_url)
self.assertEqual(r.status_code, 200)
@ -453,19 +455,18 @@ class ReviewTests(TestCase):
login_testing_unauthorized(self, "reviewsecretary", reject_url)
r = self.client.get(reject_url)
self.assertEqual(r.status_code, 200)
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
self.assertTrue(unicode(assignment.reviewer.person) in unicontent(r))
# reject
empty_outbox()
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "rejected")
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "rejected")
e = doc.latest_event()
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)
self.assertTrue("Test message" in outbox[0].get_payload(decode=True).decode("utf-8"))
@ -509,7 +510,8 @@ class ReviewTests(TestCase):
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
assignment = ReviewAssignmentFactory(review_request=review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted')
# test URL construction
query_urls = ietf.review.mailarch.construct_query_urls(review_req)
@ -525,7 +527,7 @@ class ReviewTests(TestCase):
real_fn = ietf.review.mailarch.construct_query_urls
ietf.review.mailarch.construct_query_urls = lambda review_req, query=None: { "query_data_url": "file://" + os.path.abspath(mbox_path) }
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "request_id": review_req.pk })
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "assignment_id": assignment.pk })
login_testing_unauthorized(self, "reviewsecretary", url)
r = self.client.get(url)
@ -555,7 +557,7 @@ class ReviewTests(TestCase):
f.write('Content-Type: text/html\n\n<html><body><div class="xtr"><div class="xtd no-results">No results found</div></div>')
ietf.review.mailarch.construct_query_urls = lambda review_req, query=None: { "query_data_url": "file://" + os.path.abspath(no_result_path) }
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "request_id": review_req.pk })
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "assignment_id": assignment.pk })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@ -572,18 +574,19 @@ class ReviewTests(TestCase):
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
assignment = ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first())
for r in ReviewResultName.objects.filter(slug__in=("issues", "ready")):
review_req.team.reviewteamsettings.review_results.add(r)
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": doc.name, "request_id": review_req.pk })
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": doc.name, "assignment_id": review_req.pk })
return review_req, url
return assignment, url
def test_complete_review_upload_content(self):
review_req, url = self.setup_complete_review_test()
assignment, url = self.setup_complete_review_test()
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
# get
r = self.client.get(url)
@ -611,9 +614,9 @@ class ReviewTests(TestCase):
test_file.name = "unnamed"
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "upload",
"review_content": "",
"review_url": "",
@ -621,25 +624,25 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "completed")
self.assertEqual(review_req.result_id, "ready")
self.assertEqual(review_req.reviewed_rev, review_req.doc.rev)
self.assertTrue(review_req.team.acronym.lower() in review_req.review.name)
self.assertTrue(review_req.doc.rev in review_req.review.name)
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "completed")
self.assertEqual(assignment.result_id, "ready")
self.assertEqual(assignment.reviewed_rev, assignment.review_request.doc.rev)
self.assertTrue(assignment.review_request.team.acronym.lower() in assignment.review.name)
self.assertTrue(assignment.review_request.doc.rev in assignment.review.name)
with open(os.path.join(self.review_subdir, review_req.review.name + ".txt")) as f:
with open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
self.assertEqual(f.read(), "This is a review\nwith two lines")
self.assertEqual(len(outbox), 1)
self.assertIn(review_req.team.list_email, outbox[0]["To"])
self.assertIn(review_req.reviewer.person.plain_name(), parseaddr(outbox[0]["From"])[0])
self.assertIn("This is a review", outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertTrue(assignment.review_request.team.list_email in outbox[0]["To"])
self.assertTrue(assignment.reviewer.role_set.filter(group=assignment.review_request.team,name='reviewer').first().email.address in outbox[0]["From"])
self.assertTrue("This is a review" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in assignment.review.external_url)
# Check that the review has the reviewer as author
self.assertEqual(review_req.reviewer, review_req.review.documentauthor_set.first().email)
self.assertEqual(assignment.reviewer, assignment.review.documentauthor_set.first().email)
# Check that we have a copy of the outgoing message
msgid = outbox[0]["Message-ID"]
@ -650,25 +653,25 @@ class ReviewTests(TestCase):
self.assertEqual(outbox[0].get_payload(decode=True).decode(str(outbox[0].get_charset())), message.body)
# check the review document page
url = urlreverse('ietf.doc.views_doc.document_main', kwargs={ "name": review_req.review.name })
url = urlreverse('ietf.doc.views_doc.document_main', kwargs={ "name": assignment.review.name })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
content = unicontent(r)
self.assertTrue("{} Review".format(review_req.type.name) in content)
self.assertTrue("{} Review".format(assignment.review_request.type.name) in content)
self.assertTrue("This is a review" in content)
def test_complete_review_enter_content(self):
review_req, url = self.setup_complete_review_test()
assignment, url = self.setup_complete_review_test()
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is a review\nwith two lines",
"review_url": "",
@ -676,32 +679,33 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "completed")
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "completed")
self.assertNotEqual(assignment.completed_on, None)
with open(os.path.join(self.review_subdir, review_req.review.name + ".txt")) as f:
with open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
self.assertEqual(f.read(), "This is a review\nwith two lines")
self.assertEqual(len(outbox), 1)
self.assertTrue(review_req.team.list_email in outbox[0]["To"])
self.assertTrue(assignment.review_request.team.list_email in outbox[0]["To"])
self.assertTrue("This is a review" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in assignment.review.external_url)
def test_complete_notify_ad_because_team_settings(self):
review_req, url = self.setup_complete_review_test()
review_req.team.reviewteamsettings.notify_ad_when.add(ReviewResultName.objects.get(slug='issues'))
assignment, url = self.setup_complete_review_test()
assignment.review_request.team.reviewteamsettings.notify_ad_when.add(ReviewResultName.objects.get(slug='issues'))
# TODO - it's a little surprising that the factories so far didn't give this doc an ad
review_req.doc.ad = PersonFactory()
review_req.doc.save_with_history([DocEvent.objects.create(doc=review_req.doc, rev=review_req.doc.rev, by=review_req.reviewer.person, type='changed_document',desc='added an AD')])
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
assignment.review_request.doc.ad = PersonFactory()
assignment.review_request.doc.save_with_history([DocEvent.objects.create(doc=assignment.review_request.doc, rev=assignment.review_request.doc.rev, by=assignment.reviewer.person, type='changed_document',desc='added an AD')])
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="issues").pk,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="issues").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is a review\nwith two lines",
"review_url": "",
@ -714,17 +718,17 @@ class ReviewTests(TestCase):
self.assertIn('settings indicated', outbox[-1].get_payload(decode=True).decode("utf-8"))
def test_complete_notify_ad_because_checkbox(self):
review_req, url = self.setup_complete_review_test()
review_req.doc.ad = PersonFactory()
review_req.doc.save_with_history([DocEvent.objects.create(doc=review_req.doc, rev=review_req.doc.rev, by=review_req.reviewer.person, type='changed_document',desc='added an AD')])
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
assignment, url = self.setup_complete_review_test()
assignment.review_request.doc.ad = PersonFactory()
assignment.review_request.doc.save_with_history([DocEvent.objects.create(doc=assignment.review_request.doc, rev=assignment.review_request.doc.rev, by=assignment.reviewer.person, type='changed_document',desc='added an AD')])
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="issues").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="issues").pk,
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is a review\nwith two lines",
"review_url": "",
@ -746,16 +750,16 @@ class ReviewTests(TestCase):
mock.return_value = response
# Run the test
review_req, url = self.setup_complete_review_test()
assignment, url = self.setup_complete_review_test()
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "link",
"review_content": response.content,
"review_url": "http://example.com/testreview/",
@ -763,27 +767,27 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "completed")
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "completed")
with open(os.path.join(self.review_subdir, review_req.review.name + ".txt")) as f:
with open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
self.assertEqual(f.read(), "This is a review\nwith two lines")
self.assertEqual(len(outbox), 0)
self.assertTrue("http://example.com" in review_req.review.external_url)
self.assertTrue("http://example.com" in assignment.review.external_url)
def test_partially_complete_review(self):
review_req, url = self.setup_complete_review_test()
assignment, url = self.setup_complete_review_test()
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
# partially complete
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="part-completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="part-completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is a review with a somewhat long line spanning over 80 characters to test word wrapping\nand another line",
})
@ -791,16 +795,15 @@ class ReviewTests(TestCase):
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "part-completed")
self.assertTrue(review_req.doc.rev in review_req.review.name)
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "part-completed")
self.assertTrue(assignment.review_request.doc.rev in assignment.review.name)
self.assertEqual(len(outbox), 2)
self.assertTrue("reviewsecretary@example.com" in outbox[0]["To"])
self.assertTrue("partially" in outbox[0]["Subject"].lower())
self.assertTrue("new review request" in outbox[0].get_payload(decode=True).decode("utf-8"))
self.assertTrue(review_req.team.list_email in outbox[1]["To"])
self.assertTrue(assignment.review_request.team.list_email in outbox[1]["To"])
self.assertTrue("partial review" in outbox[1]["Subject"].lower())
body = outbox[1].get_payload(decode=True).decode("utf-8")
self.assertTrue("This is a review" in body)
@ -810,30 +813,26 @@ class ReviewTests(TestCase):
self.assertTrue(not any( len(line) > 100 for line in body.splitlines() ))
self.assertTrue(any( len(line) > 80 for line in body.splitlines() ))
first_review = review_req.review
first_reviewer = review_req.reviewer
first_review = assignment.review
# complete
review_req = ReviewRequest.objects.get(state="requested", doc=review_req.doc, team=review_req.team)
self.assertEqual(review_req.reviewer, None)
review_req.reviewer = first_reviewer # same reviewer, so we can test uniquification
review_req.save()
assignment = assignment.review_request.reviewassignment_set.create(state_id="assigned", reviewer=assignment.reviewer)
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": review_req.doc.name, "request_id": review_req.pk })
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": assignment.review_request.doc.name, "assignment_id": assignment.pk })
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is another review with a really, really, really, really, really, really, really, really, really, really long line.",
})
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "completed")
self.assertTrue(review_req.doc.rev in review_req.review.name)
second_review = review_req.review
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "completed")
self.assertTrue(assignment.review_request.doc.rev in assignment.review.name)
second_review = assignment.review
self.assertTrue(first_review.name != second_review.name)
self.assertTrue(second_review.name.endswith("-2")) # uniquified
@ -845,18 +844,18 @@ class ReviewTests(TestCase):
def test_revise_review_enter_content(self):
review_req, url = self.setup_complete_review_test()
review_req.state = ReviewRequestStateName.objects.get(slug="no-response")
review_req.save()
assignment, url = self.setup_complete_review_test()
assignment.state = ReviewAssignmentStateName.objects.get(slug="no-response")
assignment.save()
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
login_testing_unauthorized(self, assignment.reviewer.person.user.username, url)
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is a review\nwith two lines",
"review_url": "",
@ -866,12 +865,12 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.state_id, "completed")
event = ReviewRequestDocEvent.objects.get(type="closed_review_request", review_request=review_req)
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, "completed")
event = ReviewAssignmentDocEvent.objects.get(type="closed_review_request", review_assignment=assignment)
self.assertEqual(event.time, datetime.datetime(2012, 12, 24, 12, 13, 14))
with open(os.path.join(self.review_subdir, review_req.review.name + ".txt")) as f:
with open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
self.assertEqual(f.read(), "This is a review\nwith two lines")
self.assertEqual(len(outbox), 0)
@ -879,9 +878,9 @@ class ReviewTests(TestCase):
# revise again
empty_outbox()
r = self.client.post(url, data={
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=review_req.team, slug="ready").pk,
"state": ReviewRequestStateName.objects.get(slug="part-completed").pk,
"reviewed_rev": review_req.doc.rev,
"result": ReviewResultName.objects.get(reviewteamsettings_review_results_set__group=assignment.review_request.team, slug="ready").pk,
"state": ReviewAssignmentStateName.objects.get(slug="part-completed").pk,
"reviewed_rev": assignment.review_request.doc.rev,
"review_submission": "enter",
"review_content": "This is a revised review",
"review_url": "",
@ -891,9 +890,9 @@ class ReviewTests(TestCase):
})
self.assertEqual(r.status_code, 302)
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.review.rev, "01")
event = ReviewRequestDocEvent.objects.get(type="closed_review_request", review_request=review_req)
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.review.rev, "01")
event = ReviewAssignmentDocEvent.objects.get(type="closed_review_request", review_assignment=assignment)
self.assertEqual(event.time, datetime.datetime(2013, 12, 24, 11, 11, 11))
self.assertEqual(len(outbox), 0)
@ -903,7 +902,8 @@ class ReviewTests(TestCase):
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
url = urlreverse('ietf.doc.views_review.edit_comment', kwargs={ "name": doc.name, "request_id": review_req.pk })
@ -924,7 +924,8 @@ class ReviewTests(TestCase):
review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,reviewer=rev_role.person.email_set.first(),deadline=datetime.datetime.now()+datetime.timedelta(days=20))
review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
url = urlreverse('ietf.doc.views_review.edit_deadline', kwargs={ "name": doc.name, "request_id": review_req.pk })
@ -942,3 +943,34 @@ class ReviewTests(TestCase):
review_req = reload_db_objects(review_req)
self.assertEqual(review_req.deadline,new_deadline)
self.assertTrue('Deadline changed' in outbox[-1]['Subject'])
def test_mark_no_response(self):
assignment = ReviewAssignmentFactory()
secr = RoleFactory(group=assignment.review_request.team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr').person
url = urlreverse('ietf.doc.views_review.mark_reviewer_assignment_no_response', kwargs={"name": assignment.review_request.doc.name, "assignment_id": assignment.pk})
login_testing_unauthorized(self, secr.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r=self.client.post(url, data={"action":"noresponse"})
self.assertEqual(r.status_code, 302)
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, 'no-response')
def test_withdraw_assignment(self):
assignment = ReviewAssignmentFactory()
secr = RoleFactory(group=assignment.review_request.team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr').person
url = urlreverse('ietf.doc.views_review.withdraw_reviewer_assignment', kwargs={"name": assignment.review_request.doc.name, "assignment_id": assignment.pk})
login_testing_unauthorized(self, secr.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r=self.client.post(url, data={"action":"withdraw"})
self.assertEqual(r.status_code, 302)
assignment = reload_db_objects(assignment)
self.assertEqual(assignment.state_id, 'withdrawn')

View file

@ -7,9 +7,11 @@ urlpatterns = [
url(r'^(?P<request_id>[0-9]+)/login/$', views_review.review_request_forced_login),
url(r'^(?P<request_id>[0-9]+)/close/$', views_review.close_request),
url(r'^(?P<request_id>[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
url(r'^(?P<request_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
url(r'^(?P<request_id>[0-9]+)/complete/$', views_review.complete_review),
url(r'^(?P<request_id>[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
url(r'^(?P<assignment_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
url(r'^(?P<assignment_id>[0-9]+)/complete/$', views_review.complete_review),
url(r'^(?P<assignment_id>[0-9]+)/withdraw/$', views_review.withdraw_reviewer_assignment),
url(r'^(?P<assignment_id>[0-9]+)/noresponse/$', views_review.mark_reviewer_assignment_no_response),
url(r'^(?P<assignment_id>[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
url(r'^(?P<request_id>[0-9]+)/editcomment/$', views_review.edit_comment),
url(r'^(?P<request_id>[0-9]+)/editdeadline/$', views_review.edit_deadline),
]

View file

@ -103,7 +103,7 @@ def fill_in_document_table_attributes(docs, have_telechat_date=False):
if d.get_state_slug() != "rfc":
d.milestones = [ m for (t, m) in sorted(((m.time, m) for m in d.groupmilestone_set.all() if m.state_id == "active")) ]
d.reviewed_by_teams = sorted(set(r.team.acronym for r in d.reviewrequest_set.filter(state__in=["requested","accepted","part-completed","completed"]).distinct().select_related('team')))
d.reviewed_by_teams = sorted(set(r.team.acronym for r in d.reviewrequest_set.filter(state__in=["assigned","accepted","part-completed","completed"]).distinct().select_related('team')))
e = d.latest_event_cache.get('started_iesg_process', None)
d.balloting_started = e.time if e else datetime.datetime.min

View file

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

View file

@ -20,9 +20,9 @@ from django.template.loader import render_to_string, TemplateDoesNotExist
from django.urls import reverse as urlreverse
from ietf.doc.models import (Document, NewRevisionDocEvent, State, DocAlias,
LastCallDocEvent, ReviewRequestDocEvent, DocumentAuthor)
from ietf.name.models import ReviewRequestStateName, ReviewResultName, DocTypeName
from ietf.review.models import ReviewRequest
LastCallDocEvent, ReviewRequestDocEvent, ReviewAssignmentDocEvent, DocumentAuthor)
from ietf.name.models import ReviewRequestStateName, ReviewAssignmentStateName, ReviewResultName, DocTypeName
from ietf.review.models import ReviewRequest, ReviewAssignment
from ietf.group.models import Group
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person, has_role
from ietf.message.models import Message
@ -30,9 +30,9 @@ from ietf.message.utils import infer_message
from ietf.person.fields import PersonEmailChoiceField, SearchablePersonField
from ietf.review.utils import (active_review_teams, assign_review_request_to_reviewer,
can_request_review_of_doc, can_manage_review_requests_for_team,
email_review_request_change, make_new_review_request_from_existing,
close_review_request_states, close_review_request,
setup_reviewer_field)
email_review_assignment_change, email_review_request_change,
close_review_request_states,
close_review_request, setup_reviewer_field)
from ietf.review import mailarch
from ietf.utils.fields import DatepickerDateField
from ietf.utils.text import strip_prefix, xslugify
@ -184,47 +184,50 @@ def review_request(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id)
is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
can_close_request = (review_req.state_id in ["requested", "accepted"]
can_close_request = (review_req.state_id in ["requested", "assigned"]
and (is_authorized_in_doc_stream(request.user, doc)
or can_manage_request))
can_assign_reviewer = (review_req.state_id in ["requested", "accepted"]
can_assign_reviewer = (review_req.state_id in ["requested", "assigned"]
and can_manage_request)
can_accept_reviewer_assignment = (review_req.state_id == "requested"
and review_req.reviewer
and (is_reviewer or can_manage_request))
can_reject_reviewer_assignment = (review_req.state_id in ["requested", "accepted"]
and review_req.reviewer
and (is_reviewer or can_manage_request))
can_complete_review = (review_req.state_id in ["requested", "accepted", "overtaken", "no-response", "part-completed", "completed"]
and review_req.reviewer
and (is_reviewer or can_manage_request))
can_edit_comment = can_request_review_of_doc(request.user, doc)
can_edit_deadline = can_edit_comment
if request.method == "POST" and request.POST.get("action") == "accept" and can_accept_reviewer_assignment:
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
review_req.save()
assignments = review_req.reviewassignment_set.all()
for assignment in assignments:
assignment.is_reviewer = user_is_person(request.user, assignment.reviewer.person)
assignment.can_accept_reviewer_assignment = (assignment.state_id == "assigned"
and (assignment.is_reviewer or can_manage_request))
assignment.can_reject_reviewer_assignment = (assignment.state_id in ["assigned", "accepted"]
and (assignment.is_reviewer or can_manage_request))
assignment.can_complete_review = (assignment.state_id in ["assigned", "accepted", "overtaken", "no-response", "part-completed", "completed"]
and (assignment.is_reviewer or can_manage_request))
# This implementation means if a reviewer accepts one assignment for a review_request, he accepts all assigned to him (for that request)
# This problematic - it's a bug (probably) for the same person to have more than one assignment for the same request.
# It is, however unintuitive, and acceptance should be refactored to be something that works on assignments, not requests
if request.method == "POST" and request.POST.get("action") == "accept":
for assignment in assignments:
if assignment.can_accept_reviewer_assignment:
assignment.state = ReviewAssignmentStateName.objects.get(slug="accepted")
assignment.save()
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
return render(request, 'doc/review/review_request.html', {
'doc': doc,
'review_req': review_req,
'can_close_request': can_close_request,
'can_reject_reviewer_assignment': can_reject_reviewer_assignment,
'can_assign_reviewer': can_assign_reviewer,
'can_accept_reviewer_assignment': can_accept_reviewer_assignment,
'can_complete_review': can_complete_review,
'can_edit_comment': can_edit_comment,
'can_edit_deadline': can_edit_deadline,
'assignments': assignments,
})
@ -245,7 +248,7 @@ class CloseReviewRequestForm(forms.Form):
@login_required
def close_request(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "assigned"])
can_request = is_authorized_in_doc_stream(request.user, doc)
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
@ -265,12 +268,13 @@ def close_request(request, name, request_id):
return render(request, 'doc/review/close_request.html', {
'doc': doc,
'review_req': review_req,
'assignments': review_req.reviewassignment_set.all(),
'form': form,
})
class AssignReviewerForm(forms.Form):
reviewer = PersonEmailChoiceField(empty_label="(None)", required=False)
reviewer = PersonEmailChoiceField(label="Assign Additional Reviewer", empty_label="(None)", required=False)
add_skip = forms.BooleanField(label='Skip next time', required=False)
def __init__(self, review_req, *args, **kwargs):
@ -281,7 +285,7 @@ class AssignReviewerForm(forms.Form):
@login_required
def assign_reviewer(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "assigned"])
if not can_manage_review_requests_for_team(request.user, review_req.team):
return HttpResponseForbidden("You do not have permission to perform this action")
@ -300,6 +304,7 @@ def assign_reviewer(request, name, request_id):
return render(request, 'doc/review/assign_reviewer.html', {
'doc': doc,
'review_req': review_req,
'assignments': review_req.reviewassignment_set.all(),
'form': form,
})
@ -307,15 +312,15 @@ class RejectReviewerAssignmentForm(forms.Form):
message_to_secretary = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional explanation of rejection, will be emailed to team secretary if filled in", strip=False)
@login_required
def reject_reviewer_assignment(request, name, request_id):
def reject_reviewer_assignment(request, name, assignment_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
review_assignment = get_object_or_404(ReviewAssignment, pk=assignment_id, state__in=["assigned", "accepted"])
if not review_req.reviewer:
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
if not review_assignment.reviewer:
return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk)
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
is_reviewer = user_is_person(request.user, review_assignment.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, review_assignment.review_request.team)
if not (is_reviewer or can_manage_request):
return HttpResponseForbidden("You do not have permission to perform this action")
@ -323,47 +328,119 @@ def reject_reviewer_assignment(request, name, request_id):
if request.method == "POST" and request.POST.get("action") == "reject":
form = RejectReviewerAssignmentForm(request.POST)
if form.is_valid():
# reject the request
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
review_req.save()
# reject the assignment
review_assignment.state = ReviewAssignmentStateName.objects.get(slug="rejected")
review_assignment.save()
ReviewRequestDocEvent.objects.create(
ReviewAssignmentDocEvent.objects.create(
type="closed_review_request",
doc=review_req.doc,
rev=review_req.doc.rev,
doc=review_assignment.review_request.doc,
rev=review_assignment.review_request.doc.rev,
by=request.user.person,
desc="Assignment of request for {} review by {} to {} was rejected".format(
review_req.type.name,
review_req.team.acronym.upper(),
review_req.reviewer.person,
review_assignment.review_request.type.name,
review_assignment.review_request.team.acronym.upper(),
review_assignment.reviewer.person,
),
review_request=review_req,
state=review_req.state,
review_assignment=review_assignment,
state=review_assignment.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("review/reviewer_assignment_rejected.txt", {
"by": request.user.person,
"message_to_secretary": form.cleaned_data.get("message_to_secretary")
})
email_review_request_change(request, review_req, "Reviewer assignment rejected", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True, notify_requested_by=False)
email_review_assignment_change(request, review_assignment, "Reviewer assignment rejected", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True, notify_requested_by=False)
return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk)
return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk)
else:
form = RejectReviewerAssignmentForm()
return render(request, 'doc/review/reject_reviewer_assignment.html', {
'doc': doc,
'review_req': review_req,
'review_req': review_assignment.review_request,
'assignments': review_assignment.review_request.reviewassignment_set.all(),
'form': form,
})
@login_required
def withdraw_reviewer_assignment(request, name, assignment_id):
get_object_or_404(Document, name=name)
review_assignment = get_object_or_404(ReviewAssignment, pk=assignment_id, state__in=["assigned", "accepted"])
can_manage_request = can_manage_review_requests_for_team(request.user, review_assignment.review_request.team)
if not can_manage_request:
return HttpResponseForbidden("You do not have permission to perform this action")
if request.method == "POST" and request.POST.get("action") == "withdraw":
review_assignment.state_id = 'withdrawn'
review_assignment.save()
ReviewAssignmentDocEvent.objects.create(
type="closed_review_request",
doc=review_assignment.review_request.doc,
rev=review_assignment.review_request.doc.rev,
by=request.user.person,
desc="Assignment of request for {} review by {} to {} was withdrawn".format(
review_assignment.review_request.type.name,
review_assignment.review_request.team.acronym.upper(),
review_assignment.reviewer.person,
),
review_assignment=review_assignment,
state=review_assignment.state,
)
msg = "Review assignment withdrawn by %s"%request.user.person
email_review_assignment_change(request, review_assignment, "Reviewer assignment withdrawn", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True, notify_requested_by=False)
return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk)
return render(request, 'doc/review/withdraw_reviewer_assignment.html', {
'assignment': review_assignment,
})
@login_required
def mark_reviewer_assignment_no_response(request, name, assignment_id):
get_object_or_404(Document, name=name)
review_assignment = get_object_or_404(ReviewAssignment, pk=assignment_id, state__in=["assigned", "accepted"])
can_manage_request = can_manage_review_requests_for_team(request.user, review_assignment.review_request.team)
if not can_manage_request:
return HttpResponseForbidden("You do not have permission to perform this action")
if request.method == "POST" and request.POST.get("action") == "noresponse":
review_assignment.state_id = 'no-response'
review_assignment.save()
ReviewAssignmentDocEvent.objects.create(
type="closed_review_request",
doc=review_assignment.review_request.doc,
rev=review_assignment.review_request.doc.rev,
by=request.user.person,
desc="Assignment of request for {} review by {} to {} was marked no-response".format(
review_assignment.review_request.type.name,
review_assignment.review_request.team.acronym.upper(),
review_assignment.reviewer.person,
),
review_assignment=review_assignment,
state=review_assignment.state,
)
msg = "Review assignment marked 'No Response' by %s"%request.user.person
email_review_assignment_change(request, review_assignment, "Reviewer assignment marked no-response", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True, notify_requested_by=False)
return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk)
return render(request, 'doc/review/mark_reviewer_assignment_no_response.html', {
'assignment': review_assignment,
})
class CompleteReviewForm(forms.Form):
state = forms.ModelChoiceField(queryset=ReviewRequestStateName.objects.filter(slug__in=("completed", "part-completed")).order_by("-order"), widget=forms.RadioSelect, initial="completed")
state = forms.ModelChoiceField(queryset=ReviewAssignmentStateName.objects.filter(slug__in=("completed", "part-completed")).order_by("-order"), widget=forms.RadioSelect, initial="completed")
reviewed_rev = forms.CharField(label="Reviewed revision", max_length=4)
result = forms.ModelChoiceField(queryset=ReviewResultName.objects.filter(used=True), widget=forms.RadioSelect, empty_label=None)
ACTIONS = [
@ -381,16 +458,16 @@ class CompleteReviewForm(forms.Form):
cc = MultiEmailField(required=False, help_text="Email addresses to send to in addition to the review team list")
email_ad = forms.BooleanField(label="Send extra email to the responsible AD suggesting early attention", required=False)
def __init__(self, review_req, is_reviewer, *args, **kwargs):
self.review_req = review_req
def __init__(self, assignment, is_reviewer, *args, **kwargs):
self.assignment = assignment
super(CompleteReviewForm, self).__init__(*args, **kwargs)
doc = self.review_req.doc
doc = self.assignment.review_request.doc
known_revisions = NewRevisionDocEvent.objects.filter(doc=doc).order_by("time", "id").values_list("rev", "time", flat=False)
revising_review = review_req.state_id not in ["requested", "accepted"]
revising_review = assignment.state_id not in ["assigned", "accepted"]
if not revising_review:
self.fields["state"].choices = [
@ -402,7 +479,7 @@ class CompleteReviewForm(forms.Form):
reviewed_rev_class = []
for r in known_revisions:
last_version = r[0]
if r[1] < review_req.time:
if r[1] < assignment.review_request.time:
kwargs["initial"]["reviewed_rev"] = r[0]
reviewed_rev_class.append('reviewer-doc-past')
else:
@ -426,13 +503,13 @@ class CompleteReviewForm(forms.Form):
" ".join("<a class=\"rev label label-default {0}\" title=\"{2:%Y-%m-%d}\">{1}</a>".format('', *r)
for i, r in enumerate(known_revisions)))
self.fields["result"].queryset = self.fields["result"].queryset.filter(reviewteamsettings_review_results_set__group=review_req.team)
self.fields["result"].queryset = self.fields["result"].queryset.filter(reviewteamsettings_review_results_set__group=assignment.review_request.team)
def format_submission_choice(label):
if revising_review:
label = label.replace(" (automatically posts to {mailing_list})", "")
return label.format(mailing_list=review_req.team.list_email or "[error: team has no mailing list set]")
return label.format(mailing_list=assignment.review_request.team.list_email or "[error: team has no mailing list set]")
self.fields["review_submission"].choices = [ (k, format_submission_choice(label)) for k, label in self.fields["review_submission"].choices]
@ -443,7 +520,7 @@ class CompleteReviewForm(forms.Form):
del self.fields["completion_time"]
def clean_reviewed_rev(self):
return clean_doc_revision(self.review_req.doc, self.cleaned_data.get("reviewed_rev"))
return clean_doc_revision(self.assignment.review_request.doc, self.cleaned_data.get("reviewed_rev"))
def clean_review_content(self):
return self.cleaned_data["review_content"].replace("\r", "")
@ -461,7 +538,7 @@ class CompleteReviewForm(forms.Form):
return url
def clean(self):
if "@" in self.review_req.reviewer.person.ascii:
if "@" in self.assignment.reviewer.person.ascii:
raise forms.ValidationError("Reviewer name must be filled in (the ASCII version is currently \"{}\" - since it contains an @ sign the name is probably still the original email address).".format(self.review_req.reviewer.person.ascii))
def require_field(f):
@ -477,40 +554,37 @@ class CompleteReviewForm(forms.Form):
require_field("review_url")
@login_required
def complete_review(request, name, request_id):
def complete_review(request, name, assignment_id):
doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id)
assignment = get_object_or_404(ReviewAssignment, pk=assignment_id)
revising_review = review_req.state_id not in ["requested", "accepted"]
revising_review = assignment.state_id not in ["assigned", "accepted"]
if not review_req.reviewer:
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
is_reviewer = user_is_person(request.user, assignment.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, assignment.review_request.team)
if not (is_reviewer or can_manage_request):
return HttpResponseForbidden("You do not have permission to perform this action")
(to, cc) = gather_address_lists('review_completed',review_req = review_req)
(to, cc) = gather_address_lists('review_completed', review_req = assignment.review_request)
if request.method == "POST":
form = CompleteReviewForm(review_req, is_reviewer,
form = CompleteReviewForm(assignment, is_reviewer,
request.POST, request.FILES)
if form.is_valid():
review_submission = form.cleaned_data['review_submission']
review = review_req.review
review = assignment.review
if not review:
# create review doc
for i in range(1, 100):
name_components = [
"review",
strip_prefix(review_req.doc.name, "draft-"),
strip_prefix(assignment.review_request.doc.name, "draft-"),
form.cleaned_data["reviewed_rev"],
review_req.team.acronym,
review_req.type.slug,
xslugify(review_req.reviewer.person.ascii_parts()[3]),
assignment.review_request.team.acronym,
assignment.review_request.type.slug,
xslugify(assignment.reviewer.person.ascii_parts()[3]),
datetime.date.today().isoformat(),
]
if i > 1:
@ -523,10 +597,10 @@ def complete_review(request, name, request_id):
break
review.type = DocTypeName.objects.get(slug="review")
review.group = review_req.team
review.group = assignment.review_request.team
review.rev = "00" if not review.rev else "{:02}".format(int(review.rev) + 1)
review.title = "{} Review of {}-{}".format(review_req.type.name, review_req.doc.name, form.cleaned_data["reviewed_rev"])
review.title = "{} Review of {}-{}".format(assignment.review_request.type.name, assignment.review_request.doc.name, form.cleaned_data["reviewed_rev"])
review.time = datetime.datetime.now()
if review_submission == "link":
review.external_url = form.cleaned_data['review_url']
@ -554,63 +628,55 @@ def complete_review(request, name, request_id):
with open(filename, 'wb') as destination:
destination.write(encoded_content)
# close review request
review_req.state = form.cleaned_data["state"]
review_req.reviewed_rev = form.cleaned_data["reviewed_rev"]
review_req.result = form.cleaned_data["result"]
review_req.review = review
review_req.save()
need_to_email_review = review_submission != "link" and review_req.team.list_email and not revising_review
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."
completion_datetime = datetime.datetime.now()
if "completion_date" in form.cleaned_data:
completion_datetime = datetime.datetime.combine(form.cleaned_data["completion_date"], form.cleaned_data.get("completion_time") or datetime.time.min)
close_event = ReviewRequestDocEvent.objects.filter(type="closed_review_request", review_request=review_req).first()
if not close_event:
close_event = ReviewRequestDocEvent(type="closed_review_request", review_request=review_req)
# complete assignment
assignment.state = form.cleaned_data["state"]
assignment.reviewed_rev = form.cleaned_data["reviewed_rev"]
assignment.result = form.cleaned_data["result"]
assignment.review = review
assignment.completed_on = completion_datetime
assignment.save()
close_event.doc = review_req.doc
close_event.rev = review_req.doc.rev
need_to_email_review = review_submission != "link" and assignment.review_request.team.list_email and not revising_review
desc = "Request for {} review by {} {}: {}. Reviewer: {}.".format(
assignment.review_request.type.name,
assignment.review_request.team.acronym.upper(),
assignment.state.name,
assignment.result.name,
assignment.reviewer.person,
)
if need_to_email_review:
desc += " " + "Sent review to list."
close_event = ReviewAssignmentDocEvent.objects.filter(type="closed_review_request", review_assignment=assignment).first()
if not close_event:
close_event = ReviewAssignmentDocEvent(type="closed_review_request", review_assignment=assignment)
close_event.doc = assignment.review_request.doc
close_event.rev = assignment.review_request.doc.rev
close_event.by = request.user.person
close_event.desc = desc
close_event.state = review_req.state
close_event.state = assignment.state
close_event.time = completion_datetime
close_event.save()
if review_req.state_id == "part-completed" and not revising_review:
existing_open_reqs = ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state__in=("requested", "accepted"))
if assignment.state_id == "part-completed" and not revising_review:
existing_assignments = ReviewAssignment.objects.filter(review_request__doc=assignment.review_request.doc, review_request__team=assignment.review_request.team, state__in=("assigned", "accepted", "completed"))
new_review_req_url = new_review_req = None
if not existing_open_reqs:
new_review_req = make_new_review_request_from_existing(review_req)
new_review_req.save()
new_review_req_url = urlreverse("ietf.doc.views_review.review_request", kwargs={ "name": new_review_req.doc.name, "request_id": new_review_req.pk })
new_review_req_url = request.build_absolute_uri(new_review_req_url)
subject = "Review of {}-{} completed partially".format(review_req.doc.name, review_req.reviewed_rev)
subject = "Review of {}-{} completed partially".format(assignment.review_request.doc.name, assignment.reviewed_rev)
msg = render_to_string("review/partially_completed_review.txt", {
"new_review_req_url": new_review_req_url,
"existing_open_reqs": existing_open_reqs,
"existing_assignments": existing_assignments,
"by": request.user.person,
})
email_review_request_change(request, review_req, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False, notify_requested_by=False)
email_review_assignment_change(request, assignment, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False, notify_requested_by=False)
role = request.user.person.role_set.filter(group=review_req.team,name='reviewer').first()
role = request.user.person.role_set.filter(group=assignment.review_request.team,name='reviewer').first()
if role and role.email.active:
author_email = role.email
frm = role.formatted_email()
@ -621,10 +687,10 @@ def complete_review(request, name, request_id):
if need_to_email_review:
# email the review
subject = "{} {} {} of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(),"partial review" if review_req.state_id == "part-completed" else "review", review_req.doc.name, review_req.reviewed_rev)
related_groups = [ review_req.team, ]
if review_req.doc.group:
related_groups.append(review_req.doc.group)
subject = "{} {} {} of {}-{}".format(assignment.review_request.team.acronym.capitalize(),assignment.review_request.type.name.lower(),"partial review" if assignment.state_id == "part-completed" else "review", assignment.review_request.doc.name, assignment.reviewed_rev)
related_groups = [ assignment.review_request.team, ]
if assignment.review_request.doc.group:
related_groups.append(assignment.review_request.doc.group)
msg = Message.objects.create(
by=request.user.person,
subject=subject,
@ -632,26 +698,26 @@ def complete_review(request, name, request_id):
to=", ".join(to),
cc=form.cleaned_data["cc"],
body = render_to_string("review/completed_review.txt", {
"review_req": review_req,
"assignment": assignment,
"content": encoded_content.decode("utf-8"),
}),
)
msg.related_groups.add(*related_groups)
msg.related_docs.add(review_req.doc)
msg.related_docs.add(assignment.review_request.doc)
msg = send_mail_message(request, msg)
list_name = mailarch.list_name_from_email(review_req.team.list_email)
list_name = mailarch.list_name_from_email(assignment.review_request.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([close_event])
if form.cleaned_data['email_ad'] or review_req.result in review_req.team.reviewteamsettings.notify_ad_when.all():
(to, cc) = gather_address_lists('review_notify_ad',review_req = review_req).as_strings()
if form.cleaned_data['email_ad'] or assignment.result in assignment.review_request.team.reviewteamsettings.notify_ad_when.all():
(to, cc) = gather_address_lists('review_notify_ad',review_req = assignment.review_request).as_strings()
msg_txt = render_to_string("review/notify_ad.txt", {
"to": to,
"cc": cc,
"review_req": review_req,
"assignment": assignment,
"settings": settings,
"explicit_request": form.cleaned_data['email_ad'],
})
@ -660,42 +726,41 @@ def complete_review(request, name, request_id):
msg.save()
send_mail_message(request, msg)
return redirect("ietf.doc.views_doc.document_main", name=review_req.review.name)
return redirect("ietf.doc.views_doc.document_main", name=assignment.review.name)
else:
initial={
"reviewed_rev": review_req.reviewed_rev,
"result": review_req.result_id,
"reviewed_rev": assignment.reviewed_rev,
"result": assignment.result_id,
"cc": ", ".join(cc),
}
try:
initial['review_content'] = render_to_string('/group/%s/review/content_templates/%s.txt' % (review_req.team.acronym, review_req.type.slug), {'review_req':review_req,'today':datetime.date.today()})
initial['review_content'] = render_to_string('/group/%s/review/content_templates/%s.txt' % (assignment.review_request.team.acronym, assignment.review_request.type.slug), {'assignment':assignment,'today':datetime.date.today()})
except TemplateDoesNotExist:
pass
form = CompleteReviewForm(review_req, is_reviewer, initial=initial)
form = CompleteReviewForm(assignment, is_reviewer, initial=initial)
mail_archive_query_urls = mailarch.construct_query_urls(review_req)
mail_archive_query_urls = mailarch.construct_query_urls(assignment.review_request)
return render(request, 'doc/review/complete_review.html', {
'doc': doc,
'review_req': review_req,
'assignment': assignment,
'form': form,
'mail_archive_query_urls': mail_archive_query_urls,
'revising_review': revising_review,
})
def search_mail_archive(request, name, request_id):
#doc = get_object_or_404(Document, name=name)
review_req = get_object_or_404(ReviewRequest, pk=request_id)
def search_mail_archive(request, name, assignment_id):
assignment = get_object_or_404(ReviewAssignment, pk=assignment_id)
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
is_reviewer = user_is_person(request.user, assignment.reviewer.person)
can_manage_request = can_manage_review_requests_for_team(request.user, assignment.review_request.team)
if not (is_reviewer or can_manage_request):
return HttpResponseForbidden("You do not have permission to perform this action")
res = mailarch.construct_query_urls(review_req, query=request.GET.get("query"))
res = mailarch.construct_query_urls(assignment.review_request, query=request.GET.get("query"))
if not res:
return JsonResponse({ "error": "Couldn't do lookup in mail archive - don't know where to look"})
@ -760,7 +825,7 @@ def edit_deadline(request, name, request_id):
if form.is_valid():
if form.cleaned_data['deadline'] != old_deadline:
form.save()
subject = "Deadline changed: {} {} review of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(), review_req.doc.name, review_req.reviewed_rev)
subject = "Deadline changed: {} {} review of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(), review_req.doc.name, review_req.requested_rev)
msg = render_to_string("review/deadline_changed.txt", {
"review_req": review_req,
"old_deadline": old_deadline,

View file

@ -239,8 +239,6 @@ class ManageReviewRequestForm(forms.Form):
close_initial = None
if review_req.pk is None:
close_initial = "no-review-version"
elif review_req.reviewer:
close_initial = "no-response"
else:
close_initial = "overtaken"

View file

@ -9,26 +9,27 @@ from ietf.utils.test_utils import login_testing_unauthorized, TestCase, uniconte
from ietf.doc.models import TelechatDocEvent
from ietf.group.models import Role
from ietf.iesg.models import TelechatDate
from ietf.person.models import Email, Person
from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.person.models import Person
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.utils import (
suggested_review_requests_for_team,
review_requests_needing_reviewer_reminder, email_reviewer_reminder,
review_requests_needing_secretary_reminder, email_secretary_reminder,
review_assignments_needing_reviewer_reminder, email_reviewer_reminder,
review_assignments_needing_secretary_reminder, email_secretary_reminder,
reviewer_rotation_list,
)
from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName
from ietf.name.models import ReviewResultName, ReviewRequestStateName, ReviewAssignmentStateName
import ietf.group.views
from ietf.utils.mail import outbox, empty_outbox
from ietf.dbtemplate.factories import DBTemplateFactory
from ietf.person.factories import PersonFactory, EmailFactory
from ietf.doc.factories import DocumentFactory
from ietf.group.factories import RoleFactory, ReviewTeamFactory
from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory
from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory
class ReviewTests(TestCase):
def test_review_requests(self):
review_req = ReviewRequestFactory(reviewer=EmailFactory())
review_req = ReviewRequestFactory(state_id='assigned')
assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned', reviewer=EmailFactory(), assigned_on = review_req.time)
group = review_req.team
for url in [urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym }),
@ -36,7 +37,7 @@ class ReviewTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn(review_req.doc.name, unicontent(r))
self.assertIn(unicode(review_req.reviewer.person), unicontent(r))
self.assertIn(assignment.reviewer.person.__unicode__(), unicontent(r))
url = urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym })
@ -50,7 +51,8 @@ class ReviewTests(TestCase):
self.assertTrue(review_req.doc.name in unicontent(r))
def test_suggested_review_requests(self):
review_req = ReviewRequestFactory()
review_req = ReviewRequestFactory(state_id='assigned')
assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned')
doc = review_req.doc
team = review_req.team
@ -89,22 +91,23 @@ class ReviewTests(TestCase):
self.assertEqual(list(suggested_review_requests_for_team(team)), [])
# blocked by versioned refusal
review_req.reviewed_rev = doc.rev
review_req.state = ReviewRequestStateName.objects.get(slug="no-review-document")
review_req.state = ReviewRequestStateName.objects.get(slug="no-review-version")
review_req.save()
self.assertEqual(list(suggested_review_requests_for_team(team)), [])
# blocked by completion
review_req.state = ReviewRequestStateName.objects.get(slug="completed")
review_req.state = ReviewRequestStateName.objects.get(slug="assigned")
review_req.save()
assignment.state = ReviewAssignmentStateName.objects.get(slug="completed")
assignment.reviewed_rev = review_req.doc.rev
assignment.save()
self.assertEqual(list(suggested_review_requests_for_team(team)), [])
# ... but not to previous version
review_req.reviewed_rev = prev_rev
review_req.state = ReviewRequestStateName.objects.get(slug="completed")
review_req.save()
assignment.reviewed_rev = prev_rev
assignment.save()
self.assertEqual(len(suggested_review_requests_for_team(team)), 1)
@ -112,17 +115,19 @@ class ReviewTests(TestCase):
team = ReviewTeamFactory()
reviewer = RoleFactory(name_id='reviewer',group=team,person__user__username='reviewer').person
ReviewerSettingsFactory(person=reviewer,team=team)
review_req1 = ReviewRequestFactory(state_id='completed',team=team,reviewer=reviewer.email())
review_req1 = ReviewRequestFactory(state_id='completed',team=team)
ReviewAssignmentFactory(review_request = review_req1, reviewer=reviewer.email())
PersonFactory(user__username='plain')
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="reviewer"),
ReviewAssignmentFactory(
review_request__doc=review_req1.doc,
review_request__team=review_req1.team,
review_request__type_id="early",
review_request__deadline=datetime.date.today() + datetime.timedelta(days=30),
review_request__state_id="assigned",
review_request__requested_by=Person.objects.get(user__username="reviewer"),
state_id = "accepted",
reviewer=reviewer.email_set.first(),
)
UnavailablePeriod.objects.create(
@ -169,169 +174,70 @@ class ReviewTests(TestCase):
def test_manage_review_requests(self):
group = ReviewTeamFactory()
reviewer = RoleFactory(name_id='reviewer',group=group,person__user__username='reviewer').person
RoleFactory(name_id='reviewer',group=group,person__user__username='reviewer').person
marsperson = RoleFactory(name_id='reviewer',group=group,person=PersonFactory(name=u"Mars Anders Chairman",user__username='marschairman')).person
review_req1 = ReviewRequestFactory(doc__pages=2,doc__shepherd=marsperson.email(),reviewer=reviewer.email(),team=group)
review_req1 = ReviewRequestFactory(doc__pages=2,doc__shepherd=marsperson.email(),team=group)
review_req2 = ReviewRequestFactory(team=group)
review_req3 = ReviewRequestFactory(team=group)
RoleFactory(name_id='chair',group=review_req1.doc.group,person=marsperson)
doc = review_req1.doc
url = urlreverse(ietf.group.views.manage_review_requests, kwargs={ 'acronym': group.acronym, "assignment_status": "assigned" })
login_testing_unauthorized(self, "secretary", url)
assigned_url = urlreverse(ietf.group.views.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id, "assignment_status": "assigned" })
unassigned_url = urlreverse(ietf.group.views.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id, "assignment_status": "unassigned" })
review_req2 = 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="reviewer"),
)
review_req3 = ReviewRequest.objects.create(
doc=review_req1.doc,
team=review_req1.team,
type_id="early",
deadline=datetime.date.today() + datetime.timedelta(days=30),
state_id="requested",
requested_by=Person.objects.get(user__username="reviewer"),
)
# 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,
)
login_testing_unauthorized(self, "secretary", unassigned_url)
# Need one more person in review team one so we can test incrementing skip_count without immediately decrementing it
another_reviewer = PersonFactory.create(name = u"Extra TestReviewer") # needs to be lexically greater than the exsting one
another_reviewer.role_set.create(name_id='reviewer', email=another_reviewer.email(), group=review_req1.team)
ReviewerSettingsFactory(team=review_req3.team, person = another_reviewer)
yet_another_reviewer = PersonFactory.create(name = u"YetAnotherExtra TestReviewer") # needs to be lexically greater than the exsting one
yet_another_reviewer.role_set.create(name_id='reviewer', email=yet_another_reviewer.email(), group=review_req1.team)
ReviewerSettingsFactory(team=review_req3.team, person = yet_another_reviewer)
# get
r = self.client.get(assigned_url)
r = self.client.get(unassigned_url)
self.assertEqual(r.status_code, 200)
self.assertTrue(review_req1.doc.name in unicontent(r))
# can't save assigned: conflict
new_reviewer = Email.objects.get(role__name="reviewer", role__group=group, person__user__username="marschairman")
# provoke conflict by posting bogus data
r = self.client.post(assigned_url, {
"reviewrequest": [str(review_req1.pk), str(review_req2.pk), str(123456)],
# close
"r{}-existing_reviewer".format(review_req1.pk): "123456",
"r{}-action".format(review_req1.pk): "close",
"r{}-close".format(review_req1.pk): "no-response",
# assign
"r{}-existing_reviewer".format(review_req2.pk): "123456",
"r{}-action".format(review_req2.pk): "assign",
"r{}-reviewer".format(review_req2.pk): new_reviewer.pk,
"action": "save-continue",
})
self.assertEqual(r.status_code, 200)
content = unicontent(r).lower()
self.assertTrue("1 request closed" in content)
self.assertTrue("2 requests changed assignment" in content)
# can't save unassigned: conflict
r = self.client.post(unassigned_url, {
"reviewrequest": [str(123456)],
"action": "save-continue",
})
self.assertEqual(r.status_code, 200)
content = unicontent(r).lower()
self.assertTrue("1 request opened" in content)
# close and reassign assigned
new_reviewer = Email.objects.get(role__name="reviewer", role__group=group, person__user__username="marschairman")
r = self.client.post(assigned_url, {
"reviewrequest": [str(review_req1.pk), str(review_req2.pk)],
# close
"r{}-existing_reviewer".format(review_req1.pk): review_req1.reviewer_id or "",
"r{}-action".format(review_req1.pk): "close",
"r{}-close".format(review_req1.pk): "no-response",
# assign
"r{}-existing_reviewer".format(review_req2.pk): review_req2.reviewer_id or "",
"r{}-action".format(review_req2.pk): "assign",
"r{}-reviewer".format(review_req2.pk): new_reviewer.pk,
"r{}-add_skip".format(review_req2.pk) : 1,
"action": "save",
})
self.assertEqual(r.status_code, 302)
# no change on unassigned
# Test that conflicts are detected
r = self.client.post(unassigned_url, {
"reviewrequest": [str(review_req3.pk)],
# no change
"r{}-existing_reviewer".format(review_req3.pk): review_req3.reviewer_id or "",
"r{}-action".format(review_req3.pk): "",
"r{}-close".format(review_req3.pk): "no-response",
"r{}-reviewer".format(review_req3.pk): "",
"r{}-existing_reviewer".format(review_req3.pk): "",
"r{}-action".format(review_req3.pk): "assign",
"r{}-reviewer".format(review_req3.pk): another_reviewer.email_set.first().pk,
"r{}-add_skip".format(review_req3.pk): 1,
"action": "save",
})
self.assertEqual(r.status_code, 200)
content = unicontent(r).lower()
self.assertTrue("2 requests opened" in content)
r = self.client.post(unassigned_url, {
"reviewrequest": [str(review_req1.pk),str(review_req2.pk),str(review_req3.pk)],
"r{}-existing_reviewer".format(review_req3.pk): "",
"r{}-action".format(review_req3.pk): "assign",
"r{}-reviewer".format(review_req3.pk): another_reviewer.email_set.first().pk,
"r{}-add_skip".format(review_req3.pk): 1,
"action": "save",
})
self.assertEqual(r.status_code, 302)
review_req1, review_req2, review_req3 = reload_db_objects(review_req1, review_req2, review_req3)
self.assertEqual(review_req1.state_id, "no-response")
self.assertEqual(review_req2.state_id, "requested")
self.assertEqual(review_req2.reviewer, new_reviewer)
settings = ReviewerSettings.objects.filter(team=review_req2.team, person=new_reviewer.person).first()
review_req3 = reload_db_objects(review_req3)
settings = ReviewerSettings.objects.filter(team=review_req3.team, person=another_reviewer).first()
self.assertEqual(settings.skip_next,1)
self.assertEqual(review_req3.state_id, "requested")
r = self.client.post(assigned_url, {
"reviewrequest": [str(review_req2.pk)],
"r{}-existing_reviewer".format(review_req2.pk): review_req2.reviewer_id or "",
"r{}-action".format(review_req2.pk): "assign",
"r{}-reviewer".format(review_req2.pk): "",
"r{}-add_skip".format(review_req2.pk) : 0,
"action": "save",
})
self.assertEqual(r.status_code, 302)
review_req2 = reload_db_objects(review_req2)
self.assertEqual(review_req2.state_id, "requested")
self.assertEqual(review_req2.reviewer, None)
self.assertEqual(review_req3.state_id, "assigned")
def test_email_open_review_assignments(self):
review_req1 = ReviewRequestFactory(reviewer=EmailFactory(person__user__username='marschairman'))
review_req1 = ReviewRequestFactory()
ReviewAssignmentFactory(review_request=review_req1,reviewer=EmailFactory(person__user__username='marschairman'))
DBTemplateFactory.create(path='/group/defaults/email/open_assignments.txt',
type_id='django',
content = """
{% autoescape off %}
Reviewer Deadline Draft
{% for r in review_requests %}{{ r.reviewer.person.plain_name|ljust:"22" }} {{ r.deadline|date:"Y-m-d" }} {{ r.doc_id }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}
{% for r in review_assignments %}{{ r.reviewer.person.plain_name|ljust:"22" }} {{ r.review_request.deadline|date:"Y-m-d" }} {{ r.review_request.doc_id }}-{% if r.review_request.requested_rev %}{{ r.review_request.requested_rev }}{% else %}{{ r.review_request.doc.rev }}{% endif %}
{% endfor %}
{% if rotation_list %}Next in the reviewer rotation:
@ -376,15 +282,14 @@ class ReviewTests(TestCase):
def test_change_reviewer_settings(self):
reviewer = ReviewerSettingsFactory(person__user__username='reviewer',expertise='Some expertise').person
review_req = ReviewRequestFactory(reviewer=reviewer.email())
RoleFactory(name_id='reviewer',group=review_req.team,person=review_req.reviewer.person)
review_req = ReviewRequestFactory()
assignment = ReviewAssignmentFactory(review_request=review_req,reviewer=reviewer.email())
RoleFactory(name_id='reviewer',group=review_req.team,person=assignment.reviewer.person)
RoleFactory(name_id='secr',group=review_req.team)
reviewer = review_req.reviewer.person
url = urlreverse(ietf.group.views.change_reviewer_settings, kwargs={
"acronym": review_req.team.acronym,
"reviewer_email": review_req.reviewer_id,
"reviewer_email": assignment.reviewer_id,
})
login_testing_unauthorized(self, reviewer.user.username, url)
@ -392,7 +297,7 @@ class ReviewTests(TestCase):
url = urlreverse(ietf.group.views.change_reviewer_settings, kwargs={
"group_type": review_req.team.type_id,
"acronym": review_req.team.acronym,
"reviewer_email": review_req.reviewer_id,
"reviewer_email": assignment.reviewer_id,
})
# get
@ -451,7 +356,7 @@ class ReviewTests(TestCase):
msg_content = outbox[0].get_payload(decode=True).decode("utf-8").lower()
self.assertTrue(start_date.isoformat(), msg_content)
self.assertTrue("indefinite", msg_content)
self.assertEqual(period.reason, "Whimsy")
self.assertEqual(period.reason, "Whimsy")
# end unavailable period
empty_outbox()
@ -526,8 +431,9 @@ class ReviewTests(TestCase):
self.assertEqual(settings.remind_days_before_deadline, 6)
def test_review_reminders(self):
review_req = ReviewRequestFactory()
review_req = ReviewRequestFactory(state_id='assigned')
reviewer = RoleFactory(name_id='reviewer',group=review_req.team,person__user__username='reviewer').person
assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned', assigned_on = review_req.time, reviewer=reviewer.email_set.first())
RoleFactory(name_id='secr',group=review_req.team,person__user__username='reviewsecretary')
ReviewerSettingsFactory(team = review_req.team, person = reviewer)
@ -551,23 +457,23 @@ class ReviewTests(TestCase):
review_req.save()
# reviewer
needing_reminders = review_requests_needing_reviewer_reminder(today - datetime.timedelta(days=1))
needing_reminders = review_assignments_needing_reviewer_reminder(today - datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
needing_reminders = review_requests_needing_reviewer_reminder(today)
self.assertEqual(list(needing_reminders), [review_req])
needing_reminders = review_assignments_needing_reviewer_reminder(today)
self.assertEqual(list(needing_reminders), [assignment])
needing_reminders = review_requests_needing_reviewer_reminder(today + datetime.timedelta(days=1))
needing_reminders = review_assignments_needing_reviewer_reminder(today + datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
# secretary
needing_reminders = review_requests_needing_secretary_reminder(today - datetime.timedelta(days=1))
needing_reminders = review_assignments_needing_secretary_reminder(today - datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
needing_reminders = review_requests_needing_secretary_reminder(today)
self.assertEqual(list(needing_reminders), [(review_req, secretary_role)])
needing_reminders = review_assignments_needing_secretary_reminder(today)
self.assertEqual(list(needing_reminders), [(assignment, secretary_role)])
needing_reminders = review_requests_needing_secretary_reminder(today + datetime.timedelta(days=1))
needing_reminders = review_assignments_needing_secretary_reminder(today + datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
# email reviewer

View file

@ -41,7 +41,7 @@ info_detail_urls = [
url(r'^archives/$', views.derived_archives),
url(r'^photos/$', views.group_photos),
url(r'^reviews/$', views.review_requests),
url(r'^reviews/manage/(?P<assignment_status>assigned|unassigned)/$', views.manage_review_requests),
url(r'^reviews/manage/(?P<assignment_status>unassigned)/$', views.manage_review_requests),
url(r'^reviews/email-assignments/$', views.email_open_review_assignments),
url(r'^reviewers/$', views.reviewer_overview),
url(r'^reviewers/(?P<reviewer_email>[\w%+-.@]+)/settings/$', views.change_reviewer_settings),

View file

@ -223,7 +223,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
if group.features.has_reviews and can_manage_review_requests_for_team(request.user, group):
import ietf.group.views
actions.append((u"Manage unassigned reviews", urlreverse(ietf.group.views.manage_review_requests, kwargs=dict(assignment_status="unassigned", **kwargs))))
actions.append((u"Manage assigned reviews", urlreverse(ietf.group.views.manage_review_requests, kwargs=dict(assignment_status="assigned", **kwargs))))
#actions.append((u"Manage assigned reviews", urlreverse(ietf.group.views.manage_review_requests, kwargs=dict(assignment_status="assigned", **kwargs))))
if Role.objects.filter(name="secr", group=group, person__user=request.user).exists():
actions.append((u"Secretary settings", urlreverse(ietf.group.views.change_review_secretary_settings, kwargs=kwargs)))

View file

@ -86,7 +86,7 @@ from ietf.meeting.helpers import get_meeting
from ietf.meeting.utils import group_sessions
from ietf.name.models import GroupTypeName, StreamName
from ietf.person.models import Email
from ietf.review.models import ReviewRequest, ReviewerSettings, ReviewSecretarySettings
from ietf.review.models import ReviewRequest, ReviewAssignment, ReviewerSettings, ReviewSecretarySettings
from ietf.review.utils import (can_manage_review_requests_for_team,
can_access_review_stats_for_team,
@ -99,7 +99,7 @@ from ietf.review.utils import (can_manage_review_requests_for_team,
current_unavailable_periods_for_reviewers,
email_reviewer_availability_change,
reviewer_rotation_list,
latest_review_requests_for_reviewers,
latest_review_assignments_for_reviewers,
augment_review_requests_with_events,
get_default_filter_re,
days_needed_to_fulfill_min_interval_for_reviewers,
@ -108,7 +108,7 @@ from ietf.doc.models import LastCallDocEvent
from ietf.name.models import ReviewRequestStateName
from ietf.name.models import ReviewAssignmentStateName
from ietf.utils.mail import send_mail_text, parse_preformatted
from ietf.ietfauth.utils import user_is_person
@ -1263,27 +1263,26 @@ def group_menu_data(request):
# --- Review views -----------------------------------------------------
def get_open_review_requests_for_team(team, assignment_status=None):
open_review_requests = ReviewRequest.objects.filter(
team=team,
state__in=("requested", "accepted")
open_review_requests = ReviewRequest.objects.filter(team=team).filter(
Q(state_id='requested') | Q(state_id='assigned',reviewassignment__state__in=('assigned','accepted'))
).prefetch_related(
"reviewer__person", "type", "state", "doc", "doc__states",
"type", "state", "doc", "doc__states",
).order_by("-time", "-id")
if assignment_status == "unassigned":
open_review_requests = suggested_review_requests_for_team(team) + list(open_review_requests.filter(reviewer=None))
open_review_requests = suggested_review_requests_for_team(team) + list(open_review_requests.filter(state_id='requested'))
elif assignment_status == "assigned":
open_review_requests = list(open_review_requests.exclude(reviewer=None))
open_review_requests = list(open_review_requests.filter(state_id='assigned'))
else:
open_review_requests = suggested_review_requests_for_team(team) + list(open_review_requests)
today = datetime.date.today()
unavailable_periods = current_unavailable_periods_for_reviewers(team)
for r in open_review_requests:
if r.reviewer:
r.reviewer_unavailable = any(p.availability == "unavailable"
for p in unavailable_periods.get(r.reviewer.person_id, []))
r.due = max(0, (today - r.deadline).days)
#today = datetime.date.today()
#unavailable_periods = current_unavailable_periods_for_reviewers(team)
#for r in open_review_requests:
#if r.reviewer:
# r.reviewer_unavailable = any(p.availability == "unavailable"
# for p in unavailable_periods.get(r.reviewer.person_id, []))
#r.due = max(0, (today - r.deadline).days)
return open_review_requests
@ -1292,25 +1291,19 @@ def review_requests(request, acronym, group_type=None):
if not group.features.has_reviews:
raise Http404
assigned_review_requests = []
unassigned_review_requests = []
unassigned_review_requests = [r for r in get_open_review_requests_for_team(group) if not r.state_id=='assigned']
for r in get_open_review_requests_for_team(group):
if r.reviewer:
assigned_review_requests.append(r)
else:
unassigned_review_requests.append(r)
open_review_assignments = list(ReviewAssignment.objects.filter(review_request__team=group, state_id__in=('assigned','accepted')).order_by('-assigned_on'))
today = datetime.date.today()
unavailable_periods = current_unavailable_periods_for_reviewers(group)
for a in open_review_assignments:
a.reviewer_unavailable = any(p.availability == "unavailable"
for p in unavailable_periods.get(a.reviewer.person_id, []))
a.due = max(0, (today - a.review_request.deadline).days)
open_review_requests = [
("Unassigned", unassigned_review_requests),
("Assigned", assigned_review_requests),
]
closed_review_assignments = ReviewAssignment.objects.filter(review_request__team=group).exclude(state_id__in=('assigned','accepted')).prefetch_related("state","result").order_by('-assigned_on')
closed_review_requests = ReviewRequest.objects.filter(
team=group,
).exclude(
state__in=("requested", "accepted")
).prefetch_related("reviewer__person", "type", "state", "doc", "result").order_by("-time", "-id")
closed_review_requests = ReviewRequest.objects.filter(team=group).exclude(state__in=("requested", "assigned")).prefetch_related("type", "state", "doc").order_by("-time", "-id")
since_choices = [
(None, "1 month"),
@ -1338,10 +1331,14 @@ def review_requests(request, acronym, group_type=None):
| Q(reviewrequestdocevent__isnull=True, time__gte=datetime.date.today() - date_limit)
).distinct()
closed_review_assignments = closed_review_assignments.filter(completed_on__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,
"unassigned_review_requests": unassigned_review_requests,
"open_review_assignments": open_review_assignments,
"closed_review_requests": closed_review_requests,
"closed_review_assignments": closed_review_assignments,
"since_choices": since_choices,
"since": since,
"can_manage_review_requests": can_manage_review_requests_for_team(request.user, group),
@ -1365,8 +1362,8 @@ def reviewer_overview(request, acronym, group_type=None):
today = datetime.date.today()
req_data_for_reviewers = latest_review_requests_for_reviewers(group)
review_state_by_slug = { n.slug: n for n in ReviewRequestStateName.objects.all() }
req_data_for_reviewers = latest_review_assignments_for_reviewers(group)
assignment_state_by_slug = { n.slug: n for n in ReviewAssignmentStateName.objects.all() }
days_needed = days_needed_to_fulfill_min_interval_for_reviewers(group)
@ -1387,15 +1384,16 @@ def reviewer_overview(request, acronym, group_type=None):
person.busy = person.id in days_needed
# TODO - What is this MAX_CLOSED_REQS trying to accomplish?
MAX_CLOSED_REQS = 10
days_since = 9999
req_data = req_data_for_reviewers.get(person.pk, [])
open_reqs = sum(1 for d in req_data if d.state in ["requested", "accepted"])
open_reqs = sum(1 for d in req_data if d.state in ["assigned", "accepted"])
latest_reqs = []
for d in req_data:
if d.state in ["requested", "accepted"] or len(latest_reqs) < MAX_CLOSED_REQS + open_reqs:
latest_reqs.append((d.req_pk, d.doc, d.reviewed_rev, d.assigned_time, d.deadline,
review_state_by_slug.get(d.state),
if d.state in ["assigned", "accepted"] or len(latest_reqs) < MAX_CLOSED_REQS + open_reqs:
latest_reqs.append((d.assignment_pk, d.doc, d.reviewed_rev, d.assigned_time, d.deadline,
assignment_state_by_slug.get(d.state),
int(math.ceil(d.assignment_to_closure_days)) if d.assignment_to_closure_days is not None else None))
if d.state in ["completed", "completed_in_time", "completed_late"]:
if d.assigned_time is not None:
@ -1474,23 +1472,6 @@ def manage_review_requests(request, acronym, group_type=None, assignment_status=
for r in opened_reqs:
review_requests_dict[r].form.add_error(None, "New request.")
for req in review_requests:
existing_reviewer = request.POST.get(req.form.prefix + "-existing_reviewer")
if existing_reviewer is None:
continue
if existing_reviewer != unicode(req.reviewer_id or ""):
msg = "Assignment was changed."
a = req.form["action"].value()
if a == "assign":
msg += " Didn't assign reviewer."
elif a == "close":
msg += " Didn't close request."
req.form.add_error(None, msg)
req.form.data[req.form.prefix + "-action"] = "" # cancel the action
newly_assigned += 1
form_results = []
for req in review_requests:
form_results.append(req.form.is_valid())
@ -1565,37 +1546,35 @@ def email_open_review_assignments(request, acronym, group_type=None):
if not can_manage_review_requests_for_team(request.user, group):
return HttpResponseForbidden("You do not have permission to perform this action")
review_requests = list(ReviewRequest.objects.filter(
team=group,
state__in=("requested", "accepted"),
).exclude(
reviewer=None,
).prefetch_related("reviewer", "type", "state", "doc").distinct().order_by("reviewer","-deadline"))
review_assignments = list(ReviewAssignment.objects.filter(
review_request__team=group,
state__in=("assigned", "accepted"),
).prefetch_related("reviewer", "review_request__type", "state", "review_request__doc").distinct().order_by("reviewer","-review_request__deadline"))
review_requests.sort(key=lambda r:r.reviewer.person.last_name()+r.reviewer.person.first_name())
review_assignments.sort(key=lambda r:r.reviewer.person.last_name()+r.reviewer.person.first_name())
for r in review_requests:
if r.doc.telechat_date():
r.section = 'For telechat %s' % r.doc.telechat_date().isoformat()
for r in review_assignments:
if r.review_request.doc.telechat_date():
r.section = 'For telechat %s' % r.review_request.doc.telechat_date().isoformat()
r.section_order='0'+r.section
elif r.type_id == 'early':
elif r.review_request.type_id == 'early':
r.section = 'Early review requests:'
r.section_order='2'
else:
r.section = 'Last calls:'
r.section_order='1'
e = r.doc.latest_event(LastCallDocEvent, type="sent_last_call")
e = r.review_request.doc.latest_event(LastCallDocEvent, type="sent_last_call")
r.lastcall_ends = e and e.expires.date().isoformat()
r.earlier_review = ReviewRequest.objects.filter(doc=r.doc,reviewer__in=r.reviewer.person.email_set.all(),state="completed")
r.earlier_review = ReviewAssignment.objects.filter(review_request__doc=r.review_request.doc,reviewer__in=r.reviewer.person.email_set.all(),state="completed")
if r.earlier_review:
req_rev = r.requested_rev or r.doc.rev
req_rev = r.review_request.requested_rev or r.review_request.doc.rev
earlier_review_rev = r.earlier_review.aggregate(Max('reviewed_rev'))['reviewed_rev__max']
if req_rev == earlier_review_rev:
r.earlier_review_mark = '**'
else:
r.earlier_review_mark = '*'
review_requests.sort(key=lambda r: r.section_order)
review_assignments.sort(key=lambda r: r.section_order)
back_url = request.GET.get("next")
if not back_url:
@ -1630,7 +1609,7 @@ def email_open_review_assignments(request, acronym, group_type=None):
template = DBTemplate.objects.get(path="/group/defaults/email/open_assignments.txt")
partial_msg = render_to_string(template.path, {
"review_requests": review_requests,
"review_assignments": review_assignments,
"rotation_list": reviewer_rotation_list(group)[:10],
"group" : group,
})
@ -1651,7 +1630,7 @@ def email_open_review_assignments(request, acronym, group_type=None):
return render(request, 'group/email_open_review_assignments.html', {
'group': group,
'review_requests': review_requests,
'review_assignments': review_assignments,
'form': form,
'back_url': back_url,
})
@ -1737,14 +1716,14 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
# the secretary might need to reassign
# assignments, so mention the current ones
review_reqs = ReviewRequest.objects.filter(state__in=["requested", "accepted"], reviewer=reviewer_role.email, team=group)
review_assignments = ReviewAssignment.objects.filter(state__in=["assigned", "accepted"], reviewer=reviewer_role.email, review_request__team=group)
msg += "\n\n"
if review_reqs:
if review_assignments:
msg += "{} is currently assigned to review:".format(reviewer_role.person)
for r in review_reqs:
for r in review_assignments:
msg += "\n\n"
msg += "{} (deadline: {})".format(r.doc_id, r.deadline.isoformat())
msg += "{} (deadline: {})".format(r.review_request.doc_id, r.review_request.deadline.isoformat())
else:
msg += "{} does not have any assignments currently.".format(reviewer_role.person)

View file

@ -12,7 +12,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import Document, LastCallDocEvent, ConsensusDocEvent
from ietf.doc.utils_search import fill_in_telechat_date
from ietf.iesg.models import TelechatDate, TelechatAgendaItem
from ietf.review.utils import review_requests_to_list_for_docs
from ietf.review.utils import review_assignments_to_list_for_docs
def get_agenda_date(date=None):
if not date:
@ -156,7 +156,7 @@ def fill_in_agenda_docs(date, sections, docs=None):
docs = docs.select_related("stream", "group").distinct()
fill_in_telechat_date(docs)
review_requests_for_docs = review_requests_to_list_for_docs(docs)
review_assignments_for_docs = review_assignments_to_list_for_docs(docs)
for doc in docs:
if doc.telechat_date() != date:
@ -182,7 +182,7 @@ def fill_in_agenda_docs(date, sections, docs=None):
if e and (e.consensus != None):
doc.consensus = "Yes" if e.consensus else "No"
doc.review_requests = review_requests_for_docs.get(doc.pk, [])
doc.review_assignments = review_assignments_for_docs.get(doc.pk, [])
elif doc.type_id == "conflrev":
doc.conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document
elif doc.type_id == "charter":

View file

@ -22,7 +22,7 @@ from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.mailinglists.models import Subscribed
from ietf.person.models import Person, Email, PersonalApiKey, PERSON_API_KEY_ENDPOINTS
from ietf.person.factories import PersonFactory, EmailFactory
from ietf.review.factories import ReviewRequestFactory
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
from ietf.review.models import ReviewWish, UnavailablePeriod
from ietf.utils.decorators import skip_coverage
@ -358,11 +358,12 @@ class IetfAuthTests(TestCase):
self.assertTrue(self.username_in_htpasswd_file(user.username))
def test_review_overview(self):
review_req = ReviewRequestFactory(reviewer=EmailFactory(person__user__username='reviewer'))
RoleFactory(name_id='reviewer',group=review_req.team,person=review_req.reviewer.person)
review_req = ReviewRequestFactory()
assignment = ReviewAssignmentFactory(review_request=review_req,reviewer=EmailFactory(person__user__username='reviewer'))
RoleFactory(name_id='reviewer',group=review_req.team,person=assignment.reviewer.person)
doc = review_req.doc
reviewer = review_req.reviewer.person
reviewer = assignment.reviewer.person
UnavailablePeriod.objects.create(
team=review_req.team,

View file

@ -65,7 +65,7 @@ from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.ietfauth.utils import role_required
from ietf.mailinglists.models import Subscribed, Whitelisted
from ietf.person.models import Person, Email, Alias, PersonalApiKey
from ietf.review.models import ReviewRequest, ReviewerSettings, ReviewWish
from ietf.review.models import ReviewerSettings, ReviewWish, ReviewAssignment
from ietf.review.utils import unavailable_periods_to_list, get_default_filter_re
from ietf.doc.fields import SearchableDocumentField
from ietf.utils.decorators import person_required
@ -433,18 +433,18 @@ class AddReviewWishForm(forms.Form):
@login_required
def review_overview(request):
open_review_requests = ReviewRequest.objects.filter(
open_review_assignments = ReviewAssignment.objects.filter(
reviewer__person__user=request.user,
state__in=["requested", "accepted"],
state__in=["assigned", "accepted"],
)
today = Date.today()
for r in open_review_requests:
r.due = max(0, (today - r.deadline).days)
for r in open_review_assignments:
r.due = max(0, (today - r.review_request.deadline).days)
closed_review_requests = ReviewRequest.objects.filter(
closed_review_assignments = ReviewAssignment.objects.filter(
reviewer__person__user=request.user,
state__in=["no-response", "part-completed", "completed"],
).order_by("-time")[:20]
).order_by("-review_request__time")[:20]
teams = Group.objects.filter(role__name="reviewer", role__person__user=request.user, state="active")
@ -483,8 +483,8 @@ def review_overview(request):
review_wishes = ReviewWish.objects.filter(person__user=request.user).prefetch_related("team")
return render(request, 'ietfauth/review_overview.html', {
'open_review_requests': open_review_requests,
'closed_review_requests': closed_review_requests,
'open_review_assignments': open_review_assignments,
'closed_review_assignments': closed_review_assignments,
'teams': teams,
'review_wishes': review_wishes,
'review_wish_form': review_wish_form,

View file

@ -9,7 +9,7 @@ from ietf.name.models import (
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
DocUrlTagName)
DocUrlTagName, ReviewAssignmentStateName)
from ietf.stats.models import CountryAlias
@ -68,6 +68,7 @@ admin.site.register(LiaisonStatementTagName, NameAdmin)
admin.site.register(MeetingTypeName, NameAdmin)
admin.site.register(NomineePositionStateName, NameAdmin)
admin.site.register(ReviewRequestStateName, NameAdmin)
admin.site.register(ReviewAssignmentStateName, NameAdmin)
admin.site.register(ReviewResultName, NameAdmin)
admin.site.register(ReviewTypeName, NameAdmin)
admin.site.register(RoleName, NameAdmin)

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-04 13:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0004_add_prefix_to_doctypenames'),
]
operations = [
migrations.CreateModel(
name='ReviewAssignmentStateName',
fields=[
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
]

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-04 14:02
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
ReviewRequestStateName = apps.get_model('name','ReviewRequestStateName')
ReviewAssignmentStateName = apps.get_model('name','ReviewAssignmentStateName')
# TODO: Remove these newly unused states in a future release
ReviewRequestStateName.objects.filter(slug__in=['accepted', 'rejected', 'no-response', 'part-completed', 'completed', 'unknown']).update(used=False)
ReviewRequestStateName.objects.create(slug = 'assigned', name = 'Assigned', desc = 'The ReviewRequest has been assigned to at least one reviewer' , used = True, order = 0)
assignment_states = [
{ 'slug': 'assigned',
'name': 'Assigned',
'desc': 'The review has been assigned to this reviewer',
'used': True,
'order': 0
},
{ 'slug':'accepted',
'name':'Accepted',
'desc':'The reviewer has accepted the assignment',
'used': True,
'order':0
},
{ 'slug':'rejected',
'name':'Rejected',
'desc':'The reviewer has rejected the assignment',
'used': True,
'order':0
},
{ 'slug':'withdrawn',
'name':'Withdrawn by Team',
'desc':'The team secretary has withdrawn the assignment',
'used': True,
'order':0
},
{ 'slug':'overtaken',
'name':'Overtaken By Events',
'desc':'The review was abandoned because of circumstances',
'used': True,
'order':0
},
{ 'slug':'no-response',
'name':'No Response',
'desc':'The reviewer did not provide a review by the deadline',
'used': True,
'order':0
},
{ 'slug':'part-completed',
'name':'Partially Completed',
'desc':'The reviewer partially completed the assignment',
'used': True,
'order':0
},
{ 'slug':'completed',
'name':'Completed',
'desc':'The reviewer completed the assignment',
'used': True,
'order':0
},
{ 'slug':'unknown',
'name':'Unknown',
'desc':'The assignment is was imported from an earlier database and its state could not be computed',
'used':'True',
'order':0
}
]
for entry in assignment_states:
ReviewAssignmentStateName.objects.create(**entry)
def reverse(apps, schema_editor):
ReviewRequestStateName = apps.get_model('name','ReviewRequestStateName')
ReviewAssignmentStateName = apps.get_model('name','ReviewAssignmentStateName')
ReviewRequestStateName.objects.filter(slug__in=['accepted', 'rejected', 'no-response', 'part-completed', 'completed', 'unknown']).update(used=True)
ReviewRequestStateName.objects.filter(slug='assigned').update(used=False)
ReviewAssignmentStateName.objects.update(used=False)
class Migration(migrations.Migration):
dependencies = [
('name', '0005_reviewassignmentstatename'),
('doc','0011_reviewassignmentdocevent'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -95,8 +95,9 @@ class LiaisonStatementEventTypeName(NameModel):
class LiaisonStatementTagName(NameModel):
"Action Required, Action Taken"
class ReviewRequestStateName(NameModel):
"""Requested, Accepted, Rejected, Withdrawn, Overtaken By Events,
No Response, No Review of Version, No Review of Document, Partially Completed, Completed"""
"""Requested, Assigned, Withdrawn, Overtaken By Events, No Review of Version, No Review of Document"""
class ReviewAssignmentStateName(NameModel):
"""Accepted, Rejected, Withdrawn, Overtaken By Events, No Response, Partially Completed, Completed"""
class ReviewTypeName(NameModel):
"""Early Review, Last Call, Telechat"""
class ReviewResultName(NameModel):

View file

@ -14,8 +14,9 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName, )
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, )
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -428,6 +429,20 @@ class ReviewRequestStateNameResource(ModelResource):
}
api.name.register(ReviewRequestStateNameResource())
class ReviewAssignmentStateNameResource(ModelResource):
class Meta:
cache = SimpleCache()
queryset = ReviewAssignmentStateName.objects.all()
#resource_name = 'reviewassignmentstatename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(ReviewAssignmentStateNameResource())
class ReviewTypeNameResource(ModelResource):
class Meta:
cache = SimpleCache()
@ -567,3 +582,5 @@ class AgendaTypeNameResource(ModelResource):
"order": ALL,
}
api.name.register(AgendaTypeNameResource())

View file

@ -3,7 +3,7 @@ import simple_history
from django.contrib import admin
from ietf.review.models import (ReviewerSettings, ReviewSecretarySettings, UnavailablePeriod,
ReviewWish, NextReviewerInTeam, ReviewRequest, ReviewTeamSettings )
ReviewWish, NextReviewerInTeam, ReviewRequest, ReviewAssignment, ReviewTeamSettings )
class ReviewerSettingsAdmin(simple_history.admin.SimpleHistoryAdmin):
def acronym(self, obj):
@ -53,14 +53,23 @@ admin.site.register(NextReviewerInTeam, NextReviewerInTeamAdmin)
class ReviewRequestAdmin(admin.ModelAdmin):
list_display = ["doc", "time", "type", "team", "deadline"]
list_display_links = ["doc"]
list_filter = ["team", "type", "state", "result"]
list_filter = ["team", "type", "state"]
ordering = ["-id"]
raw_id_fields = ["doc", "team", "requested_by", "reviewer", "review"]
raw_id_fields = ["doc", "team", "requested_by"]
date_hierarchy = "time"
search_fields = ["doc__name", "reviewer__person__name"]
search_fields = ["doc__name"]
admin.site.register(ReviewRequest, ReviewRequestAdmin)
class ReviewAssignmentAdmin(admin.ModelAdmin):
list_display = ["review_request", "reviewer", "assigned_on", "result"]
list_filter = ["result", "state"]
ordering = ["-id"]
raw_id_fields = ["reviewer", "result"]
search_fields = ["review_request__doc__name"]
admin.site.register(ReviewAssignment, ReviewAssignmentAdmin)
class ReviewTeamSettingsAdmin(admin.ModelAdmin):
list_display = ["group", ]
search_fields = ["group__acronym", ]

View file

@ -1,7 +1,7 @@
import factory
import datetime
from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewerSettings
from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings
from ietf.name.models import ReviewTypeName, ReviewResultName
class ReviewTeamSettingsFactory(factory.DjangoModelFactory):
@ -39,6 +39,15 @@ class ReviewRequestFactory(factory.DjangoModelFactory):
deadline = datetime.datetime.today()+datetime.timedelta(days=14)
requested_by = factory.SubFactory('ietf.person.factories.PersonFactory')
class ReviewAssignmentFactory(factory.DjangoModelFactory):
class Meta:
model = ReviewAssignment
review_request = factory.SubFactory('ietf.review.factories.ReviewRequestFactory')
state_id = 'assigned'
reviewer = factory.SubFactory('ietf.person.factories.EmailFactory')
assigned_on = datetime.datetime.now()
class ReviewerSettingsFactory(factory.DjangoModelFactory):
class Meta:
model = ReviewerSettings

View file

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-04 14:27
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('doc', '0009_move_non_url_externalurls_to_uploaded_filename'),
('name', '0005_reviewassignmentstatename'),
('person', '0008_auto_20181014_1448'),
('review', '0008_remove_reviewrequest_old_id'),
]
operations = [
migrations.CreateModel(
name='ReviewAssignment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('assigned_on', models.DateTimeField(blank=True, null=True)),
('completed_on', models.DateTimeField(blank=True, null=True)),
('reviewed_rev', models.CharField(blank=True, max_length=16, verbose_name=b'reviewed revision')),
('mailarch_url', models.URLField(blank=True, null=True)),
('result', ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='name.ReviewResultName')),
('review', ietf.utils.models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='doc.Document')),
],
),
migrations.RenameField(
model_name='reviewrequest',
old_name='result',
new_name='unused_result',
),
migrations.RenameField(
model_name='reviewrequest',
old_name='review',
new_name='unused_review',
),
migrations.RenameField(
model_name='reviewrequest',
old_name='reviewed_rev',
new_name='unused_reviewed_rev',
),
migrations.RenameField(
model_name='reviewrequest',
old_name='reviewer',
new_name='unused_reviewer',
),
migrations.AddField(
model_name='reviewassignment',
name='review_request',
field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.ReviewRequest'),
),
migrations.AddField(
model_name='reviewassignment',
name='reviewer',
field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Email'),
),
migrations.AddField(
model_name='reviewassignment',
name='state',
field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ReviewAssignmentStateName'),
),
]

View file

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-04 14:34
from __future__ import unicode_literals
from tqdm import tqdm
from django.db import migrations
def assigned_time(model, request):
e = model.objects.filter(doc=request.doc, type="assigned_review_request", review_request=request).order_by('-time', '-id').first()
return e.time if e and e.time else None
def done_time(model, request):
if request.unused_review and request.unused_review.time:
return request.unused_review.time
e = model.objects.filter(doc=request.doc, type="closed_review_request", review_request=request).order_by('-time', '-id').first()
time = e.time if e and e.time else None
return time if time else request.time
def map_request_state_to_assignment_state(req_state_id):
if req_state_id == 'requested':
return 'assigned'
elif req_state_id in ('no-review-document', 'no-review-version'):
return 'withdrawn'
else:
return req_state_id
def forward(apps, schema_editor):
ReviewRequest = apps.get_model('review', 'ReviewRequest')
ReviewAssignment = apps.get_model('review', 'ReviewAssignment')
Document = apps.get_model('doc', 'Document')
ReviewRequestDocEvent = apps.get_model('doc','ReviewRequestDocEvent')
ReviewAssignmentDocEvent = apps.get_model('doc','ReviewAssignmentDocEvent')
print('') # introduce a newline before tqdm starts writing
for request in tqdm(ReviewRequest.objects.exclude(unused_reviewer__isnull=True)):
assignment_state = map_request_state_to_assignment_state(request.state_id)
if not (assignment_state in (u'assigned', u'accepted', u'completed', u'no-response', u'overtaken', u'part-completed', u'rejected', u'withdrawn', u'unknown')):
print ("Trouble with review_request",request.pk,"with state",request.state_id)
exit(-1)
ReviewAssignment.objects.create(
review_request = request,
state_id = assignment_state,
reviewer = request.unused_reviewer,
assigned_on = assigned_time(ReviewRequestDocEvent, request),
completed_on = done_time(ReviewRequestDocEvent, request),
review = request.unused_review,
reviewed_rev = request.unused_reviewed_rev,
result = request.unused_result,
mailarch_url = request.unused_review and request.unused_review.external_url,
)
Document.objects.filter(type_id='review').update(external_url='')
ReviewRequest.objects.filter(state_id__in=('accepted', 'rejected', 'no-response', 'part-completed', 'completed', 'unknown')).update(state_id='assigned')
ReviewRequest.objects.filter(state_id='requested',unused_reviewer__isnull=False).update(state_id='assigned')
for req_event in tqdm(ReviewRequestDocEvent.objects.filter(type="assigned_review_request",review_request__unused_reviewer__isnull=False)):
ReviewAssignmentDocEvent.objects.create(
time = req_event.time,
type = req_event.type,
by = req_event.by,
doc = req_event.doc,
rev = req_event.rev,
desc = req_event.desc,
review_assignment = req_event.review_request.reviewassignment_set.first(),
state_id = 'assigned'
)
for req_event in tqdm(ReviewRequestDocEvent.objects.filter(type="closed_review_request",
state_id__in=('completed', 'no-response', 'part-completed', 'rejected', 'unknown', 'withdrawn'),
review_request__unused_reviewer__isnull=False)):
ReviewAssignmentDocEvent.objects.create(
time = req_event.time,
type = req_event.type,
by = req_event.by,
doc = req_event.doc,
rev = req_event.rev,
desc = req_event.desc,
review_assignment = req_event.review_request.reviewassignment_set.first(),
state_id = req_event.state_id
)
ReviewRequestDocEvent.objects.filter(type="closed_review_request",
state_id__in=('completed', 'no-response', 'part-completed', 'rejected', 'unknown', 'withdrawn'),
review_request__unused_reviewer__isnull=False).delete()
def reverse(apps, schema_editor):
ReviewAssignment = apps.get_model('review', 'ReviewAssignment')
ReviewRequestDocEvent = apps.get_model('doc','ReviewRequestDocEvent')
ReviewAssignmentDocEvent = apps.get_model('doc','ReviewAssignmentDocEvent')
for assignment in tqdm(ReviewAssignment.objects.all()):
assignment.review_request.unused_review.external_url = assignment.mailarch_url
assignment.review_request.unused_review.save()
assignment.review_request.state_id = assignment.state_id
assignment.review_request.save()
for asgn_event in tqdm(ReviewAssignmentDocEvent.objects.filter(state_id__in=('completed', 'no-response', 'part-completed', 'rejected', 'unknown', 'withdrawn'))):
ReviewRequestDocEvent.objects.create(
time = asgn_event.time,
type = asgn_event.type,
by = asgn_event.by,
doc = asgn_event.doc,
rev = asgn_event.rev,
desc = asgn_event.desc,
review_request = asgn_event.review_request,
state_id = asgn_event.state_id
)
ReviewAssignmentDocEvent.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('review', '0009_refactor_review_request'),
('doc','0011_reviewassignmentdocevent')
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -7,7 +7,7 @@ from django.db import models
from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.person.models import Person, Email
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName, ReviewAssignmentStateName
from ietf.utils.validators import validate_regular_expression_string
from ietf.utils.models import ForeignKey, OneToOneField
@ -105,9 +105,7 @@ class NextReviewerInTeam(models.Model):
verbose_name_plural = "next reviewer in team settings"
class ReviewRequest(models.Model):
"""Represents a request for a review and the process it goes through.
There should be one ReviewRequest entered for each combination of
document, rev, and reviewer."""
"""Represents a request for a review and the process it goes through."""
state = ForeignKey(ReviewRequestStateName)
# Fields filled in on the initial record creation - these
@ -121,34 +119,38 @@ class ReviewRequest(models.Model):
requested_rev = models.CharField(verbose_name="requested revision", max_length=16, blank=True, help_text="Fill in if a specific revision is to be reviewed, e.g. 02")
comment = models.TextField(verbose_name="Requester's comments and instructions", max_length=2048, blank=True, help_text="Provide any additional information to show to the review team secretary and reviewer", default='')
# Fields filled in as reviewer is assigned and as the review is
# uploaded. Once these are filled in and we progress beyond being
# requested/assigned, any changes to the assignment happens by
# closing down the current request and making a new one, copying
# the request-part fields above.
reviewer = ForeignKey(Email, blank=True, null=True)
# Moved to class ReviewAssignment:
# These exist only to facilitate data migrations. They will be removed in the next release.
unused_reviewer = ForeignKey(Email, blank=True, null=True)
review = OneToOneField(Document, blank=True, null=True)
reviewed_rev = models.CharField(verbose_name="reviewed revision", max_length=16, blank=True)
result = ForeignKey(ReviewResultName, blank=True, null=True)
unused_review = OneToOneField(Document, blank=True, null=True)
unused_reviewed_rev = models.CharField(verbose_name="reviewed revision", max_length=16, blank=True)
unused_result = ForeignKey(ReviewResultName, blank=True, null=True)
def __unicode__(self):
return u"%s review on %s by %s %s" % (self.type, self.doc, self.team, self.state)
def other_requests(self):
return self.doc.reviewrequest_set.exclude(id=self.id)
def all_completed_assignments_for_doc(self):
return ReviewAssignment.objects.filter(review_request__doc=self.doc, state__in=['completed','part-completed'])
def other_completed_requests(self):
return self.other_requests().filter(state_id__in=['completed','part-completed'])
def request_closed_time(self):
return self.doc.request_closed_time(self) or self.time
class ReviewAssignment(models.Model):
""" One of possibly many reviews assigned in response to a ReviewRequest """
review_request = ForeignKey(ReviewRequest)
state = ForeignKey(ReviewAssignmentStateName)
reviewer = ForeignKey(Email)
assigned_on = models.DateTimeField(blank=True, null=True)
completed_on = models.DateTimeField(blank=True, null=True)
review = OneToOneField(Document, blank=True, null=True)
reviewed_rev = models.CharField(verbose_name="reviewed revision", max_length=16, blank=True)
result = ForeignKey(ReviewResultName, blank=True, null=True)
mailarch_url = models.URLField(blank=True, null = True)
def __unicode__(self):
return u"Assignment for %s (%s) : %s %s of %s" % (self.reviewer.person, self.state, self.review_request.team.acronym, self.review_request.type, self.review_request.doc)
def review_done_time(self):
# First check if this is completed review having review and if so take time from there.
if self.review and self.review.time:
return self.review.time
# If not, then it is closed review, so it either has event in doc or if not then take
# time from the request.
time = self.doc.request_closed_time(self)
return time if time else self.time
def get_default_review_types():
return ReviewTypeName.objects.filter(slug__in=['early','lc','telechat'])

View file

@ -7,7 +7,7 @@ from tastypie.cache import SimpleCache
from ietf import api
from ietf.api import ToOneField # pyflakes:ignore
from ietf.review.models import (ReviewerSettings, ReviewRequest,
from ietf.review.models import (ReviewerSettings, ReviewRequest, ReviewAssignment,
UnavailablePeriod, ReviewWish, NextReviewerInTeam,
ReviewSecretarySettings, ReviewTeamSettings,
HistoricalReviewerSettings )
@ -45,9 +45,6 @@ class ReviewRequestResource(ModelResource):
doc = ToOneField(DocumentResource, 'doc')
team = ToOneField(GroupResource, 'team')
requested_by = ToOneField(PersonResource, 'requested_by')
reviewer = ToOneField(EmailResource, 'reviewer', null=True)
review = ToOneField(DocumentResource, 'review', null=True)
result = ToOneField(ReviewResultNameResource, 'result', null=True)
class Meta:
queryset = ReviewRequest.objects.all()
serializer = api.Serializer()
@ -59,17 +56,38 @@ class ReviewRequestResource(ModelResource):
"deadline": ALL,
"requested_rev": ALL,
"comment": ALL,
"reviewed_rev": ALL,
"state": ALL_WITH_RELATIONS,
"type": ALL_WITH_RELATIONS,
"doc": ALL_WITH_RELATIONS,
"team": ALL_WITH_RELATIONS,
"requested_by": ALL_WITH_RELATIONS,
}
api.review.register(ReviewRequestResource())
from ietf.name.resources import ReviewAssignmentStateNameResource
class ReviewAssignmentResource(ModelResource):
review_request = ToOneField(ReviewRequest, 'review_request')
state = ToOneField(ReviewAssignmentStateNameResource, 'state')
reviewer = ToOneField(EmailResource, 'reviewer', null=True)
review = ToOneField(DocumentResource, 'review', null=True)
result = ToOneField(ReviewResultNameResource, 'result', null=True)
class Meta:
queryset = ReviewAssignment.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'reviewassignment'
filtering = {
"id": ALL,
"reviewed_rev": ALL,
"mailarch_url": ALL,
"assigned_on": ALL,
"completed_on": ALL,
"state": ALL_WITH_RELATIONS,
"reviewer": ALL_WITH_RELATIONS,
"review": ALL_WITH_RELATIONS,
"result": ALL_WITH_RELATIONS,
}
api.review.register(ReviewRequestResource())
api.review.register(ReviewAssignmentResource())
from ietf.person.resources import PersonResource
from ietf.group.resources import GroupResource

View file

@ -13,13 +13,13 @@ from django.contrib.sites.models import Site
import debug # pyflakes:ignore
from ietf.group.models import Group, Role
from ietf.doc.models import (Document, ReviewRequestDocEvent, State,
from ietf.doc.models import (Document, ReviewRequestDocEvent, ReviewAssignmentDocEvent, State,
LastCallDocEvent, TelechatDocEvent,
DocumentAuthor, DocAlias)
from ietf.iesg.models import TelechatDate
from ietf.person.models import Person
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
from ietf.review.models import (ReviewRequest, ReviewRequestStateName, ReviewTypeName,
from ietf.review.models import (ReviewRequest, ReviewAssignment, ReviewRequestStateName, ReviewTypeName,
ReviewerSettings, UnavailablePeriod, ReviewWish, NextReviewerInTeam,
ReviewTeamSettings, ReviewSecretarySettings)
from ietf.utils.mail import send_mail, get_email_addresses_from_text
@ -29,7 +29,7 @@ def active_review_teams():
return Group.objects.filter(reviewteamsettings__isnull=False,state="active")
def close_review_request_states():
return ReviewRequestStateName.objects.filter(used=True).exclude(slug__in=["requested", "accepted", "rejected", "part-completed", "completed"])
return ReviewRequestStateName.objects.filter(used=True).exclude(slug__in=["requested", "assigned"])
def can_request_review_of_doc(user, doc):
if not user.is_authenticated:
@ -55,14 +55,14 @@ def can_access_review_stats_for_team(user, team):
return (Role.objects.filter(name__in=("secr", "reviewer"), person__user=user, group=team).exists()
or has_role(user, ["Secretariat", "Area Director"]))
def review_requests_to_list_for_docs(docs):
request_qs = ReviewRequest.objects.filter(
state__in=["requested", "accepted", "part-completed", "completed"],
def review_assignments_to_list_for_docs(docs):
assignment_qs = ReviewAssignment.objects.filter(
state__in=["assigned", "accepted", "part-completed", "completed"],
).prefetch_related("result")
doc_names = [d.name for d in docs]
return extract_revision_ordered_review_requests_for_documents_and_replaced(request_qs, doc_names)
return extract_revision_ordered_review_assignments_for_documents_and_replaced(assignment_qs, doc_names)
def augment_review_requests_with_events(review_reqs):
req_dict = { r.pk: r for r in review_reqs }
@ -72,7 +72,7 @@ def augment_review_requests_with_events(review_reqs):
def no_review_from_teams_on_doc(doc, rev):
return Group.objects.filter(
reviewrequest__doc__name=doc.name,
reviewrequest__reviewed_rev=rev,
reviewrequest__requested_rev=rev,
reviewrequest__state__slug="no-review-version",
).distinct()
@ -145,9 +145,9 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
"""Returns person_id -> days needed until min_interval is fulfilled
for reviewer (in case it is necessary to wait, otherwise reviewer
is absent in result)."""
latest_assignments = dict(ReviewRequest.objects.filter(
team=team,
).values_list("reviewer__person").annotate(Max("time")))
latest_assignments = dict(ReviewAssignment.objects.filter(
review_request__team=team,
).values_list("reviewer__person").annotate(Max("assigned_on")))
min_intervals = dict(ReviewerSettings.objects.filter(team=team).values_list("person_id", "min_interval"))
@ -166,40 +166,39 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
return res
ReviewRequestData = namedtuple("ReviewRequestData", [
"req_pk", "doc", "doc_pages", "req_time", "state", "assigned_time", "deadline", "reviewed_rev", "result", "team", "reviewer",
ReviewAssignmentData = namedtuple("ReviewAssignmentData", [
"assignment_pk", "doc", "doc_pages", "req_time", "state", "assigned_time", "deadline", "reviewed_rev", "result", "team", "reviewer",
"late_days",
"request_to_assignment_days", "assignment_to_closure_days", "request_to_closure_days"])
def extract_review_request_data(teams=None, reviewers=None, time_from=None, time_to=None, ordering=[]):
"""Yield data on each review request, sorted by (*ordering, time)
def extract_review_assignment_data(teams=None, reviewers=None, time_from=None, time_to=None, ordering=[]):
"""Yield data on each review assignment, sorted by (*ordering, assigned_on)
for easy use with itertools.groupby. Valid entries in *ordering are "team" and "reviewer"."""
filters = Q()
if teams:
filters &= Q(team__in=teams)
filters &= Q(review_request__team__in=teams)
if reviewers:
filters &= Q(reviewer__person__in=reviewers)
if time_from:
filters &= Q(time__gte=time_from)
filters &= Q(review_request__time__gte=time_from)
if time_to:
filters &= Q(time__lte=time_to)
filters &= Q(review_request__time__lte=time_to)
# we may be dealing with a big bunch of data, so treat it carefully
event_qs = ReviewRequest.objects.filter(filters)
# This doesn't do the left-outer join on docevent that the previous code did. These variables could be renamed
event_qs = ReviewAssignment.objects.filter(filters)
# left outer join with RequestRequestDocEvent for request/assign/close time
event_qs = event_qs.values_list(
"pk", "doc", "doc__pages", "time", "state", "deadline", "reviewed_rev", "result", "team",
"reviewer__person", "reviewrequestdocevent__time", "reviewrequestdocevent__type"
"pk", "review_request__doc", "review_request__doc__pages", "review_request__time", "state", "review_request__deadline", "reviewed_rev", "result", "review_request__team",
"reviewer__person", "assigned_on", "completed_on"
)
event_qs = event_qs.order_by(*[o.replace("reviewer", "reviewer__person") for o in ordering] + ["time", "pk", "-reviewrequestdocevent__time"])
event_qs = event_qs.order_by(*[o.replace("reviewer", "reviewer__person").replace("team","review_request__team") for o in ordering] + ["review_request__time", "assigned_on", "pk", "completed_on"])
def positive_days(time_from, time_to):
if time_from is None or time_to is None:
@ -212,33 +211,31 @@ def extract_review_request_data(teams=None, reviewers=None, time_from=None, time
else:
return 0.0
for _, events in itertools.groupby(event_qs.iterator(), lambda t: t[0]):
requested_time = assigned_time = closed_time = None
requested_time = assigned_time = closed_time = None
for e in events:
req_pk, doc, doc_pages, req_time, state, deadline, reviewed_rev, result, team, reviewer, event_time, event_type = e
for assignment in event_qs:
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
assignment_pk, doc, doc_pages, req_time, state, deadline, reviewed_rev, result, team, reviewer, assigned_on, completed_on = assignment
requested_time = req_time
assigned_time = assigned_on
closed_time = completed_on
late_days = positive_days(datetime.datetime.combine(deadline, datetime.time.max), closed_time)
request_to_assignment_days = positive_days(requested_time, assigned_time)
assignment_to_closure_days = positive_days(assigned_time, closed_time)
request_to_closure_days = positive_days(requested_time, closed_time)
d = ReviewRequestData(req_pk, doc, doc_pages, req_time, state, assigned_time, deadline, reviewed_rev, result, team, reviewer,
d = ReviewAssignmentData(assignment_pk, doc, doc_pages, req_time, state, assigned_time, deadline, reviewed_rev, result, team, reviewer,
late_days, request_to_assignment_days, assignment_to_closure_days,
request_to_closure_days)
yield d
def aggregate_raw_period_review_request_stats(review_request_data, count=None):
def aggregate_raw_period_review_assignment_stats(review_assignment_data, count=None):
"""Take a sequence of review request data from
extract_review_request_data and aggregate them."""
extract_review_assignment_data and aggregate them."""
state_dict = defaultdict(int)
late_state_dict = defaultdict(int)
@ -246,8 +243,8 @@ def aggregate_raw_period_review_request_stats(review_request_data, count=None):
assignment_to_closure_days_list = []
assignment_to_closure_days_count = 0
for (req_pk, doc, doc_pages, req_time, state, assigned_time, deadline, reviewed_rev, result, team, reviewer,
late_days, request_to_assignment_days, assignment_to_closure_days, request_to_closure_days) in review_request_data:
for (assignment_pk, doc, doc_pages, req_time, state, assigned_time, deadline, reviewed_rev, result, team, reviewer,
late_days, request_to_assignment_days, assignment_to_closure_days, request_to_closure_days) in review_assignment_data:
if count == "pages":
c = doc_pages
else:
@ -266,7 +263,7 @@ def aggregate_raw_period_review_request_stats(review_request_data, count=None):
return state_dict, late_state_dict, result_dict, assignment_to_closure_days_list, assignment_to_closure_days_count
def sum_period_review_request_stats(raw_aggregation):
def sum_period_review_assignment_stats(raw_aggregation):
"""Compute statistics from aggregated review request data for one aggregation point."""
state_dict, late_state_dict, result_dict, assignment_to_closure_days_list, assignment_to_closure_days_count = raw_aggregation
@ -274,11 +271,11 @@ def sum_period_review_request_stats(raw_aggregation):
res["state"] = state_dict
res["result"] = result_dict
res["open"] = sum(state_dict.get(s, 0) for s in ("requested", "accepted"))
res["open"] = sum(state_dict.get(s, 0) for s in ("assigned", "accepted"))
res["completed"] = sum(state_dict.get(s, 0) for s in ("completed", "part-completed"))
res["not_completed"] = sum(state_dict.get(s, 0) for s in state_dict if s in ("rejected", "withdrawn", "overtaken", "no-response"))
res["open_late"] = sum(late_state_dict.get(s, 0) for s in ("requested", "accepted"))
res["open_late"] = sum(late_state_dict.get(s, 0) for s in ("assigned", "accepted"))
res["open_in_time"] = res["open"] - res["open_late"]
res["completed_late"] = sum(late_state_dict.get(s, 0) for s in ("completed", "part-completed"))
res["completed_in_time"] = res["completed"] - res["completed_late"]
@ -287,7 +284,7 @@ def sum_period_review_request_stats(raw_aggregation):
return res
def sum_raw_review_request_aggregations(raw_aggregations):
def sum_raw_review_assignment_aggregations(raw_aggregations):
"""Collapse a sequence of aggregations into one aggregation."""
state_dict = defaultdict(int)
late_state_dict = defaultdict(int)
@ -309,34 +306,61 @@ def sum_raw_review_request_aggregations(raw_aggregations):
return state_dict, late_state_dict, result_dict, assignment_to_closure_days_list, assignment_to_closure_days_count
def latest_review_requests_for_reviewers(team, days_back=365):
"""Collect and return stats for reviewers on latest requests, in
extract_review_request_data format."""
def latest_review_assignments_for_reviewers(team, days_back=365):
"""Collect and return stats for reviewers on latest assignments, in
extract_review_assignment_data format."""
extracted_data = extract_review_request_data(
extracted_data = extract_review_assignment_data(
teams=[team],
time_from=datetime.date.today() - datetime.timedelta(days=days_back),
ordering=["reviewer"],
)
req_data_for_reviewers = {
assignment_data_for_reviewers = {
reviewer: list(reversed(list(req_data_items)))
for reviewer, req_data_items in itertools.groupby(extracted_data, key=lambda data: data.reviewer)
}
return req_data_for_reviewers
return assignment_data_for_reviewers
def make_new_review_request_from_existing(review_req):
obj = ReviewRequest()
obj.time = review_req.time
obj.type = review_req.type
obj.doc = review_req.doc
obj.team = review_req.team
obj.deadline = review_req.deadline
obj.requested_rev = review_req.requested_rev
obj.requested_by = review_req.requested_by
obj.state = ReviewRequestStateName.objects.get(slug="requested")
return obj
def email_review_assignment_change(request, review_assignment, subject, msg, by, notify_secretary, notify_reviewer, notify_requested_by):
system_email = Person.objects.get(name="(System)").formatted_email()
to = set()
def extract_email_addresses(objs):
for o in objs:
if o and o.person!=by:
e = o.formatted_email()
if e != system_email:
to.add(e)
if notify_secretary:
rts = ReviewTeamSettings.objects.filter(group=review_assignment.review_request.team).first()
if rts and rts.secr_mail_alias and rts.secr_mail_alias.strip() != '':
for addr in get_email_addresses_from_text(rts.secr_mail_alias):
to.add(addr)
else:
extract_email_addresses(Role.objects.filter(name="secr", group=review_assignment.review_request.team).distinct())
if notify_reviewer:
extract_email_addresses([review_assignment.reviewer])
if notify_requested_by:
extract_email_addresses([review_assignment.review_request.requested_by.email()])
if not to:
return
to = list(to)
url = urlreverse("ietf.doc.views_review.review_request_forced_login", kwargs={ "name": review_assignment.review_request.doc.name, "request_id": review_assignment.review_request.pk })
url = request.build_absolute_uri(url)
send_mail(request, to, request.user.person.formatted_email(), subject, "review/review_request_changed.txt", {
"review_req_url": url,
"review_req": review_assignment.review_request,
"msg": msg,
})
def email_review_request_change(request, review_req, subject, msg, by, notify_secretary, notify_reviewer, notify_requested_by):
@ -362,7 +386,8 @@ def email_review_request_change(request, review_req, subject, msg, by, notify_se
else:
extract_email_addresses(Role.objects.filter(name="secr", group=review_req.team).distinct())
if notify_reviewer:
extract_email_addresses([review_req.reviewer])
for assignment in review_req.reviewassignment_set.all():
extract_email_addresses([assignment.reviewer])
if notify_requested_by:
extract_email_addresses([review_req.requested_by.email()])
@ -423,24 +448,21 @@ def email_reviewer_availability_change(request, team, reviewer_role, msg, by):
})
def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=False):
assert review_req.state_id in ("requested", "accepted")
assert review_req.state_id in ("requested", "assigned")
if reviewer == review_req.reviewer:
if review_req.reviewassignment_set.filter(reviewer=reviewer).exists():
return
if review_req.reviewer:
email_review_request_change(
request, review_req,
"Unassigned from review of %s" % review_req.doc.name,
"%s has cancelled your assignment to the review." % request.user.person,
by=request.user.person, notify_secretary=False, notify_reviewer=True, notify_requested_by=False)
# Note that assigning a review no longer unassigns other reviews
review_req.state = ReviewRequestStateName.objects.get(slug="requested")
review_req.reviewer = reviewer
review_req.save()
if review_req.state_id != 'assigned':
review_req.state_id = 'assigned'
review_req.save()
assignment = review_req.reviewassignment_set.create(state_id='assigned', reviewer = reviewer, assigned_on = datetime.datetime.now())
if review_req.reviewer:
possibly_advance_next_reviewer_for_team(review_req.team, review_req.reviewer.person_id, add_skip)
if reviewer:
possibly_advance_next_reviewer_for_team(review_req.team, reviewer.person_id, add_skip)
ReviewRequestDocEvent.objects.create(
type="assigned_review_request",
@ -450,26 +472,40 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa
desc="Request for {} review by {} is assigned to {}".format(
review_req.type.name,
review_req.team.acronym.upper(),
review_req.reviewer.person if review_req.reviewer else "(None)",
reviewer.person if reviewer else "(None)",
),
review_request=review_req,
state=None,
state_id='assigned',
)
ReviewAssignmentDocEvent.objects.create(
type="assigned_review_request",
doc=review_req.doc,
rev=review_req.doc.rev,
by=request.user.person,
desc="Request for {} review by {} is assigned to {}".format(
review_req.type.name,
review_req.team.acronym.upper(),
reviewer.person if reviewer else "(None)",
),
review_assignment=assignment,
state_id='assigned',
)
msg = "%s has assigned you as a reviewer for this document." % request.user.person.ascii
prev_team_reviews = ReviewRequest.objects.filter(
doc=review_req.doc,
prev_team_reviews = ReviewAssignment.objects.filter(
review_request__doc=review_req.doc,
state="completed",
team=review_req.team,
review_request__team=review_req.team,
)
if prev_team_reviews.exists():
msg = msg + '\n\nThis team has completed other reviews of this document:\n'
for req in prev_team_reviews:
for assignment in prev_team_reviews:
msg += u'%s %s -%s %s\n'% (
req.review_done_time().strftime('%d %b %Y'),
req.reviewer.person.ascii,
req.reviewed_rev or req.requested_rev,
req.result.name,
assignment.completed_on.strftime('%d %b %Y'),
assignment.reviewer.person.ascii,
assignment.reviewed_rev or assignment.review_request.requested_rev,
assignment.result.name,
)
email_review_request_change(
@ -526,8 +562,9 @@ def close_review_request(request, review_req, close_state):
suggested_req = review_req.pk is None
review_req.state = close_state
if close_state.slug == "no-review-version":
review_req.reviewed_rev = review_req.requested_rev or review_req.doc.rev # save rev for later reference
# This field no longer exists, and it's not clear what the later reference was...
# if close_state.slug == "no-review-version":
# review_req.reviewed_rev = review_req.requested_rev or review_req.doc.rev # save rev for later reference
review_req.save()
if not suggested_req:
@ -542,6 +579,19 @@ def close_review_request(request, review_req, close_state):
state=review_req.state,
)
for assignment in review_req.reviewassignment_set.filter(state_id__in=['assigned','accepted']):
assignment.state_id = 'withdrawn'
assignment.save()
ReviewAssignmentDocEvent.objects.create(
type='closed_review_request',
doc=review_req.doc,
rev=review_req.doc.rev,
by=request.user.person,
desc="Request closed, assignment withdrawn: {} {} {} review".format(assignment.reviewer.person.plain_name, assignment.review_request.type.name, assignment.review_request.team.acronym.upper()),
review_assignment=assignment,
state=assignment.state,
)
email_review_request_change(
request, review_req,
"Closed review request for {}: {}".format(review_req.doc.name, close_state.name),
@ -636,7 +686,7 @@ def suggested_review_requests_for_team(team):
seen_deadlines[doc_pk] = deadline
# filter those with existing requests
# filter those with existing explicit requests
existing_requests = defaultdict(list)
for r in ReviewRequest.objects.filter(doc__in=requests.iterkeys(), team=team):
existing_requests[r.doc_id].append(r)
@ -646,18 +696,67 @@ def suggested_review_requests_for_team(team):
return False
no_review_document = existing.state_id == "no-review-document"
pending = (existing.state_id in ("requested", "accepted")
no_review_rev = ( existing.state_id == "no-review-version") and (not existing.requested_rev or existing.requested_rev == request.doc.rev)
pending = (existing.state_id == "assigned"
and existing.reviewassignment_set.filter(state_id__in=("assigned", "accepted")).exists()
and (not existing.requested_rev or existing.requested_rev == request.doc.rev))
completed_or_closed = (existing.state_id not in ("part-completed", "rejected", "overtaken", "no-response")
and existing.reviewed_rev == request.doc.rev)
request_closed = existing.state_id not in ('requested','assigned')
# at least one assignment was completed for the requested version or the current doc version if no specific version was requested:
some_assignment_completed = existing.reviewassignment_set.filter(reviewed_rev=existing.requested_rev or existing.doc.rev, state_id='completed').exists()
return no_review_document or pending or completed_or_closed
return any([no_review_document, no_review_rev, pending, request_closed, some_assignment_completed])
res = [r for r in requests.itervalues()
if not any(blocks(e, r) for e in existing_requests[r.doc_id])]
res.sort(key=lambda r: (r.deadline, r.doc_id), reverse=True)
return res
def extract_revision_ordered_review_assignments_for_documents_and_replaced(review_assignment_queryset, names):
"""Extracts all review assignments for document names (including replaced ancestors), return them neatly sorted."""
names = set(names)
replaces = extract_complete_replaces_ancestor_mapping_for_docs(names)
assignments_for_each_doc = defaultdict(list)
for r in review_assignment_queryset.filter(review_request__doc__in=set(e for l in replaces.itervalues() for e in l) | names).order_by("-reviewed_rev","-assigned_on", "-id").iterator():
assignments_for_each_doc[r.review_request.doc_id].append(r)
# now collect in breadth-first order to keep the revision order intact
res = defaultdict(list)
for name in names:
front = replaces.get(name, [])
res[name].extend(assignments_for_each_doc.get(name, []))
seen = set()
while front:
replaces_assignments = []
next_front = []
for replaces_name in front:
if replaces_name in seen:
continue
seen.add(replaces_name)
assignments = assignments_for_each_doc.get(replaces_name, [])
if assignments:
replaces_assignments.append(assignments)
next_front.extend(replaces.get(replaces_name, []))
# in case there are multiple replaces, move the ones with
# the latest reviews up front
replaces_assignments.sort(key=lambda l: l[0].assigned_on, reverse=True)
for assignments in replaces_assignments:
res[name].extend(assignments)
# move one level down
front = next_front
return res
def extract_revision_ordered_review_requests_for_documents_and_replaced(review_request_queryset, names):
"""Extracts all review requests for document names (including replaced ancestors), return them neatly sorted."""
@ -666,7 +765,7 @@ def extract_revision_ordered_review_requests_for_documents_and_replaced(review_r
replaces = extract_complete_replaces_ancestor_mapping_for_docs(names)
requests_for_each_doc = defaultdict(list)
for r in review_request_queryset.filter(doc__in=set(e for l in replaces.itervalues() for e in l) | names).order_by("-reviewed_rev", "-time", "-id").iterator():
for r in review_request_queryset.filter(doc__in=set(e for l in replaces.itervalues() for e in l) | names).order_by("-time", "-id").iterator():
requests_for_each_doc[r.doc_id].append(r)
# now collect in breadth-first order to keep the revision order intact
@ -704,10 +803,12 @@ def extract_revision_ordered_review_requests_for_documents_and_replaced(review_r
return res
# TODO : Change this field to deal with multiple already assigned reviewers
def setup_reviewer_field(field, review_req):
field.queryset = field.queryset.filter(role__name="reviewer", role__group=review_req.team)
if review_req.reviewer:
field.initial = review_req.reviewer_id
one_assignment = review_req.reviewassignment_set.first()
if one_assignment:
field.initial = one_assignment.reviewer_id
choices = make_assignment_choices(field.queryset, review_req)
if not field.required:
@ -752,15 +853,15 @@ def make_assignment_choices(email_queryset, review_req):
# previous review of document
has_reviewed_previous = ReviewRequest.objects.filter(
doc=doc,
reviewer__person__in=possible_person_ids,
state="completed",
reviewassignment__reviewer__person__in=possible_person_ids,
reviewassignment__state="completed",
team=team,
)
).distinct()
if review_req.pk is not None:
has_reviewed_previous = has_reviewed_previous.exclude(pk=review_req.pk)
has_reviewed_previous = set(has_reviewed_previous.values_list("reviewer__person", flat=True))
has_reviewed_previous = set(has_reviewed_previous.values_list("reviewassignment__reviewer__person", flat=True))
# review wishes
wish_to_review = set(ReviewWish.objects.filter(team=team, person__in=possible_person_ids, doc=doc).values_list("person", flat=True))
@ -780,7 +881,7 @@ def make_assignment_choices(email_queryset, review_req):
unavailable_periods = current_unavailable_periods_for_reviewers(team)
# reviewers statistics
req_data_for_reviewers = latest_review_requests_for_reviewers(team)
assignment_data_for_reviewers = latest_review_assignments_for_reviewers(team)
ranking = []
for e in possible_emails:
@ -835,21 +936,21 @@ def make_assignment_choices(email_queryset, review_req):
# stats
stats = []
req_data = req_data_for_reviewers.get(e.person_id, [])
assignment_data = assignment_data_for_reviewers.get(e.person_id, [])
currently_open = sum(1 for d in req_data if d.state in ["requested", "accepted"])
pages = sum(rd.doc_pages for rd in req_data if rd.state in ["requested", "accepted"])
currently_open = sum(1 for d in assignment_data if d.state in ["assigned", "accepted"])
pages = sum(rd.doc_pages for rd in assignment_data if rd.state in ["assigned", "accepted"])
if currently_open > 0:
stats.append("currently {count} open, {pages} pages".format(count=currently_open, pages=pages))
could_have_completed = [d for d in req_data if d.state in ["part-completed", "completed", "no-response"]]
could_have_completed = [d for d in assignment_data if d.state in ["part-completed", "completed", "no-response"]]
if could_have_completed:
no_response = len([d for d in req_data if d.state == 'no-response'])
no_response = len([d for d in assignment_data if d.state == 'no-response'])
if no_response:
stats.append("%s no response" % no_response)
part_completed = len([d for d in req_data if d.state == 'part-completed'])
part_completed = len([d for d in assignment_data if d.state == 'part-completed'])
if part_completed:
stats.append("%s partially complete" % part_completed)
completed = len([d for d in req_data if d.state == 'completed'])
completed = len([d for d in assignment_data if d.state == 'completed'])
if completed:
stats.append("%s fully completed" % completed)
@ -870,21 +971,19 @@ def make_assignment_choices(email_queryset, review_req):
return [(r["email"].pk, r["label"]) for r in ranking]
def review_requests_needing_reviewer_reminder(remind_date):
reqs_qs = ReviewRequest.objects.filter(
state__in=("requested", "accepted"),
def review_assignments_needing_reviewer_reminder(remind_date):
assignment_qs = ReviewAssignment.objects.filter(
state__in=("assigned", "accepted"),
reviewer__person__reviewersettings__remind_days_before_deadline__isnull=False,
reviewer__person__reviewersettings__team=F("team"),
).exclude(
reviewer=None
).values_list("pk", "deadline", "reviewer__person__reviewersettings__remind_days_before_deadline").distinct()
reviewer__person__reviewersettings__team=F("review_request__team"),
).values_list("pk", "review_request__deadline", "reviewer__person__reviewersettings__remind_days_before_deadline").distinct()
req_pks = []
for r_pk, deadline, remind_days in reqs_qs:
assignment_pks = []
for a_pk, deadline, remind_days in assignment_qs:
if (deadline - remind_date).days == remind_days:
req_pks.append(r_pk)
assignment_pks.append(a_pk)
return ReviewRequest.objects.filter(pk__in=req_pks).select_related("reviewer", "reviewer__person", "state", "team")
return ReviewAssignment.objects.filter(pk__in=assignment_pks).select_related("reviewer", "reviewer__person", "state", "review_request__team")
def email_reviewer_reminder(review_request):
team = review_request.team
@ -911,24 +1010,24 @@ def email_reviewer_reminder(review_request):
"remind_days": remind_days,
})
def review_requests_needing_secretary_reminder(remind_date):
reqs_qs = ReviewRequest.objects.filter(
state__in=("requested", "accepted"),
team__role__person__reviewsecretarysettings__remind_days_before_deadline__isnull=False,
team__role__person__reviewsecretarysettings__team=F("team"),
def review_assignments_needing_secretary_reminder(remind_date):
assignment_qs = ReviewAssignment.objects.filter(
state__in=("assigned", "accepted"),
review_request__team__role__person__reviewsecretarysettings__remind_days_before_deadline__isnull=False,
review_request__team__role__person__reviewsecretarysettings__team=F("review_request__team"),
).exclude(
reviewer=None
).values_list("pk", "deadline", "team__role", "team__role__person__reviewsecretarysettings__remind_days_before_deadline").distinct()
).values_list("pk", "review_request__deadline", "review_request__team__role", "review_request__team__role__person__reviewsecretarysettings__remind_days_before_deadline").distinct()
req_pks = {}
for r_pk, deadline, secretary_role_pk, remind_days in reqs_qs:
assignment_pks = {}
for a_pk, deadline, secretary_role_pk, remind_days in assignment_qs:
if (deadline - remind_date).days == remind_days:
req_pks[r_pk] = secretary_role_pk
assignment_pks[a_pk] = secretary_role_pk
review_reqs = { r.pk: r for r in ReviewRequest.objects.filter(pk__in=req_pks.keys()).select_related("reviewer", "reviewer__person", "state", "team") }
secretary_roles = { r.pk: r for r in Role.objects.filter(pk__in=req_pks.values()).select_related("email", "person") }
review_assignments = { a.pk: a for a in ReviewAssignment.objects.filter(pk__in=assignment_pks.keys()).select_related("reviewer", "reviewer__person", "state", "review_request__team") }
secretary_roles = { r.pk: r for r in Role.objects.filter(pk__in=assignment_pks.values()).select_related("email", "person") }
return [ (review_reqs[req_pk], secretary_roles[secretary_role_pk]) for req_pk, secretary_role_pk in req_pks.iteritems() ]
return [ (review_assignments[a_pk], secretary_roles[secretary_role_pk]) for a_pk, secretary_role_pk in assignment_pks.iteritems() ]
def email_secretary_reminder(review_request, secretary_role):
team = review_request.team

View file

@ -451,7 +451,11 @@ table tbody.meta th:nth-child(2), table tbody.meta td:nth-child(2) {
width: 9em;
}
table tbody.panel-meta th, table tbody.panel-meta td { border-top: 0; }
table tbody.panel-meta th:first-child, table tbody.panel-meta td:first-child {
text-align: right;
width: 9em;
}
td.area-director div { border-bottom: solid #ccc 1px; }

View file

@ -4,6 +4,8 @@ from mock import patch
from pyquery import PyQuery
from requests import Response
import debug # pyflakes:ignore
from django.urls import reverse as urlreverse
from django.contrib.auth.models import User
@ -18,7 +20,7 @@ from ietf.meeting.factories import MeetingFactory
from ietf.person.factories import PersonFactory
from ietf.person.models import Person, Email
from ietf.name.models import FormalLanguageName, DocRelationshipName, CountryName
from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory
from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory
from ietf.stats.models import MeetingRegistration, CountryAlias
from ietf.stats.utils import get_meeting_registration_data
@ -157,7 +159,8 @@ class StatisticsTests(TestCase):
def test_review_stats(self):
reviewer = PersonFactory()
review_req = ReviewRequestFactory(reviewer=reviewer.email_set.first())
review_req = ReviewRequestFactory(state_id='assigned')
ReviewAssignmentFactory(review_request=review_req, state_id='assigned', reviewer=reviewer.email_set.first())
RoleFactory(group=review_req.team,name_id='reviewer',person=reviewer)
ReviewerSettingsFactory(team=review_req.team, person=reviewer)
PersonFactory(user__username='plain')

View file

@ -19,15 +19,15 @@ from django.utils.text import slugify
import debug # pyflakes:ignore
from ietf.review.utils import (extract_review_request_data,
aggregate_raw_period_review_request_stats,
ReviewRequestData,
sum_period_review_request_stats,
sum_raw_review_request_aggregations)
from ietf.review.utils import (extract_review_assignment_data,
aggregate_raw_period_review_assignment_stats,
ReviewAssignmentData,
sum_period_review_assignment_stats,
sum_raw_review_assignment_aggregations)
from ietf.submit.models import Submission
from ietf.group.models import Role, Group
from ietf.person.models import Person
from ietf.name.models import ReviewRequestStateName, ReviewResultName, CountryName, DocRelationshipName
from ietf.name.models import ReviewResultName, CountryName, DocRelationshipName, ReviewAssignmentStateName
from ietf.person.name import plain_name
from ietf.doc.models import DocAlias, Document, State, DocEvent
from ietf.meeting.models import Meeting
@ -1020,7 +1020,7 @@ def review_stats(request, stats_type=None, acronym=None):
possible_stats_types = [
("completion", "Completion status"),
("results", "Review results"),
("states", "Request states"),
("states", "Assignment states"),
]
if level == "team":
@ -1094,7 +1094,7 @@ def review_stats(request, stats_type=None, acronym=None):
query_reviewers = None
group_by_objs = { t.pk: t for t in query_teams }
group_by_index = ReviewRequestData._fields.index("team")
group_by_index = ReviewAssignmentData._fields.index("team")
elif level == "reviewer":
for t in teams:
@ -1105,9 +1105,9 @@ def review_stats(request, stats_type=None, acronym=None):
return HttpResponseRedirect(urlreverse(review_stats))
query_reviewers = list(Person.objects.filter(
email__reviewrequest__time__gte=from_time,
email__reviewrequest__time__lte=to_time,
email__reviewrequest__team=reviewers_for_team,
email__reviewassignment__review_request__time__gte=from_time,
email__reviewassignment__review_request__time__lte=to_time,
email__reviewassignment__review_request__team=reviewers_for_team,
**reviewer_filter_args.get(t.pk, {})
).distinct())
query_reviewers.sort(key=lambda p: p.last_name())
@ -1115,7 +1115,7 @@ def review_stats(request, stats_type=None, acronym=None):
query_teams = [t]
group_by_objs = { r.pk: r for r in query_reviewers }
group_by_index = ReviewRequestData._fields.index("reviewer")
group_by_index = ReviewAssignmentData._fields.index("reviewer")
# now filter and aggregate the data
possible_teams = possible_completion_types = possible_results = possible_states = None
@ -1139,22 +1139,23 @@ def review_stats(request, stats_type=None, acronym=None):
)
query_teams = [t for t in query_teams if t.acronym in selected_teams]
extracted_data = extract_review_request_data(query_teams, query_reviewers, from_time, to_time)
extracted_data = extract_review_assignment_data(query_teams, query_reviewers, from_time, to_time)
req_time_index = ReviewRequestData._fields.index("req_time")
req_time_index = ReviewAssignmentData._fields.index("req_time")
def time_key_fn(t):
d = t[req_time_index].date()
#d -= datetime.timedelta(days=d.weekday()) # weekly
d -= datetime.timedelta(days=d.day) # monthly
# NOTE: Earlier releases had an off-by-one error here - some stat counts may move a month.
d -= datetime.timedelta(days=d.day-1) # monthly
return d
found_results = set()
found_states = set()
aggrs = []
for d, request_data_items in itertools.groupby(extracted_data, key=time_key_fn):
raw_aggr = aggregate_raw_period_review_request_stats(request_data_items, count=count)
aggr = sum_period_review_request_stats(raw_aggr)
raw_aggr = aggregate_raw_period_review_assignment_stats(request_data_items, count=count)
aggr = sum_period_review_assignment_stats(raw_aggr)
aggrs.append((d, aggr))
@ -1164,7 +1165,7 @@ def review_stats(request, stats_type=None, acronym=None):
found_states.add(slug)
results = ReviewResultName.objects.filter(slug__in=found_results)
states = ReviewRequestStateName.objects.filter(slug__in=found_states)
states = ReviewAssignmentStateName.objects.filter(slug__in=found_states)
# choice
@ -1213,7 +1214,7 @@ def review_stats(request, stats_type=None, acronym=None):
}])
else: # tabular data
extracted_data = extract_review_request_data(query_teams, query_reviewers, from_time, to_time, ordering=[level])
extracted_data = extract_review_assignment_data(query_teams, query_reviewers, from_time, to_time, ordering=[level])
data = []
@ -1221,10 +1222,10 @@ def review_stats(request, stats_type=None, acronym=None):
found_states = set()
raw_aggrs = []
for group_pk, request_data_items in itertools.groupby(extracted_data, key=lambda t: t[group_by_index]):
raw_aggr = aggregate_raw_period_review_request_stats(request_data_items, count=count)
raw_aggr = aggregate_raw_period_review_assignment_stats(request_data_items, count=count)
raw_aggrs.append(raw_aggr)
aggr = sum_period_review_request_stats(raw_aggr)
aggr = sum_period_review_assignment_stats(raw_aggr)
# skip zero-valued rows
if aggr["open"] == 0 and aggr["completed"] == 0 and aggr["not_completed"] == 0:
@ -1241,12 +1242,12 @@ def review_stats(request, stats_type=None, acronym=None):
# add totals row
if len(raw_aggrs) > 1:
totals = sum_period_review_request_stats(sum_raw_review_request_aggregations(raw_aggrs))
totals = sum_period_review_assignment_stats(sum_raw_review_assignment_aggregations(raw_aggrs))
totals["obj"] = "Totals"
data.append(totals)
results = ReviewResultName.objects.filter(slug__in=found_results)
states = ReviewRequestStateName.objects.filter(slug__in=found_states)
states = ReviewAssignmentStateName.objects.filter(slug__in=found_states)
# massage states/results breakdowns for template rendering
for aggr in data:

View file

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

View file

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

View file

@ -11,14 +11,14 @@
{% block content %}
{% origin %}
<h1>{% if revising_review %}Revise{% else %}Complete{% endif %} review<br>
<small>{{ review_req.doc.name }}</small>
<small>{{ assignment.review_request.doc.name }}</small>
</h1>
<p>
<div><strong>Review type:</strong> {{ review_req.team.acronym }} - {{ review_req.type }} review </div>
<div><strong>Requested version for review:</strong> {{ review_req.requested_rev|default:"Current" }} </div>
<div><strong>Requsted:</strong> {{ review_req.time|date:"Y-m-d" }} </div>
<div><strong>Reviewer:</strong> {{ review_req.reviewer.person.name }}</div>
<div><strong>Review type:</strong> {{ assignment.review_request.team.acronym }} - {{ assignment.review_request.type }} review </div>
<div><strong>Requested version for review:</strong> {{ assignment.review_request.requested_rev|default:"Current" }} </div>
<div><strong>Requested:</strong> {{ assignment.review_request.time|date:"Y-m-d" }} </div>
<div><strong>Reviewer:</strong> {{ assignment.reviewer.person.name }}</div>
</p>
{% if not revising_review %}
@ -37,7 +37,7 @@
{% bootstrap_form form layout="horizontal" %}
{% buttons %}
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=assignment.review_request.pk %}">Cancel</a>
<button type="submit" class="btn btn-primary">{% if revising_review %}Revise{% else %}Complete{% endif %} review</button>
{% endbuttons %}
@ -95,7 +95,7 @@
{% block js %}
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
<script>
var searchMailArchiveUrl = "{% url "ietf.doc.views_review.search_mail_archive" name=review_req.doc.name request_id=review_req.pk %}";
var searchMailArchiveUrl = "{% url "ietf.doc.views_review.search_mail_archive" name=assignment.review_request.doc.name assignment_id=assignment.pk %}";
</script>
<script src="{% static 'ietf/js/complete-review.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2019, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block title %}Mark review assignment for {{ assignment.review_request.doc.name }} as No Response{% endblock %}
{% block content %}
{% origin %}
<h1>No-Response: Review assignment<br><small>{{ assignment.review_request.doc.name }}</small></h1>
<p>Mark review assignment for {{ assignment.reviewer.person }} as <strong>No Response</strong></p>
<form method="post">
{% csrf_token %}
{% buttons %}
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=assignment.review_request.doc.canonical_name request_id=assignment.review_request.pk %}">Cancel</a>
<button type="submit" class="btn btn-primary" name="action" value="noresponse">Mark assignment as no-response</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -10,7 +10,7 @@
{% include "doc/review/request_info.html" %}
<p>{{ review_req.reviewer.person }} is currently assigned to do the review. Do you want to reject this assignment?</p>
<p>Do you want to reject this assignment?</p>
<form method="post">
{% csrf_token %}

View file

@ -1,4 +1,4 @@
{# Copyright The IETF Trust 2017, All Rights Reserved #}{% load origin %}{% origin %}
{# Copyright The IETF Trust 2017, All Rights Reserved #}{% load origin bootstrap3 %}{% origin %}
<table class="table table-condensed">
<tbody class="meta">
<tr>
@ -68,14 +68,20 @@
</tr>
{% endif %}
{% if doc.time %}
<tr>
<th></th>
<th>Other Reviews</th>
<th>Draft last updated</th>
<td>{{ doc.time|date:"Y-m-d" }}</td>
</tr>
{% endif %}
<tr>
<th></th>
<th>Completed reviews</th>
<td>
{% for req in review_req.other_completed_requests %}
{% if req.reviewer == review_req.reviewer %}<strong>{% endif %}
<a href="{% url "ietf.doc.views_review.review_request" name=req.doc request_id=req.id %}">{{req.team.acronym|capfirst}} {{req.type.name}} review of -{{req.reviewed_rev}} by {{req.reviewer.person.plain_name}}</a> {% if req.reviewed_rev != req.doc.rev %} (<a href="{{ rfcdiff_base_url }}?url1={{ req.doc.name }}-{{ req.reviewed_rev }}&amp;url2={{ req.doc.name }}-{{ req.doc.rev }}">diff</a>){% endif %}<br>
{% if req.reviewer == review_req.reviewer %}</strong>{% endif %}
{% for a in review_req.all_completed_assignments_for_doc %}
<a href="{% url "ietf.doc.views_doc.document_main" name=a.review.name %}">{{a.review_request.team.acronym|capfirst}} {{a.review_request.type.name}} review of -{{a.reviewed_rev}} by {{a.reviewer.person.plain_name}}</a> {% if a.reviewed_rev != a.review_request.doc.rev %} (<a href="{{ rfcdiff_base_url }}?url1={{ a.review_request.doc.name }}-{{ a.reviewed_rev }}&amp;url2={{ a.review_request.doc.name }}-{{ a.review_request.doc.rev }}">diff</a>){% endif %}<br>
{% endfor %}
</td>
</tr>
@ -92,115 +98,107 @@
</tr>
{% endif %}
</tbody>
<tbody class="meta">
<tr>
<th>Review</th>
<th>State</th>
<td>{{ review_req.state.name }}
{% if snapshot %}
<span class="label label-warning">Snapshot</span>
{% endif %}
</td>
</tr>
<tr>
<th></th>
<th>Reviewer</th>
<td>
{% if review_req.reviewer %}
{{ review_req.reviewer.person }}
{% else %}
None assigned yet
{% endif %}
{% if can_assign_reviewer %}
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.assign_reviewer" name=doc.name request_id=review_req.pk %}"><span class="fa fa-user"></span> {% if review_req.reviewer %}Reassign{% else %}Assign{% endif %} reviewer</a>
{% endif %}
{% if review_req.reviewer %}
{% if can_reject_reviewer_assignment or can_accept_reviewer_assignment %}
<div class="reviewer-assignment-not-accepted">
{% if review_req.state_id == "requested"%}
<em>Assignment not accepted yet:</em>
{% else %}
<em>Assignment accepted:</em>
{% endif %}
{% if can_reject_reviewer_assignment %}
<a class="btn btn-danger btn-xs" href="{% url "ietf.doc.views_review.reject_reviewer_assignment" name=doc.name request_id=review_req.pk %}"><span class="fa fa-ban"></span> Reject</a>
{% endif %}
{% if can_accept_reviewer_assignment %}
<form style="display:inline" method="post" action="{% url "ietf.doc.views_review.review_request" name=doc.name request_id=review_req.pk %}">{% csrf_token %}<button class="btn btn-success btn-xs" type="submit" name="action" value="accept"><span class="fa fa-check"></span> Accept</button></form>
{% endif %}
</div>
{% endif %}
{% endif %}
</td>
</tr>
<tr>
<th></th>
<th>Review</th>
<td>
{% if review_req.review %}
<a href="{{ review_req.review.get_absolute_url }}">{{ review_req.review.name }}</a>
{% elif review_req.state_id == "requested" or review_req.state_id == "accepted" %}
Not completed yet
{% else %}
Not available
{% endif %}
{% if can_complete_review %}
<a class="btn btn-primary btn-xs" href="{% url "ietf.doc.views_review.complete_review" name=doc.name request_id=review_req.pk %}"><span class="fa fa-pencil-square-o"></span> {% if review_req.state_id == "requested" or review_req.state_id == "accepted" %}Complete review{% else %}Correct review{% endif %}</a>
{% endif %}
</td>
</tr>
{% if review_req.review and review_req.review.external_url %}
<tr>
<th></th>
<th>Posted at</th>
<td>
<a href="{{ review_req.review.external_url }}">{{ review_req.review.external_url }}</a>
</td>
</tr>
{% endif %}
{% if review_req.reviewed_rev %}
<tr>
<th></th>
<th>Reviewed rev.</th>
<td><a href="{% url "ietf.doc.views_doc.document_main" name=review_req.doc.name rev=review_req.reviewed_rev %}">{{ review_req.reviewed_rev }}</a> {% if review_req.reviewed_rev != review_req.doc.rev %}(document currently at {{ review_req.doc.rev }}){% endif %}</td>
</tr>
{% endif %}
{% if review_req.result %}
<tr>
<th></th>
<th>Review result</th>
<td>{{ review_req.result.name }}</td>
</tr>
{% endif %}
{% if doc.time %}
<tr>
<th></th>
<th>Draft last updated</th>
<td>{{ doc.time|date:"Y-m-d" }}</td>
</tr>
{% endif %}
{% if review_req.state_id == "completed" or review_req.state_id == "part-completed" %}
<tr>
<th></th>
<th>Review completed:</th>
<td>
{{ review_req.review_done_time|date:"Y-m-d" }}
</td>
</tr>
{% endif %}
</tbody>
</table>
<p><strong>Assignments</strong></p>
{% if can_assign_reviewer %}
<p>
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.assign_reviewer" name=doc.name request_id=review_req.pk %}"><span class="fa fa-user"></span> Assign reviewer</a>
</p>
{% endif %}
{% for assignment in assignments %}
<div class="panel panel-default">
<div class="panel-heading">
{{ assignment.reviewer.person }}
{% if assignment.can_reject_reviewer_assignment or assignment.can_accept_reviewer_assignment %}
<div class="reviewer-assignment-not-accepted">
{% if assignment.state_id == "assigned"%}
<em>Assignment not accepted yet:</em>
{% else %}
<em>Assignment accepted:</em>
{% endif %}
{% if assignment.can_reject_reviewer_assignment %}
<a class="btn btn-danger btn-xs" href="{% url "ietf.doc.views_review.reject_reviewer_assignment" name=doc.name assignment_id=assignment.pk %}"><span class="fa fa-ban"></span> Reject</a>
{% endif %}
{% if assignment.can_accept_reviewer_assignment %}
<form style="display:inline" method="post" action="{% url "ietf.doc.views_review.review_request" name=doc.name request_id=review_req.pk %}">{% csrf_token %}<button class="btn btn-success btn-xs" type="submit" name="action" value="accept"><span class="fa fa-check"></span> Accept</button></form>
{% endif %}
</div>
{% endif %}
</div>
<div class="panel-body">
<table class="table table-condensed">
<tbody class="panel-meta">
<tr>
<th>State</th>
<td>{{ assignment.state.name }}
{% if snapshot %}
<span class="label label-warning">Snapshot</span>
{% endif %}
</td>
</tr>
{% if assignment.state_id != "withdrawn" and assignment.state_id != "no-response" and assignment.state_id != "rejected" %}
<tr>
<th>Review</th>
<td>
{% if assignment.review %}
<a href="{{ assignment.review.get_absolute_url }}">{{ assignment.review.name }}</a>
{% elif assignment.state_id == "assigned" or assignment.state_id == "accepted" %}
Not completed yet
{% else %}
Not available
{% endif %}
{% if assignment.can_complete_review %}
<a class="btn btn-primary btn-xs" href="{% url "ietf.doc.views_review.complete_review" name=doc.name assignment_id=assignment.pk %}"><span class="fa fa-pencil-square-o"></span> {% if assignment.state_id == "assigned" or assignment.state_id == "accepted" %}Complete review{% else %}Correct review{% endif %}</a>
{% endif %}
{% if assignment.state_id == "assigned" or assignment.state_id == "accepted" %}
{% if can_assign_reviewer %}
<a class="btn btn-danger btn-xs" href="{% url "ietf.doc.views_review.mark_reviewer_assignment_no_response" name=doc.name assignment_id=assignment.pk %}"><span class="fa fa-ban"></span> No response</a>
<a class="btn btn-danger btn-xs" href="{% url "ietf.doc.views_review.withdraw_reviewer_assignment" name=doc.name assignment_id=assignment.pk %}"><span class="fa fa-ban"></span> Withdraw</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endif %}
{% if assignment.review and assignment.review.external_url %}
<tr>
<th>Posted at</th>
<td>
<a href="{{ assignment.review.external_url }}">{{ assignment.review.external_url }}</a>
</td>
</tr>
{% endif %}
{% if assignment.reviewed_rev %}
<tr>
<th>Reviewed rev.</th>
<td><a href="{% url "ietf.doc.views_doc.document_main" name=review_req.doc.name rev=assignment.reviewed_rev %}">{{ assignment.reviewed_rev }}</a> {% if assignment.reviewed_rev != review_req.doc.rev %}(document currently at {{ review_req.doc.rev }}){% endif %}</td>
</tr>
{% endif %}
{% if assignment.result %}
<tr>
<th>Review result</th>
<td>{{ assignment.result.name }}</td>
</tr>
{% endif %}
{% if assignment.state_id == "completed" or assignment.state_id == "part-completed" %}
<tr>
<th>Review completed:</th>
<td>
{{ assignment.completed_on|date:"Y-m-d" }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{% endfor %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2019, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block title %}Withdraw review assignment for {{ assignment.review_request.doc.name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Withdraw review assignment<br><small>{{ assignment.review_request.doc.name }}</small></h1>
<p>Withdraw review assignment for {{ assignment.reviewer.person }}</p>
<form method="post">
{% csrf_token %}
{% buttons %}
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=assignment.review_request.doc.canonical_name request_id=assignment.review_request.pk %}">Cancel</a>
<button type="submit" class="btn btn-primary" name="action" value="withdraw">Withdraw assignment</button>
{% endbuttons %}
</form>
{% endblock %}

View file

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

View file

@ -1,12 +0,0 @@
<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 "ietf.doc.views_doc.document_main" 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 %}{% if review_request.result %}:
{{ review_request.result.name }}{% endif %} {% if review_request.state_id == "part-completed" %}(partially completed){% endif %}
</a>
{% else %}
<i>
<a href="{% url "ietf.doc.views_review.review_request" review_request.doc_id review_request.pk %}">{{ review_request.team.acronym|upper }} {{ review_request.type.name }} Review
- due: {{ review_request.deadline|date:"Y-m-d" }}</a></i>
{% endif %}
</div>

View file

@ -11,7 +11,7 @@
<h1>Email summary of assigned review requests for {{ group.acronym }}</h1>
{% if review_requests %}
{% if review_assignments %}
<form class="email-open-review-assignments" method="post">{% csrf_token %}
{% bootstrap_form form %}

View file

@ -17,61 +17,103 @@
<h1 class="pull-right"><a href="{% url "ietf.stats.views.review_stats" %}" class="icon-link">&nbsp;<span class="small fa fa-bar-chart">&nbsp;</span></a></h1>
{% endif %}
{% for label, review_requests in open_review_requests %}
{% if review_requests %}
{% if unassigned_review_requests %}
<h2>{{ label }} open review requests</h2>
<h2 id="unassigned-review-requests">Unassigned review requests</h2>
<table class="table table-condensed table-striped tablesorter">
<thead>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th class="col-md-4">Request</th>
<th class="col-md-1">Type</th>
<th class="col-md-2">Requested</th>
<th class="col-md-1">Deadline</th>
{% if review_requests.0.reviewer %}
<th class="col-md-2">Reviewer</th>
<th class="col-md-1">Document state</th>
{% else %}
<th clas="col-md-3">Document state</th>
{% endif %}
<th class="col-md-1">IESG Telechat</th>
</tr>
</thead>
<tbody>
{% for r in unassigned_review_requests %}
<tr>
<th class="col-md-4">Request</th>
<th class="col-md-1">Type</th>
<th class="col-md-2">Requested</th>
<th class="col-md-1">Deadline</th>
{% if review_requests.0.reviewer %}
<th class="col-md-2">Reviewer</th>
<th class="col-md-1">Document state</th>
{% else %}
<th clas="col-md-3">Document state</th>
<td>{% if r.pk != None %}<a href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}">{% endif %}{{ r.doc.name }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}{% if r.pk != None %}</a>{% endif %}</td>
<td>{{ r.type.name }}</td>
<td><span style="display:none">X</span>{% if r.pk %}{{ r.time|date:"Y-m-d" }} by {{r.requested_by.plain_name}}{% else %}<em>auto-suggested</em>{% endif %}</td>
<td><span style="display:none">X</span>
{{ r.deadline|date:"Y-m-d" }}
{% 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>
{% if r.reviewer %}
<td>
{{ r.reviewer.person }}
{% if r.state_id == "accepted" %}<span class="label label-default">Accepted</span>{% endif %}
{% if r.reviewer_unavailable %}<span class="label label-danger">Unavailable</span>{% endif %}
</td>
{% endif %}
<th class="col-md-1">IESG Telechat</th>
</tr>
</thead>
<tbody>
{% for r in review_requests %}
<tr>
<td>{% if r.pk != None %}<a href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}">{% endif %}{{ r.doc.name }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}{% if r.pk != None %}</a>{% endif %}</td>
<td>{{ r.type.name }}</td>
<td><span style="display:none">X</span>{% if r.pk %}{{ r.time|date:"Y-m-d" }} by {{r.requested_by.plain_name}}{% else %}<em>auto-suggested</em>{% endif %}</td>
<td><span style="display:none">X</span>
{{ r.deadline|date:"Y-m-d" }}
{% 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>
{% if r.reviewer %}
<td>
{{ r.reviewer.person }}
{% if r.state_id == "accepted" %}<span class="label label-default">Accepted</span>{% endif %}
{% if r.reviewer_unavailable %}<span class="label label-danger">Unavailable</span>{% endif %}
</td>
<td>
{{ r.doc.friendly_state }}
</td>
<td>
{% if r.doc.telechat_date %}
{{ r.doc.telechat_date }}
{% endif %}
<td>
{{ r.doc.friendly_state }}
</td>
<td>
{% if r.doc.telechat_date %}
{{ r.doc.telechat_date }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
{% endif %}
<h2 id="closed-review-requests">Closed review requests</h2>
{% if open_review_assignments %}
<h2 id="open_review_assignments">Open review assignments</h2>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th class="col-md-4">Request</th>
<th class="col-md-1">Type</th>
<th class="col-md-2">Assigned</th>
<th class="col-md-1">Deadline</th>
<th class="col-md-2">Reviewer</th>
<th class="col-md-1">Document state</th>
<th class="col-md-1">IESG Telechat</th>
</tr>
</thead>
<tbody>
{% for a in open_review_assignments %}
<tr>
<td><a href="{% url "ietf.doc.views_review.review_request" name=a.review_request.doc.name request_id=a.review_request.pk %}">{{ a.review_request.doc.name }}-{% if a.review_request.requested_rev %}{{ a.review_requests.requested_rev }}{% else %}{{ a.review_request.doc.rev }}{% endif %}</a></td>
<td>{{ a.review_request.type.name }}</td>
<td><span style="display:none">X</span>{{ a.assigned_on|date:"Y-m-d" }}</td>
<td><span style="display:none">X</span>
{{ a.review_request.deadline|date:"Y-m-d" }}
{% if a.due %}<span class="label label-warning" title="{{ a.due }} day{{ a.due|pluralize }} past deadline">{{ a.due }} day{{ a.due|pluralize }}</span>{% endif %}
</td>
<td>
{{ a.reviewer.person }}
{% if a.state_id == "accepted" %}<span class="label label-default">Accepted</span>{% endif %}
{% if a.reviewer_unavailable %}<span class="label label-danger">Unavailable</span>{% endif %}
</td>
<td>
{{ a.review_request.doc.friendly_state }}
</td>
<td>
{% if a.review_request.doc.telechat_date %}
{{ a.review_request.doc.telechat_date }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2 id="closed-review-requests">Closed review requests and assignments</h2>
<form class="closed-review-filter" action="#closed-review-requests">
Past:
@ -83,7 +125,8 @@
</form>
{% if closed_review_requests %}
<table class="table table-condensed table-striped materials tablesorter">
<h3>Closed review requests</h3>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Request</th>
@ -91,9 +134,7 @@
<th>Requested</th>
<th>Deadline</th>
<th>Closed</th>
<th>Reviewer</th>
<th>State</th>
<th>Result</th>
</tr>
</thead>
<tbody>
@ -103,27 +144,49 @@
<td>{{ r.type }}</td>
<td><span style="display:none">X</span>{{ r.time|date:"Y-m-d" }} by {{ r.requested_by.plain_name }}</td>
<td>{{ r.deadline|date:"Y-m-d" }}</td>
<td>{{ r.review_done_time|date:"Y-m-d" }}</td>
<td>
{% if r.reviewer %}
{{ r.reviewer.person }}
{% else %}
<em>not yet assigned</em>
{% endif %}
</td>
<td>{{ r.request_closed_time|date:"Y-m-d" }}</td>
<td>{{ r.state.name }}</td>
<td>
{% if r.result %}
{{ r.result.name }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No closed requests found.</p>
{% endif %}
{% if closed_review_assignments %}
<h3>Closed review assignments</h3>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Request</th>
<th>Type</th>
<th>Assigned</th>
<th>Deadline</th>
<th>Closed</th>
<th>Reviewer</th>
<th>State</th>
<th>Result</th>
</tr>
</thead>
<tbody>
{% for a in closed_review_assignments %}
<tr>
<td><a href="{% url "ietf.doc.views_review.review_request" name=a.review_request.doc.name request_id=a.review_request.pk %}">{{ a.review_request.doc.name }}{% if a.review_request.requested_rev %}-{{ a.review_request.requested_rev }}{% endif %}</a></td>
<td>{{ a.review_request.type }}</td>
<td>{{ a.assigned_on|date:"Y-m-d" }}</td>
<td>{{ a.review_request.deadline|date:"Y-m-d" }}</td>
<td>{{ a.completed_on|date:"Y-m-d" }}</td>
<td>{{ a.reviewer.person }}</td>
<td>{{ a.state }}</td>
<td>{% if a.result %}{{ a.result }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if not closed_review_requests and not closed_review_assignments %}
<div>None found</div>
{% endif %}
{% endblock %}

View file

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

View file

@ -5,7 +5,7 @@
{% endif %} Token: {{ doc.ad }}{% if doc.iana_review_state %}
IANA Review: {{ doc.iana_review_state }}{% endif %}{% if doc.consensus %}
Consensus: {{ doc.consensus }}{% endif %}{% if doc.lastcall_expires %}
Last call expires: {{ doc.lastcall_expires|date:"Y-m-d" }}{% endif %}{% if doc.review_requests %}
Reviews: {% for review_request in doc.review_requests %}{% with current_doc_name=doc.name current_rev=doc.rev %}{% if not forloop.first %} {% endif %}{{ review_request.team.acronym|upper }} {{ review_request.type.name }} Review{% if review_request.state_id == "completed" or review_request.state_id == "part-completed" %}{% 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 %}{% else %} - due: {{ review_request.deadline|date:"Y-m-d" }}{% endif %}{% endwith %}
Last call expires: {{ doc.lastcall_expires|date:"Y-m-d" }}{% endif %}{% if doc.review_assignments %}
Reviews: {% for assignment in doc.review_assignments %}{% with current_doc_name=doc.name current_rev=doc.rev %}{% if not forloop.first %} {% endif %}{{ assignment.review_request.team.acronym|upper }} {{ assignment.review_request.type.name }} Review{% if assignment.state_id == "completed" or assignment.state_id == "part-completed" %}{% if assignment.reviewed_rev and assignment.reviewed_rev != current_rev or assignment.review_request.doc_id != current_doc_name %} (of {% if assignment.review_request.doc_id != current_doc_name %}{{ assignment.review_request.doc_id }}{% endif %}-{{ assignment.reviewed_rev }}){% endif %}{% if assignment.result %}: {{ assignment.result.name }}{% endif %} {% if assignment.state_id == "part-completed" %}(partially completed){% endif %}{% else %} - due: {{ assignment.review_request.deadline|date:"Y-m-d" }}{% endif %}{% endwith %}
{% endfor %}{% endif %}
{% with doc.active_defer_event as defer %}{% if defer %} Was deferred by {{defer.by}} on {{defer.time|date:"Y-m-d"}}{% endif %}{% endwith %}

View file

@ -17,7 +17,7 @@
<h2>Assigned reviews</h2>
{% if open_review_requests %}
{% if open_review_assignments %}
<table class="table table-condensed table-striped">
<thead>
<tr>
@ -30,15 +30,15 @@
</tr>
</thead>
<tbody>
{% for r in open_review_requests %}
{% for r in open_review_assignments %}
<tr>
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}</a></td>
<td>{% if r.requested_rev %}{{ r.requested_rev }}{% else %}Current{% endif %}</td>
<td>{{r.doc.rev}}</td>
<td><a href="{% url 'ietf.group.views.review_requests' acronym=r.team.acronym %}">{{ r.team.acronym }}</a></td>
<td>{{ r.type.name }}</td>
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.review_request.doc.name request_id=r.review_request.pk %}"{% endif %}>{{ r.review_request.doc.name }}</a></td>
<td>{% if r.review_request.requested_rev %}{{ r.review_request.requested_rev }}{% else %}Current{% endif %}</td>
<td>{{r.review_request.doc.rev}}</td>
<td><a href="{% url 'ietf.group.views.review_requests' acronym=r.review_request.team.acronym %}">{{ r.review_request.team.acronym }}</a></td>
<td>{{ r.review_request.type.name }}</td>
<td>
{{ r.deadline|date:"Y-m-d" }}
{{ r.review_request.deadline|date:"Y-m-d" }}
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
</td>
</tr>
@ -50,9 +50,9 @@
{% endif %}
<h2>Latest closed review requests</h2>
<h2>Latest closed review assignments</h2>
{% if closed_review_requests %}
{% if closed_review_assignments %}
<table class="table table-condensed table-striped">
<thead>
<tr>
@ -66,14 +66,14 @@
</tr>
</thead>
<tbody>
{% for r in closed_review_requests %}
{% for r in closed_review_assignments %}
<tr>
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}</a></td>
<td>{{r.reviewed_rev|default:"See review"}}{% if r.requested_rev %}{% if r.requested_rev != r.reviewed_rev %}({{ r.requested_rev }} requested){% endif %}{% endif %}</td>
<td><a href="{% url 'ietf.group.views.review_requests' acronym=r.team.acronym %}">{{ r.team.acronym }}</a></td>
<td>{{ r.type.name }}</td>
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.review_request.doc.name request_id=r.review_request.pk %}"{% endif %}>{{ r.review_request.doc.name }}</a></td>
<td>{{r.reviewed_rev|default:"See review"}}{% if r.review_request.requested_rev %}{% if r.review_request.requested_rev != r.reviewed_rev %}({{ r.review_request.requested_rev }} requested){% endif %}{% endif %}</td>
<td><a href="{% url 'ietf.group.views.review_requests' acronym=r.review_request.team.acronym %}">{{ r.review_request.team.acronym }}</a></td>
<td>{{ r.review_request.type.name }}</td>
<td>
{{ r.deadline|date:"Y-m-d" }}
{{ r.review_request.deadline|date:"Y-m-d" }}
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
</td>
<td><span class="{% if r.state_id == "completed" or r.state_id == "part-completed" %}bg-success{% endif %}">{{ r.state.name }}</span></td>

View file

@ -1,9 +1,8 @@
{% load ietf_filters %}{% autoescape off %}{% filter maybewordwrap:80 %}{% if review_req.state_id == "part-completed" %}
Review is partially done. Another review request has been registered for
completing it.
{% load ietf_filters %}{% autoescape off %}{% filter maybewordwrap:80 %}{% if assignment.state_id == "part-completed" %}
Review is partially done. Another assignment may be needed to complete it.
{% endif %}Reviewer: {{ review_req.reviewer.person }}
Review result: {{ review_req.result.name }}
{% endif %}Reviewer: {{ assignment.reviewer.person }}
Review result: {{ assignment.result.name }}
{{ content }}
{% endfilter %}{% endautoescape %}

View file

@ -1,13 +1,13 @@
{% load ietf_filters %}{% autoescape off %}From: {{settings.DEFAULT_FROM_EMAIL}}
To: {{to}}{% if cc %}
Cc: {{cc}}{% endif %}
Subject: "{{review_req.result}}" review submitted for {{review_req.doc}}{% if review_req.reviewed_rev %}-{{review_req.reviewed_rev}}{% endif %}
Subject: "{{assignment.result}}" review submitted for {{assignment.review_request.doc}}{% if assignment.reviewed_rev %}-{{assignment.reviewed_rev}}{% endif %}
{{review_req.reviewer.person.name}} has submitted a "{{review_req.result}}" review result for {{review_req.doc}}{% if review_req.reviewed_rev %}-{{review_req.reviewed_rev}}{% endif %}.
{{assignment.reviewer.person}} has submitted a "{{assignment.result}}" review result for {{assignment.review_request.doc}}{% if assignment.reviewed_rev %}-{{assignment.reviewed_rev}}{% endif %}.
The review is available at {{settings.IDTRACKER_BASE_URL}}{% url 'ietf.doc.views_doc.document_main' name=review_req.review.name %}
The review is available at {{settings.IDTRACKER_BASE_URL}}{% url 'ietf.doc.views_doc.document_main' name=assignment.review.name %}
The document is available at {{settings.IDTRACKER_BASE_URL}}{% url 'ietf.doc.views_doc.document_main' name=review_req.doc.name %}
The document is available at {{settings.IDTRACKER_BASE_URL}}{% url 'ietf.doc.views_doc.document_main' name=assignment.review_request.doc.name %}
This message was sent because {% if explicit_request %}the reviewer indicated it should be on the review completion form{% else %}the review team settings indicated it should be{% endif %}.

View file

@ -1,10 +1,7 @@
{% autoescape off %}Review was partially completed by {{ by }}.
{% if new_review_req_url %}
A new review request has been added for completing the review:
{{ new_review_req_url }}
{% else %}
Found {{ existing_open_reqs|length }} open review request{{ existing_open_reqs|pluralize }} on the document so a new
review request has not been added.
A new reviewer may need to be assigned to get a complete review.
{% if existing_assignments %}
The following are already assigned to review this document:{% for assignment in existing_assignments %}
{{ assignment.reviewer.name }} : {{ assignment.state }}{% endfor %}
{% endif %}{% endautoescape %}

View file

@ -12,7 +12,28 @@
<p>
This page will explain the purpose and content of each screen in the I-D Submission Tool, and the actions that result by clicking the form buttons on each screen.
The specification for this tool can be found in <a href="https://www.ietf.org/rfc/rfc4228.txt?number=4228">RFC 4228</a>.
</p>
<p>
Internet-Drafts are working documents of the Internet Engineering
Task Force (IETF), its areas, and its working groups. Note that other
groups may also distribute working documents as Internet-Drafts.
</p>
<p>
Internet-Drafts are draft documents, and are valid for a maximum of
six months. They may be updated, replaced, or obsoleted by other
documents at any time.
</p>
<p>
The list of current Internet-Drafts can be accessed at
<a href="https://www.ietf.org/ietf/1id-abstracts.txt">https://www.ietf.org/ietf/1id-abstracts.txt</a>.
</p>
<p>
An API for automated draft submission is available as an alternative to this webpage at
<a href="https://datatracker.ietf.org/api/submit/">https://datatracker.ietf.org/api/submit/</a>.
</p>
<h3>Upload screen</h3>
@ -20,6 +41,17 @@
The Upload screen is the first screen that a user will see when he or she starts the I-D submission process. A user can submit four different formats of an I-D, plain text, XML, PDF, and postscript, at the same time. Failure to submit at least one of a plain-text or xml version will cause an error, and an error screen will be displayed.
</p>
<p>
By submitting your I-D, you are granting some rights to the IETF Trust. Before you submit your I-D,
review the information on the NOTE WELL tab and <a href="https://tools.ietf.org/html/bcp78">BCP 78</a>,
"Rights Contributors Provide to the IETF Trust".
</p>
<p>
Before you submit your I-D, it is recommended that you check it for nits
using the <b><a href="https://tools.ietf.org/tools/idnits/">idnits tool</a></b>.
</p>
<p>
<b>Form buttons and resulting actions</b>:
</p>
@ -133,4 +165,9 @@
<p>
Please send problem reports to <a href="mailto:ietf-action@ietf.org">ietf-action@ietf.org</a>.
</p>
<p>
The specification for this tool can be found in <a href="https://www.ietf.org/rfc/rfc4228.txt?number=4228">RFC 4228</a>.
</p>
{% endblock %}

View file

@ -12,27 +12,14 @@
<p class="bg-warning">WARNING: currently in draft submission blackout period</p>
{% endif %}
<p>This page is used to submit Internet-Drafts to the Internet-Draft repository.</p>
{% if form.cutoff_warning %}
<div class="cutoff-warning">
{{ form.cutoff_warning|safe }}
</div>
{% endif %}
<p>This page is used to submit Internet-Drafts to the
Internet-Draft repository. The list of current Internet-Drafts can be
accessed at <a href="https://www.ietf.org/ietf/1id-abstracts.txt">https://www.ietf.org/ietf/1id-abstracts.txt</a>.
An API for automated draft submission is available at
<a href="https://datatracker.ietf.org/api/submit/">https://datatracker.ietf.org/api/submit/</a>.
</p>
<p>Internet-Drafts are working documents of the Internet Engineering
Task Force (IETF), its areas, and its working groups. Note that other
groups may also distribute working documents as Internet-Drafts.</p>
<p>Internet-Drafts are draft documents, and are valid for a maximum of
six months. They may be updated, replaced, or obsoleted by other
documents at any time.</p>
{% if not form.shutdown or user|has_role:"Secretariat" %}
<p>If you run into problems when submitting an Internet-Draft
using this and the following pages, you may alternatively submit
@ -42,7 +29,13 @@
</p>
<p>
Before you submit your draft, it is recommended that you check it for nits
By submitting your I-D, you are granting some rights to the IETF Trust. Before you submit your I-D,
review the information on the NOTE WELL tab and <a href="https://tools.ietf.org/html/bcp78">BCP 78</a>,
"Rights Contributors Provide to the IETF Trust".
</p>
<p>
Before you submit your I-D, it is recommended that you check it for nits
using the <b><a href="https://tools.ietf.org/tools/idnits/">idnits tool</a></b>.
</p>