From 871a4b653b7599d565a07267e8089c102a14009f Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Thu, 24 Oct 2019 12:37:59 +0000 Subject: [PATCH] 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 --- ietf/doc/tests_review.py | 87 ++++++++- ietf/doc/urls_review.py | 8 +- ietf/doc/views_doc.py | 7 +- ietf/doc/views_review.py | 169 +++++++++++++----- ietf/review/mailarch.py | 6 +- ietf/templates/doc/document_draft.html | 5 + .../templates/doc/review/complete_review.html | 42 +++-- .../doc/review/submit_unsolicited_review.html | 28 +++ 8 files changed, 285 insertions(+), 67 deletions(-) create mode 100644 ietf/templates/doc/review/submit_unsolicited_review.html diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index 7c16ec2b9..a72344e12 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -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
No results found
') - 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() diff --git a/ietf/doc/urls_review.py b/ietf/doc/urls_review.py index 23b217c50..7adce5145 100644 --- a/ietf/doc/urls_review.py +++ b/ietf/doc/urls_review.py @@ -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[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'^%(acronym)s/submitunsolicitedreview/$' % settings.URL_REGEXPS, views_review.complete_review), + url(r'^submitunsolicitedreview/$', views_review.submit_unsolicited_review_choose_team), 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'^assignment/(?P[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[0-9]+)/editcomment/$', views_review.edit_comment), url(r'^(?P[0-9]+)/editdeadline/$', views_review.edit_deadline), ] diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 3f1ea00a5..cffe273aa 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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, diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index d74adde1c..6568a900f 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -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("{1}".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"}) diff --git a/ietf/review/mailarch.py b/ietf/review/mailarch.py index fc50f1075..3ad1dbd30 100644 --- a/ietf/review/mailarch.py +++ b/ietf/review/mailarch.py @@ -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 diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index f74b3b8c8..fc70217ea 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -224,6 +224,11 @@ {% if can_request_review %}
Request review + {% if can_submit_unsolicited_review_for_teams|length == 1 %} + Submit unsolicited review + {% elif can_submit_unsolicited_review_for_teams %} + Submit unsolicited review + {% endif %}
{% endif %} diff --git a/ietf/templates/doc/review/complete_review.html b/ietf/templates/doc/review/complete_review.html index 606c73246..b2e9b8d0e 100644 --- a/ietf/templates/doc/review/complete_review.html +++ b/ietf/templates/doc/review/complete_review.html @@ -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 %} @@ -10,18 +10,26 @@ {% block content %} {% origin %} -

{% if revising_review %}Revise{% else %}Complete{% endif %} review
- {{ assignment.review_request.doc.name }} +

{% if revising_review %}Revise{% elif assignment %}Complete{% else %}Submit unsolicited{% endif %} review
+ {{ doc.name }}

-

-

Review type: {{ assignment.review_request.team.acronym }} - {{ assignment.review_request.type }} review
-
Requested version for review: {{ assignment.review_request.requested_rev|default:"Current" }}
-
Requested: {{ assignment.review_request.time|date:"Y-m-d" }}
-
Reviewer: {{ assignment.reviewer.person.name }}
-

+ {% if assignment %} +

+

Review type: {{ assignment.review_request.team.acronym }} - {{ assignment.review_request.type }} review
+
Requested version for review: {{ assignment.review_request.requested_rev|default:"Current" }}
+
Requested: {{ assignment.review_request.time|date:"Y-m-d" }}
+
Reviewer: {{ assignment.reviewer.person.name }}
+

+ {% else %} +

+ You are submitting an unsolicited review for this document for the {{ team }}. + This process should only be used for unsolicited reviews. + A review request and assignment will be created automatically upon submitting this review. +

+ {% endif %} - {% if not revising_review %} + {% if assignment and not revising_review %}

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 @@

If you enter the review below, the review will be sent to {{ review_to|join:", " }} {% if review_cc %}, with a Cc to {{ review_cc|join:", " }}{% endif %}.

- {% else %} + {% elif assignment %}

You can revise this review by entering the results below.

{% endif %} @@ -40,7 +48,11 @@ {% bootstrap_form form layout="horizontal" %} {% buttons %} - Cancel + {% if assignment %} + Cancel + {% else %} + Cancel + {% endif %} {% endbuttons %} @@ -98,7 +110,11 @@ {% block js %} {% endblock %} diff --git a/ietf/templates/doc/review/submit_unsolicited_review.html b/ietf/templates/doc/review/submit_unsolicited_review.html new file mode 100644 index 000000000..ba0f4a8ce --- /dev/null +++ b/ietf/templates/doc/review/submit_unsolicited_review.html @@ -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 %} +

Submit unsolicited review
+ {{ doc.name }} +

+ +

+ You are submitting an unsolicited review for this document. + First, select the team for which you will be submitting this review. +

+ +
+ {% csrf_token %} + + {% bootstrap_form form layout="horizontal" %} + + {% buttons %} + + {% endbuttons %} +
+ +{% endblock %}