Add review team secretary reminders, like those for reviewers
- Legacy-Id: 12283
This commit is contained in:
parent
a5c70ff9e3
commit
ceec1254f2
|
@ -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))
|
||||
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
29
ietf/review/migrations/0004_reviewsecretarysettings.py
Normal file
29
ietf/review/migrations/0004_reviewsecretarysettings.py
Normal 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,),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
20
ietf/templates/group/change_review_secretary_settings.html
Normal file
20
ietf/templates/group/change_review_secretary_settings.html
Normal 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 %}
|
|
@ -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>
|
||||
|
||||
|
|
8
ietf/templates/review/secretary_reminder.txt
Normal file
8
ietf/templates/review/secretary_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 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 %}
|
Loading…
Reference in a new issue