Add support for setting reviewer queue policies per team.

- Legacy-Id: 17052
This commit is contained in:
Sasha Romijn 2019-11-18 17:29:25 +00:00
parent 1e8dda0440
commit abedd2d970
10 changed files with 171 additions and 21 deletions

View file

@ -1,7 +1,9 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
from django.contrib import admin
from ietf.name.models import (
AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName, DBTemplateTypeName,
AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName,
DBTemplateTypeName,
DocRelationshipName, DocReminderTypeName, DocTagName, DocTypeName, DraftSubmissionStateName,
FeedbackTypeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
@ -9,7 +11,7 @@ from ietf.name.models import (
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
DocUrlTagName, ReviewAssignmentStateName)
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName)
from ietf.stats.models import CountryAlias
@ -70,6 +72,7 @@ admin.site.register(NomineePositionStateName, NameAdmin)
admin.site.register(ReviewRequestStateName, NameAdmin)
admin.site.register(ReviewAssignmentStateName, NameAdmin)
admin.site.register(ReviewResultName, NameAdmin)
admin.site.register(ReviewerQueuePolicyName, NameAdmin)
admin.site.register(ReviewTypeName, NameAdmin)
admin.site.register(RoleName, NameAdmin)
admin.site.register(RoomResourceName, NameAdmin)

View file

@ -10971,6 +10971,26 @@
"model": "name.reviewresultname",
"pk": "serious-issues"
},
{
"fields": {
"desc": "",
"name": "Rotate alphabetically",
"order": 1,
"used": true
},
"model": "name.reviewerqueuepolicyname",
"pk": "RotateAlphabetically"
},
{
"fields": {
"desc": "",
"name": "Least recently used",
"order": 2,
"used": true
},
"model": "name.reviewerqueuepolicyname",
"pk": "LeastRecentlyUsed"
},
{
"fields": {
"desc": "",

View file

@ -0,0 +1,38 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-11-18 08:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
def forward(apps, schema_editor):
ReviewerQueuePolicyName = apps.get_model('name', 'ReviewerQueuePolicyName')
ReviewerQueuePolicyName.objects.create(slug='RotateAlphabetically', name='Rotate alphabetically')
ReviewerQueuePolicyName.objects.create(slug='LeastRecentlyUsed', name='Least recently used')
def reverse(self, apps):
pass
dependencies = [
('name', '0007_fix_m2m_slug_id_length'),
]
operations = [
migrations.CreateModel(
name='ReviewerQueuePolicyName',
fields=[
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
migrations.RunPython(forward, reverse),
]

View file

@ -110,6 +110,8 @@ class ReviewResultName(NameModel):
"""Almost ready, Has issues, Has nits, Not Ready,
On the right track, Ready, Ready with issues,
Ready with nits, Serious Issues"""
class ReviewerQueuePolicyName(NameModel):
"""RotateAlphabetically, LeastRecentlyUsed"""
class TopicAudienceName(NameModel):
"""General, Nominee, Nomcom Member"""
class ContinentName(NameModel):

View file

@ -1,3 +1,4 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
# Autogenerated by the makeresources management command 2015-08-27 11:01 PDT
from ietf.api import ModelResource
from ietf.api import ToOneField # pyflakes:ignore
@ -7,16 +8,24 @@ from tastypie.cache import SimpleCache
from ietf import api
from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintName,
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName, DocReminderTypeName,
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName, FeedbackTypeName,
FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, )
from ietf.name.models import (AgendaTypeName, BallotPositionName, ConstraintName,
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName,
DocReminderTypeName,
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName,
FeedbackTypeName,
FormalLanguageName, GroupMilestoneStateName, GroupStateName,
GroupTypeName,
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName,
IprEventTypeName,
IprLicenseTypeName, LiaisonStatementEventTypeName,
LiaisonStatementPurposeName,
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName,
NomineePositionStateName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName,
ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName,
StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName)
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -471,6 +480,19 @@ class ReviewResultNameResource(ModelResource):
}
api.name.register(ReviewResultNameResource())
class ReviewerQueuePolicyNameResource(ModelResource):
class Meta:
cache = SimpleCache()
queryset = ReviewerQueuePolicyName.objects.all()
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(ReviewerQueuePolicyNameResource())
class TopicAudienceNameResource(ModelResource):
class Meta:
cache = SimpleCache()

View file

@ -1,14 +1,17 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
import factory
import datetime
from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings
from ietf.name.models import ReviewTypeName, ReviewResultName
class ReviewTeamSettingsFactory(factory.DjangoModelFactory):
class Meta:
model = ReviewTeamSettings
group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review')
reviewer_queue_policy_id = 'RotateAlphabetically'
@factory.post_generation
def review_types(obj, create, extracted, **kwargs):

View file

@ -0,0 +1,23 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-11-18 08:50
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('name', '0008_reviewerqueuepolicyname'),
('review', '0020_add_request_assignment_next'),
]
operations = [
migrations.AddField(
model_name='reviewteamsettings',
name='reviewer_queue_policy',
field=models.ForeignKey(default='RotateAlphabetically', on_delete=django.db.models.deletion.PROTECT, to='name.ReviewerQueuePolicyName'),
),
]

View file

@ -14,7 +14,8 @@ from django.utils.encoding import python_2_unicode_compatible
from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.person.models import Person, Email
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName, ReviewAssignmentStateName
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName, \
ReviewAssignmentStateName, ReviewerQueuePolicyName
from ietf.utils.validators import validate_regular_expression_string
from ietf.utils.models import ForeignKey, OneToOneField
@ -184,6 +185,7 @@ class ReviewTeamSettings(models.Model):
"""Holds configuration specific to groups that are review teams"""
group = OneToOneField(Group)
autosuggest = models.BooleanField(default=True, verbose_name="Automatically suggest possible review requests")
reviewer_queue_policy = models.ForeignKey(ReviewerQueuePolicyName, default='RotateAlphabetically', on_delete=models.PROTECT)
review_types = models.ManyToManyField(ReviewTypeName, default=get_default_review_types)
review_results = models.ManyToManyField(ReviewResultName, default=get_default_review_results, related_name='reviewteamsettings_review_results_set')
notify_ad_when = models.ManyToManyField(ReviewResultName, related_name='reviewteamsettings_notify_ad_set', blank=True)

View file

@ -13,7 +13,7 @@ from ietf.group.models import Role
from ietf.person.models import Person
import debug # pyflakes:ignore
from ietf.review.models import NextReviewerInTeam, ReviewerSettings, ReviewWish, ReviewRequest, \
ReviewAssignment
ReviewAssignment, ReviewTeamSettings
from ietf.review.utils import (current_unavailable_periods_for_reviewers,
days_needed_to_fulfill_min_interval_for_reviewers,
get_default_filter_re,
@ -28,7 +28,17 @@ Terminology used here should match terminology used in that document.
def get_reviewer_queue_policy(team):
return RotateAlphabeticallyReviewerQueuePolicy(team)
try:
settings = ReviewTeamSettings.objects.get(group=team)
except ReviewTeamSettings.DoesNotExist:
raise ValueError('Request for a reviewer queue policy for team {} '
'which has no ReviewTeamSettings'.format(team))
try:
policy = QUEUE_POLICY_NAME_MAPPING[settings.reviewer_queue_policy.slug]
except KeyError:
raise ValueError('Team {} has unknown reviewer queue policy: '
'{}'.format(team, settings.reviewer_queue_policy.slug))
return policy(team)
class AbstractReviewerQueuePolicy:
@ -406,3 +416,8 @@ class LeastRecentlyUsedReviewerQueuePolicy(AbstractReviewerQueuePolicy):
return rotation_list
QUEUE_POLICY_NAME_MAPPING = {
'RotateAlphabetically': RotateAlphabeticallyReviewerQueuePolicy,
'LeastRecentlyUsed': LeastRecentlyUsedReviewerQueuePolicy,
}

View file

@ -3,16 +3,38 @@
from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory
from ietf.group.factories import ReviewTeamFactory
from ietf.group.models import Group, Role
from ietf.name.models import ReviewerQueuePolicyName
from ietf.person.fields import PersonEmailChoiceField
from ietf.person.models import Email
from ietf.review.factories import ReviewAssignmentFactory, ReviewRequestFactory
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish
from ietf.review.policies import get_reviewer_queue_policy, AssignmentOrderResolver, \
LeastRecentlyUsedReviewerQueuePolicy, RotateAlphabeticallyReviewerQueuePolicy
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish, \
ReviewTeamSettings
from ietf.review.policies import (AssignmentOrderResolver, LeastRecentlyUsedReviewerQueuePolicy,
RotateAlphabeticallyReviewerQueuePolicy,
get_reviewer_queue_policy)
from ietf.utils.test_data import create_person
from ietf.utils.test_utils import TestCase
class GetReviewerQueuePolicyTest(TestCase):
def test_valid_policy(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"), settings__reviewer_queue_policy_id='LeastRecentlyUsed')
policy = get_reviewer_queue_policy(team)
self.assertEqual(policy.__class__, LeastRecentlyUsedReviewerQueuePolicy)
def test_missing_settings(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
ReviewTeamSettings.objects.all().delete()
with self.assertRaises(ValueError):
get_reviewer_queue_policy(team)
def test_invalid_policy_name(self):
ReviewerQueuePolicyName.objects.create(slug='invalid')
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"), settings__reviewer_queue_policy_id='invalid')
with self.assertRaises(ValueError):
get_reviewer_queue_policy(team)
class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
"""
These tests also cover the common behaviour in RotateAlphabeticallyReviewerQueuePolicy,
@ -63,7 +85,7 @@ class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
def test_setup_reviewer_field(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
policy = get_reviewer_queue_policy(team)
policy = RotateAlphabeticallyReviewerQueuePolicy(team)
reviewer_0 = create_person(team, "reviewer", name="Test Reviewer-0", username="testreviewer0")
reviewer_1 = create_person(team, "reviewer", name="Test Reviewer-1", username="testreviewer1")
review_req = ReviewRequestFactory(team=team, type_id='early')
@ -80,7 +102,7 @@ class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
def test_recommended_assignment_order(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
policy = get_reviewer_queue_policy(team)
policy = RotateAlphabeticallyReviewerQueuePolicy(team)
reviewer_high = create_person(team, "reviewer", name="Test Reviewer-1-high", username="testreviewerhigh")
reviewer_low = create_person(team, "reviewer", name="Test Reviewer-0-low", username="testreviewerlow")
@ -100,7 +122,7 @@ class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
def test_update_policy_state_for_assignment(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
policy = get_reviewer_queue_policy(team)
policy = RotateAlphabeticallyReviewerQueuePolicy(team)
# make a bunch of reviewers
reviewers = [