diff --git a/ietf/bin/send-review-reminders b/ietf/bin/send-review-reminders
index 36eb5a235..eb21325fa 100755
--- a/ietf/bin/send-review-reminders
+++ b/ietf/bin/send-review-reminders
@@ -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))
diff --git a/ietf/dbtemplate/migrations/0003_adjust_review_templates.py b/ietf/dbtemplate/migrations/0003_adjust_review_templates.py
new file mode 100644
index 000000000..5a5ffc48b
--- /dev/null
+++ b/ietf/dbtemplate/migrations/0003_adjust_review_templates.py
@@ -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
+
+.
+
+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
+
+.
+
+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
+
+.
+
+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
+
+.
+
+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)
+ ]
diff --git a/ietf/dbtemplate/migrations/0004_adjust_assignment_email_summary_templates.py b/ietf/dbtemplate/migrations/0004_adjust_assignment_email_summary_templates.py
new file mode 100644
index 000000000..0c4463a74
--- /dev/null
+++ b/ietf/dbtemplate/migrations/0004_adjust_assignment_email_summary_templates.py
@@ -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
+
+.
+
+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
+
+.
+
+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:
+
+
+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
+
+.
+
+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
+
+.
+
+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:
+
+
+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),
+ ]
diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py
index 55285b49d..d61ccb191 100644
--- a/ietf/doc/admin.py
+++ b/ietf/doc/admin.py
@@ -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)
diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py
index 10d866f7b..f5f669181 100644
--- a/ietf/doc/factories.py
+++ b/ietf/doc/factories.py
@@ -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
diff --git a/ietf/doc/migrations/0011_reviewassignmentdocevent.py b/ietf/doc/migrations/0011_reviewassignmentdocevent.py
new file mode 100644
index 000000000..63df9a962
--- /dev/null
+++ b/ietf/doc/migrations/0011_reviewassignmentdocevent.py
@@ -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',),
+ ),
+ ]
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 47d0fa81f..484122922 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -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)
diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py
index 62f928ced..64fd71a15 100644
--- a/ietf/doc/resources.py
+++ b/ietf/doc/resources.py
@@ -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())
diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py
index 24b229243..34de0d551 100644
--- a/ietf/doc/tests_review.py
+++ b/ietf/doc/tests_review.py
@@ -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
No results found
')
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')
+
diff --git a/ietf/doc/urls_review.py b/ietf/doc/urls_review.py
index d3cba6ffa..23b217c50 100644
--- a/ietf/doc/urls_review.py
+++ b/ietf/doc/urls_review.py
@@ -7,9 +7,11 @@ urlpatterns = [
url(r'^(?P[0-9]+)/login/$', views_review.review_request_forced_login),
url(r'^(?P[0-9]+)/close/$', views_review.close_request),
url(r'^(?P[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
- url(r'^(?P[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
- url(r'^(?P[0-9]+)/complete/$', views_review.complete_review),
- url(r'^(?P[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
+ url(r'^(?P[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
+ url(r'^(?P[0-9]+)/complete/$', views_review.complete_review),
+ url(r'^(?P[0-9]+)/withdraw/$', views_review.withdraw_reviewer_assignment),
+ url(r'^(?P[0-9]+)/noresponse/$', views_review.mark_reviewer_assignment_no_response),
+ url(r'^(?P[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
url(r'^(?P[0-9]+)/editcomment/$', views_review.edit_comment),
url(r'^(?P[0-9]+)/editdeadline/$', views_review.edit_deadline),
]
diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py
index 6b61d73db..31b365e27 100644
--- a/ietf/doc/utils_search.py
+++ b/ietf/doc/utils_search.py
@@ -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
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index e4166edd5..e9597a506 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -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,
))
diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py
index 9103a65c1..fb5a55039 100644
--- a/ietf/doc/views_review.py
+++ b/ietf/doc/views_review.py
@@ -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("{1}".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,
diff --git a/ietf/group/forms.py b/ietf/group/forms.py
index 1cbc56ee8..635312263 100644
--- a/ietf/group/forms.py
+++ b/ietf/group/forms.py
@@ -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"
diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py
index 92bed8aab..021172a98 100644
--- a/ietf/group/tests_review.py
+++ b/ietf/group/tests_review.py
@@ -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
diff --git a/ietf/group/urls.py b/ietf/group/urls.py
index 6f02b8b27..3a9a70235 100644
--- a/ietf/group/urls.py
+++ b/ietf/group/urls.py
@@ -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/(?Passigned|unassigned)/$', views.manage_review_requests),
+ url(r'^reviews/manage/(?Punassigned)/$', views.manage_review_requests),
url(r'^reviews/email-assignments/$', views.email_open_review_assignments),
url(r'^reviewers/$', views.reviewer_overview),
url(r'^reviewers/(?P[\w%+-.@]+)/settings/$', views.change_reviewer_settings),
diff --git a/ietf/group/utils.py b/ietf/group/utils.py
index 8ae517dd2..2fb088b51 100644
--- a/ietf/group/utils.py
+++ b/ietf/group/utils.py
@@ -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)))
diff --git a/ietf/group/views.py b/ietf/group/views.py
index c6a405439..a60ff5d90 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -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)
diff --git a/ietf/iesg/agenda.py b/ietf/iesg/agenda.py
index a221c1d59..46a22adf3 100644
--- a/ietf/iesg/agenda.py
+++ b/ietf/iesg/agenda.py
@@ -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":
diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py
index fbc5831ee..e1ff2ed50 100644
--- a/ietf/ietfauth/tests.py
+++ b/ietf/ietfauth/tests.py
@@ -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,
diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py
index dd1dbda97..4fe2e1349 100644
--- a/ietf/ietfauth/views.py
+++ b/ietf/ietfauth/views.py
@@ -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,
diff --git a/ietf/name/admin.py b/ietf/name/admin.py
index 9eb0f5cc5..3924b7910 100644
--- a/ietf/name/admin.py
+++ b/ietf/name/admin.py
@@ -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)
diff --git a/ietf/name/migrations/0005_reviewassignmentstatename.py b/ietf/name/migrations/0005_reviewassignmentstatename.py
new file mode 100644
index 000000000..dbf253e51
--- /dev/null
+++ b/ietf/name/migrations/0005_reviewassignmentstatename.py
@@ -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,
+ },
+ ),
+ ]
diff --git a/ietf/name/migrations/0006_adjust_statenames.py b/ietf/name/migrations/0006_adjust_statenames.py
new file mode 100644
index 000000000..18c464a8c
--- /dev/null
+++ b/ietf/name/migrations/0006_adjust_statenames.py
@@ -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)
+ ]
diff --git a/ietf/name/models.py b/ietf/name/models.py
index ac96ab9bc..5a257c22f 100644
--- a/ietf/name/models.py
+++ b/ietf/name/models.py
@@ -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):
diff --git a/ietf/name/resources.py b/ietf/name/resources.py
index 76dca2a15..83f8d0b6f 100644
--- a/ietf/name/resources.py
+++ b/ietf/name/resources.py
@@ -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())
+
+
diff --git a/ietf/review/admin.py b/ietf/review/admin.py
index 691cee738..4fbe41059 100644
--- a/ietf/review/admin.py
+++ b/ietf/review/admin.py
@@ -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", ]
diff --git a/ietf/review/factories.py b/ietf/review/factories.py
index 80353b4be..7e7a2480f 100644
--- a/ietf/review/factories.py
+++ b/ietf/review/factories.py
@@ -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
diff --git a/ietf/review/migrations/0009_refactor_review_request.py b/ietf/review/migrations/0009_refactor_review_request.py
new file mode 100644
index 000000000..766dc209e
--- /dev/null
+++ b/ietf/review/migrations/0009_refactor_review_request.py
@@ -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'),
+ ),
+ ]
diff --git a/ietf/review/migrations/0010_populate_review_assignments.py b/ietf/review/migrations/0010_populate_review_assignments.py
new file mode 100644
index 000000000..f18b63d61
--- /dev/null
+++ b/ietf/review/migrations/0010_populate_review_assignments.py
@@ -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)
+ ]
diff --git a/ietf/review/models.py b/ietf/review/models.py
index d4a89619d..dab758d50 100644
--- a/ietf/review/models.py
+++ b/ietf/review/models.py
@@ -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'])
diff --git a/ietf/review/resources.py b/ietf/review/resources.py
index 059b801c5..abbf10202 100644
--- a/ietf/review/resources.py
+++ b/ietf/review/resources.py
@@ -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
diff --git a/ietf/review/utils.py b/ietf/review/utils.py
index 32994b329..aa9fc888d 100644
--- a/ietf/review/utils.py
+++ b/ietf/review/utils.py
@@ -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
diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index ad2a0c9aa..19485abcf 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -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; }
diff --git a/ietf/stats/tests.py b/ietf/stats/tests.py
index ee7571f4b..13656a723 100644
--- a/ietf/stats/tests.py
+++ b/ietf/stats/tests.py
@@ -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')
diff --git a/ietf/stats/views.py b/ietf/stats/views.py
index f2848b36f..4eeac6506 100644
--- a/ietf/stats/views.py
+++ b/ietf/stats/views.py
@@ -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:
diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html
index d8c4ad95c..884b2db37 100644
--- a/ietf/templates/doc/document_draft.html
+++ b/ietf/templates/doc/document_draft.html
@@ -204,14 +204,14 @@
{% endif %}
{% endfor %}
- {% if review_requests or can_request_review %}
+ {% if review_assignments or can_request_review %}
Reviews
- {% 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 %}
diff --git a/ietf/templates/doc/document_review.html b/ietf/templates/doc/document_review.html
index 263ee5cfd..efa13552f 100644
--- a/ietf/templates/doc/document_review.html
+++ b/ietf/templates/doc/document_review.html
@@ -43,8 +43,8 @@
Other reviews
- {% 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 %}
{% endif %}
- {% if doc.review_requests %}
+ {% if doc.review_assignments %}
Reviews
- {% 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 %}
{% endif %}
diff --git a/ietf/templates/iesg/agenda_doc.txt b/ietf/templates/iesg/agenda_doc.txt
index 49ed89043..c5700d795 100644
--- a/ietf/templates/iesg/agenda_doc.txt
+++ b/ietf/templates/iesg/agenda_doc.txt
@@ -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 %}
diff --git a/ietf/templates/ietfauth/review_overview.html b/ietf/templates/ietfauth/review_overview.html
index 1451f5458..e79259a5b 100644
--- a/ietf/templates/ietfauth/review_overview.html
+++ b/ietf/templates/ietfauth/review_overview.html
@@ -17,7 +17,7 @@
Assigned reviews
- {% if open_review_requests %}
+ {% if open_review_assignments %}
@@ -30,15 +30,15 @@
- {% for r in open_review_requests %}
+ {% for r in open_review_assignments %}
diff --git a/ietf/templates/review/completed_review.txt b/ietf/templates/review/completed_review.txt
index ca4eb28fd..bdbe321ca 100644
--- a/ietf/templates/review/completed_review.txt
+++ b/ietf/templates/review/completed_review.txt
@@ -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 %}
diff --git a/ietf/templates/review/notify_ad.txt b/ietf/templates/review/notify_ad.txt
index 9e5196a9a..7dc585b2d 100644
--- a/ietf/templates/review/notify_ad.txt
+++ b/ietf/templates/review/notify_ad.txt
@@ -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 %}.
diff --git a/ietf/templates/review/partially_completed_review.txt b/ietf/templates/review/partially_completed_review.txt
index 6d518357c..8316ee7fd 100644
--- a/ietf/templates/review/partially_completed_review.txt
+++ b/ietf/templates/review/partially_completed_review.txt
@@ -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 %}
diff --git a/ietf/templates/submit/tool_instructions.html b/ietf/templates/submit/tool_instructions.html
index df4c1a977..0b31b9331 100644
--- a/ietf/templates/submit/tool_instructions.html
+++ b/ietf/templates/submit/tool_instructions.html
@@ -12,7 +12,28 @@
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 RFC 4228.
+
+
+
+ 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.
+
+
+
+ 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.
+
@@ -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.
+
+ 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 BCP 78,
+ "Rights Contributors Provide to the IETF Trust".
+
+
+
+ Before you submit your I-D, it is recommended that you check it for nits
+ using the idnits tool.
+
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.
-
-
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.
-
{% if not form.shutdown or user|has_role:"Secretariat" %}
If you run into problems when submitting an Internet-Draft
using this and the following pages, you may alternatively submit
@@ -42,7 +29,13 @@
- 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 BCP 78,
+ "Rights Contributors Provide to the IETF Trust".
+
+
+
+ Before you submit your I-D, it is recommended that you check it for nits
using the idnits tool.