Add review team secretary reminders, like those for reviewers

- Legacy-Id: 12283
This commit is contained in:
Ole Laursen 2016-11-08 16:24:28 +00:00
parent a5c70ff9e3
commit ceec1254f2
11 changed files with 222 additions and 21 deletions

View file

@ -18,8 +18,18 @@ import django
django.setup()
import datetime
from ietf.review.utils import review_requests_needing_reviewer_reminder, email_reviewer_reminder
from ietf.review.utils import (
review_requests_needing_reviewer_reminder, email_reviewer_reminder,
review_requests_needing_secretary_reminder, email_secretary_reminder,
)
for review_req in review_requests_needing_reviewer_reminder(datetime.date.today()):
today = datetime.date.today()
for review_req in review_requests_needing_reviewer_reminder(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))
for review_req, secretary_role in review_requests_needing_secretary_reminder(today):
email_secretary_reminder(review_req, secretary_role)
print("Emailed reminder to {} for review of {} in {} (req. id {})".format(review_req.secretary_role.email.address, review_req.doc_id, review_req.team.acronym, review_req.pk))

View file

@ -7,11 +7,15 @@ from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.test_data import make_test_data, make_review_data
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects
from ietf.doc.models import TelechatDocEvent
from ietf.group.models import Role
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.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.utils import (
suggested_review_requests_for_team,
review_requests_needing_reviewer_reminder, email_reviewer_reminder,
review_requests_needing_secretary_reminder, email_secretary_reminder,
)
from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName
import ietf.group.views_review
from ietf.utils.mail import outbox, empty_outbox
@ -403,23 +407,33 @@ class ReviewTests(TestCase):
self.assertTrue(start_date.isoformat(), msg_content)
self.assertTrue(end_date.isoformat(), msg_content)
def test_reviewer_reminders(self):
def test_review_reminders(self):
doc = make_test_data()
review_req = make_review_data(doc)
remind_days = 6
reviewer = Person.objects.get(user__username="reviewer")
settings = ReviewerSettings.objects.get(team=review_req.team, person=reviewer)
settings.remind_days_before_deadline = 6
settings.save()
reviewer_settings = ReviewerSettings.objects.get(team=review_req.team, person=reviewer)
reviewer_settings.remind_days_before_deadline = remind_days
reviewer_settings.save()
secretary = Person.objects.get(user__username="reviewsecretary")
secretary_role = Role.objects.get(group=review_req.team, name="secr", person=secretary)
secretary_settings = ReviewSecretarySettings(team=review_req.team, person=secretary)
secretary_settings.remind_days_before_deadline = remind_days
secretary_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.deadline = today + datetime.timedelta(days=remind_days)
review_req.save()
# reviewer
needing_reminders = review_requests_needing_reviewer_reminder(today - datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
@ -429,7 +443,25 @@ class ReviewTests(TestCase):
needing_reminders = review_requests_needing_reviewer_reminder(today + datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
# secretary
needing_reminders = review_requests_needing_secretary_reminder(today - datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
needing_reminders = review_requests_needing_secretary_reminder(today)
self.assertEqual(list(needing_reminders), [(review_req, secretary_role)])
needing_reminders = review_requests_needing_secretary_reminder(today + datetime.timedelta(days=1))
self.assertEqual(list(needing_reminders), [])
# email reviewer
empty_outbox()
email_reviewer_reminder(review_req)
self.assertEqual(len(outbox), 1)
self.assertTrue(review_req.doc_id in outbox[0].get_payload(decode=True).decode("utf-8"))
# email secretary
empty_outbox()
email_secretary_reminder(review_req, secretary_role)
self.assertEqual(len(outbox), 1)
self.assertTrue(review_req.doc_id in outbox[0].get_payload(decode=True).decode("utf-8"))

View file

@ -35,5 +35,6 @@ urlpatterns = patterns('',
(r'^reviews/email-assignments/$', views_review.email_open_review_assignments),
(r'^reviewers/$', views_review.reviewer_overview),
(r'^reviewers/(?P<reviewer_email>[\w%+-.@]+)/settings/$', views_review.change_reviewer_settings),
(r'^secretarysettings/$', views_review.change_secretary_settings),
url(r'^email-aliases/$', RedirectView.as_view(pattern_name='ietf.group.views.email',permanent=False),name='old_group_email_aliases'),
)

View file

@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.group.models import Group, RoleHistory
from ietf.group.models import Group, RoleHistory, Role
from ietf.person.models import Email
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
from ietf.ietfauth.utils import has_role
@ -216,6 +216,10 @@ def construct_group_menu_context(request, group, selected, group_type, others):
actions.append((u"Manage unassigned reviews", urlreverse(ietf.group.views_review.manage_review_requests, kwargs=dict(assignment_status="unassigned", **kwargs))))
actions.append((u"Manage assigned reviews", urlreverse(ietf.group.views_review.manage_review_requests, kwargs=dict(assignment_status="assigned", **kwargs))))
if Role.objects.filter(name="secr", group=group, person__user=request.user).exists():
actions.append((u"Secretary settings", urlreverse(ietf.group.views_review.change_secretary_settings, kwargs=kwargs)))
if group.state_id != "conclude" and (is_admin or can_manage):
actions.append((u"Edit group", urlreverse("group_edit", kwargs=kwargs)))

View file

@ -8,7 +8,7 @@ from django.core.urlresolvers import reverse as urlreverse
from django import forms
from django.template.loader import render_to_string
from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod
from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.utils import (can_manage_review_requests_for_team,
can_access_review_stats_for_team,
close_review_request_states,
@ -563,3 +563,41 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
'period_form': period_form,
'unavailable_periods': unavailable_periods,
})
class ReviewSecretarySettingsForm(forms.ModelForm):
class Meta:
model = ReviewSecretarySettings
fields = ['remind_days_before_deadline']
@login_required
def change_secretary_settings(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
if not Role.objects.filter(name="secr", group=group, person__user=request.user).exists():
raise Http404
person = request.user.person
settings = (ReviewSecretarySettings.objects.filter(person=person, team=group).first()
or ReviewSecretarySettings(person=person, team=group))
import ietf.group.views_review
back_url = urlreverse(ietf.group.views_review.review_requests, kwargs={ "acronym": acronym, "group_type": group.type_id })
# settings
if request.method == "POST":
settings_form = ReviewSecretarySettingsForm(request.POST, instance=settings)
if settings_form.is_valid():
settings_form.save()
return HttpResponseRedirect(back_url)
else:
settings_form = ReviewSecretarySettingsForm(instance=settings)
return render(request, 'group/change_review_secretary_settings.html', {
'group': group,
'back_url': back_url,
'settings_form': settings_form,
})

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('person', '0014_auto_20160613_0751'),
('group', '0009_auto_20150930_0758'),
('review', '0003_auto_20161018_0254'),
]
operations = [
migrations.CreateModel(
name='ReviewSecretarySettings',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('remind_days_before_deadline', models.IntegerField(help_text=b"To get an email reminder in case an assigned review gets near its deadline, enter the number of days before a review deadline you want to receive it. Clear the field if you don't want a reminder.", null=True, blank=True)),
('person', models.ForeignKey(to='person.Person')),
('team', models.ForeignKey(to='group.Group')),
],
options={
'verbose_name_plural': 'review secretary settings',
},
bases=(models.Model,),
),
]

View file

@ -8,9 +8,7 @@ from ietf.person.models import Person, Email
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName
class ReviewerSettings(models.Model):
"""Keeps track of admin data associated with the reviewer in the
particular team. There will be one record for each combination of
reviewer and team."""
"""Keeps track of admin data associated with a reviewer in a team."""
team = models.ForeignKey(Group, limit_choices_to=~models.Q(resultusedinreviewteam=None))
person = models.ForeignKey(Person)
INTERVALS = [
@ -23,7 +21,7 @@ class ReviewerSettings(models.Model):
min_interval = models.IntegerField(verbose_name="Can review at most", choices=INTERVALS, blank=True, null=True)
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.")
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 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)
@ -31,6 +29,18 @@ class ReviewerSettings(models.Model):
class Meta:
verbose_name_plural = "reviewer settings"
class ReviewSecretarySettings(models.Model):
"""Keeps track of admin data associated with a secretary in a team."""
team = models.ForeignKey(Group, limit_choices_to=~models.Q(resultusedinreviewteam=None))
person = models.ForeignKey(Person)
remind_days_before_deadline = models.IntegerField(null=True, blank=True, help_text="To get an email reminder in case a reviewer forgets to do an assigned review, enter the number of days before 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)
class Meta:
verbose_name_plural = "review secretary settings"
class UnavailablePeriod(models.Model):
team = models.ForeignKey(Group, limit_choices_to=~models.Q(resultusedinreviewteam=None))
person = models.ForeignKey(Person)

View file

@ -13,7 +13,8 @@ from ietf.iesg.models import TelechatDate
from ietf.person.models import Person
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
from ietf.review.models import (ReviewRequest, ReviewRequestStateName, ReviewTypeName, TypeUsedInReviewTeam,
ReviewerSettings, UnavailablePeriod, ReviewWish, NextReviewerInTeam)
ReviewerSettings, UnavailablePeriod, ReviewWish, NextReviewerInTeam,
ReviewSecretarySettings)
from ietf.utils.mail import send_mail
from ietf.doc.utils import extract_complete_replaces_ancestor_mapping_for_docs
@ -840,8 +841,10 @@ def email_reviewer_reminder(review_request):
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 })
import ietf.ietfauth.views
overview_url = urlreverse(ietf.ietfauth.views.review_overview)
import ietf.doc.views_review
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
@ -855,3 +858,48 @@ def email_reviewer_reminder(review_request):
"deadline_days": deadline_days,
"remind_days": remind_days,
})
def review_requests_needing_secretary_reminder(remind_date):
reqs_qs = ReviewRequest.objects.filter(
state__in=("requested", "accepted"),
team__role__person__reviewsecretarysettings__remind_days_before_deadline__isnull=False,
team__role__person__reviewsecretarysettings__team=F("team"),
).exclude(
reviewer=None
).values_list("pk", "deadline", "team__role", "team__role__person__reviewsecretarysettings__remind_days_before_deadline").distinct()
req_pks = {}
for r_pk, deadline, secretary_role_pk, remind_days in reqs_qs:
if (deadline - remind_date).days == remind_days:
req_pks[r_pk] = secretary_role_pk
review_reqs = { r.pk: r for r in ReviewRequest.objects.filter(pk__in=req_pks.keys()).select_related("reviewer", "reviewer__person", "state", "team") }
secretary_roles = { r.pk: r for r in Role.objects.filter(pk__in=req_pks.values()).select_related("email", "person") }
return [ (review_reqs[req_pk], secretary_roles[secretary_role_pk]) for req_pk, secretary_role_pk in req_pks.iteritems() ]
def email_secretary_reminder(review_request, secretary_role):
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())
import ietf.group.views_review
settings_url = urlreverse(ietf.group.views_review.change_secretary_settings, kwargs={ "acronym": team.acronym, "group_type": team.type_id })
import ietf.doc.views_review
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 = ReviewSecretarySettings.objects.filter(person=secretary_role.person_id, 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/secretary_reminder.txt", {
"review_request_url": "https://{}{}".format(domain, request_url),
"settings_url": "https://{}{}".format(domain, settings_url),
"review_request": review_request,
"deadline_days": deadline_days,
"remind_days": remind_days,
})

View file

@ -0,0 +1,20 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load ietf_filters staticfiles bootstrap3 %}
{% block content %}
{% origin %}
<h1>{% block title %}Change your review secretary settings for {{ group.acronym }}{% endblock %}</h1>
<form class="change-review-secretary-settings" method="post">{% csrf_token %}
{% bootstrap_form settings_form %}
{% buttons %}
<a href="{{ back_url }}" class="btn btn-default pull-right">Cancel</a>
<button class="btn btn-primary" type="submit" name="action" value="change_settings">Save</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -16,7 +16,8 @@
<h1>Manage {{ assignment_status }} open review requests for {{ group.acronym }}</h1>
<p>Other options:
<a href="{% url "ietf.group.views_review.reviewer_overview" group_type=group.type_id acronym=group.acronym %}">Reviewers in team</a>
<a href="{% url "ietf.group.views_review.review_requests" group_type=group.type_id acronym=group.acronym %}">All review requests</a>
- <a href="{% url "ietf.group.views_review.reviewer_overview" group_type=group.type_id acronym=group.acronym %}">Reviewers</a>
- <a href="{% url "ietf.group.views_review.email_open_review_assignments" group_type=group.type_id acronym=group.acronym %}?next={{ request.get_full_path|urlencode }}">Email open assignments summary</a>
{% if other_assignment_status %}
- <a href="{% url "ietf.group.views_review.manage_review_requests" group_type=group.type_id acronym=group.acronym assignment_status=other_assignment_status %}">Manage {{ other_assignment_status }} reviews</a>
@ -46,7 +47,7 @@
<h3 class="panel-title">
<span class="pull-right">
{{ r.type.name }}
- deadline: {{ r.deadline|date:"Y-m-d" }}
- deadline {{ r.deadline|date:"Y-m-d" }}
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
</span>

View 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 as secretary of the review team because you have configured the Datatracker to remind you {{ remind_days }} day{{ remind_days|pluralize }} before deadlines in {{ review_request.team.name }}. You can change your settings here:
{{ settings_url }}
{% endfilter %}{% endautoescape %}