Skip to content

Commit abedd2d

Browse files
author
Sasha Romijn
committed
Add support for setting reviewer queue policies per team.
- Legacy-Id: 17052
1 parent 1e8dda0 commit abedd2d

File tree

10 files changed

+171
-21
lines changed

10 files changed

+171
-21
lines changed

ietf/name/admin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
# Copyright The IETF Trust 2016-2019, All Rights Reserved
12
from django.contrib import admin
23

34
from ietf.name.models import (
4-
AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName, DBTemplateTypeName,
5+
AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName,
6+
DBTemplateTypeName,
57
DocRelationshipName, DocReminderTypeName, DocTagName, DocTypeName, DraftSubmissionStateName,
68
FeedbackTypeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
79
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
810
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
911
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
1012
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
1113
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
12-
DocUrlTagName, ReviewAssignmentStateName)
14+
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName)
1315

1416
from ietf.stats.models import CountryAlias
1517

@@ -70,6 +72,7 @@ class ImportantDateNameAdmin(NameAdmin):
7072
admin.site.register(ReviewRequestStateName, NameAdmin)
7173
admin.site.register(ReviewAssignmentStateName, NameAdmin)
7274
admin.site.register(ReviewResultName, NameAdmin)
75+
admin.site.register(ReviewerQueuePolicyName, NameAdmin)
7376
admin.site.register(ReviewTypeName, NameAdmin)
7477
admin.site.register(RoleName, NameAdmin)
7578
admin.site.register(RoomResourceName, NameAdmin)

ietf/name/fixtures/names.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10971,6 +10971,26 @@
1097110971
"model": "name.reviewresultname",
1097210972
"pk": "serious-issues"
1097310973
},
10974+
{
10975+
"fields": {
10976+
"desc": "",
10977+
"name": "Rotate alphabetically",
10978+
"order": 1,
10979+
"used": true
10980+
},
10981+
"model": "name.reviewerqueuepolicyname",
10982+
"pk": "RotateAlphabetically"
10983+
},
10984+
{
10985+
"fields": {
10986+
"desc": "",
10987+
"name": "Least recently used",
10988+
"order": 2,
10989+
"used": true
10990+
},
10991+
"model": "name.reviewerqueuepolicyname",
10992+
"pk": "LeastRecentlyUsed"
10993+
},
1097410994
{
1097510995
"fields": {
1097610996
"desc": "",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright The IETF Trust 2019, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
# Generated by Django 1.11.23 on 2019-11-18 08:35
4+
from __future__ import unicode_literals
5+
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
def forward(apps, schema_editor):
11+
ReviewerQueuePolicyName = apps.get_model('name', 'ReviewerQueuePolicyName')
12+
ReviewerQueuePolicyName.objects.create(slug='RotateAlphabetically', name='Rotate alphabetically')
13+
ReviewerQueuePolicyName.objects.create(slug='LeastRecentlyUsed', name='Least recently used')
14+
15+
def reverse(self, apps):
16+
pass
17+
18+
dependencies = [
19+
('name', '0007_fix_m2m_slug_id_length'),
20+
]
21+
22+
operations = [
23+
migrations.CreateModel(
24+
name='ReviewerQueuePolicyName',
25+
fields=[
26+
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
27+
('name', models.CharField(max_length=255)),
28+
('desc', models.TextField(blank=True)),
29+
('used', models.BooleanField(default=True)),
30+
('order', models.IntegerField(default=0)),
31+
],
32+
options={
33+
'ordering': ['order', 'name'],
34+
'abstract': False,
35+
},
36+
),
37+
migrations.RunPython(forward, reverse),
38+
]

ietf/name/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ class ReviewResultName(NameModel):
110110
"""Almost ready, Has issues, Has nits, Not Ready,
111111
On the right track, Ready, Ready with issues,
112112
Ready with nits, Serious Issues"""
113+
class ReviewerQueuePolicyName(NameModel):
114+
"""RotateAlphabetically, LeastRecentlyUsed"""
113115
class TopicAudienceName(NameModel):
114116
"""General, Nominee, Nomcom Member"""
115117
class ContinentName(NameModel):

ietf/name/resources.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Copyright The IETF Trust 2016-2019, All Rights Reserved
12
# Autogenerated by the makeresources management command 2015-08-27 11:01 PDT
23
from ietf.api import ModelResource
34
from ietf.api import ToOneField # pyflakes:ignore
@@ -7,16 +8,24 @@
78

89
from ietf import api
910

10-
from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintName,
11-
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName, DocReminderTypeName,
12-
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName, FeedbackTypeName,
13-
FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
14-
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
15-
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
16-
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
17-
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
18-
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
19-
TopicAudienceName, )
11+
from ietf.name.models import (AgendaTypeName, BallotPositionName, ConstraintName,
12+
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName,
13+
DocReminderTypeName,
14+
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName,
15+
FeedbackTypeName,
16+
FormalLanguageName, GroupMilestoneStateName, GroupStateName,
17+
GroupTypeName,
18+
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName,
19+
IprEventTypeName,
20+
IprLicenseTypeName, LiaisonStatementEventTypeName,
21+
LiaisonStatementPurposeName,
22+
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName,
23+
NomineePositionStateName,
24+
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName,
25+
ReviewTypeName,
26+
RoleName, RoomResourceName, SessionStatusName, StdLevelName,
27+
StreamName, TimeSlotTypeName,
28+
TopicAudienceName, ReviewerQueuePolicyName)
2029

2130
class TimeSlotTypeNameResource(ModelResource):
2231
class Meta:
@@ -471,6 +480,19 @@ class Meta:
471480
}
472481
api.name.register(ReviewResultNameResource())
473482

483+
class ReviewerQueuePolicyNameResource(ModelResource):
484+
class Meta:
485+
cache = SimpleCache()
486+
queryset = ReviewerQueuePolicyName.objects.all()
487+
filtering = {
488+
"slug": ALL,
489+
"name": ALL,
490+
"desc": ALL,
491+
"used": ALL,
492+
"order": ALL,
493+
}
494+
api.name.register(ReviewerQueuePolicyNameResource())
495+
474496
class TopicAudienceNameResource(ModelResource):
475497
class Meta:
476498
cache = SimpleCache()

ietf/review/factories.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
# Copyright The IETF Trust 2016-2019, All Rights Reserved
12
import factory
23
import datetime
34

45
from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings
56
from ietf.name.models import ReviewTypeName, ReviewResultName
67

8+
79
class ReviewTeamSettingsFactory(factory.DjangoModelFactory):
810
class Meta:
911
model = ReviewTeamSettings
1012

1113
group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review')
14+
reviewer_queue_policy_id = 'RotateAlphabetically'
1215

1316
@factory.post_generation
1417
def review_types(obj, create, extracted, **kwargs):
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright The IETF Trust 2019, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
# Generated by Django 1.11.23 on 2019-11-18 08:50
4+
from __future__ import unicode_literals
5+
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('name', '0008_reviewerqueuepolicyname'),
14+
('review', '0020_add_request_assignment_next'),
15+
]
16+
17+
operations = [
18+
migrations.AddField(
19+
model_name='reviewteamsettings',
20+
name='reviewer_queue_policy',
21+
field=models.ForeignKey(default='RotateAlphabetically', on_delete=django.db.models.deletion.PROTECT, to='name.ReviewerQueuePolicyName'),
22+
),
23+
]

ietf/review/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from ietf.doc.models import Document
1515
from ietf.group.models import Group
1616
from ietf.person.models import Person, Email
17-
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName, ReviewAssignmentStateName
17+
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName, \
18+
ReviewAssignmentStateName, ReviewerQueuePolicyName
1819
from ietf.utils.validators import validate_regular_expression_string
1920
from ietf.utils.models import ForeignKey, OneToOneField
2021

@@ -184,6 +185,7 @@ class ReviewTeamSettings(models.Model):
184185
"""Holds configuration specific to groups that are review teams"""
185186
group = OneToOneField(Group)
186187
autosuggest = models.BooleanField(default=True, verbose_name="Automatically suggest possible review requests")
188+
reviewer_queue_policy = models.ForeignKey(ReviewerQueuePolicyName, default='RotateAlphabetically', on_delete=models.PROTECT)
187189
review_types = models.ManyToManyField(ReviewTypeName, default=get_default_review_types)
188190
review_results = models.ManyToManyField(ReviewResultName, default=get_default_review_results, related_name='reviewteamsettings_review_results_set')
189191
notify_ad_when = models.ManyToManyField(ReviewResultName, related_name='reviewteamsettings_notify_ad_set', blank=True)

ietf/review/policies.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ietf.person.models import Person
1414
import debug # pyflakes:ignore
1515
from ietf.review.models import NextReviewerInTeam, ReviewerSettings, ReviewWish, ReviewRequest, \
16-
ReviewAssignment
16+
ReviewAssignment, ReviewTeamSettings
1717
from ietf.review.utils import (current_unavailable_periods_for_reviewers,
1818
days_needed_to_fulfill_min_interval_for_reviewers,
1919
get_default_filter_re,
@@ -28,7 +28,17 @@
2828

2929

3030
def get_reviewer_queue_policy(team):
31-
return RotateAlphabeticallyReviewerQueuePolicy(team)
31+
try:
32+
settings = ReviewTeamSettings.objects.get(group=team)
33+
except ReviewTeamSettings.DoesNotExist:
34+
raise ValueError('Request for a reviewer queue policy for team {} '
35+
'which has no ReviewTeamSettings'.format(team))
36+
try:
37+
policy = QUEUE_POLICY_NAME_MAPPING[settings.reviewer_queue_policy.slug]
38+
except KeyError:
39+
raise ValueError('Team {} has unknown reviewer queue policy: '
40+
'{}'.format(team, settings.reviewer_queue_policy.slug))
41+
return policy(team)
3242

3343

3444
class AbstractReviewerQueuePolicy:
@@ -406,3 +416,8 @@ def default_reviewer_rotation_list(self, include_unavailable=False, dont_skip_pe
406416

407417
return rotation_list
408418

419+
420+
QUEUE_POLICY_NAME_MAPPING = {
421+
'RotateAlphabetically': RotateAlphabeticallyReviewerQueuePolicy,
422+
'LeastRecentlyUsed': LeastRecentlyUsedReviewerQueuePolicy,
423+
}

ietf/review/test_policies.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,38 @@
33
from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory
44
from ietf.group.factories import ReviewTeamFactory
55
from ietf.group.models import Group, Role
6+
from ietf.name.models import ReviewerQueuePolicyName
67
from ietf.person.fields import PersonEmailChoiceField
78
from ietf.person.models import Email
89
from ietf.review.factories import ReviewAssignmentFactory, ReviewRequestFactory
9-
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish
10-
from ietf.review.policies import get_reviewer_queue_policy, AssignmentOrderResolver, \
11-
LeastRecentlyUsedReviewerQueuePolicy, RotateAlphabeticallyReviewerQueuePolicy
10+
from ietf.review.models import ReviewerSettings, NextReviewerInTeam, UnavailablePeriod, ReviewWish, \
11+
ReviewTeamSettings
12+
from ietf.review.policies import (AssignmentOrderResolver, LeastRecentlyUsedReviewerQueuePolicy,
13+
RotateAlphabeticallyReviewerQueuePolicy,
14+
get_reviewer_queue_policy)
1215
from ietf.utils.test_data import create_person
1316
from ietf.utils.test_utils import TestCase
1417

1518

19+
class GetReviewerQueuePolicyTest(TestCase):
20+
def test_valid_policy(self):
21+
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"), settings__reviewer_queue_policy_id='LeastRecentlyUsed')
22+
policy = get_reviewer_queue_policy(team)
23+
self.assertEqual(policy.__class__, LeastRecentlyUsedReviewerQueuePolicy)
24+
25+
def test_missing_settings(self):
26+
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
27+
ReviewTeamSettings.objects.all().delete()
28+
with self.assertRaises(ValueError):
29+
get_reviewer_queue_policy(team)
30+
31+
def test_invalid_policy_name(self):
32+
ReviewerQueuePolicyName.objects.create(slug='invalid')
33+
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"), settings__reviewer_queue_policy_id='invalid')
34+
with self.assertRaises(ValueError):
35+
get_reviewer_queue_policy(team)
36+
37+
1638
class RotateAlphabeticallyReviewerQueuePolicyTest(TestCase):
1739
"""
1840
These tests also cover the common behaviour in RotateAlphabeticallyReviewerQueuePolicy,
@@ -63,7 +85,7 @@ def test_default_reviewer_rotation_list(self):
6385

6486
def test_setup_reviewer_field(self):
6587
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
66-
policy = get_reviewer_queue_policy(team)
88+
policy = RotateAlphabeticallyReviewerQueuePolicy(team)
6789
reviewer_0 = create_person(team, "reviewer", name="Test Reviewer-0", username="testreviewer0")
6890
reviewer_1 = create_person(team, "reviewer", name="Test Reviewer-1", username="testreviewer1")
6991
review_req = ReviewRequestFactory(team=team, type_id='early')
@@ -80,7 +102,7 @@ def test_setup_reviewer_field(self):
80102

81103
def test_recommended_assignment_order(self):
82104
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
83-
policy = get_reviewer_queue_policy(team)
105+
policy = RotateAlphabeticallyReviewerQueuePolicy(team)
84106
reviewer_high = create_person(team, "reviewer", name="Test Reviewer-1-high", username="testreviewerhigh")
85107
reviewer_low = create_person(team, "reviewer", name="Test Reviewer-0-low", username="testreviewerlow")
86108

@@ -100,7 +122,7 @@ def test_recommended_assignment_order(self):
100122

101123
def test_update_policy_state_for_assignment(self):
102124
team = ReviewTeamFactory(acronym="rotationteam", name="Review Team", list_email="rotationteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
103-
policy = get_reviewer_queue_policy(team)
125+
policy = RotateAlphabeticallyReviewerQueuePolicy(team)
104126

105127
# make a bunch of reviewers
106128
reviewers = [

0 commit comments

Comments
 (0)