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 django.contrib import admin
from ietf.name.models import ( from ietf.name.models import (
AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName, DBTemplateTypeName, AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName,
DBTemplateTypeName,
DocRelationshipName, DocReminderTypeName, DocTagName, DocTypeName, DraftSubmissionStateName, DocRelationshipName, DocReminderTypeName, DocTagName, DocTypeName, DraftSubmissionStateName,
FeedbackTypeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName, FeedbackTypeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName, ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
@ -9,7 +11,7 @@ from ietf.name.models import (
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName, LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName, ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
DocUrlTagName, ReviewAssignmentStateName) DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName)
from ietf.stats.models import CountryAlias from ietf.stats.models import CountryAlias
@ -70,6 +72,7 @@ admin.site.register(NomineePositionStateName, NameAdmin)
admin.site.register(ReviewRequestStateName, NameAdmin) admin.site.register(ReviewRequestStateName, NameAdmin)
admin.site.register(ReviewAssignmentStateName, NameAdmin) admin.site.register(ReviewAssignmentStateName, NameAdmin)
admin.site.register(ReviewResultName, NameAdmin) admin.site.register(ReviewResultName, NameAdmin)
admin.site.register(ReviewerQueuePolicyName, NameAdmin)
admin.site.register(ReviewTypeName, NameAdmin) admin.site.register(ReviewTypeName, NameAdmin)
admin.site.register(RoleName, NameAdmin) admin.site.register(RoleName, NameAdmin)
admin.site.register(RoomResourceName, NameAdmin) admin.site.register(RoomResourceName, NameAdmin)

View file

@ -10971,6 +10971,26 @@
"model": "name.reviewresultname", "model": "name.reviewresultname",
"pk": "serious-issues" "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": { "fields": {
"desc": "", "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, """Almost ready, Has issues, Has nits, Not Ready,
On the right track, Ready, Ready with issues, On the right track, Ready, Ready with issues,
Ready with nits, Serious Issues""" Ready with nits, Serious Issues"""
class ReviewerQueuePolicyName(NameModel):
"""RotateAlphabetically, LeastRecentlyUsed"""
class TopicAudienceName(NameModel): class TopicAudienceName(NameModel):
"""General, Nominee, Nomcom Member""" """General, Nominee, Nomcom Member"""
class ContinentName(NameModel): 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 # Autogenerated by the makeresources management command 2015-08-27 11:01 PDT
from ietf.api import ModelResource from ietf.api import ModelResource
from ietf.api import ToOneField # pyflakes:ignore from ietf.api import ToOneField # pyflakes:ignore
@ -7,16 +8,24 @@ from tastypie.cache import SimpleCache
from ietf import api from ietf import api
from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintName, from ietf.name.models import (AgendaTypeName, BallotPositionName, ConstraintName,
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName, DocReminderTypeName, ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName,
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName, FeedbackTypeName, DocReminderTypeName,
FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName, DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName,
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName, FeedbackTypeName,
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName,
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName, GroupTypeName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName, ImportantDateName, IntendedStdLevelName, IprDisclosureStateName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, IprEventTypeName,
TopicAudienceName, ) IprLicenseTypeName, LiaisonStatementEventTypeName,
LiaisonStatementPurposeName,
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName,
NomineePositionStateName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName,
ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName,
StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName)
class TimeSlotTypeNameResource(ModelResource): class TimeSlotTypeNameResource(ModelResource):
class Meta: class Meta:
@ -471,6 +480,19 @@ class ReviewResultNameResource(ModelResource):
} }
api.name.register(ReviewResultNameResource()) 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 TopicAudienceNameResource(ModelResource):
class Meta: class Meta:
cache = SimpleCache() cache = SimpleCache()

View file

@ -1,14 +1,17 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
import factory import factory
import datetime import datetime
from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings
from ietf.name.models import ReviewTypeName, ReviewResultName from ietf.name.models import ReviewTypeName, ReviewResultName
class ReviewTeamSettingsFactory(factory.DjangoModelFactory): class ReviewTeamSettingsFactory(factory.DjangoModelFactory):
class Meta: class Meta:
model = ReviewTeamSettings model = ReviewTeamSettings
group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review') group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review')
reviewer_queue_policy_id = 'RotateAlphabetically'
@factory.post_generation @factory.post_generation
def review_types(obj, create, extracted, **kwargs): 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.doc.models import Document
from ietf.group.models import Group from ietf.group.models import Group
from ietf.person.models import Person, Email 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.validators import validate_regular_expression_string
from ietf.utils.models import ForeignKey, OneToOneField from ietf.utils.models import ForeignKey, OneToOneField
@ -184,6 +185,7 @@ class ReviewTeamSettings(models.Model):
"""Holds configuration specific to groups that are review teams""" """Holds configuration specific to groups that are review teams"""
group = OneToOneField(Group) group = OneToOneField(Group)
autosuggest = models.BooleanField(default=True, verbose_name="Automatically suggest possible review requests") 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_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') 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) 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 from ietf.person.models import Person
import debug # pyflakes:ignore import debug # pyflakes:ignore
from ietf.review.models import NextReviewerInTeam, ReviewerSettings, ReviewWish, ReviewRequest, \ from ietf.review.models import NextReviewerInTeam, ReviewerSettings, ReviewWish, ReviewRequest, \
ReviewAssignment ReviewAssignment, ReviewTeamSettings
from ietf.review.utils import (current_unavailable_periods_for_reviewers, from ietf.review.utils import (current_unavailable_periods_for_reviewers,
days_needed_to_fulfill_min_interval_for_reviewers, days_needed_to_fulfill_min_interval_for_reviewers,
get_default_filter_re, get_default_filter_re,
@ -28,7 +28,17 @@ Terminology used here should match terminology used in that document.
def get_reviewer_queue_policy(team): 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: class AbstractReviewerQueuePolicy:
@ -406,3 +416,8 @@ class LeastRecentlyUsedReviewerQueuePolicy(AbstractReviewerQueuePolicy):
return rotation_list 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.doc.factories import WgDraftFactory, IndividualDraftFactory
from ietf.group.factories import ReviewTeamFactory from ietf.group.factories import ReviewTeamFactory
from ietf.group.models import Group, Role from ietf.group.models import Group, Role
from ietf.name.models import ReviewerQueuePolicyName
from ietf.person.fields import PersonEmailChoiceField from ietf.person.fields import PersonEmailChoiceField
from ietf.person.models import Email from ietf.person.models import Email
from ietf.review.factories import ReviewAssignmentFactory, ReviewRequestFactory from ietf.review.factories import ReviewAssignmentFactory, ReviewRequestFactory
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish, \
from ietf.review.policies import get_reviewer_queue_policy, AssignmentOrderResolver, \ ReviewTeamSettings
LeastRecentlyUsedReviewerQueuePolicy, RotateAlphabeticallyReviewerQueuePolicy from ietf.review.policies import (AssignmentOrderResolver, LeastRecentlyUsedReviewerQueuePolicy,
RotateAlphabeticallyReviewerQueuePolicy,
get_reviewer_queue_policy)
from ietf.utils.test_data import create_person from ietf.utils.test_data import create_person
from ietf.utils.test_utils import TestCase 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): class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
""" """
These tests also cover the common behaviour in RotateAlphabeticallyReviewerQueuePolicy, These tests also cover the common behaviour in RotateAlphabeticallyReviewerQueuePolicy,
@ -63,7 +85,7 @@ class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
def test_setup_reviewer_field(self): def test_setup_reviewer_field(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut")) 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_0 = create_person(team, "reviewer", name="Test Reviewer-0", username="testreviewer0")
reviewer_1 = create_person(team, "reviewer", name="Test Reviewer-1", username="testreviewer1") reviewer_1 = create_person(team, "reviewer", name="Test Reviewer-1", username="testreviewer1")
review_req = ReviewRequestFactory(team=team, type_id='early') review_req = ReviewRequestFactory(team=team, type_id='early')
@ -80,7 +102,7 @@ class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
def test_recommended_assignment_order(self): def test_recommended_assignment_order(self):
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut")) 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_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") 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): 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")) 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 # make a bunch of reviewers
reviewers = [ reviewers = [