diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index 99e307b90..b6058ad0d 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -13,6 +13,8 @@ from ietf.name.models import ReviewResultName, ReviewRequestStateName 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, unicontent, reload_db_objects +from ietf.utils.mail import outbox, empty_outbox + def make_review_data(doc): team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team") @@ -20,7 +22,7 @@ def make_review_data(doc): p = Person.objects.get(user__username="plain") role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team) - reviewer = Reviewer.objects.create(role=role, frequency=14, skip_next=0) + Reviewer.objects.create(team=team, person=p, frequency=14, skip_next=0) review_req = ReviewRequest.objects.create( doc=doc, @@ -28,10 +30,13 @@ def make_review_data(doc): type_id="early", deadline=datetime.datetime.now() + datetime.timedelta(days=20), state_id="ready", - reviewer=reviewer, + reviewer=role, reviewed_rev="01", ) + p = Person.objects.get(user__username="marschairman") + role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team) + return review_req class ReviewTests(TestCase): @@ -65,29 +70,6 @@ class ReviewTests(TestCase): 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_req = make_review_data(doc) - review_team = review_req.team - - 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, state="requested") - self.assertEqual(req.state_id, "requested") - self.assertEqual(req.team, review_team) - def test_doc_page(self): # FIXME: fill in pass @@ -134,13 +116,56 @@ class ReviewTests(TestCase): self.assertEqual(e.type, "changed_review_request") self.assertTrue("Withdrew" in e.desc) - def test_reject_request_assignment(self): + def test_assign_reviewer(self): + doc = make_test_data() + review_req = make_review_data(doc) + review_req.state = ReviewRequestStateName.objects.get(slug="requested") + review_req.reviewer = None + review_req.save() + + assign_url = urlreverse('ietf.doc.views_review.assign_reviewer', kwargs={ "name": doc.name, "request_id": review_req.pk }) + + + # follow link + req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk }) + self.client.login(username="secretary", password="secretary+password") + r = self.client.get(req_url) + self.assertEqual(r.status_code, 200) + self.assertTrue(assign_url in unicontent(r)) + self.client.logout() + + # get assign page + login_testing_unauthorized(self, "secretary", assign_url) + r = self.client.get(assign_url) + self.assertEqual(r.status_code, 200) + + # assign + reviewer = Role.objects.filter(name="reviewer", group=review_req.team).first() + r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk }) + 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) + + # re-assign + review_req.state = ReviewRequestStateName.objects.get(slug="accepted") + review_req.save() + reviewer = Role.objects.filter(name="reviewer", group=review_req.team).exclude(pk=reviewer.pk).first() + r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk }) + 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) + + def test_reject_reviewer_assignment(self): doc = make_test_data() review_req = make_review_data(doc) review_req.state = ReviewRequestStateName.objects.get(slug="accepted") review_req.save() - reject_url = urlreverse('ietf.doc.views_review.reject_request_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk }) + reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk }) # follow link @@ -155,16 +180,18 @@ class ReviewTests(TestCase): login_testing_unauthorized(self, "secretary", reject_url) r = self.client.get(reject_url) self.assertEqual(r.status_code, 200) - self.assertTrue(unicode(review_req.reviewer.role.person) in unicontent(r)) + self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r)) # reject - r = self.client.post(reject_url, { "action": "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") e = doc.latest_event() self.assertEqual(e.type, "changed_review_request") - self.assertTrue("unassigned" in e.desc) - self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="accepted").count(), 1) + 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) diff --git a/ietf/doc/urls_review.py b/ietf/doc/urls_review.py index 8ed0b5094..527833c0a 100644 --- a/ietf/doc/urls_review.py +++ b/ietf/doc/urls_review.py @@ -5,6 +5,7 @@ urlpatterns = patterns('', url(r'^$', views_review.request_review), url(r'^(?P[0-9]+)/$', views_review.review_request), url(r'^(?P[0-9]+)/withdraw/$', views_review.withdraw_request), - url(r'^(?P[0-9]+)/rejectassignment/$', views_review.reject_request_assignment), + url(r'^(?P[0-9]+)/assignreviewer/$', views_review.assign_reviewer), + url(r'^(?P[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment), ) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 3aaa1604b..500b283bf 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -89,22 +89,6 @@ 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 can_manage_review_requests_for_team(user, team): - if not user.is_authenticated(): - return False - - return Role.objects.filter(name="secretary", person__user=user, group=team).exists() or has_role(user, "Secretariat") - def two_thirds_rule( recused=0 ): # For standards-track, need positions from 2/3 of the non-recused current IESG. active = Role.objects.filter(name="ad",group__type="area",group__state="active").count() diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 0e7bc7838..23d430a72 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -48,8 +48,7 @@ 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, - can_request_review_of_doc ) + get_initial_notify, make_notify_changed_event, crawl_history, default_consensus ) 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 @@ -63,6 +62,7 @@ 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 def render_document_top(request, doc, tab, name): tabs = [] diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index 5c8face33..a0722a276 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -6,10 +6,12 @@ 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, can_manage_review_requests_for_team from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person -from ietf.review.models import ReviewRequest, ReviewRequestStateName -from ietf.review.utils import active_review_teams +from ietf.name.models import ReviewRequestStateName +from ietf.group.models import Role +from ietf.review.models import ReviewRequest +from ietf.review.utils import active_review_teams, assign_review_request_to_reviewer +from ietf.review.utils import can_request_review_of_doc, can_manage_review_requests_for_team from ietf.utils.fields import DatepickerDateField class RequestReviewForm(forms.ModelForm): @@ -89,7 +91,6 @@ def request_review(request, name): time=review_req.time, ) - # FIXME: if I'm a reviewer, auto-assign to myself? return redirect('doc_view', name=doc.name) else: @@ -104,21 +105,25 @@ 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.role.person) + is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.person) can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team) can_withdraw_request = (review_req.state_id in ["requested", "accepted"] and is_authorized_in_doc_stream(request.user, doc)) - can_reject_request_assignment = (review_req.state_id in ["requested", "accepted"] - and review_req.reviewer_id is not None - and (is_reviewer or can_manage_req)) + can_assign_reviewer = (review_req.state_id in ["requested", "accepted"] + and is_authorized_in_doc_stream(request.user, doc)) + + can_reject_reviewer_assignment = (review_req.state_id in ["requested", "accepted"] + and review_req.reviewer_id is not None + and (is_reviewer or can_manage_req)) return render(request, 'doc/review/review_request.html', { 'doc': doc, 'review_req': review_req, 'can_withdraw_request': can_withdraw_request, - 'can_reject_request_assignment': can_reject_request_assignment, + 'can_reject_reviewer_assignment': can_reject_reviewer_assignment, + 'can_assign_reviewer': can_assign_reviewer, }) def withdraw_request(request, name, request_id): @@ -150,52 +155,96 @@ def withdraw_request(request, name, request_id): 'review_req': review_req, }) -class RejectRequestAssignmentForm(forms.Form): +class PersonEmailLabeledRoleModelChoiceField(forms.ModelChoiceField): + def __init__(self, *args, **kwargs): + if not "queryset" in kwargs: + kwargs["queryset"] = Role.objects.select_related("person", "email") + super(PersonEmailLabeledRoleModelChoiceField, self).__init__(*args, **kwargs) + + def label_from_instance(self, role): + return u"{} <{}>".format(role.person.name, role.email.address) + +class AssignReviewerForm(forms.Form): + reviewer = PersonEmailLabeledRoleModelChoiceField(widget=forms.RadioSelect, empty_label="(None)", required=False) + + def __init__(self, review_req, *args, **kwargs): + super(AssignReviewerForm, self).__init__(*args, **kwargs) + f = self.fields["reviewer"] + f.queryset = f.queryset.filter(name="reviewer", group=review_req.team) + if review_req.reviewer: + f.initial = review_req.reviewer_id + +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"]) + + can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team) + + if not can_manage_req: + return HttpResponseForbidden("You do not have permission to perform this action") + + if request.method == "POST" and request.POST.get("action") == "assign": + form = AssignReviewerForm(review_req, request.POST) + if form.is_valid(): + reviewer = form.cleaned_data["reviewer"] + assign_review_request_to_reviewer(review_req, reviewer, request.user.person) + + return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk) + else: + form = AssignReviewerForm(review_req) + + return render(request, 'doc/review/assign_reviewer.html', { + 'doc': doc, + 'review_req': review_req, + 'form': form, + }) + +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") -def reject_request_assignment(request, name, request_id): +def reject_reviewer_assignment(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"]) 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.role.person) + is_reviewer = user_is_person(request.user, review_req.reviewer.person) can_manage_req = can_manage_review_requests_for_team(request.user, review_req.team) if not (is_reviewer or can_manage_req): return HttpResponseForbidden("You do not have permission to perform this action") if request.method == "POST" and request.POST.get("action") == "reject": - # reject the old request - prev_state = review_req.state + # reject the request review_req.state = ReviewRequestStateName.objects.get(slug="rejected") review_req.save() - # assignment of reviewer is currently considered minutia, so - # not reported in the log - if prev_state.slug == "accepted": - DocEvent.objects.create( - type="changed_review_request", - doc=doc, - by=request.user.person, - desc="Request for {} review by {} is unassigned".format(review_req.type.name, review_req.team.acronym.upper()), - ) - - # make a new, open review request - ReviewRequest.objects.create( + DocEvent.objects.create( + type="changed_review_request", + doc=review_req.doc, + 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, + ), + ) + + # make a new unassigned review request + new_review_req = ReviewRequest.objects.create( time=review_req.time, type=review_req.type, doc=review_req.doc, team=review_req.team, deadline=review_req.deadline, requested_rev=review_req.requested_rev, - state=prev_state, + state=ReviewRequestStateName.objects.get(slug="requested"), ) - return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk) + return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk) - return render(request, 'doc/review/reject_request_assignment.html', { + return render(request, 'doc/review/reject_reviewer_assignment.html', { 'doc': doc, 'review_req': review_req, }) diff --git a/ietf/review/migrations/0001_initial.py b/ietf/review/migrations/0001_initial.py index 2c19afa60..d9187804b 100644 --- a/ietf/review/migrations/0001_initial.py +++ b/ietf/review/migrations/0001_initial.py @@ -10,6 +10,7 @@ class Migration(migrations.Migration): ('group', '0008_auto_20160505_0523'), ('name', '0012_insert_review_name_data'), ('doc', '0012_auto_20160207_0537'), + ('person', '0006_auto_20160503_0937'), ] operations = [ @@ -17,11 +18,12 @@ class Migration(migrations.Migration): 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)), + ('frequency', models.IntegerField(default=30, help_text=b'Can review every N days')), + ('unavailable_until', 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.OneToOneField(to='group.Role')), + ('person', models.ForeignKey(to='person.Person')), + ('team', models.ForeignKey(to='group.Group')), ], options={ }, @@ -38,7 +40,7 @@ class Migration(migrations.Migration): ('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)), + ('reviewer', models.ForeignKey(blank=True, to='group.Role', null=True)), ('state', models.ForeignKey(to='name.ReviewRequestStateName')), ('team', models.ForeignKey(to='group.Group')), ('type', models.ForeignKey(to='name.ReviewTypeName')), diff --git a/ietf/review/models.py b/ietf/review/models.py index 7e166333c..530e0c939 100644 --- a/ietf/review/models.py +++ b/ietf/review/models.py @@ -2,36 +2,42 @@ from django.db import models from ietf.doc.models import Document from ietf.group.models import Group, Role +from ietf.person.models import Person 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.OneToOneField(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") + """Keeps track of admin data associated with the reviewer in the + particular team. There will be one record for each combination of + reviewer and team.""" + team = models.ForeignKey(Group) + person = models.ForeignKey(Person) + frequency = models.IntegerField(help_text="Can review every N days", default=30) + unavailable_until = 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): - """ + """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. - """ - # Fields filled in on the initial record creation: + document, rev, and reviewer.""" + state = models.ForeignKey(ReviewRequestStateName) + + # Fields filled in on the initial record creation - these + # constitute the request part. 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) + + # Fields filled in as reviewer is assigned and as the review is + # uploaded. Once these are filled in and we progress beyond the + # states 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 = models.ForeignKey(Role, 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) diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 0a1bd86bb..43d495e15 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -1,6 +1,48 @@ -from ietf.group.models import Group +from ietf.group.models import Group, Role +from ietf.doc.models import DocEvent +from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream +from ietf.review.models import ReviewRequestStateName def active_review_teams(): # if there's a ReviewResultName defined, it's a review team return Group.objects.filter(state="active").exclude(reviewresultname=None) + +def can_request_review_of_doc(user, doc): + if not user.is_authenticated(): + return False + + return is_authorized_in_doc_stream(user, doc) + +def can_manage_review_requests_for_team(user, team): + if not user.is_authenticated(): + return False + + return Role.objects.filter(name="secretary", person__user=user, group=team).exists() or has_role(user, "Secretariat") + +def assign_review_request_to_reviewer(review_req, reviewer, by): + assert review_req.state_id in ("requested", "accepted") + + if review_req.reviewer == reviewer: + return + + prev_state = review_req.state + prev_reviewer = review_req.reviewer + + review_req.state = ReviewRequestStateName.objects.get(slug="requested") + review_req.reviewer = reviewer + review_req.save() + + DocEvent.objects.create( + type="changed_review_request", + doc=review_req.doc, + by=by, + 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)", + ), + ) + if prev_state.slug != "requested" and prev_reviewer: + # FIXME: email old reviewer? + pass diff --git a/ietf/templates/doc/review/assign_reviewer.html b/ietf/templates/doc/review/assign_reviewer.html new file mode 100644 index 000000000..0e7059b03 --- /dev/null +++ b/ietf/templates/doc/review/assign_reviewer.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2016, All Rights Reserved #} +{% load origin bootstrap3 static %} + +{% block title %}Assign reviewer for {{ review_req.doc.name }}{% endblock %} + +{% block content %} + {% origin %} +

Assign reviewer
{{ review_req.doc.name }}

+ +
+ {% csrf_token %} + + {% bootstrap_form form %} + + {% buttons %} + Cancel + + {% endbuttons %} +
+ +{% endblock %} diff --git a/ietf/templates/doc/review/reject_request_assignment.html b/ietf/templates/doc/review/reject_reviewer_assignment.html similarity index 84% rename from ietf/templates/doc/review/reject_request_assignment.html rename to ietf/templates/doc/review/reject_reviewer_assignment.html index 9ccd6c56b..edc8d9696 100644 --- a/ietf/templates/doc/review/reject_request_assignment.html +++ b/ietf/templates/doc/review/reject_reviewer_assignment.html @@ -8,7 +8,7 @@ {% origin %}

Reject review assignment
{{ review_req.doc.name }}

-

{{ review_req.reviewer.role.person }} is currently assigned to do the review. Do you want to reject this assignment?

+

{{ review_req.reviewer.person }} is currently assigned to do the review. Do you want to reject this assignment?

{% csrf_token %} diff --git a/ietf/templates/doc/review/review_request.html b/ietf/templates/doc/review/review_request.html index 954c4adcf..844ff3c94 100644 --- a/ietf/templates/doc/review/review_request.html +++ b/ietf/templates/doc/review/review_request.html @@ -60,19 +60,25 @@ {{ review_req.state.name }} - {% if review_req.reviewer %} Reviewer - {{ review_req.reviewer.role.person }} + {% if review_req.reviewer %} + {{ review_req.reviewer.person }} + {% else %} + None assigned yet + {% endif %} - {% if can_reject_request_assignment %} - Reject request assignment + {% if can_assign_reviewer %} + {% if review_req.reviewer %}Reassign{% else %}Assign{% endif %} reviewer + {% endif %} + + {% if can_reject_reviewer_assignment %} + Reject reviewer assignment {% endif %} - {% endif %} {% if review_req.review %} @@ -94,7 +100,7 @@ Result of review - {{ review_req.result.name } + {{ review_req.result.name }} {% endif %}