Fix #2217 - Allow submission of unsolicited reviews by secretaries.
- For team secretaries, a button "Submit unsolicited review" will now appear next to "Request review" on the document's main page. - If the secretary is a secretary for multiple teams, they are taken through an intermediate page to select for which team they are submitting their review. - The form is similar (and using the same code) as the usual review completion, with a few extra fields for the review type and reviewer, which would usually already be known. - When submitting the review, a ReviewRequest and ReviewAssignment are automatically created. The assignment is then immediately closed in the usual way. - Other workflows are unchanged. The issues with the review form in #2061 are slightly worse for the unsolicited review scenario, but that will be improved when #2061 is fixed. Commit ready for merge. - Legacy-Id: 16924
This commit is contained in:
parent
93797b3fc1
commit
871a4b653b
|
@ -27,7 +27,8 @@ from ietf.doc.models import DocumentAuthor, RelatedDocument, DocEvent, ReviewReq
|
|||
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, ReviewAssignmentStateName
|
||||
from ietf.name.models import ReviewResultName, ReviewRequestStateName, ReviewAssignmentStateName, \
|
||||
ReviewTypeName
|
||||
from ietf.person.models import Email, Person
|
||||
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
|
||||
from ietf.review.models import (ReviewRequest, ReviewerSettings,
|
||||
|
@ -563,7 +564,7 @@ class ReviewTests(TestCase):
|
|||
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)
|
||||
query_urls = ietf.review.mailarch.construct_query_urls(doc, review_team)
|
||||
self.assertTrue(review_req.doc.name in query_urls["query_data_url"])
|
||||
|
||||
# test parsing
|
||||
|
@ -574,9 +575,9 @@ class ReviewTests(TestCase):
|
|||
# to work, the module (and not the function) must be
|
||||
# imported in the view
|
||||
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) }
|
||||
|
||||
ietf.review.mailarch.construct_query_urls = lambda doc, team, query=None: { "query_data_url": "file://" + os.path.abspath(mbox_path) }
|
||||
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "assignment_id": assignment.pk })
|
||||
url2 = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "acronym": review_team.acronym })
|
||||
login_testing_unauthorized(self, "reviewsecretary", url)
|
||||
|
||||
r = self.client.get(url)
|
||||
|
@ -584,6 +585,11 @@ class ReviewTests(TestCase):
|
|||
messages = r.json()["messages"]
|
||||
self.assertEqual(len(messages), 2)
|
||||
|
||||
r = self.client.get(url2)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
messages = r.json()["messages"]
|
||||
self.assertEqual(len(messages), 2)
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
self.assertEqual(messages[0]["url"], "https://www.example.com/testmessage")
|
||||
|
@ -604,7 +610,7 @@ class ReviewTests(TestCase):
|
|||
no_result_path = os.path.join(self.review_dir, "mailarch_no_result.html")
|
||||
with io.open(no_result_path, "w") as f:
|
||||
f.write('Content-Type: text/html\n\n<html><body><div class="xtr"><div class="xtd no-results">No results found</div></div>')
|
||||
ietf.review.mailarch.construct_query_urls = lambda review_req, query=None: { "query_data_url": "file://" + os.path.abspath(no_result_path) }
|
||||
ietf.review.mailarch.construct_query_urls = lambda doc, team, 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, "assignment_id": assignment.pk })
|
||||
|
||||
|
@ -617,6 +623,27 @@ class ReviewTests(TestCase):
|
|||
finally:
|
||||
ietf.review.mailarch.construct_query_urls = real_fn
|
||||
|
||||
def test_submit_unsolicited_review_choose_team(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"))
|
||||
secretary = RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com', name_id='secr')
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.submit_unsolicited_review_choose_team',
|
||||
kwargs={'name': doc.name})
|
||||
redirect_url = urlreverse("ietf.doc.views_review.complete_review",
|
||||
kwargs={'name': doc.name, 'acronym': review_team.acronym})
|
||||
login_testing_unauthorized(self, secretary.person.user.username, url)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, review_team.name)
|
||||
|
||||
r = self.client.post(url, data={'team': review_team.pk})
|
||||
self.assertRedirects(r, redirect_url)
|
||||
|
||||
r = self.client.post(url, data={'team': review_team.pk + 5})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def setup_complete_review_test(self):
|
||||
doc = WgDraftFactory(group__acronym='mars',rev='01')
|
||||
NewRevisionDocEventFactory(doc=doc,rev='01')
|
||||
|
@ -872,6 +899,56 @@ class ReviewTests(TestCase):
|
|||
self.assertEqual(len(outbox), 0)
|
||||
self.assertTrue("http://example.com" in assignment.review.external_url)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_complete_unsolicited_review_link_to_mailing_list_by_secretary(self, mock):
|
||||
# Mock up the url response for the request.get() call to retrieve the mailing list url
|
||||
response = Response()
|
||||
response.status_code = 200
|
||||
response._content = b"This is a review\nwith two lines"
|
||||
mock.return_value = response
|
||||
|
||||
# Run the test
|
||||
doc = WgDraftFactory(group__acronym='mars', rev='01')
|
||||
NewRevisionDocEventFactory(doc=doc, 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')
|
||||
secretary_role = RoleFactory(group=review_team, person__user__username='reviewsecretary', person__user__email='reviewsecretary@example.com', name_id='secr')
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.complete_review',
|
||||
kwargs={"name": doc.name, "acronym": review_team.acronym})
|
||||
|
||||
login_testing_unauthorized(self, secretary_role.person.user.username, url)
|
||||
|
||||
empty_outbox()
|
||||
|
||||
r = self.client.post(url, data={
|
||||
"result": ReviewResultName.objects.get(slug="ready").pk,
|
||||
"state": ReviewAssignmentStateName.objects.get(slug="completed").pk,
|
||||
"reviewed_rev": '01',
|
||||
"review_submission": "link",
|
||||
"review_content": response.content.decode(),
|
||||
"review_url": "http://example.com/testreview/",
|
||||
"review_file": "",
|
||||
"review_type": ReviewTypeName.objects.get(slug="early").pk,
|
||||
"reviewer": rev_role.person.pk,
|
||||
"completion_date": "2012-12-24",
|
||||
"completion_time": "12:13:14",
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = doc.reviewrequest_set.get()
|
||||
assignment = review_req.reviewassignment_set.get()
|
||||
self.assertEqual(review_req.type_id, "early")
|
||||
self.assertEqual(review_req.requested_by, secretary_role.person)
|
||||
self.assertEqual(assignment.reviewer, rev_role.person.role_email('reviewer'))
|
||||
self.assertEqual(assignment.state_id, "completed")
|
||||
|
||||
with io.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 assignment.review.external_url)
|
||||
|
||||
def test_partially_complete_review(self):
|
||||
assignment, url = self.setup_complete_review_test()
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright The IETF Trust 2016-2019, All Rights Reserved
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.doc import views_review
|
||||
from ietf.utils.urls import url
|
||||
|
||||
|
@ -9,9 +12,12 @@ urlpatterns = [
|
|||
url(r'^(?P<request_id>[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
|
||||
url(r'^(?P<assignment_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
|
||||
url(r'^(?P<assignment_id>[0-9]+)/complete/$', views_review.complete_review),
|
||||
url(r'^%(acronym)s/submitunsolicitedreview/$' % settings.URL_REGEXPS, views_review.complete_review),
|
||||
url(r'^submitunsolicitedreview/$', views_review.submit_unsolicited_review_choose_team),
|
||||
url(r'^(?P<assignment_id>[0-9]+)/withdraw/$', views_review.withdraw_reviewer_assignment),
|
||||
url(r'^(?P<assignment_id>[0-9]+)/noresponse/$', views_review.mark_reviewer_assignment_no_response),
|
||||
url(r'^(?P<assignment_id>[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
|
||||
url(r'^assignment/(?P<assignment_id>[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
|
||||
url(r'^team/%(acronym)s/searchmailarchive/$' % settings.URL_REGEXPS, views_review.search_mail_archive),
|
||||
url(r'^(?P<request_id>[0-9]+)/editcomment/$', views_review.edit_comment),
|
||||
url(r'^(?P<request_id>[0-9]+)/editdeadline/$', views_review.edit_deadline),
|
||||
]
|
||||
|
|
|
@ -64,7 +64,7 @@ from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_wi
|
|||
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
|
||||
add_events_message_info, get_unicode_document_content, build_doc_meta_block)
|
||||
from ietf.community.utils import augment_docs_with_tracking_info
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.models import Role, Group
|
||||
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
|
||||
from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person,
|
||||
role_required, is_individual_draft_author)
|
||||
|
@ -317,6 +317,10 @@ def document_main(request, name, rev=None):
|
|||
consensus = nice_consensus(e and e.consensus)
|
||||
|
||||
can_request_review = can_request_review_of_doc(request.user, doc)
|
||||
can_submit_unsolicited_review_for_teams = None
|
||||
if request.user.is_authenticated:
|
||||
can_submit_unsolicited_review_for_teams = Group.objects.filter(
|
||||
reviewteamsettings__isnull=False, role__person__user=request.user, role__name='secr')
|
||||
|
||||
# mailing list search archive
|
||||
search_archive = "www.ietf.org/mail-archive/web/"
|
||||
|
@ -419,6 +423,7 @@ def document_main(request, name, rev=None):
|
|||
can_edit_replaces=can_edit_replaces,
|
||||
can_view_possibly_replaces=can_view_possibly_replaces,
|
||||
can_request_review=can_request_review,
|
||||
can_submit_unsolicited_review_for_teams=can_submit_unsolicited_review_for_teams,
|
||||
|
||||
rfc_number=rfc_number,
|
||||
draft_name=draft_name,
|
||||
|
|
|
@ -24,7 +24,9 @@ from django.urls import reverse as urlreverse
|
|||
|
||||
from ietf.doc.models import (Document, NewRevisionDocEvent, State, DocAlias,
|
||||
LastCallDocEvent, ReviewRequestDocEvent, ReviewAssignmentDocEvent, DocumentAuthor)
|
||||
from ietf.name.models import ReviewRequestStateName, ReviewAssignmentStateName, ReviewResultName, DocTypeName
|
||||
from ietf.name.models import ReviewRequestStateName, ReviewAssignmentStateName, ReviewResultName, \
|
||||
DocTypeName, ReviewTypeName
|
||||
from ietf.person.models import Person
|
||||
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
|
||||
|
@ -451,10 +453,43 @@ def mark_reviewer_assignment_no_response(request, name, assignment_id):
|
|||
})
|
||||
|
||||
|
||||
class SubmitUnsolicitedReviewTeamChoiceForm(forms.Form):
|
||||
team = forms.ModelChoiceField(queryset=Group.objects.filter(reviewteamsettings__isnull=False), widget=forms.RadioSelect, empty_label=None)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(SubmitUnsolicitedReviewTeamChoiceForm, self).__init__(*args, **kwargs)
|
||||
self.fields['team'].queryset = self.fields['team'].queryset.filter(role__person__user=user, role__name='secr')
|
||||
|
||||
|
||||
def submit_unsolicited_review_choose_team(request, name):
|
||||
"""
|
||||
If a user is submitting an unsolicited review, and is allowed to do this for more
|
||||
than one team, they are routed through this small view to pick a team.
|
||||
This is needed as the complete review form needs to be specific for a team.
|
||||
"""
|
||||
if not request.user.is_authenticated:
|
||||
# This view only produces a redirect, so it's open for any user
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
if request.method == "POST":
|
||||
form = SubmitUnsolicitedReviewTeamChoiceForm(request.user, request.POST)
|
||||
if form.is_valid():
|
||||
return redirect("ietf.doc.views_review.complete_review",
|
||||
name=doc.name, acronym=form.cleaned_data['team'].acronym)
|
||||
else:
|
||||
form = SubmitUnsolicitedReviewTeamChoiceForm(user=request.user)
|
||||
return render(request, 'doc/review/submit_unsolicited_review.html', {
|
||||
'doc': doc,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
class CompleteReviewForm(forms.Form):
|
||||
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)
|
||||
review_type = forms.ModelChoiceField(queryset=ReviewTypeName.objects.filter(used=True), widget=forms.RadioSelect, empty_label=None)
|
||||
reviewer = forms.ModelChoiceField(queryset=Person.objects.all(), widget=forms.Select)
|
||||
|
||||
ACTIONS = [
|
||||
("enter", "Enter review content (automatically posts to {mailing_list})"),
|
||||
("upload", "Upload review content in text file (automatically posts to {mailing_list})"),
|
||||
|
@ -470,16 +505,15 @@ 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, assignment, is_reviewer, *args, **kwargs):
|
||||
def __init__(self, assignment, doc, team, is_reviewer, *args, **kwargs):
|
||||
self.assignment = assignment
|
||||
self.doc = doc
|
||||
|
||||
super(CompleteReviewForm, self).__init__(*args, **kwargs)
|
||||
|
||||
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 = assignment.state_id not in ["assigned", "accepted"]
|
||||
revising_review = assignment.state_id not in ["assigned", "accepted"] if assignment else False
|
||||
|
||||
if not revising_review:
|
||||
self.fields["state"].choices = [
|
||||
|
@ -487,7 +521,7 @@ class CompleteReviewForm(forms.Form):
|
|||
for slug, label in self.fields["state"].choices
|
||||
]
|
||||
|
||||
if 'initial' in kwargs:
|
||||
if 'initial' in kwargs and assignment:
|
||||
reviewed_rev_class = []
|
||||
for r in known_revisions:
|
||||
last_version = r[0]
|
||||
|
@ -515,16 +549,24 @@ class CompleteReviewForm(forms.Form):
|
|||
" ".join("<a class=\"rev label label-default {0}\" title=\"{2:%Y-%m-%d}\">{1}</a>".format('', *r)
|
||||
for i, r in enumerate(known_revisions)))
|
||||
|
||||
self.fields["result"].queryset = self.fields["result"].queryset.filter(reviewteamsettings_review_results_set__group=assignment.review_request.team)
|
||||
self.fields["result"].queryset = self.fields["result"].queryset.filter(reviewteamsettings_review_results_set__group=team)
|
||||
|
||||
def format_submission_choice(label):
|
||||
if revising_review:
|
||||
label = label.replace(" (automatically posts to {mailing_list})", "")
|
||||
|
||||
return label.format(mailing_list=assignment.review_request.team.list_email or "[error: team has no mailing list set]")
|
||||
return label.format(mailing_list=team.list_email or "[error: team has no mailing list set]")
|
||||
|
||||
if assignment:
|
||||
del self.fields["review_type"]
|
||||
del self.fields["reviewer"]
|
||||
else:
|
||||
self.fields["review_type"].queryset = self.fields["review_type"].queryset.filter(
|
||||
reviewteamsettings__group=team)
|
||||
self.fields["reviewer"].queryset = self.fields["reviewer"].queryset.filter(role__name="reviewer", role__group=team)
|
||||
|
||||
self.fields["review_submission"].choices = [ (k, format_submission_choice(label)) for k, label in self.fields["review_submission"].choices]
|
||||
|
||||
|
||||
if revising_review:
|
||||
del self.fields["cc"]
|
||||
elif is_reviewer:
|
||||
|
@ -532,7 +574,7 @@ class CompleteReviewForm(forms.Form):
|
|||
del self.fields["completion_time"]
|
||||
|
||||
def clean_reviewed_rev(self):
|
||||
return clean_doc_revision(self.assignment.review_request.doc, self.cleaned_data.get("reviewed_rev"))
|
||||
return clean_doc_revision(self.doc, self.cleaned_data.get("reviewed_rev"))
|
||||
|
||||
def clean_review_content(self):
|
||||
return self.cleaned_data["review_content"].replace("\r", "")
|
||||
|
@ -550,7 +592,7 @@ class CompleteReviewForm(forms.Form):
|
|||
return url
|
||||
|
||||
def clean(self):
|
||||
if "@" in self.assignment.reviewer.person.ascii:
|
||||
if self.assignment and "@" 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):
|
||||
|
@ -566,35 +608,67 @@ class CompleteReviewForm(forms.Form):
|
|||
require_field("review_url")
|
||||
|
||||
@login_required
|
||||
def complete_review(request, name, assignment_id):
|
||||
def complete_review(request, name, assignment_id=None, acronym=None):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
assignment = get_object_or_404(ReviewAssignment, pk=assignment_id)
|
||||
|
||||
revising_review = assignment.state_id not in ["assigned", "accepted"]
|
||||
|
||||
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")
|
||||
|
||||
team_acronym = assignment.review_request.team.acronym.lower()
|
||||
request_type = assignment.review_request.type
|
||||
mailtrigger_slug = 'review_completed_{}_{}'.format(team_acronym, request_type.slug)
|
||||
# Description is only used if the mailtrigger does not exist yet.
|
||||
mailtrigger_desc = 'Recipients when a {} {} review is completed'.format(team_acronym, request_type)
|
||||
to, cc = gather_address_lists(
|
||||
mailtrigger_slug,
|
||||
create_from_slug_if_not_exists='review_completed',
|
||||
desc_if_not_exists=mailtrigger_desc,
|
||||
review_req=assignment.review_request
|
||||
)
|
||||
if assignment_id:
|
||||
assignment = get_object_or_404(ReviewAssignment, pk=assignment_id)
|
||||
|
||||
revising_review = assignment.state_id not in ["assigned", "accepted"]
|
||||
|
||||
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")
|
||||
|
||||
team = assignment.review_request.team
|
||||
team_acronym = assignment.review_request.team.acronym.lower()
|
||||
request_type = assignment.review_request.type
|
||||
mailtrigger_slug = 'review_completed_{}_{}'.format(team_acronym, request_type.slug)
|
||||
# Description is only used if the mailtrigger does not exist yet.
|
||||
mailtrigger_desc = 'Recipients when a {} {} review is completed'.format(team_acronym, request_type)
|
||||
to, cc = gather_address_lists(
|
||||
mailtrigger_slug,
|
||||
create_from_slug_if_not_exists='review_completed',
|
||||
desc_if_not_exists=mailtrigger_desc,
|
||||
review_req=assignment.review_request
|
||||
)
|
||||
else:
|
||||
team = get_object_or_404(Group, acronym=acronym)
|
||||
if not can_manage_review_requests_for_team(request.user, team):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
assignment = None
|
||||
is_reviewer = False
|
||||
revising_review = False
|
||||
request_type = None
|
||||
to, cc = [], []
|
||||
|
||||
if request.method == "POST":
|
||||
form = CompleteReviewForm(assignment, is_reviewer,
|
||||
form = CompleteReviewForm(assignment, doc, team, is_reviewer,
|
||||
request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
review_submission = form.cleaned_data['review_submission']
|
||||
|
||||
if not assignment:
|
||||
# If this is an unsolicited review, create a new request and assignment.
|
||||
# The assignment will be immediately closed after, sharing the usual
|
||||
# processes for regular assigned reviews.
|
||||
review_request = ReviewRequest.objects.create(
|
||||
state_id='assigned',
|
||||
type=form.cleaned_data['review_type'],
|
||||
doc=doc,
|
||||
team=team,
|
||||
deadline=datetime.date.today(),
|
||||
requested_by=Person.objects.get(user=request.user),
|
||||
requested_rev=form.cleaned_data['reviewed_rev'],
|
||||
)
|
||||
assignment = ReviewAssignment.objects.create(
|
||||
review_request=review_request,
|
||||
state_id='assigned',
|
||||
reviewer=form.cleaned_data['reviewer'].role_email('reviewer', group=team),
|
||||
assigned_on=datetime.datetime.now(),
|
||||
)
|
||||
request_type = form.cleaned_data['review_type']
|
||||
|
||||
review = assignment.review
|
||||
if not review:
|
||||
|
@ -772,23 +846,24 @@ def complete_review(request, name, assignment_id):
|
|||
return redirect("ietf.doc.views_doc.document_main", name=assignment.review.name)
|
||||
else:
|
||||
initial={
|
||||
"reviewed_rev": assignment.reviewed_rev,
|
||||
"result": assignment.result_id,
|
||||
"reviewed_rev": assignment.reviewed_rev if assignment else None,
|
||||
"result": assignment.result_id if assignment else None,
|
||||
"cc": ", ".join(cc),
|
||||
}
|
||||
|
||||
try:
|
||||
initial['review_content'] = render_to_string('/group/%s/review/content_templates/%s.txt' % (assignment.review_request.team.acronym,
|
||||
request_type.slug), {'assignment':assignment, 'today':datetime.date.today()})
|
||||
except TemplateDoesNotExist:
|
||||
except (TemplateDoesNotExist, AttributeError):
|
||||
pass
|
||||
|
||||
form = CompleteReviewForm(assignment, is_reviewer, initial=initial)
|
||||
form = CompleteReviewForm(assignment, doc, team, is_reviewer, initial=initial)
|
||||
|
||||
mail_archive_query_urls = mailarch.construct_query_urls(assignment.review_request)
|
||||
mail_archive_query_urls = mailarch.construct_query_urls(doc, team)
|
||||
|
||||
return render(request, 'doc/review/complete_review.html', {
|
||||
'doc': doc,
|
||||
'team': team,
|
||||
'assignment': assignment,
|
||||
'form': form,
|
||||
'mail_archive_query_urls': mail_archive_query_urls,
|
||||
|
@ -797,16 +872,22 @@ def complete_review(request, name, assignment_id):
|
|||
'review_cc': cc,
|
||||
})
|
||||
|
||||
def search_mail_archive(request, name, assignment_id):
|
||||
assignment = get_object_or_404(ReviewAssignment, pk=assignment_id)
|
||||
def search_mail_archive(request, name, acronym=None, assignment_id=None):
|
||||
if assignment_id:
|
||||
assignment = get_object_or_404(ReviewAssignment, pk=assignment_id)
|
||||
team = assignment.review_request.team
|
||||
else:
|
||||
assignment = None
|
||||
team = get_object_or_404(Group, acronym=acronym)
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
|
||||
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)
|
||||
is_reviewer = assignment and user_is_person(request.user, assignment.reviewer.person)
|
||||
can_manage_request = can_manage_review_requests_for_team(request.user, 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(assignment.review_request, query=request.GET.get("query"))
|
||||
res = mailarch.construct_query_urls(doc, team, query=request.GET.get("query"))
|
||||
if not res:
|
||||
return JsonResponse({ "error": "Couldn't do lookup in mail archive - don't know where to look"})
|
||||
|
||||
|
|
|
@ -42,13 +42,13 @@ def hash_list_message_id(list_name, msgid):
|
|||
sha.update(force_bytes(list_name))
|
||||
return base64.urlsafe_b64encode(sha.digest()).rstrip(b"=")
|
||||
|
||||
def construct_query_urls(review_req, query=None):
|
||||
list_name = list_name_from_email(review_req.team.list_email)
|
||||
def construct_query_urls(doc, team, query=None):
|
||||
list_name = list_name_from_email(team.list_email)
|
||||
if not list_name:
|
||||
return None
|
||||
|
||||
if not query:
|
||||
query = review_req.doc.name
|
||||
query = doc.name
|
||||
|
||||
encoded_query = "?" + urlencode({
|
||||
"qdr": "c", # custom time frame
|
||||
|
|
|
@ -224,6 +224,11 @@
|
|||
{% 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>
|
||||
{% if can_submit_unsolicited_review_for_teams|length == 1 %}
|
||||
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.complete_review" doc.name can_submit_unsolicited_review_for_teams.0.acronym %}"><span class="fa fa-pencil-square-o"></span> Submit unsolicited review</a>
|
||||
{% elif can_submit_unsolicited_review_for_teams %}
|
||||
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.submit_unsolicited_review_choose_team" doc.name %}"><span class="fa fa-pencil-square-o"></span> Submit unsolicited review</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}{% if revising_review %}Revise{% else %}Complete{% endif %} review of {{ review_req.doc.name }}{% endblock %}
|
||||
{% block title %}{% if revising_review %}Revise{% elif assignment %}Complete{% else %}Submit unsolicited{% endif %} review of {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
|
||||
|
@ -10,18 +10,26 @@
|
|||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>{% if revising_review %}Revise{% else %}Complete{% endif %} review<br>
|
||||
<small>{{ assignment.review_request.doc.name }}</small>
|
||||
<h1>{% if revising_review %}Revise{% elif assignment %}Complete{% else %}Submit unsolicited{% endif %} review<br>
|
||||
<small>{{ doc.name }}</small>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
<div><strong>Review type:</strong> {{ assignment.review_request.team.acronym }} - {{ assignment.review_request.type }} review </div>
|
||||
<div><strong>Requested version for review:</strong> {{ assignment.review_request.requested_rev|default:"Current" }} </div>
|
||||
<div><strong>Requested:</strong> {{ assignment.review_request.time|date:"Y-m-d" }} </div>
|
||||
<div><strong>Reviewer:</strong> {{ assignment.reviewer.person.name }}</div>
|
||||
</p>
|
||||
{% if assignment %}
|
||||
<p>
|
||||
<div><strong>Review type:</strong> {{ assignment.review_request.team.acronym }} - {{ assignment.review_request.type }} review </div>
|
||||
<div><strong>Requested version for review:</strong> {{ assignment.review_request.requested_rev|default:"Current" }} </div>
|
||||
<div><strong>Requested:</strong> {{ assignment.review_request.time|date:"Y-m-d" }} </div>
|
||||
<div><strong>Reviewer:</strong> {{ assignment.reviewer.person.name }}</div>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You are submitting an <strong>unsolicited</strong> review for this document for the {{ team }}.
|
||||
<strong>This process should only be used for unsolicited reviews.</strong>
|
||||
A review request and assignment will be created automatically upon submitting this review.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if not revising_review %}
|
||||
{% if assignment and not revising_review %}
|
||||
<p>The review findings should be made available here and the review
|
||||
posted to the mailing list. If you enter the findings below, the
|
||||
system will post the review for you. If you already have posted
|
||||
|
@ -30,7 +38,7 @@
|
|||
<p>If you enter the review below, the review will be sent
|
||||
to <em>{{ review_to|join:", " }}</em>
|
||||
{% if review_cc %}, with a Cc to <em>{{ review_cc|join:", " }}</em>{% endif %}.</p>
|
||||
{% else %}
|
||||
{% elif assignment %}
|
||||
<p>You can revise this review by entering the results below.</p>
|
||||
{% endif %}
|
||||
|
||||
|
@ -40,7 +48,11 @@
|
|||
{% bootstrap_form form layout="horizontal" %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=assignment.review_request.pk %}">Cancel</a>
|
||||
{% if assignment %}
|
||||
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=assignment.review_request.pk %}">Cancel</a>
|
||||
{% else %}
|
||||
<a class="btn btn-default" href="{{ doc.get_absolute_url }}">Cancel</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary">{% if revising_review %}Revise{% else %}Complete{% endif %} review</button>
|
||||
{% endbuttons %}
|
||||
|
||||
|
@ -98,7 +110,11 @@
|
|||
{% block js %}
|
||||
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
<script>
|
||||
var searchMailArchiveUrl = "{% url "ietf.doc.views_review.search_mail_archive" name=assignment.review_request.doc.name assignment_id=assignment.pk %}";
|
||||
{% if assignment %}
|
||||
var searchMailArchiveUrl = "{% url "ietf.doc.views_review.search_mail_archive" name=doc.name assignment_id=assignment.pk %}";
|
||||
{% else %}
|
||||
var searchMailArchiveUrl = "{% url "ietf.doc.views_review.search_mail_archive" name=doc.name acronym=team.acronym %}";
|
||||
{% endif %}
|
||||
</script>
|
||||
<script src="{% static 'ietf/js/complete-review.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
28
ietf/templates/doc/review/submit_unsolicited_review.html
Normal file
28
ietf/templates/doc/review/submit_unsolicited_review.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}Submit an unsolicited review of {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Submit unsolicited review<br>
|
||||
<small>{{ doc.name }}</small>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
You are submitting an <strong>unsolicited</strong> review for this document.
|
||||
First, select the team for which you will be submitting this review.
|
||||
</p>
|
||||
|
||||
<form class="form-horizontal" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Continue</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in a new issue