Add a simple reminder system for reviewers so that they can opt in to
being reminded X days before deadline of an assignment - Legacy-Id: 12101
This commit is contained in:
parent
5e030ed206
commit
59180240af
25
ietf/bin/send-reviewer-reminders
Executable file
25
ietf/bin/send-reviewer-reminders
Executable file
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os, sys
|
||||
import syslog
|
||||
|
||||
# boilerplate
|
||||
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
sys.path = [ basedir ] + sys.path
|
||||
os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings"
|
||||
|
||||
virtualenv_activation = os.path.join(basedir, "bin", "activate_this.py")
|
||||
if os.path.exists(virtualenv_activation):
|
||||
execfile(virtualenv_activation, dict(__file__=virtualenv_activation))
|
||||
|
||||
syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER)
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
import datetime
|
||||
from ietf.review.utils import review_requests_needing_reviewer_reminder, email_reviewer_reminder
|
||||
|
||||
for review_req in review_requests_needing_reviewer_reminder(datetime.date.today()):
|
||||
email_reviewer_reminder(review_req)
|
||||
print("Emailed reminder to {} for review of {} in {} (req. id {})".format(review_req.reviewer.address, review_req.doc_id, review_req.team.acronym, review_req.pk))
|
|
@ -11,6 +11,7 @@ from ietf.iesg.models import TelechatDate
|
|||
from ietf.person.models import Email, Person
|
||||
from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod
|
||||
from ietf.review.utils import suggested_review_requests_for_team
|
||||
from ietf.review.utils import review_requests_needing_reviewer_reminder, email_reviewer_reminder
|
||||
from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName
|
||||
import ietf.group.views_review
|
||||
from ietf.utils.mail import outbox, empty_outbox
|
||||
|
@ -316,12 +317,14 @@ class ReviewTests(TestCase):
|
|||
"min_interval": "7",
|
||||
"filter_re": "test-[regexp]",
|
||||
"skip_next": "2",
|
||||
"remind_days_before_deadline": "6"
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
settings = ReviewerSettings.objects.get(person=reviewer, team=review_req.team)
|
||||
self.assertEqual(settings.min_interval, 7)
|
||||
self.assertEqual(settings.filter_re, "test-[regexp]")
|
||||
self.assertEqual(settings.skip_next, 2)
|
||||
self.assertEqual(settings.remind_days_before_deadline, 6)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue("reviewer availability" in outbox[0]["subject"].lower())
|
||||
self.assertTrue("frequency changed", unicode(outbox[0]).lower())
|
||||
|
@ -370,3 +373,35 @@ class ReviewTests(TestCase):
|
|||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue(start_date.isoformat(), unicode(outbox[0]).lower())
|
||||
self.assertTrue(end_date.isoformat(), unicode(outbox[0]).lower())
|
||||
|
||||
def test_reviewer_reminders(self):
|
||||
doc = make_test_data()
|
||||
|
||||
reviewer = Person.objects.get(name="Plain Man")
|
||||
|
||||
review_req = make_review_data(doc)
|
||||
|
||||
settings = ReviewerSettings.objects.get(team=review_req.team, person=reviewer)
|
||||
settings.remind_days_before_deadline = 6
|
||||
settings.save()
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
review_req.reviewer = reviewer.email_set.first()
|
||||
review_req.deadline = today + datetime.timedelta(days=settings.remind_days_before_deadline)
|
||||
review_req.save()
|
||||
|
||||
needing_reminders = review_requests_needing_reviewer_reminder(today - datetime.timedelta(days=1))
|
||||
self.assertEqual(list(needing_reminders), [])
|
||||
|
||||
needing_reminders = review_requests_needing_reviewer_reminder(today)
|
||||
self.assertEqual(list(needing_reminders), [review_req])
|
||||
|
||||
needing_reminders = review_requests_needing_reviewer_reminder(today + datetime.timedelta(days=1))
|
||||
self.assertEqual(list(needing_reminders), [])
|
||||
|
||||
empty_outbox()
|
||||
email_reviewer_reminder(review_req)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
print outbox[0]
|
||||
self.assertTrue(review_req.doc_id in unicode(outbox[0]))
|
||||
|
|
|
@ -350,7 +350,7 @@ def email_open_review_assignments(request, acronym, group_type=None):
|
|||
class ReviewerSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ReviewerSettings
|
||||
fields = ['min_interval', 'filter_re', 'skip_next']
|
||||
fields = ['min_interval', 'filter_re', 'skip_next', 'remind_days_before_deadline']
|
||||
|
||||
class AddUnavailablePeriodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@ -406,7 +406,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
|
|||
back_url = request.GET.get("next")
|
||||
if not back_url:
|
||||
import ietf.group.views_review
|
||||
back_url = urlreverse(ietf.group.views_review.review_requests, kwargs={ "group_type": group.type_id, "acronym": group.acronym})
|
||||
back_url = urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ "group_type": group.type_id, "acronym": group.acronym})
|
||||
|
||||
# settings
|
||||
if request.method == "POST" and request.POST.get("action") == "change_settings":
|
||||
|
|
|
@ -26,6 +26,17 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ResultUsedInReviewTeam',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('result', models.ForeignKey(to='name.ReviewResultName')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReviewerSettings',
|
||||
fields=[
|
||||
|
@ -33,6 +44,7 @@ class Migration(migrations.Migration):
|
|||
('min_interval', models.IntegerField(default=30, verbose_name=b'Can review at most', choices=[(7, b'Once per week'), (14, b'Once per fortnight'), (30, b'Once per month'), (61, b'Once per two months'), (91, b'Once per quarter')])),
|
||||
('filter_re', models.CharField(help_text=b'Draft names matching regular expression should not be assigned', max_length=255, verbose_name=b'Filter regexp', blank=True)),
|
||||
('skip_next', models.IntegerField(default=0, verbose_name=b'Skip next assignments')),
|
||||
('remind_days_before_deadline', models.IntegerField(null=True, blank=True)),
|
||||
('person', models.ForeignKey(to='person.Person')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
|
@ -63,10 +75,12 @@ class Migration(migrations.Migration):
|
|||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ResultUsedInReviewTeam',
|
||||
name='ReviewWish',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('result', models.ForeignKey(to='name.ReviewResultName')),
|
||||
('time', models.DateTimeField(default=datetime.datetime.now)),
|
||||
('doc', models.ForeignKey(to='doc.Document')),
|
||||
('person', models.ForeignKey(to='person.Person')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
options={
|
||||
|
@ -84,19 +98,6 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReviewWish',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('time', models.DateTimeField(default=datetime.datetime.now)),
|
||||
('doc', models.ForeignKey(to='doc.Document')),
|
||||
('person', models.ForeignKey(to='person.Person')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UnavailablePeriod',
|
||||
fields=[
|
||||
|
|
|
@ -23,6 +23,7 @@ class ReviewerSettings(models.Model):
|
|||
min_interval = models.IntegerField(default=30, verbose_name="Can review at most", choices=INTERVALS)
|
||||
filter_re = models.CharField(max_length=255, verbose_name="Filter regexp", blank=True, help_text="Draft names matching regular expression should not be assigned")
|
||||
skip_next = models.IntegerField(default=0, verbose_name="Skip next assignments")
|
||||
remind_days_before_deadline = models.IntegerField(null=True, blank=True, help_text="To get an email reminder in case you forget to do an assigned review, enter the number of days before a review deadline you want to receive it. Clear the field if you don't want a reminder.")
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} in {}".format(self.person, self.team)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import datetime, re, itertools
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models import Q, Max
|
||||
from django.db.models import Q, Max, F
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.doc.models import (Document, ReviewRequestDocEvent, State,
|
||||
|
@ -674,3 +675,42 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
ranking.sort(key=lambda r: r["scores"], reverse=True)
|
||||
|
||||
return [(r["email"].pk, r["label"]) for r in ranking]
|
||||
|
||||
def review_requests_needing_reviewer_reminder(remind_date):
|
||||
reqs_qs = ReviewRequest.objects.filter(
|
||||
state__in=("requested", "accepted"),
|
||||
reviewer__person__reviewersettings__remind_days_before_deadline__isnull=False,
|
||||
reviewer__person__reviewersettings__team=F("team"),
|
||||
).exclude(
|
||||
reviewer=None
|
||||
).values_list("pk", "deadline", "reviewer__person__reviewersettings__remind_days_before_deadline").distinct()
|
||||
|
||||
req_pks = []
|
||||
for r_pk, deadline, remind_days in reqs_qs:
|
||||
if (deadline - remind_date).days == remind_days:
|
||||
req_pks.append(r_pk)
|
||||
|
||||
return ReviewRequest.objects.filter(pk__in=req_pks).select_related("reviewer", "reviewer__person", "state", "team")
|
||||
|
||||
def email_reviewer_reminder(review_request):
|
||||
team = review_request.team
|
||||
|
||||
deadline_days = (review_request.deadline - datetime.date.today()).days
|
||||
|
||||
subject = "Reminder: deadline for review of {} in {} is {}".format(review_request.doc_id, team.acronym, review_request.deadline.isoformat())
|
||||
|
||||
overview_url = urlreverse("ietf.ietfauth.views.review_overview")
|
||||
request_url = urlreverse("ietf.doc.views_review.review_request", kwargs={ "name": review_request.doc_id, "request_id": review_request.pk })
|
||||
|
||||
domain = Site.objects.get_current().domain
|
||||
|
||||
settings = ReviewerSettings.objects.filter(person=review_request.reviewer.person, team=team).first()
|
||||
remind_days = settings.remind_days_before_deadline if settings else 0
|
||||
|
||||
send_mail(None, [review_request.reviewer.formatted_email()], None, subject, "review/reviewer_reminder.txt", {
|
||||
"reviewer_overview_url": "https://{}{}".format(domain, overview_url),
|
||||
"review_request_url": "https://{}{}".format(domain, request_url),
|
||||
"review_request": review_request,
|
||||
"deadline_days": deadline_days,
|
||||
"remind_days": remind_days,
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<a href="{% if review_request.review %}{% url "doc_view" review_request.review.name %}{% else %}{% url "ietf.doc.views_review.review_request" review_request.doc_id review_request.pk %}{% endif %}">
|
||||
{{ review_request.team.acronym|upper }} {{ review_request.type.name }} Review{% if review_request.reviewed_rev and review_request.reviewed_rev != current_rev or review_request.doc_id != current_doc_name %} (of {% if review_request.doc_id != current_doc_name %}{{ review_request.doc_id }}{% endif %}-{{ review_request.reviewed_rev }}){% endif %}{% if review_request.result %}:
|
||||
{{ review_request.result.name }}{% endif %} {% if review_request.state_id == "part-completed" %}(partially completed){% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<i>
|
||||
<a href="{% url "ietf.doc.views_review.review_request" review_request.doc_id review_request.pk %}">{{ review_request.team.acronym|upper }} {{ review_request.type.name }} Review
|
||||
|
|
|
@ -134,7 +134,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th>Filter regexp</th>
|
||||
<td><code>{{ t.reviewer_settings.filter_re|default:"(None)" }}</code></td>
|
||||
<td>{% if t.reviewer_settings.filter_re %}<code>{{ t.reviewer_settings.filter_re }}</code>{% else %}(None){% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Remind days before deadline</th>
|
||||
<td>{{ t.reviewer_settings.remind_days_before_deadline|default:"(Do not remind)" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Unavailable periods</th>
|
||||
|
|
8
ietf/templates/review/reviewer_reminder.txt
Normal file
8
ietf/templates/review/reviewer_reminder.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% autoescape off %}{% filter wordwrap:70 %}This is just a friendly reminder that the deadline for the review of {{ review_request.doc_id }} is in {{ deadline_days }} day{{ deadline_days|pluralize }}:
|
||||
|
||||
{{ review_request_url }}
|
||||
|
||||
You are receiving this reminder because you have configured the Datatracker to remind you {{ remind_days }} day{{ remind_days|pluralize }} before deadlines in {{ review_request.team.name }}. You can see your reviews and change your settings here:
|
||||
|
||||
{{ reviewer_overview_url }}
|
||||
{% endfilter %}{% endautoescape %}
|
Loading…
Reference in a new issue