From afb818c29819a6886bca7dd4aa5d4d3ba57e70e3 Mon Sep 17 00:00:00 2001
From: Robert Sparks <rjsparks@nostrum.com>
Date: Fri, 21 Feb 2020 23:08:20 +0000
Subject: [PATCH 1/2] Allow review team secretaries and the secretariat to
 reset the next reviewer in queue for review teams using the
 RotateAlphabetically policy. Partially addresses #2879. Commit ready for
 merge.  - Legacy-Id: 17325

---
 ietf/group/factories.py                     |  7 +--
 ietf/group/tests_review.py                  | 62 +++++++++++++++++++--
 ietf/group/urls.py                          |  3 +-
 ietf/group/views.py                         | 46 ++++++++++++++-
 ietf/templates/group/reviewer_overview.html |  7 ++-
 5 files changed, 111 insertions(+), 14 deletions(-)

diff --git a/ietf/group/factories.py b/ietf/group/factories.py
index 35f802189..64468ece8 100644
--- a/ietf/group/factories.py
+++ b/ietf/group/factories.py
@@ -18,14 +18,9 @@ class GroupFactory(factory.DjangoModelFactory):
     list_email = factory.LazyAttribute(lambda a: '%s@ietf.org'% a.acronym)
     uses_milestone_dates = True
 
-class ReviewTeamFactory(factory.DjangoModelFactory):
-    class Meta:
-        model = Group
+class ReviewTeamFactory(GroupFactory):
 
     type_id = 'review'
-    name = factory.Faker('sentence',nb_words=6)
-    acronym = factory.Sequence(lambda n: 'acronym%d' %n)
-    state_id = 'active'
 
     @factory.post_generation
     def settings(obj, create, extracted, **kwargs):
diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py
index 139266f36..f498a2edb 100644
--- a/ietf/group/tests_review.py
+++ b/ietf/group/tests_review.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2016-2019, All Rights Reserved
+# Copyright The IETF Trust 2016-2020, All Rights Reserved
 # -*- coding: utf-8 -*-
 
 
@@ -19,7 +19,7 @@ from ietf.group.models import Role
 from ietf.iesg.models import TelechatDate
 from ietf.person.models import Person
 from ietf.review.models import ( ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings, 
-    ReviewTeamSettings )
+    ReviewTeamSettings, NextReviewerInTeam )
 from ietf.review.utils import (
     suggested_review_requests_for_team,
     review_assignments_needing_reviewer_reminder, email_reviewer_reminder,
@@ -33,7 +33,7 @@ from ietf.utils.mail import outbox, empty_outbox
 from ietf.dbtemplate.factories import DBTemplateFactory
 from ietf.person.factories import PersonFactory, EmailFactory
 from ietf.doc.factories import DocumentFactory
-from ietf.group.factories import RoleFactory, ReviewTeamFactory
+from ietf.group.factories import RoleFactory, ReviewTeamFactory, GroupFactory
 from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory
 
 class ReviewTests(TestCase):
@@ -929,4 +929,58 @@ class BulkAssignmentTests(TestCase):
         self.assertEqual(r.status_code,302)
         self.assertEqual(expected_ending_head_of_rotation, policy.default_reviewer_rotation_list()[0])
         self.assertMailboxContains(outbox, subject='Last Call assignment', text='Requested by', count=4)
-        
+     
+class ResetNextReviewerInTeamTests(TestCase):
+
+    def test_reviewer_overview_navigation(self):
+        group = ReviewTeamFactory(settings__reviewer_queue_policy_id = 'RotateAlphabetically')
+        url = urlreverse(ietf.group.views.reviewer_overview, kwargs={ 'acronym': group.acronym })
+
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertFalse(q('#reset_next_reviewer'))
+
+        self.client.login(username="secretary", password="secretary+password")
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertTrue(q('#reset_next_reviewer'))
+
+        group.reviewteamsettings.reviewer_queue_policy_id='LeastRecentlyUsed'
+        group.reviewteamsettings.save()
+
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertFalse(q('#reset_next_reviewer'))
+
+
+    def test_reset_next_reviewer(self):
+        PersonFactory(user__username='plain')
+        for group in (GroupFactory(), ReviewTeamFactory(settings__reviewer_queue_policy_id='LeastRecentlyUsed')):
+            url = urlreverse('ietf.group.views.reset_next_reviewer', kwargs=dict(acronym=group.acronym))
+            r = self.client.get(url)
+            self.assertEqual(r.status_code, 302)
+            self.client.login(username='plain',password='plain+password')
+            r = self.client.get(url)
+            self.assertEqual(r.status_code, 404)
+            self.client.logout()
+
+        group = ReviewTeamFactory(settings__reviewer_queue_policy_id='RotateAlphabetically')
+        secr = RoleFactory(name_id='secr',group=group).person
+        reviewers = RoleFactory.create_batch(10, name_id='reviewer',group=group)
+        NextReviewerInTeam.objects.create(team = group, next_reviewer=reviewers[4].person)
+
+        target_index = 6
+        url = urlreverse('ietf.group.views.reset_next_reviewer', kwargs=dict(acronym=group.acronym))
+        for user in (secr.user.username, 'secretary'):
+            login_testing_unauthorized(self,user,url)
+            r = self.client.get(url)
+            self.assertEqual(r.status_code,200)
+            r = self.client.post(url,{'next_reviewer':reviewers[target_index].person.pk})
+            self.assertEqual(r.status_code,302)
+            self.assertEqual(NextReviewerInTeam.objects.get(team=group).next_reviewer, reviewers[target_index].person)
+            self.client.logout()
+            target_index += 2
+
diff --git a/ietf/group/urls.py b/ietf/group/urls.py
index 20af6f73b..4e563d9c6 100644
--- a/ietf/group/urls.py
+++ b/ietf/group/urls.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2007, All Rights Reserved
+# Copyright The IETF Trust 2013-2020, All Rights Reserved
 
 from django.conf import settings
 from django.conf.urls import include
@@ -45,6 +45,7 @@ info_detail_urls = [
     url(r'^reviewers/$', views.reviewer_overview),
     url(r'^reviewers/(?P<reviewer_email>[\w%+-.@]+)/settings/$', views.change_reviewer_settings),
     url(r'^secretarysettings/$', views.change_review_secretary_settings),
+    url(r'^reset_next_reviewer/$', views.reset_next_reviewer),
     url(r'^email-aliases/$', RedirectView.as_view(pattern_name=views.email,permanent=False),name='ietf.group.urls_info_details.redirect.email'),
 ]
 
diff --git a/ietf/group/views.py b/ietf/group/views.py
index a6805e0ff..6994625b2 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -90,7 +90,7 @@ from ietf.mailtrigger.utils import gather_relevant_expansions
 from ietf.meeting.helpers import get_meeting
 from ietf.meeting.utils import group_sessions, add_event_info_to_session_qs
 from ietf.name.models import GroupTypeName, StreamName
-from ietf.person.models import Email
+from ietf.person.models import Email, Person
 from ietf.review.models import (ReviewRequest, ReviewAssignment, ReviewerSettings, 
                                 ReviewSecretarySettings, UnavailablePeriod )
 from ietf.review.policies import get_reviewer_queue_policy
@@ -1405,6 +1405,8 @@ def reviewer_overview(request, acronym, group_type=None):
 
     can_manage = can_manage_review_requests_for_team(request.user, group)
 
+    can_reset_next_reviewer = can_manage and group.reviewteamsettings.reviewer_queue_policy_id == 'RotateAlphabetically'
+
     reviewers = get_reviewer_queue_policy(group).default_reviewer_rotation_list(include_unavailable=True)
 
     reviewer_settings = { s.person_id: s for s in ReviewerSettings.objects.filter(team=group) }
@@ -1478,7 +1480,8 @@ def reviewer_overview(request, acronym, group_type=None):
     return render(request, 'group/reviewer_overview.html',
                   construct_group_menu_context(request, group, "reviewers", group_type, {
                       "reviewers": reviewers,
-                      "can_access_stats": can_access_review_stats_for_team(request.user, group)
+                      "can_access_stats": can_access_review_stats_for_team(request.user, group),
+                      "can_reset_next_reviewer": can_reset_next_reviewer,
                   }))
 
 
@@ -1933,3 +1936,42 @@ def add_comment(request, acronym, group_type=None):
         form = AddCommentForm()
 
     return render(request, 'group/add_comment.html', { 'group':group, 'form':form, })
+
+class ResetNextReviewerForm(forms.Form):
+    next_reviewer = forms.ChoiceField()
+
+    def __init__(self, *args, **kwargs):
+        instance = kwargs.pop('instance')
+        super(ResetNextReviewerForm, self).__init__(*args, **kwargs)
+        self.fields['next_reviewer'].choices = [ (p.pk, p.plain_name()) for p in get_reviewer_queue_policy(instance.team).default_reviewer_rotation_list(include_unavailable=True)]
+
+@login_required
+def reset_next_reviewer(request, acronym, group_type=None):
+    group = get_group_or_404(acronym, group_type)
+    if not group.features.has_reviews:
+        raise Http404
+    if group.reviewteamsettings.reviewer_queue_policy_id != 'RotateAlphabetically':
+        raise Http404
+
+    if not Role.objects.filter(name="secr", group=group, person__user=request.user).exists() and not has_role(request.user, "Secretariat"):
+        return HttpResponseForbidden("You don't have permission to access this view")
+
+    instance = group.nextreviewerinteam_set.first()
+    if not instance:
+        raise Http404
+
+    if request.method == 'POST':
+        form = ResetNextReviewerForm(request.POST,instance=instance)
+        if form.is_valid():
+            instance.next_reviewer = Person.objects.get(pk=form.cleaned_data['next_reviewer'])
+            instance.save()
+            return redirect('ietf.group.views.reviewer_overview', acronym = group.acronym )
+    else:
+        form = ResetNextReviewerForm(instance=instance)
+
+    return render(request, 'group/reset_next_reviewer.html', { 'group':group, 'form': form,})
+
+
+
+
+
diff --git a/ietf/templates/group/reviewer_overview.html b/ietf/templates/group/reviewer_overview.html
index a3734ca61..c1edb9e7e 100644
--- a/ietf/templates/group/reviewer_overview.html
+++ b/ietf/templates/group/reviewer_overview.html
@@ -1,5 +1,5 @@
 {% extends "group/group_base.html" %}
-{# Copyright The IETF Trust 2015, All Rights Reserved #}
+{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
 {% load origin %}{% origin %}
 
 {% load ietf_filters staticfiles bootstrap3 %}
@@ -27,6 +27,11 @@
     <p class="skip-next">Will be skipped the next time at the top of rotation.</p>
     <p class="completely-unavailable">Is not available to do reviews at this time.</p>
   </div>
+  {% if can_reset_next_reviewer %}
+    <div>
+      <a href="{% url 'ietf.group.views.reset_next_reviewer' acronym=group.acronym %}" class="btn btn-default" id="reset_next_reviewer">Reset head of queue</a>
+    </div>
+  {% endif %}
 
   {% if reviewers %}
     <table class="table reviewer-overview tablesorter">

From 92b2f06829071a2eece00232defe16a759b253c3 Mon Sep 17 00:00:00 2001
From: Robert Sparks <rjsparks@nostrum.com>
Date: Thu, 27 Feb 2020 16:50:16 +0000
Subject: [PATCH 2/2] Add missed template for commit r17325. Commit ready for
 merge.  - Legacy-Id: 17357

---
 ietf/templates/group/reset_next_reviewer.html | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)
 create mode 100644 ietf/templates/group/reset_next_reviewer.html

diff --git a/ietf/templates/group/reset_next_reviewer.html b/ietf/templates/group/reset_next_reviewer.html
new file mode 100644
index 000000000..cbae7205d
--- /dev/null
+++ b/ietf/templates/group/reset_next_reviewer.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
+{% load origin %}{% origin %}
+
+{% load ietf_filters staticfiles bootstrap3 %}
+
+{% block content %}
+<h2>Set next reviewer in queue for {{ group.acronym }}</h2>
+<form id="dbtemplate-edit" role="form" method="post">
+  {% csrf_token %}
+
+  {% bootstrap_form form %}
+
+  {% buttons %}
+    <button class="btn btn-default" type="submit">Save</button>
+    <a class="btn btn-default" href="{% url 'ietf.group.views.reviewer_overview' acronym=group.acronym %}">Cancel</a>
+  {% endbuttons %}
+
+</form>
+{% endblock %}
\ No newline at end of file