From afb818c29819a6886bca7dd4aa5d4d3ba57e70e3 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 21 Feb 2020 23:08:20 +0000 Subject: [PATCH] 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[\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 @@

Will be skipped the next time at the top of rotation.

Is not available to do reviews at this time.

+ {% if can_reset_next_reviewer %} +
+ Reset head of queue +
+ {% endif %} {% if reviewers %}