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
This commit is contained in:
Robert Sparks 2020-02-21 23:08:20 +00:00
parent 52d21e29cb
commit afb818c298
5 changed files with 111 additions and 14 deletions

View file

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

View file

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

View file

@ -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'),
]

View file

@ -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,})

View file

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