Add support for setting reviewer queue policies per team.
- Legacy-Id: 17052
This commit is contained in:
parent
1e8dda0440
commit
abedd2d970
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
38
ietf/name/migrations/0008_reviewerqueuepolicyname.py
Normal file
38
ietf/name/migrations/0008_reviewerqueuepolicyname.py
Normal 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),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 = [
|
||||
|
|
Loading…
Reference in a new issue