Add review tracking models, add a request review page (with test), show

review requests on doc page
 - Legacy-Id: 11206
This commit is contained in:
Ole Laursen 2016-05-19 15:35:30 +00:00
parent 54c4c5efc5
commit 64a65340a2
21 changed files with 753 additions and 8 deletions

View file

@ -703,7 +703,10 @@ EVENT_TYPES = [
# RFC Editor
("rfc_editor_received_announcement", "Announcement was received by RFC Editor"),
("requested_publication", "Publication at RFC Editor requested")
("requested_publication", "Publication at RFC Editor requested"),
# review
("requested_review", "Requested review"),
]
class DocEvent(models.Model):

82
ietf/doc/tests_review.py Normal file
View file

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
import datetime
from pyquery import PyQuery
from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.review.models import ReviewRequest
from ietf.person.models import Person
from ietf.group.models import Group, Role
from ietf.name.models import ReviewResultName
from ietf.utils.test_utils import TestCase
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
def make_review_data():
review_team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
review_team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
p = Person.objects.get(user__username="plain")
Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=review_team)
return review_team
class ReviewTests(TestCase):
def test_request_review(self):
doc = make_test_data()
review_team = make_review_data()
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
login_testing_unauthorized(self, "secretary", url)
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
deadline_date = datetime.date.today() + datetime.timedelta(days=10)
# post request
r = self.client.post(url, {
"type": "early",
"team": review_team.pk,
"deadline_date": deadline_date.isoformat(),
"requested_rev": "01"
})
self.assertEqual(r.status_code, 302)
req = ReviewRequest.objects.get(doc=doc)
self.assertEqual(req.deadline.date(), deadline_date)
self.assertEqual(req.deadline.time(), datetime.time(23, 59, 59))
self.assertEqual(req.state_id, "requested")
self.assertEqual(req.team, review_team)
self.assertEqual(req.requested_rev, "01")
self.assertEqual(doc.latest_event().type, "requested_review")
def test_request_review_by_reviewer(self):
doc = make_test_data()
review_team = make_review_data()
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
login_testing_unauthorized(self, "plain", url)
# post request
deadline_date = datetime.date.today() + datetime.timedelta(days=10)
r = self.client.post(url, {
"type": "early",
"team": review_team.pk,
"deadline_date": deadline_date.isoformat(),
"requested_rev": "01"
})
self.assertEqual(r.status_code, 302)
req = ReviewRequest.objects.get(doc=doc)
self.assertEqual(req.state_id, "requested")
self.assertEqual(req.team, review_team)
def test_doc_page(self):
pass

View file

@ -36,6 +36,7 @@ from django.views.generic import RedirectView
from ietf.doc import views_search, views_draft, views_ballot
from ietf.doc import views_status_change
from ietf.doc import views_doc
from ietf.doc import views_review
session_patterns = [
url(r'^add$', views_doc.add_sessionpresentation),
@ -73,6 +74,8 @@ urlpatterns = patterns('',
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/$', views_doc.document_ballot, name="doc_ballot"),
(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?doc.json$', views_doc.document_json),
(r'^(?P<name>[A-Za-z0-9._+-]+)/ballotpopup/(?P<ballot_id>[0-9]+)/$', views_doc.ballot_popup),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/requestreview/$', views_review.request_review),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/review/(?P<request_id>[0-9]+)/$', views_review.review),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'),

View file

@ -16,7 +16,7 @@ from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, NewRevi
from ietf.doc.models import save_document_in_history
from ietf.name.models import DocReminderTypeName, DocRelationshipName
from ietf.group.models import Role
from ietf.ietfauth.utils import has_role
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
from ietf.utils import draft, markup_txt
from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists
@ -89,6 +89,15 @@ def can_adopt_draft(user, doc):
group__state="active",
person__user=user).exists())
def can_request_review_of_doc(user, doc):
if not user.is_authenticated():
return False
from ietf.review.utils import active_review_teams
if Role.objects.filter(name="reviewer", person__user=user, group__in=active_review_teams()):
return True
return is_authorized_in_doc_stream(user, doc)
def two_thirds_rule( recused=0 ):
# For standards-track, need positions from 2/3 of the non-recused current IESG.

View file

@ -48,7 +48,8 @@ from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDo
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus)
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus,
can_request_review_of_doc )
from ietf.community.utils import augment_docs_with_tracking_info
from ietf.group.models import Role
from ietf.group.utils import can_manage_group, can_manage_materials
@ -61,6 +62,7 @@ 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
def render_document_top(request, doc, tab, name):
tabs = []
@ -279,8 +281,8 @@ def document_main(request, name, rev=None):
can_edit_stream_info = is_authorized_in_doc_stream(request.user, doc)
can_edit_shepherd_writeup = can_edit_stream_info or user_is_person(request.user, doc.shepherd and doc.shepherd.person) or has_role(request.user, ["Area Director"])
can_edit_notify = can_edit_shepherd_writeup
can_edit_consensus = False
can_edit_consensus = False
consensus = nice_consensus(default_consensus(doc))
if doc.stream_id == "ietf" and iesg_state:
show_in_states = set(IESG_BALLOT_ACTIVE_STATES)
@ -294,6 +296,8 @@ def document_main(request, name, rev=None):
e = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
consensus = nice_consensus(e and e.consensus)
can_request_review = can_request_review_of_doc(request.user, doc)
# mailing list search archive
search_archive = "www.ietf.org/mail-archive/web/"
if doc.stream_id == "ietf" and group.type_id == "wg" and group.list_archive:
@ -353,6 +357,8 @@ 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 = ReviewRequest.objects.filter(doc=doc)
return render_to_response("doc/document_draft.html",
dict(doc=doc,
group=group,
@ -374,6 +380,7 @@ def document_main(request, name, rev=None):
can_edit_consensus=can_edit_consensus,
can_edit_replaces=can_edit_replaces,
can_view_possibly_replaces=can_view_possibly_replaces,
can_request_review=can_request_review,
rfc_number=rfc_number,
draft_name=draft_name,
@ -412,6 +419,7 @@ def document_main(request, name, rev=None):
search_archive=search_archive,
actions=actions,
presentations=presentations,
review_requests=review_requests,
),
context_instance=RequestContext(request))

108
ietf/doc/views_review.py Normal file
View file

@ -0,0 +1,108 @@
import datetime
from django.http import HttpResponseForbidden
from django.shortcuts import render, get_object_or_404, redirect
from django.core.urlresolvers import reverse as urlreverse
from django import forms
from django.contrib.auth.decorators import login_required
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent
from ietf.doc.utils import can_request_review_of_doc
from ietf.ietfauth.utils import is_authorized_in_doc_stream
from ietf.review.models import ReviewRequest, ReviewRequestStateName
from ietf.review.utils import active_review_teams
from ietf.utils.fields import DatepickerDateField
class RequestReviewForm(forms.ModelForm):
deadline_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" })
deadline_time = forms.TimeField(widget=forms.TextInput(attrs={ 'placeholder': "HH:MM" }), help_text="If time is not specified, end of day is assumed", required=False)
class Meta:
model = ReviewRequest
fields = ('type', 'team', 'deadline', 'requested_rev')
def __init__(self, user, doc, *args, **kwargs):
super(RequestReviewForm, self).__init__(*args, **kwargs)
self.doc = doc
self.fields['type'].widget = forms.RadioSelect(choices=[t for t in self.fields['type'].choices if t[0]])
f = self.fields["team"]
f.queryset = active_review_teams()
if not is_authorized_in_doc_stream(user, doc): # user is a reviewer
f.queryset = f.queryset.filter(role__name="reviewer", role__person__user=user)
if len(f.queryset) < 6:
f.widget = forms.RadioSelect(choices=[t for t in f.choices if t[0]])
self.fields["deadline"].required = False
self.fields["requested_rev"].label = "Document revision"
def clean_deadline_date(self):
v = self.cleaned_data.get('deadline_date')
if v < datetime.date.today():
raise forms.ValidationError("Select a future date.")
return v
def clean_requested_rev(self):
rev = self.cleaned_data.get("requested_rev")
if rev:
rev = rev.rjust(2, "0")
if not NewRevisionDocEvent.objects.filter(doc=self.doc, rev=rev).exists():
raise forms.ValidationError("Could not find revision '{}' of the document.".format(rev))
return rev
def clean(self):
deadline_date = self.cleaned_data.get('deadline_date')
deadline_time = self.cleaned_data.get('deadline_time', None)
if deadline_date:
if deadline_time is None:
deadline_time = datetime.time(23, 59, 59)
self.cleaned_data["deadline"] = datetime.datetime.combine(deadline_date, deadline_time)
return self.cleaned_data
@login_required
def request_review(request, name):
doc = get_object_or_404(Document, name=name)
if not can_request_review_of_doc(request.user, doc):
return HttpResponseForbidden("You do not have permission to perform this action")
if request.method == "POST":
form = RequestReviewForm(request.user, doc, request.POST)
if form.is_valid():
review_req = form.save(commit=False)
review_req.doc = doc
review_req.state = ReviewRequestStateName.objects.get(slug="requested", used=True)
review_req.save()
DocEvent.objects.create(
type="requested_review",
doc=doc,
by=request.user.person,
desc="{} review by {} requested".format(review_req.type.name, review_req.team.acronym.upper()),
)
# FIXME: if I'm a reviewer, auto-assign to myself?
return redirect('doc_view', name=doc.name)
else:
form = RequestReviewForm(request.user, doc)
return render(request, 'doc/review/request_review.html', {
'doc': doc,
'form': form,
})
def review(request, name, request_id):
doc = get_object_or_404(Document, name=name)
review_request = get_object_or_404(ReviewRequest, pk=request_id)
print doc, review_request

View file

@ -3,7 +3,8 @@ from ietf.name.models import (GroupTypeName, GroupStateName, RoleName, StreamNam
DocRelationshipName, DocTypeName, DocTagName, StdLevelName, IntendedStdLevelName,
DocReminderTypeName, BallotPositionName, SessionStatusName, TimeSlotTypeName,
ConstraintName, NomineePositionStateName, FeedbackTypeName, DBTemplateTypeName,
DraftSubmissionStateName, RoomResourceName)
DraftSubmissionStateName, RoomResourceName,
ReviewRequestStateName, ReviewTypeName, ReviewResultName)
class NameAdmin(admin.ModelAdmin):
@ -35,3 +36,6 @@ admin.site.register(FeedbackTypeName, NameAdmin)
admin.site.register(DBTemplateTypeName, NameAdmin)
admin.site.register(DraftSubmissionStateName, NameAdmin)
admin.site.register(RoomResourceName, NameAdmin)
admin.site.register(ReviewRequestStateName, NameAdmin)
admin.site.register(ReviewTypeName, NameAdmin)
admin.site.register(ReviewResultName, NameAdmin)

View file

@ -188,7 +188,7 @@
"order": 0,
"revname": "Conflict reviewed by",
"used": true,
"name": "Conflict reviews",
"name": "conflict reviews",
"desc": ""
},
"model": "name.docrelationshipname",
@ -1752,6 +1752,205 @@
"model": "name.nomineepositionstatename",
"pk": "declined"
},
{
"fields": {
"order": 1,
"used": true,
"name": "Requested",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "requested"
},
{
"fields": {
"order": 2,
"used": true,
"name": "Accepted",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "accepted"
},
{
"fields": {
"order": 3,
"used": true,
"name": "Rejected",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "rejected"
},
{
"fields": {
"order": 4,
"used": true,
"name": "Withdrawn",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "withdrawn"
},
{
"fields": {
"order": 5,
"used": true,
"name": "Overtaken By Events",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "overtaken"
},
{
"fields": {
"order": 6,
"used": true,
"name": "No Response",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "noresponse"
},
{
"fields": {
"order": 7,
"used": true,
"name": "Completed",
"desc": ""
},
"model": "name.reviewrequeststatename",
"pk": "completed"
},
{
"fields": {
"order": 1,
"used": true,
"teams": [],
"name": "Almost Ready",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "almost-ready"
},
{
"fields": {
"order": 2,
"used": true,
"teams": [],
"name": "Has Issues",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "issues"
},
{
"fields": {
"order": 3,
"used": true,
"teams": [],
"name": "Has Nits",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "nits"
},
{
"fields": {
"order": 4,
"used": true,
"teams": [],
"name": "Not Ready",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "not-ready"
},
{
"fields": {
"order": 5,
"used": true,
"teams": [],
"name": "On the Right Track",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "right-track"
},
{
"fields": {
"order": 6,
"used": true,
"teams": [],
"name": "Ready",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "ready"
},
{
"fields": {
"order": 7,
"used": true,
"teams": [],
"name": "Ready with Issues",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "ready-issues"
},
{
"fields": {
"order": 8,
"used": true,
"teams": [],
"name": "Ready with Nits",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "ready-nits"
},
{
"fields": {
"order": 9,
"used": true,
"teams": [],
"name": "Serious Issues",
"desc": ""
},
"model": "name.reviewresultname",
"pk": "serious-issues"
},
{
"fields": {
"order": 1,
"used": true,
"name": "Early",
"desc": ""
},
"model": "name.reviewtypename",
"pk": "early"
},
{
"fields": {
"order": 2,
"used": true,
"name": "Last Call",
"desc": ""
},
"model": "name.reviewtypename",
"pk": "lc"
},
{
"fields": {
"order": 3,
"used": true,
"name": "Telechat",
"desc": ""
},
"model": "name.reviewtypename",
"pk": "telechat"
},
{
"fields": {
"order": 0,
@ -1942,6 +2141,16 @@
"model": "name.rolename",
"pk": "matman"
},
{
"fields": {
"order": 14,
"used": true,
"name": "Reviewer",
"desc": ""
},
"model": "name.rolename",
"pk": "reviewer"
},
{
"fields": {
"order": 0,

View file

@ -1,5 +1,7 @@
#!/usr/bin/python
# simple script for exporting name related base data for the tests
# boiler plate
import os, sys
import django

View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('group', '0008_auto_20160505_0523'),
('name', '0010_new_liaison_names'),
]
operations = [
migrations.CreateModel(
name='ReviewRequestStateName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order'],
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ReviewResultName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
('teams', models.ManyToManyField(help_text=b"Which teams this result can be set for. This also implicitly defines which teams are review teams - if there are no possible review results defined for a given team, it can't be a review team.", to='group.Group', blank=True)),
],
options={
'ordering': ['order'],
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ReviewTypeName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order'],
'abstract': False,
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def insert_initial_review_data(apps, schema_editor):
ReviewRequestStateName = apps.get_model("name", "ReviewRequestStateName")
ReviewRequestStateName.objects.get_or_create(slug="requested", name="Requested", order=1)
ReviewRequestStateName.objects.get_or_create(slug="accepted", name="Accepted", order=2)
ReviewRequestStateName.objects.get_or_create(slug="rejected", name="Rejected", order=3)
ReviewRequestStateName.objects.get_or_create(slug="withdrawn", name="Withdrawn", order=4)
ReviewRequestStateName.objects.get_or_create(slug="overtaken", name="Overtaken By Events", order=5)
ReviewRequestStateName.objects.get_or_create(slug="noresponse", name="No Response", order=6)
ReviewRequestStateName.objects.get_or_create(slug="completed", name="Completed", order=7)
ReviewTypeName = apps.get_model("name", "ReviewTypeName")
ReviewTypeName.objects.get_or_create(slug="early", name="Early", order=1)
ReviewTypeName.objects.get_or_create(slug="lc", name="Last Call", order=2)
ReviewTypeName.objects.get_or_create(slug="telechat", name="Telechat", order=3)
ReviewResultName = apps.get_model("name", "ReviewResultName")
ReviewResultName.objects.get_or_create(slug="almost-ready", name="Almost Ready", order=1)
ReviewResultName.objects.get_or_create(slug="issues", name="Has Issues", order=2)
ReviewResultName.objects.get_or_create(slug="nits", name="Has Nits", order=3)
ReviewResultName.objects.get_or_create(slug="not-ready", name="Not Ready", order=4)
ReviewResultName.objects.get_or_create(slug="right-track", name="On the Right Track", order=5)
ReviewResultName.objects.get_or_create(slug="ready", name="Ready", order=6)
ReviewResultName.objects.get_or_create(slug="ready-issues", name="Ready with Issues", order=7)
ReviewResultName.objects.get_or_create(slug="ready-nits", name="Ready with Nits", order=8)
ReviewResultName.objects.get_or_create(slug="serious-issues", name="Serious Issues", order=9)
RoleName = apps.get_model("name", "RoleName")
RoleName.objects.get_or_create(slug="reviewer", name="Reviewer", order=max(r.order for r in RoleName.objects.all()) + 1)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('name', '0011_reviewrequeststatename_reviewresultname_reviewtypename'),
('group', '0001_initial'),
]
operations = [
migrations.RunPython(insert_initial_review_data, noop),
]

View file

@ -87,3 +87,14 @@ class LiaisonStatementEventTypeName(NameModel):
"Submitted, Modified, Approved, Posted, Killed, Resurrected, MsgIn, MsgOut, Comment"
class LiaisonStatementTagName(NameModel):
"Action Required, Action Taken"
class ReviewRequestStateName(NameModel):
"""Requested, Accepted, Rejected, Withdrawn, Overtaken By Events,
No Response , Completed"""
class ReviewTypeName(NameModel):
"""Early Review, Last Call, Telechat"""
class ReviewResultName(NameModel):
"""Almost ready, Has issues, Has nits, Not Ready,
On the right track, Ready, Ready with issues,
Ready with nits, Serious Issues"""
teams = models.ManyToManyField("group.Group", help_text="Which teams this result can be set for. This also implicitly defines which teams are review teams - if there are no possible review results defined for a given team, it can't be a review team.", blank=True)

View file

@ -13,7 +13,8 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte
IprEventTypeName, GroupMilestoneStateName, SessionStatusName, DocReminderTypeName,
ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName,
LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName,
BallotPositionName, DBTemplateTypeName, NomineePositionStateName)
BallotPositionName, DBTemplateTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewTypeName, ReviewResultName)
class TimeSlotTypeNameResource(ModelResource):
@ -413,3 +414,46 @@ class NomineePositionStateNameResource(ModelResource):
}
api.name.register(NomineePositionStateNameResource())
class ReviewRequestStateNameResource(ModelResource):
class Meta:
cache = SimpleCache()
queryset = ReviewRequestStateName.objects.all()
#resource_name = 'reviewrequeststatename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(ReviewRequestStateNameResource())
class ReviewTypeNameResource(ModelResource):
class Meta:
cache = SimpleCache()
queryset = ReviewTypeName.objects.all()
#resource_name = 'reviewtypename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(ReviewTypeNameResource())
class ReviewResultNameResource(ModelResource):
class Meta:
cache = SimpleCache()
queryset = ReviewResultName.objects.all()
#resource_name = 'reviewresultname'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
"teams": ALL_WITH_RELATIONS,
}
api.name.register(ReviewResultNameResource())

0
ietf/review/__init__.py Normal file
View file

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('group', '0008_auto_20160505_0523'),
('name', '0012_insert_review_name_data'),
('doc', '0012_auto_20160207_0537'),
]
operations = [
migrations.CreateModel(
name='Reviewer',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('frequency', models.IntegerField(help_text=b'Can review every N days')),
('available', models.DateTimeField(help_text=b'When will this reviewer be available again', null=True, blank=True)),
('filter_re', models.CharField(max_length=255, blank=True)),
('skip_next', models.IntegerField(help_text=b'Skip the next N review assignments')),
('role', models.ForeignKey(to='group.Role')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ReviewRequest',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('time', models.DateTimeField(auto_now_add=True)),
('deadline', models.DateTimeField()),
('requested_rev', models.CharField(help_text=b'Fill in if a specific revision is to be reviewed, e.g. 02', max_length=16, verbose_name=b'requested revision', blank=True)),
('reviewed_rev', models.CharField(max_length=16, verbose_name=b'reviewed revision', blank=True)),
('doc', models.ForeignKey(related_name='review_request_set', to='doc.Document')),
('result', models.ForeignKey(blank=True, to='name.ReviewResultName', null=True)),
('review', models.OneToOneField(null=True, blank=True, to='doc.Document')),
('reviewer', models.ForeignKey(blank=True, to='review.Reviewer', null=True)),
('state', models.ForeignKey(to='name.ReviewRequestStateName')),
('team', models.ForeignKey(to='group.Group')),
('type', models.ForeignKey(to='name.ReviewTypeName')),
],
options={
},
bases=(models.Model,),
),
]

View file

40
ietf/review/models.py Normal file
View file

@ -0,0 +1,40 @@
from django.db import models
from ietf.doc.models import Document
from ietf.group.models import Group, Role
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName
class Reviewer(models.Model):
"""
These records associate reviewers with review teams and keep track
of admin data associated with the reviewer in the particular team.
There will be one record for each combination of reviewer and team.
"""
role = models.ForeignKey(Role)
frequency = models.IntegerField(help_text="Can review every N days")
available = models.DateTimeField(blank=True, null=True, help_text="When will this reviewer be available again")
filter_re = models.CharField(max_length=255, blank=True)
skip_next = models.IntegerField(help_text="Skip the next N review assignments")
class ReviewRequest(models.Model):
"""
There should be one ReviewRequest entered for each combination of
document, rev, and reviewer.
"""
# Fields filled in on the initial record creation:
time = models.DateTimeField(auto_now_add=True)
type = models.ForeignKey(ReviewTypeName)
doc = models.ForeignKey(Document, related_name='review_request_set')
team = models.ForeignKey(Group)
deadline = models.DateTimeField()
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")
state = models.ForeignKey(ReviewRequestStateName)
# Fields filled in as reviewer is assigned, and as the review
# is uploaded
reviewer = models.ForeignKey(Reviewer, blank=True, null=True)
review = models.OneToOneField(Document, blank=True, null=True)
reviewed_rev = models.CharField(verbose_name="reviewed revision", max_length=16, blank=True)
result = models.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)

6
ietf/review/utils.py Normal file
View file

@ -0,0 +1,6 @@
from ietf.group.models import Group
def active_review_teams():
# if there's a ReviewResultName defined, it's a review team
return Group.objects.filter(state="active").exclude(reviewresultname=None)

View file

@ -273,6 +273,7 @@ INSTALLED_APPS = (
'ietf.person',
'ietf.redirects',
'ietf.release',
'ietf.review',
'ietf.submit',
'ietf.sync',
'ietf.utils',

View file

@ -192,6 +192,28 @@
</td>
</tr>
{% if review_requests or can_request_review %}
<tr>
<th></th>
<th>Reviews</th>
<td class="edit"></td>
<td>
{% for r in review_requests %}
<div>
<a href="{% url "ietf.doc.views_review.review" doc.name r.pk %}">{{ r.team.acronym|upper }} {{ r.type.name }} Review ({{ r.state.name }})</a>
</div>
{% endfor %}
{% if can_request_review %}
<div>
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.request_review" doc.name %}"><span class="fa fa-check-circle-o"></span> Request review</a>
</div>
{% endif %}
</td>
</tr>
{% endif %}
{% if conflict_reviews %}
<tr>
<th></th>

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2016, All Rights Reserved #}
{% load origin bootstrap3 static %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
{% endblock %}
{% block title %}Request review of {{ doc.name }} {% endblock %}
{% block content %}
{% origin %}
<h1>Request review<br><small>{{ doc.name }}</small></h1>
<p>Submit a request to have the document reviewed.</p>
<form class="form-horizontal" method="post">
{% csrf_token %}
{% bootstrap_field form.type layout="horizontal" %}
{% bootstrap_field form.team layout="horizontal" %}
{% bootstrap_field form.deadline_date layout="horizontal" %}
{% bootstrap_field form.deadline_time layout="horizontal" %}
{% bootstrap_field form.requested_rev layout="horizontal" %}
{% buttons %}
<button type="submit" class="btn btn-primary" name="save_addresses" value="Save">Request review</button>
<a class="btn btn-default pull-right" href="{% url "doc_view" name=doc.canonical_name %}">Back</a>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
{% endblock %}