Add personal review overview page for reviewers, add page for editing
reviewer availability settings, emailing the reviewer/secretary as necessary, add tests for these pages. Fix a bunch of bugs. - Legacy-Id: 11998
This commit is contained in:
parent
0a8f3dbe02
commit
6da25e6bd9
|
@ -12,7 +12,7 @@ from pyquery import PyQuery
|
|||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.review.models import ReviewRequest, ReviewTeamResult, ReviewerSettings
|
||||
from ietf.review.models import ReviewRequest, ReviewTeamResult, ReviewerSettings, ReviewWish, UnavailablePeriod
|
||||
import ietf.review.mailarch
|
||||
from ietf.person.models import Email, Person
|
||||
from ietf.name.models import ReviewResultName, ReviewRequestStateName, ReviewTypeName, DocRelationshipName
|
||||
|
@ -166,9 +166,17 @@ class ReviewTests(TestCase):
|
|||
|
||||
reviewer_settings = ReviewerSettings.objects.get(person__email=plain_email)
|
||||
reviewer_settings.filter_re = doc.name
|
||||
reviewer_settings.unavailable_until = datetime.datetime.now() + datetime.timedelta(days=10)
|
||||
reviewer_settings.save()
|
||||
|
||||
UnavailablePeriod.objects.create(
|
||||
team=review_req.team,
|
||||
person=plain_email.person,
|
||||
start_date=datetime.date.today() - datetime.timedelta(days=10),
|
||||
availability="unavailable",
|
||||
)
|
||||
|
||||
ReviewWish.objects.create(person=plain_email.person, team=review_req.team, doc=doc)
|
||||
|
||||
assign_url = urlreverse('ietf.doc.views_review.assign_reviewer', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
|
||||
|
||||
|
@ -188,9 +196,10 @@ class ReviewTests(TestCase):
|
|||
plain_label = q("option[value=\"{}\"]".format(plain_email.address)).text().lower()
|
||||
self.assertIn("ready for", plain_label)
|
||||
self.assertIn("reviewed document before", plain_label)
|
||||
self.assertIn("wishes to review", plain_label)
|
||||
self.assertIn("is author", plain_label)
|
||||
self.assertIn("regexp matches", plain_label)
|
||||
self.assertIn("unavailable until", plain_label)
|
||||
self.assertIn("unavailable", plain_label)
|
||||
|
||||
# assign
|
||||
empty_outbox()
|
||||
|
@ -422,6 +431,15 @@ class ReviewTests(TestCase):
|
|||
|
||||
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
|
||||
|
||||
# check the review document page
|
||||
url = urlreverse('doc_view', kwargs={ "name": review_req.review.name })
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
content = unicontent(r)
|
||||
self.assertTrue("{} Review".format(review_req.type.name) in content)
|
||||
self.assertTrue("This is a review" in content)
|
||||
|
||||
|
||||
def test_complete_review_enter_content(self):
|
||||
review_req, url = self.setup_complete_review_test()
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@ 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.review.models import ReviewRequest, ReviewRequestStateName
|
||||
from ietf.doc.models import TelechatDocEvent
|
||||
from ietf.iesg.models import TelechatDate
|
||||
from ietf.person.models import Email, Person
|
||||
from ietf.review.models import ReviewRequest, ReviewRequestStateName, ReviewerSettings, UnavailablePeriod
|
||||
from ietf.review.utils import suggested_review_requests_for_team
|
||||
import ietf.group.views_review
|
||||
from ietf.utils.mail import outbox, empty_outbox
|
||||
|
@ -190,3 +190,85 @@ class ReviewTests(TestCase):
|
|||
self.assertEqual(outbox[0]["subject"], "Test subject")
|
||||
self.assertTrue("Test body" in unicode(outbox[0]))
|
||||
|
||||
def test_change_reviewer_settings(self):
|
||||
doc = make_test_data()
|
||||
|
||||
reviewer = Person.objects.get(name="Plain Man")
|
||||
|
||||
review_req = make_review_data(doc)
|
||||
review_req.reviewer = reviewer.email_set.first()
|
||||
review_req.save()
|
||||
|
||||
url = urlreverse(ietf.group.views_review.change_reviewer_settings, kwargs={
|
||||
"group_type": review_req.team.type_id,
|
||||
"acronym": review_req.team.acronym,
|
||||
"reviewer_email": review_req.reviewer_id,
|
||||
})
|
||||
|
||||
login_testing_unauthorized(self, reviewer.user.username, url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# set settings
|
||||
empty_outbox()
|
||||
r = self.client.post(url, {
|
||||
"action": "change_settings",
|
||||
"min_interval": "7",
|
||||
"filter_re": "test-[regexp]",
|
||||
"skip_next": "2",
|
||||
})
|
||||
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(len(outbox), 1)
|
||||
self.assertTrue("reviewer availability" in outbox[0]["subject"].lower())
|
||||
self.assertTrue("frequency changed", unicode(outbox[0]).lower())
|
||||
self.assertTrue("skip next", unicode(outbox[0]).lower())
|
||||
|
||||
# add unavailable period
|
||||
start_date = datetime.date.today() + datetime.timedelta(days=10)
|
||||
empty_outbox()
|
||||
r = self.client.post(url, {
|
||||
"action": "add_period",
|
||||
'start_date': start_date.isoformat(),
|
||||
'end_date': "",
|
||||
'availability': "unavailable",
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
period = UnavailablePeriod.objects.get(person=reviewer, team=review_req.team, start_date=start_date)
|
||||
self.assertEqual(period.end_date, None)
|
||||
self.assertEqual(period.availability, "unavailable")
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue(start_date.isoformat(), unicode(outbox[0]).lower())
|
||||
self.assertTrue("indefinite", unicode(outbox[0]).lower())
|
||||
|
||||
# end unavailable period
|
||||
empty_outbox()
|
||||
end_date = start_date + datetime.timedelta(days=10)
|
||||
r = self.client.post(url, {
|
||||
"action": "end_period",
|
||||
'period_id': period.pk,
|
||||
'end_date': end_date.isoformat(),
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
period = reload_db_objects(period)
|
||||
self.assertEqual(period.end_date, end_date)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue(start_date.isoformat(), unicode(outbox[0]).lower())
|
||||
self.assertTrue("indefinite", unicode(outbox[0]).lower())
|
||||
|
||||
# delete unavailable period
|
||||
empty_outbox()
|
||||
r = self.client.post(url, {
|
||||
"action": "delete_period",
|
||||
'period_id': period.pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(UnavailablePeriod.objects.filter(person=reviewer, team=review_req.team, start_date=start_date).count(), 0)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue(start_date.isoformat(), unicode(outbox[0]).lower())
|
||||
self.assertTrue(end_date.isoformat(), unicode(outbox[0]).lower())
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.conf.urls import patterns, include
|
|||
from django.views.generic import RedirectView
|
||||
from django.conf import settings
|
||||
|
||||
from ietf.group import views, views_edit, views_review
|
||||
from ietf.group import views, views_edit
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', views.active_groups),
|
||||
|
@ -21,7 +21,5 @@ urlpatterns = patterns('',
|
|||
(r'^email-aliases/$', 'ietf.group.views.email_aliases'),
|
||||
(r'^bofs/create/$', views_edit.edit, {'action': "create", }, "bof_create"),
|
||||
(r'^photos/$', views.chair_photos),
|
||||
(r'^reviews/$', views.review_requests),
|
||||
(r'^reviews/manage/$', views_review.manage_review_requests),
|
||||
(r'^%(acronym)s/' % settings.URL_REGEXPS, include('ietf.group.urls_info_details')),
|
||||
)
|
||||
|
|
|
@ -33,5 +33,7 @@ urlpatterns = patterns('',
|
|||
(r'^reviews/$', views.review_requests),
|
||||
(r'^reviews/manage/$', views_review.manage_review_requests),
|
||||
(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),
|
||||
url(r'^email-aliases/$', RedirectView.as_view(pattern_name='ietf.group.views.email',permanent=False),name='old_group_email_aliases'),
|
||||
)
|
||||
|
|
|
@ -70,7 +70,9 @@ from ietf.ietfauth.utils import has_role
|
|||
from ietf.meeting.utils import group_sessions
|
||||
from ietf.meeting.helpers import get_meeting
|
||||
from ietf.review.models import ReviewRequest
|
||||
from ietf.review.utils import can_manage_review_requests_for_team, suggested_review_requests_for_team
|
||||
from ietf.review.utils import (can_manage_review_requests_for_team,
|
||||
suggested_review_requests_for_team,
|
||||
current_unavailable_periods_for_reviewers)
|
||||
|
||||
def roles(group, role_name):
|
||||
return Role.objects.filter(group=group, name=role_name).select_related("email", "person")
|
||||
|
@ -669,12 +671,17 @@ def review_requests(request, acronym, group_type=None):
|
|||
team=group, state__in=("requested", "accepted")
|
||||
).prefetch_related("reviewer", "type", "state").order_by("-time", "-id"))
|
||||
|
||||
open_review_requests += suggested_review_requests_for_team(group)
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(group)
|
||||
for review_req in open_review_requests:
|
||||
if review_req.reviewer:
|
||||
review_req.reviewer_unavailable = any(p.availability == "unavailable"
|
||||
for p in unavailable_periods.get(review_req.reviewer.person_id, []))
|
||||
|
||||
open_review_requests = suggested_review_requests_for_team(group) + open_review_requests
|
||||
|
||||
today = datetime.date.today()
|
||||
for r in open_review_requests:
|
||||
delta = today - r.deadline
|
||||
r.due = max(0, delta.days)
|
||||
r.due = max(0, (today - r.deadline).days)
|
||||
|
||||
closed_review_requests = ReviewRequest.objects.filter(
|
||||
team=group,
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
from django.shortcuts import render, redirect
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
import datetime
|
||||
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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
|
||||
from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod
|
||||
from ietf.review.utils import (can_manage_review_requests_for_team, close_review_request_states,
|
||||
extract_revision_ordered_review_requests_for_documents,
|
||||
assign_review_request_to_reviewer,
|
||||
close_review_request,
|
||||
setup_reviewer_field,
|
||||
suggested_review_requests_for_team)
|
||||
suggested_review_requests_for_team,
|
||||
unavailability_periods_to_list,
|
||||
current_unavailable_periods_for_reviewers,
|
||||
email_reviewer_availability_change)
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.utils import get_group_or_404
|
||||
from ietf.person.fields import PersonEmailChoiceField
|
||||
from ietf.utils.mail import send_mail_text
|
||||
from ietf.utils.fields import DatepickerDateField
|
||||
from ietf.ietfauth.utils import user_is_person
|
||||
|
||||
|
||||
class ManageReviewRequestForm(forms.Form):
|
||||
|
@ -71,17 +80,25 @@ def manage_review_requests(request, acronym, group_type=None):
|
|||
if not can_manage_review_requests_for_team(request.user, group):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
review_requests = list(ReviewRequest.objects.filter(
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(group)
|
||||
|
||||
open_review_requests = list(ReviewRequest.objects.filter(
|
||||
team=group, state__in=("requested", "accepted")
|
||||
).prefetch_related("reviewer", "type", "state").order_by("-time", "-id"))
|
||||
|
||||
review_requests += suggested_review_requests_for_team(group)
|
||||
for review_req in open_review_requests:
|
||||
if review_req.reviewer:
|
||||
review_req.reviewer_unavailable = any(p.availability == "unavailable"
|
||||
for p in unavailable_periods.get(review_req.reviewer.person_id, []))
|
||||
|
||||
review_requests = suggested_review_requests_for_team(group) + open_review_requests
|
||||
|
||||
document_requests = extract_revision_ordered_review_requests_for_documents(
|
||||
ReviewRequest.objects.filter(state__in=("part-completed", "completed"), team=group).prefetch_related("result"),
|
||||
set(r.doc_id for r in review_requests),
|
||||
)
|
||||
|
||||
|
||||
# we need a mutable query dict for resetting upon saving with
|
||||
# conflicts
|
||||
query_dict = request.POST.copy() if request.method == "POST" else None
|
||||
|
@ -204,6 +221,7 @@ def email_open_review_assignments(request, acronym, group_type=None):
|
|||
else:
|
||||
to = group.list_email
|
||||
subject = "Open review assignments in {}".format(group.acronym)
|
||||
# FIXME: add rotation info
|
||||
body = render_to_string("group/email_open_review_assignments.txt", {
|
||||
"review_requests": review_requests,
|
||||
})
|
||||
|
@ -220,3 +238,195 @@ def email_open_review_assignments(request, acronym, group_type=None):
|
|||
'form': form,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def reviewer_overview(request, acronym, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_reviews:
|
||||
raise Http404
|
||||
|
||||
reviewer_roles = Role.objects.filter(name="reviewer", group=group)
|
||||
|
||||
if not (any(user_is_person(request.user, r) for r in reviewer_roles)
|
||||
or can_manage_review_requests_for_team(request.user, group)):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
|
||||
class ReviewerSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ReviewerSettings
|
||||
fields = ['min_interval', 'filter_re', 'skip_next']
|
||||
|
||||
class AddUnavailablePeriodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UnavailablePeriod
|
||||
fields = ['start_date', 'end_date', 'availability']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AddUnavailablePeriodForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required)
|
||||
self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["end_date"].label, help_text=self.fields["end_date"].help_text, required=self.fields["end_date"].required)
|
||||
|
||||
self.fields['availability'].widget = forms.RadioSelect(choices=UnavailablePeriod.LONG_AVAILABILITY_CHOICES)
|
||||
|
||||
def clean(self):
|
||||
start = self.cleaned_data.get("start_date")
|
||||
end = self.cleaned_data.get("end_date")
|
||||
if start and end and start > end:
|
||||
self.add_error("start_date", "Start date must be before or equal to end date.")
|
||||
return self.cleaned_data
|
||||
|
||||
class EndUnavailablePeriodForm(forms.Form):
|
||||
def __init__(self, start_date, *args, **kwargs):
|
||||
super(EndUnavailablePeriodForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1", "start-date": start_date.isoformat() })
|
||||
|
||||
self.start_date = start_date
|
||||
|
||||
def clean_end_date(self):
|
||||
end = self.cleaned_data["end_date"]
|
||||
if end < self.start_date:
|
||||
raise forms.ValidationError("End date must be equal to or come after start date.")
|
||||
return end
|
||||
|
||||
|
||||
@login_required
|
||||
def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
|
||||
group = get_group_or_404(acronym, group_type)
|
||||
if not group.features.has_reviews:
|
||||
raise Http404
|
||||
|
||||
reviewer_role = get_object_or_404(Role, name="reviewer", group=group, email=reviewer_email)
|
||||
reviewer = reviewer_role.person
|
||||
|
||||
if not (user_is_person(request.user, reviewer)
|
||||
or can_manage_review_requests_for_team(request.user, group)):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
settings = (ReviewerSettings.objects.filter(person=reviewer, team=group).first()
|
||||
or ReviewerSettings(person=reviewer, team=group))
|
||||
|
||||
back_url = request.GET.get("next")
|
||||
if not back_url:
|
||||
import ietf.group.views
|
||||
back_url = urlreverse(ietf.group.views.review_requests, kwargs={ "group_type": group.type_id, "acronym": group.acronym})
|
||||
|
||||
# settings
|
||||
if request.method == "POST" and request.POST.get("action") == "change_settings":
|
||||
prev_min_interval = settings.get_min_interval_display()
|
||||
prev_skip_next = settings.skip_next
|
||||
settings_form = ReviewerSettingsForm(request.POST, instance=settings)
|
||||
if settings_form.is_valid():
|
||||
settings = settings_form.save()
|
||||
|
||||
changes = []
|
||||
if settings.get_min_interval_display() != prev_min_interval:
|
||||
changes.append("Frequency changed to \"{}\" from \"{}\".".format(settings.get_min_interval_display(), prev_min_interval))
|
||||
if settings.skip_next != prev_skip_next:
|
||||
changes.append("Skip next assignments changed to {} from {}.".format(settings.skip_next, prev_skip_next))
|
||||
|
||||
if changes:
|
||||
email_reviewer_availability_change(request, group, reviewer_role, "\n\n".join(changes), request.user.person)
|
||||
|
||||
return HttpResponseRedirect(back_url)
|
||||
else:
|
||||
settings_form = ReviewerSettingsForm(instance=settings)
|
||||
|
||||
# periods
|
||||
unavailable_periods = unavailability_periods_to_list().filter(person=reviewer, team=group)
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "add_period":
|
||||
period_form = AddUnavailablePeriodForm(request.POST)
|
||||
if period_form.is_valid():
|
||||
period = period_form.save(commit=False)
|
||||
period.team = group
|
||||
period.person = reviewer
|
||||
period.save()
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
in_the_past = period.end_date and period.end_date < today
|
||||
|
||||
if not in_the_past:
|
||||
msg = "Unavailable for review: {} - {} ({})".format(
|
||||
period.start_date.isoformat(),
|
||||
period.end_date.isoformat() if period.end_date else "indefinite",
|
||||
period.get_availability_display(),
|
||||
)
|
||||
|
||||
if period.availability == "unavailable":
|
||||
# the secretary might need to reassign
|
||||
# assignments, so mention the current ones
|
||||
|
||||
review_reqs = ReviewRequest.objects.filter(state__in=["requested", "accepted"], reviewer=reviewer_role.email, team=group)
|
||||
msg += "\n\n"
|
||||
|
||||
if review_reqs:
|
||||
msg += "{} is currently assigned to review:".format(reviewer_role.person)
|
||||
for r in review_reqs:
|
||||
msg += "\n\n"
|
||||
msg += "{} (deadline: {})".format(r.doc_id, r.deadline.isoformat())
|
||||
else:
|
||||
msg += "{} does not have any assignments currently.".format(reviewer_role.person)
|
||||
|
||||
email_reviewer_availability_change(request, group, reviewer_role, msg, request.user.person)
|
||||
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
else:
|
||||
period_form = AddUnavailablePeriodForm()
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "delete_period":
|
||||
period_id = request.POST.get("period_id")
|
||||
if period_id is not None:
|
||||
for period in unavailable_periods:
|
||||
if str(period.pk) == period_id:
|
||||
period.delete()
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
in_the_past = period.end_date and period.end_date < today
|
||||
|
||||
if not in_the_past:
|
||||
msg = "Removed unavailable period: {} - {} ({})".format(
|
||||
period.start_date.isoformat(),
|
||||
period.end_date.isoformat() if period.end_date else "indefinite",
|
||||
period.get_availability_display(),
|
||||
)
|
||||
|
||||
email_reviewer_availability_change(request, group, reviewer_role, msg, request.user.person)
|
||||
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
for p in unavailable_periods:
|
||||
if not p.end_date:
|
||||
p.end_form = EndUnavailablePeriodForm(p.start_date, request.POST if request.method == "POST" and request.POST.get("action") == "end_period" else None)
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "end_period":
|
||||
period_id = request.POST.get("period_id")
|
||||
for period in unavailable_periods:
|
||||
if str(period.pk) == period_id:
|
||||
if not period.end_date and period.end_form.is_valid():
|
||||
period.end_date = period.end_form.cleaned_data["end_date"]
|
||||
period.save()
|
||||
|
||||
msg = "Set end date of unavailable period: {} - {} ({})".format(
|
||||
period.start_date.isoformat(),
|
||||
period.end_date.isoformat() if period.end_date else "indefinite",
|
||||
period.get_availability_display(),
|
||||
)
|
||||
|
||||
email_reviewer_availability_change(request, group, reviewer_role, msg, request.user.person)
|
||||
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
|
||||
|
||||
return render(request, 'group/change_reviewer_settings.html', {
|
||||
'group': group,
|
||||
'reviewer_email': reviewer_email,
|
||||
'back_url': back_url,
|
||||
'settings_form': settings_form,
|
||||
'period_form': period_form,
|
||||
'unavailable_periods': unavailable_periods,
|
||||
})
|
||||
|
|
|
@ -12,12 +12,13 @@ from django.contrib.auth.models import User
|
|||
from django.conf import settings
|
||||
|
||||
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
|
||||
from ietf.utils.test_data import make_test_data
|
||||
from ietf.utils.test_data import make_test_data, make_review_data
|
||||
from ietf.utils.mail import outbox, empty_outbox
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.group.models import Group, Role, RoleName
|
||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||
from ietf.mailinglists.models import Subscribed
|
||||
from ietf.review.models import ReviewWish
|
||||
from ietf.utils.decorators import skip_coverage
|
||||
|
||||
import ietf.ietfauth.views
|
||||
|
@ -338,6 +339,41 @@ class IetfAuthTests(TestCase):
|
|||
self.assertEqual(len(q("form .has-error")), 0)
|
||||
self.assertTrue(self.username_in_htpasswd_file(user.username))
|
||||
|
||||
def test_review_overview(self):
|
||||
doc = make_test_data()
|
||||
|
||||
reviewer = Person.objects.get(name="Plain Man")
|
||||
|
||||
review_req = make_review_data(doc)
|
||||
review_req.reviewer = reviewer.email_set.first()
|
||||
review_req.save()
|
||||
|
||||
url = urlreverse(ietf.ietfauth.views.review_overview)
|
||||
|
||||
login_testing_unauthorized(self, reviewer.user.username, url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(review_req.doc.name in unicontent(r))
|
||||
|
||||
# wish to review
|
||||
r = self.client.post(url, {
|
||||
"action": "add_wish",
|
||||
'doc': doc.pk,
|
||||
"team": review_req.team_id,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(ReviewWish.objects.filter(doc=doc, team=review_req.team).count(), 1)
|
||||
|
||||
# delete wish
|
||||
r = self.client.post(url, {
|
||||
"action": "delete_wish",
|
||||
'wish_id': ReviewWish.objects.get(doc=doc, team=review_req.team).pk,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(ReviewWish.objects.filter(doc=doc, team=review_req.team).count(), 0)
|
||||
|
||||
def test_htpasswd_file_with_python(self):
|
||||
# make sure we test both Python and call-out to binary
|
||||
settings.USE_PYTHON_HTDIGEST = True
|
||||
|
|
|
@ -21,4 +21,5 @@ urlpatterns = patterns('ietf.ietfauth.views',
|
|||
url(r'^reset/confirm/(?P<auth>[^/]+)/$', 'confirm_password_reset'),
|
||||
url(r'^confirmnewemail/(?P<auth>[^/]+)/$', 'confirm_new_email'),
|
||||
(r'whitelist/add/?$', add_account_whitelist),
|
||||
url(r'^review/$', 'review_overview'),
|
||||
)
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
|
||||
# Copyright The IETF Trust 2007, All Rights Reserved
|
||||
|
||||
from datetime import datetime as DateTime, timedelta as TimeDelta
|
||||
from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404 #, HttpResponse, HttpResponseRedirect
|
||||
|
@ -43,17 +44,21 @@ from django.contrib.auth.decorators import login_required
|
|||
import django.core.signing
|
||||
from django.contrib.sites.models import Site
|
||||
from django.contrib.auth.models import User
|
||||
from django import forms
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.models import Role, Group
|
||||
from ietf.ietfauth.forms import RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, WhitelistForm
|
||||
from ietf.ietfauth.forms import get_person_form, RoleEmailForm, NewEmailForm
|
||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.mailinglists.models import Subscribed, Whitelisted
|
||||
from ietf.person.models import Person, Email, Alias
|
||||
from ietf.review.models import ReviewRequest, ReviewerSettings, ReviewWish
|
||||
from ietf.review.utils import unavailability_periods_to_list
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.doc.fields import SearchableDocumentField
|
||||
|
||||
def index(request):
|
||||
return render(request, 'registration/index.html')
|
||||
|
@ -389,3 +394,74 @@ def add_account_whitelist(request):
|
|||
'success': success,
|
||||
})
|
||||
|
||||
class AddReviewWishForm(forms.Form):
|
||||
doc = SearchableDocumentField(label="Document", doc_type="draft")
|
||||
team = forms.ModelChoiceField(queryset=Group.objects.all(), empty_label="(Choose review team)")
|
||||
|
||||
def __init__(self, teams, *args, **kwargs):
|
||||
super(AddReviewWishForm, self).__init__(*args, **kwargs)
|
||||
|
||||
f = self.fields["team"]
|
||||
f.queryset = teams
|
||||
if len(f.queryset) == 1:
|
||||
f.initial = f.queryset[0].pk
|
||||
f.widget = forms.HiddenInput()
|
||||
|
||||
@login_required
|
||||
def review_overview(request):
|
||||
open_review_requests = ReviewRequest.objects.filter(
|
||||
reviewer__person__user=request.user,
|
||||
state__in=["requested", "accepted"],
|
||||
)
|
||||
today = Date.today()
|
||||
for r in open_review_requests:
|
||||
r.due = max(0, (today - r.deadline).days)
|
||||
|
||||
closed_review_requests = ReviewRequest.objects.filter(
|
||||
reviewer__person__user=request.user,
|
||||
state__in=["no-response", "part-completed", "completed"],
|
||||
).order_by("-time")[:20]
|
||||
|
||||
teams = Group.objects.filter(role__name="reviewer", role__person__user=request.user, state="active")
|
||||
|
||||
settings = { o.team_id: o for o in ReviewerSettings.objects.filter(person__user=request.user, team__in=teams) }
|
||||
|
||||
unavailable_periods = defaultdict(list)
|
||||
for o in unavailability_periods_to_list().filter(person__user=request.user, team__in=teams):
|
||||
unavailable_periods[o.team_id].append(o)
|
||||
|
||||
roles = { o.group_id: o for o in Role.objects.filter(name="reviewer", person__user=request.user, group__in=teams) }
|
||||
|
||||
for t in teams:
|
||||
t.reviewer_settings = settings.get(t.pk) or ReviewerSettings()
|
||||
t.unavailable_periods = unavailable_periods.get(t.pk, [])
|
||||
t.role = roles.get(t.pk)
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "add_wish":
|
||||
review_wish_form = AddReviewWishForm(teams, request.POST)
|
||||
if review_wish_form.is_valid():
|
||||
ReviewWish.objects.get_or_create(
|
||||
person=request.user.person,
|
||||
doc=review_wish_form.cleaned_data["doc"],
|
||||
team=review_wish_form.cleaned_data["team"],
|
||||
)
|
||||
|
||||
return redirect(review_overview)
|
||||
else:
|
||||
review_wish_form = AddReviewWishForm(teams)
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "delete_wish":
|
||||
wish_id = request.POST.get("wish_id")
|
||||
if wish_id is not None:
|
||||
ReviewWish.objects.filter(pk=wish_id, person=request.user.person).delete()
|
||||
return redirect(review_overview)
|
||||
|
||||
review_wishes = ReviewWish.objects.filter(person__user=request.user).prefetch_related("team")
|
||||
|
||||
return render(request, 'ietfauth/review_overview.html', {
|
||||
'open_review_requests': open_review_requests,
|
||||
'closed_review_requests': closed_review_requests,
|
||||
'teams': teams,
|
||||
'review_wishes': review_wishes,
|
||||
'review_wish_form': review_wish_form,
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
('group', '0008_auto_20160505_0523'),
|
||||
('name', '0010_new_liaison_names'),
|
||||
('name', '0013_add_group_type_verbose_name_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
@ -55,7 +55,7 @@ def noop(apps, schema_editor):
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0011_reviewrequeststatename_reviewresultname_reviewtypename'),
|
||||
('name', '0014_reviewrequeststatename_reviewresultname_reviewtypename'),
|
||||
('group', '0001_initial'),
|
||||
('doc', '0001_initial'),
|
||||
]
|
||||
|
|
|
@ -50,7 +50,7 @@ class SearchablePersonsField(forms.CharField):
|
|||
model=Person, # or Email
|
||||
hint_text="Type in name to search for person.",
|
||||
*args, **kwargs):
|
||||
kwargs["max_length"] = 1000
|
||||
kwargs["max_length"] = 10000
|
||||
self.max_entries = max_entries
|
||||
self.only_users = only_users
|
||||
assert model in [ Email, Person ]
|
||||
|
|
|
@ -16,8 +16,9 @@ django.setup()
|
|||
import datetime
|
||||
from collections import namedtuple
|
||||
from django.db import connections
|
||||
from ietf.review.models import ReviewRequest, ReviewerSettings, ReviewResultName
|
||||
from ietf.review.models import ReviewRequestStateName, ReviewTypeName, ReviewTeamResult
|
||||
from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultName,
|
||||
ReviewRequestStateName, ReviewTypeName, ReviewTeamResult,
|
||||
UnavailablePeriod)
|
||||
from ietf.group.models import Group, Role, RoleName
|
||||
from ietf.person.models import Person, Email, Alias
|
||||
import argparse
|
||||
|
@ -113,8 +114,8 @@ with db_con.cursor() as c:
|
|||
print "created reviewer", reviewer.pk, unicode(reviewer).encode("utf-8")
|
||||
|
||||
if autopolicy_days.get(row.autopolicy):
|
||||
reviewer.frequency = autopolicy_days.get(row.autopolicy)
|
||||
reviewer.unavailable_until = parse_timestamp(row.available)
|
||||
reviewer.min_interval = autopolicy_days.get(row.autopolicy)
|
||||
|
||||
reviewer.filter_re = row.donotassign
|
||||
try:
|
||||
reviewer.skip_next = int(row.autopolicy)
|
||||
|
@ -122,6 +123,22 @@ with db_con.cursor() as c:
|
|||
pass
|
||||
reviewer.save()
|
||||
|
||||
unavailable_until = parse_timestamp(row.available)
|
||||
if unavailable_until:
|
||||
today = datetime.date.today()
|
||||
end_date = unavailable_until.date()
|
||||
if end_date >= today:
|
||||
UnavailablePeriod.objects.filter(person=email.person, team=team).delete()
|
||||
|
||||
UnavailablePeriod.objects.create(
|
||||
team=team,
|
||||
person=email.person,
|
||||
start_date=today,
|
||||
end_date=end_date,
|
||||
availability="unavailable",
|
||||
)
|
||||
|
||||
|
||||
# review requests
|
||||
|
||||
# check that we got the needed names
|
||||
|
|
|
@ -19,10 +19,9 @@ class Migration(migrations.Migration):
|
|||
name='ReviewerSettings',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('frequency', models.IntegerField(default=30, help_text=b'Can review every N days', 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')])),
|
||||
('unavailable_until', models.DateTimeField(help_text=b'When will this reviewer be available again', null=True, blank=True)),
|
||||
('filter_re', models.CharField(max_length=255, blank=True)),
|
||||
('skip_next', models.IntegerField(default=0, help_text=b'Skip the next N review assignments')),
|
||||
('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')),
|
||||
('person', models.ForeignKey(to='person.Person')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
|
@ -63,4 +62,31 @@ 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=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('start_date', models.DateField(default=datetime.date.today, help_text=b"Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away.")),
|
||||
('end_date', models.DateField(help_text=b'Leaving the end date blank means that the period continues indefinitely. You can end it later.', null=True, blank=True)),
|
||||
('availability', models.CharField(max_length=30, choices=[(b'canfinish', b'Can do follow-ups'), (b'unavailable', b'Completely unavailable')])),
|
||||
('person', models.ForeignKey(to='person.Person')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -13,28 +13,69 @@ class ReviewerSettings(models.Model):
|
|||
reviewer and team."""
|
||||
team = models.ForeignKey(Group)
|
||||
person = models.ForeignKey(Person)
|
||||
FREQUENCIES = [
|
||||
INTERVALS = [
|
||||
(7, "Once per week"),
|
||||
(14, "Once per fortnight"),
|
||||
(30, "Once per month"),
|
||||
(61, "Once per two months"),
|
||||
(91, "Once per quarter"),
|
||||
]
|
||||
frequency = models.IntegerField(default=30, help_text="Can review every N days", choices=FREQUENCIES)
|
||||
unavailable_until = models.DateTimeField(blank=True, null=True, help_text="When will this reviewer be available again")
|
||||
filter_re = models.CharField(max_length=255, blank=True)
|
||||
skip_next = models.IntegerField(default=0, help_text="Skip the next N review assignments")
|
||||
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")
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} in {}".format(self.person, self.team)
|
||||
|
||||
class UnavailablePeriod(models.Model):
|
||||
team = models.ForeignKey(Group)
|
||||
person = models.ForeignKey(Person)
|
||||
start_date = models.DateField(default=datetime.date.today, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away.")
|
||||
end_date = models.DateField(blank=True, null=True, help_text="Leaving the end date blank means that the period continues indefinitely. You can end it later.")
|
||||
AVAILABILITY_CHOICES = [
|
||||
("canfinish", "Can do follow-ups"),
|
||||
("unavailable", "Completely unavailable"),
|
||||
]
|
||||
LONG_AVAILABILITY_CHOICES = [
|
||||
("canfinish", "Can do follow-up reviews and finish outstanding reviews"),
|
||||
("unavailable", "Completely unavailable - reassign any outstanding reviews"),
|
||||
]
|
||||
availability = models.CharField(max_length=30, choices=AVAILABILITY_CHOICES)
|
||||
|
||||
def state(self):
|
||||
import datetime
|
||||
today = datetime.date.today()
|
||||
if self.start_date <= today:
|
||||
if not self.end_date or today <= self.end_date:
|
||||
return "active"
|
||||
else:
|
||||
return "past"
|
||||
else:
|
||||
return "future"
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} is unavailable in {} {} - {}".format(self.person, self.team.acronym, self.start_date, self.end_date or "")
|
||||
|
||||
class ReviewWish(models.Model):
|
||||
"""Reviewer wishes to review a document when it becomes available for review."""
|
||||
time = models.DateTimeField(default=datetime.datetime.now)
|
||||
team = models.ForeignKey(Group)
|
||||
person = models.ForeignKey(Person)
|
||||
doc = models.ForeignKey(Document)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} wishes to review {} in {}".format(self.person, self.doc.name, self.team.acronym)
|
||||
|
||||
class ReviewTeamResult(models.Model):
|
||||
"""Captures that a result name is valid for a given team for new
|
||||
reviews. This also implicitly defines which teams are review
|
||||
teams - if there are no possible review results valid for a given
|
||||
team, it can't be a review team."""
|
||||
team = models.ForeignKey(Group)
|
||||
result = models.ForeignKey(ReviewResultName)
|
||||
"""Captures that a result name is valid for a given team for new
|
||||
reviews. This also implicitly defines which teams are review
|
||||
teams - if there are no possible review results valid for a given
|
||||
team, it can't be a review team."""
|
||||
team = models.ForeignKey(Group)
|
||||
result = models.ForeignKey(ReviewResultName)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"{} in {}".format(self.result.name, self.group.acronym)
|
||||
|
||||
class ReviewRequest(models.Model):
|
||||
"""Represents a request for a review and the process it goes through.
|
||||
|
|
|
@ -7,7 +7,8 @@ from tastypie.cache import SimpleCache
|
|||
from ietf import api
|
||||
from ietf.api import ToOneField # pyflakes:ignore
|
||||
|
||||
from ietf.review.models import ReviewerSettings, ReviewRequest, ReviewTeamResult
|
||||
from ietf.review.models import (ReviewerSettings, ReviewRequest, ReviewTeamResult,
|
||||
UnavailablePeriod, ReviewWish)
|
||||
|
||||
|
||||
from ietf.person.resources import PersonResource
|
||||
|
@ -22,8 +23,7 @@ class ReviewerSettingsResource(ModelResource):
|
|||
#resource_name = 'reviewer'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"frequency": ALL,
|
||||
"unavailable_until": ALL,
|
||||
"min_interval": ALL,
|
||||
"filter_re": ALL,
|
||||
"skip_next": ALL,
|
||||
"team": ALL_WITH_RELATIONS,
|
||||
|
@ -82,3 +82,46 @@ class ReviewTeamResultResource(ModelResource):
|
|||
}
|
||||
api.review.register(ReviewTeamResultResource())
|
||||
|
||||
|
||||
|
||||
from ietf.person.resources import PersonResource
|
||||
from ietf.group.resources import GroupResource
|
||||
class UnavailablePeriodResource(ModelResource):
|
||||
team = ToOneField(GroupResource, 'team')
|
||||
person = ToOneField(PersonResource, 'person')
|
||||
class Meta:
|
||||
queryset = UnavailablePeriod.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'unavailableperiod'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"start_date": ALL,
|
||||
"end_date": ALL,
|
||||
"availability": ALL,
|
||||
"team": ALL_WITH_RELATIONS,
|
||||
"person": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.review.register(UnavailablePeriodResource())
|
||||
|
||||
from ietf.person.resources import PersonResource
|
||||
from ietf.group.resources import GroupResource
|
||||
from ietf.doc.resources import DocumentResource
|
||||
class ReviewWishResource(ModelResource):
|
||||
team = ToOneField(GroupResource, 'team')
|
||||
person = ToOneField(PersonResource, 'person')
|
||||
doc = ToOneField(DocumentResource, 'doc')
|
||||
class Meta:
|
||||
queryset = ReviewWish.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'reviewwish'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"time": ALL,
|
||||
"team": ALL_WITH_RELATIONS,
|
||||
"person": ALL_WITH_RELATIONS,
|
||||
"doc": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.review.register(ReviewWishResource())
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import datetime, re
|
|||
from collections import defaultdict
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.group.models import Group, Role
|
||||
|
@ -9,7 +10,8 @@ from ietf.doc.models import Document, DocEvent, State, LastCallDocEvent, Documen
|
|||
from ietf.iesg.models import TelechatDate
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
|
||||
from ietf.review.models import ReviewRequest, ReviewRequestStateName, ReviewTypeName, ReviewerSettings
|
||||
from ietf.review.models import (ReviewRequest, ReviewRequestStateName, ReviewTypeName,
|
||||
ReviewerSettings, UnavailablePeriod, ReviewWish)
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.doc.utils import extract_complete_replaces_ancestor_mapping_for_docs
|
||||
|
||||
|
@ -34,12 +36,13 @@ def can_manage_review_requests_for_team(user, team, allow_non_team_personnel=Tru
|
|||
or (allow_non_team_personnel and has_role(user, "Secretariat")))
|
||||
|
||||
def review_requests_to_list_for_doc(doc):
|
||||
return extract_revision_ordered_review_requests_for_documents(
|
||||
ReviewRequest.objects.filter(
|
||||
state__in=["requested", "accepted", "part-completed", "completed"],
|
||||
).prefetch_related("result"),
|
||||
[doc.name]
|
||||
).get(doc.pk, [])
|
||||
request_qs = ReviewRequest.objects.filter(
|
||||
state__in=["requested", "accepted", "part-completed", "completed"],
|
||||
).prefetch_related("result")
|
||||
|
||||
doc_names = [doc.name]
|
||||
|
||||
return extract_revision_ordered_review_requests_for_documents(request_qs, doc_names).get(doc.pk, [])
|
||||
|
||||
def no_review_from_teams_on_doc(doc, rev):
|
||||
return Group.objects.filter(
|
||||
|
@ -48,6 +51,27 @@ def no_review_from_teams_on_doc(doc, rev):
|
|||
reviewrequest__state="no-review-version",
|
||||
).distinct()
|
||||
|
||||
def unavailability_periods_to_list(past_days=30):
|
||||
return UnavailablePeriod.objects.filter(
|
||||
Q(end_date=None) | Q(end_date__gte=datetime.date.today() - datetime.timedelta(days=past_days)),
|
||||
).order_by("start_date")
|
||||
|
||||
def current_unavailable_periods_for_reviewers(team):
|
||||
"""Return dict with currently active unavailable periods for reviewers."""
|
||||
today = datetime.date.today()
|
||||
|
||||
unavailable_period_qs = UnavailablePeriod.objects.filter(
|
||||
Q(end_date__gte=today) | Q(end_date=None),
|
||||
start_date__lte=today,
|
||||
team=team,
|
||||
).order_by("end_date")
|
||||
|
||||
res = defaultdict(list)
|
||||
for period in unavailable_period_qs:
|
||||
res[period.person_id].append(period)
|
||||
|
||||
return res
|
||||
|
||||
def make_new_review_request_from_existing(review_req):
|
||||
obj = ReviewRequest()
|
||||
obj.time = review_req.time
|
||||
|
@ -61,6 +85,47 @@ def make_new_review_request_from_existing(review_req):
|
|||
return obj
|
||||
|
||||
def email_review_request_change(request, review_req, subject, msg, by, notify_secretary, notify_reviewer, notify_requested_by):
|
||||
"""Notify stakeholders about change, skipping a party if the change
|
||||
was done by that party."""
|
||||
|
||||
system_email = Person.objects.get(name="(System)").formatted_email()
|
||||
|
||||
to = []
|
||||
|
||||
def extract_email_addresses(objs):
|
||||
if any(o.person == by for o in objs if o):
|
||||
l = []
|
||||
else:
|
||||
l = []
|
||||
for o in objs:
|
||||
if o:
|
||||
e = o.formatted_email()
|
||||
if e != system_email:
|
||||
l.append(e)
|
||||
|
||||
for e in l:
|
||||
if e not in to:
|
||||
to.append(e)
|
||||
|
||||
if notify_secretary:
|
||||
extract_email_addresses(Role.objects.filter(name="secr", group=review_req.team).distinct())
|
||||
if notify_reviewer:
|
||||
extract_email_addresses([review_req.reviewer])
|
||||
if notify_requested_by:
|
||||
extract_email_addresses([review_req.requested_by.email()])
|
||||
|
||||
if not to:
|
||||
return
|
||||
|
||||
url = urlreverse("ietf.doc.views_review.review_request", kwargs={ "name": review_req.doc.name, "request_id": review_req.pk })
|
||||
url = request.build_absolute_uri(url)
|
||||
send_mail(request, to, None, subject, "review/review_request_changed.txt", {
|
||||
"review_req_url": url,
|
||||
"review_req": review_req,
|
||||
"msg": msg,
|
||||
})
|
||||
|
||||
def email_reviewer_availability_change(request, team, reviewer_role, msg, by):
|
||||
"""Notify possibly both secretary and reviewer about change, skipping
|
||||
a party if the change was done by that party."""
|
||||
|
||||
|
@ -83,22 +148,23 @@ def email_review_request_change(request, review_req, subject, msg, by, notify_se
|
|||
if e not in to:
|
||||
to.append(e)
|
||||
|
||||
if notify_secretary:
|
||||
extract_email_addresses(Role.objects.filter(name__in=["secr", "delegate"], group=review_req.team).distinct())
|
||||
if notify_reviewer:
|
||||
extract_email_addresses([review_req.reviewer])
|
||||
if notify_requested_by:
|
||||
extract_email_addresses([review_req.requested_by.email()])
|
||||
|
||||
extract_email_addresses(Role.objects.filter(name="secr", group=team).distinct())
|
||||
|
||||
extract_email_addresses([reviewer_role])
|
||||
|
||||
if not to:
|
||||
return
|
||||
|
||||
url = urlreverse("ietf.doc.views_review.review_request", kwargs={ "name": review_req.doc.name, "request_id": review_req.pk })
|
||||
subject = "Reviewer availability of {} changed in {}".format(reviewer_role.person, team.acronym)
|
||||
|
||||
url = urlreverse("ietf.group.views_review.reviewer_overview", kwargs={ "group_type": team.type_id, "acronym": team.acronym })
|
||||
url = request.build_absolute_uri(url)
|
||||
send_mail(request, to, None, subject, "doc/mail/review_request_changed.txt", {
|
||||
"review_req_url": url,
|
||||
"review_req": review_req,
|
||||
send_mail(request, to, None, subject, "review/reviewer_availability_changed.txt", {
|
||||
"reviewer_overview_url": url,
|
||||
"reviewer": reviewer_role.person,
|
||||
"team": team,
|
||||
"msg": msg,
|
||||
"by": by,
|
||||
})
|
||||
|
||||
def assign_review_request_to_reviewer(request, review_req, reviewer):
|
||||
|
@ -181,7 +247,6 @@ def suggested_review_requests_for_team(team):
|
|||
continue
|
||||
|
||||
requests[doc.pk] = ReviewRequest(
|
||||
time=None,
|
||||
type=last_call_type,
|
||||
doc=doc,
|
||||
team=team,
|
||||
|
@ -211,7 +276,6 @@ def suggested_review_requests_for_team(team):
|
|||
continue
|
||||
|
||||
requests[doc.pk] = ReviewRequest(
|
||||
time=None,
|
||||
type=telechat_type,
|
||||
doc=doc,
|
||||
team=team,
|
||||
|
@ -306,10 +370,19 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
team = review_req.team
|
||||
|
||||
possible_emails = list(email_queryset)
|
||||
possible_person_ids = [e.person_id for e in possible_emails]
|
||||
|
||||
aliases = DocAlias.objects.filter(document=doc).values_list("name", flat=True)
|
||||
|
||||
reviewers = { r.person_id: r for r in ReviewerSettings.objects.filter(team=team, person__in=[e.person_id for e in possible_emails]) }
|
||||
# settings
|
||||
reviewer_settings = {
|
||||
r.person_id: r
|
||||
for r in ReviewerSettings.objects.filter(team=team, person__in=possible_person_ids)
|
||||
}
|
||||
|
||||
for p in possible_person_ids:
|
||||
if p not in reviewer_settings:
|
||||
reviewer_settings[p] = ReviewerSettings()
|
||||
|
||||
# time since past assignment
|
||||
latest_assignment_for_reviewer = dict(ReviewRequest.objects.filter(
|
||||
|
@ -328,8 +401,8 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
|
||||
has_reviewed_previous = set(has_reviewed_previous.values_list("reviewer", flat=True))
|
||||
|
||||
# review indications
|
||||
would_like_to_review = set() # FIXME: fill in
|
||||
# review wishes
|
||||
wish_to_review = set(ReviewWish.objects.filter(team=team, person__in=possible_person_ids, doc=doc).values_list("person", flat=True))
|
||||
|
||||
# connections
|
||||
connections = {}
|
||||
|
@ -343,23 +416,19 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
for e in DocumentAuthor.objects.filter(document=doc, author__in=possible_emails).values_list("author", flat=True):
|
||||
connections[e] = "is author of document"
|
||||
|
||||
now = datetime.datetime.now()
|
||||
# unavailable periods
|
||||
unavailable_periods = current_unavailable_periods_for_reviewers(team)
|
||||
|
||||
def add_boolean_score(scores, direction, expr, explanation):
|
||||
scores.append(int(bool(expr)) * direction)
|
||||
if expr:
|
||||
explanations.append(explanation)
|
||||
now = datetime.datetime.now()
|
||||
|
||||
ranking = []
|
||||
for e in possible_emails:
|
||||
reviewer = reviewers.get(e.person_id)
|
||||
if not reviewer:
|
||||
reviewer = ReviewerSettings()
|
||||
settings = reviewer_settings.get(e.person_id)
|
||||
|
||||
days_past = None
|
||||
latest = latest_assignment_for_reviewer.get(e.pk)
|
||||
if latest is not None:
|
||||
days_past = (now - latest).days - reviewer.frequency
|
||||
days_past = (now - latest).days - settings.min_interval
|
||||
|
||||
if days_past is None:
|
||||
ready_for = "first time"
|
||||
|
@ -369,7 +438,7 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
ready_for = "ready for {} {}".format(d, "day" if d == 1 else "days")
|
||||
else:
|
||||
d = -d
|
||||
ready_for = "frequency exceeded, ready in {} {}".format(d, "day" if d == 1 else "days")
|
||||
ready_for = "max frequency exceeded, ready in {} {}".format(d, "day" if d == 1 else "days")
|
||||
|
||||
|
||||
# we sort the reviewers by separate axes, listing the most
|
||||
|
@ -377,13 +446,31 @@ def make_assignment_choices(email_queryset, review_req):
|
|||
scores = []
|
||||
explanations = []
|
||||
|
||||
def add_boolean_score(direction, expr, explanation=None):
|
||||
scores.append(direction if expr else -direction)
|
||||
if expr and explanation:
|
||||
explanations.append(explanation)
|
||||
|
||||
explanations.append(ready_for) # show ready for explanation first, but sort it after the other issues
|
||||
|
||||
add_boolean_score(scores, +1, e.pk in has_reviewed_previous, "reviewed document before")
|
||||
add_boolean_score(scores, +1, e.pk in would_like_to_review, "wants to review document")
|
||||
add_boolean_score(scores, -1, e.pk in connections, connections.get(e.pk)) # reviewer is somehow connected: bad
|
||||
add_boolean_score(scores, -1, reviewer.filter_re and any(re.search(reviewer.filter_re, n) for n in aliases), "filter regexp matches")
|
||||
add_boolean_score(scores, -1, reviewer.unavailable_until and reviewer.unavailable_until > now, "unavailable until {}".format((reviewer.unavailable_until or now).strftime("%Y-%m-%d %H:%M:%S")))
|
||||
periods = unavailable_periods.get(e.person_id, [])
|
||||
unavailable_at_the_moment = periods and not (e.pk in has_reviewed_previous and all(p.availability == "canfinish" for p in periods))
|
||||
add_boolean_score(-1, unavailable_at_the_moment)
|
||||
|
||||
def format_period(p):
|
||||
if p.end_date:
|
||||
res = "unavailable until {}".format(p.end_date.isoformat())
|
||||
else:
|
||||
res = "unavailable indefinitely"
|
||||
return "{} ({})".format(res, p.get_availability_display())
|
||||
|
||||
if periods:
|
||||
explanations.append(", ".join(format_period(p) for p in periods))
|
||||
|
||||
add_boolean_score(+1, e.pk in has_reviewed_previous, "reviewed document before")
|
||||
add_boolean_score(+1, e.person_id in wish_to_review, "wishes to review document")
|
||||
add_boolean_score(-1, e.pk in connections, connections.get(e.pk)) # reviewer is somehow connected: bad
|
||||
add_boolean_score(-1, settings.filter_re and any(re.search(settings.filter_re, n) for n in aliases), "filter regexp matches")
|
||||
|
||||
scores.append(100000 if days_past is None else days_past)
|
||||
|
||||
|
|
|
@ -515,6 +515,18 @@ form.email-open-review-assignments [name=body] {
|
|||
font-family: monospace;
|
||||
}
|
||||
|
||||
table.unavailable-periods td {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.unavailable-period-past {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.unavailable-period-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* === Photo pages ========================================================== */
|
||||
|
||||
.photo-name {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{% autoescape off %}
|
||||
{{ review_req.type.name }} review of: {{ review_req.doc.name }} ({% if review_req.requested_rev %}rev. {{ review_req.requested_rev }}{% else %}no specific version{% endif %})
|
||||
Deadline: {{ review_req.deadline|date:"Y-m-d" }}
|
||||
|
||||
{{ review_req_url }}
|
||||
|
||||
{{ msg|wordwrap:72 }}
|
||||
|
||||
{% endautoescape %}
|
87
ietf/templates/group/change_reviewer_settings.html
Normal file
87
ietf/templates/group/change_reviewer_settings.html
Normal file
|
@ -0,0 +1,87 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}{% origin %}
|
||||
|
||||
{% load ietf_filters staticfiles bootstrap3 %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Change reviewer settings for {{ group.acronym }} for {{ reviewer_email }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>Change reviewer settings for {{ group.acronym }} for {{ reviewer_email }}</h1>
|
||||
|
||||
<h3>Settings</h3>
|
||||
|
||||
<form class="change-reviewer-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>
|
||||
|
||||
<h3>Unavailable periods</h3>
|
||||
|
||||
<p>You can register periods where reviews should not be assigned.</p>
|
||||
|
||||
{% if unavailable_periods %}
|
||||
<table class="table">
|
||||
{% for o in unavailable_periods %}
|
||||
<tr class="unavailable-period-{{ o.state }}">
|
||||
<td>
|
||||
{{ o.start_date }} - {{ o.end_date|default:"indefinite" }}
|
||||
</td>
|
||||
<td>{{ o.get_availability_display }}</td>
|
||||
<td>
|
||||
{% if not o.end_date %}
|
||||
<form method="post" class="form-inline" style="display:inline-block">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="period_id" value="{{ o.pk }}">
|
||||
{% bootstrap_form o.end_form layout="inline" %}
|
||||
<button type="submit" class="btn btn-default btn-sm" name="action" value="end_period">End period</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="period_id" value="{{ o.pk }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm" name="action" value="delete_period">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No periods found.</p>
|
||||
{% endif %}
|
||||
|
||||
<div><a class="btn btn-default" data-toggle="collapse" data-target="#add-new-period">Add a new period</a></div>
|
||||
|
||||
<div id="add-new-period" {% if not period_form.errors %}class="collapse"{% endif %}>
|
||||
<h4>Add a new period</h4>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form period_form %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary" name="action" value="add_period">Add period</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p style="padding-top: 2em;">
|
||||
<a href="{{ back_url }}" class="btn btn-default">Back</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
<div>
|
||||
<small>
|
||||
<a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{% if r.time %}Req: {{ r.time|date:"Y-m-d" }}{% else %}<em>auto-suggested</em>{% endif %} - {{ r.type.name }}</a>
|
||||
<a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{% if r.pk != None %}Req: {{ r.time|date:"Y-m-d" }}{% else %}<em>auto-suggested</em>{% endif %} - {{ r.type.name }}</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
@ -84,7 +84,11 @@
|
|||
|
||||
<span class="assign-action">
|
||||
{% if r.reviewer %}
|
||||
<button type="button" class="btn btn-default btn-sm" title="Click to reassign reviewer">{{ r.reviewer.person }}{% if r.state_id == "accepted" %} <span class="label label-default">accp</span>{% endif %}</button>
|
||||
<button type="button" class="btn btn-default btn-sm" title="Click to reassign reviewer">
|
||||
{{ r.reviewer.person }}
|
||||
{% if r.state_id == "accepted" %} <span class="label label-default">accp</span>{% endif %}
|
||||
{% if r.reviewer_unavailable %}<span class="label label-danger">unavail</span>{% endif %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-default btn-sm" title="Click to assign reviewer"><em>not yet assigned</em></button>
|
||||
{% endif %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<h2>Open review requests</h2>
|
||||
|
||||
{% if open_review_requests %}
|
||||
<table class="table table-condensed table-striped materials tablesorter">
|
||||
<table class="table table-condensed table-striped tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request</th>
|
||||
|
@ -31,14 +31,16 @@
|
|||
<tr>
|
||||
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}</a></td>
|
||||
<td>{{ r.type.name }}</td>
|
||||
<td>{% if r.time %}{{ r.time|date:"Y-m-d" }}{% else %}<em>auto-suggested</em>{% endif %}</td>
|
||||
<td>{% if r.pk %}{{ r.time|date:"Y-m-d" }}{% else %}<em>auto-suggested</em>{% endif %}</td>
|
||||
<td>
|
||||
{{ r.deadline|date:"Y-m-d" }}
|
||||
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if r.reviewer %}
|
||||
{{ r.reviewer.person }} {% if r.state_id == "accepted" %}<span class="label label-default">Accepted</span>{% endif %}
|
||||
{{ r.reviewer.person }}
|
||||
{% if r.state_id == "accepted" %}<span class="label label-default">Accepted</span>{% endif %}
|
||||
{% if r.reviewer_unavailable %}<span class="label label-danger">Unavailable</span>{% endif %}
|
||||
{% elif r.pk != None %}
|
||||
<em>not yet assigned</em>
|
||||
{% endif %}
|
||||
|
|
173
ietf/templates/ietfauth/review_overview.html
Normal file
173
ietf/templates/ietfauth/review_overview.html
Normal file
|
@ -0,0 +1,173 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% load bootstrap3 static %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Review overview for {{ request.user }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Review overview for {{ request.user }}</h1>
|
||||
|
||||
<h2>Assigned reviews</h2>
|
||||
|
||||
{% if open_review_requests %}
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request</th>
|
||||
<th>Team</th>
|
||||
<th>Type</th>
|
||||
<th>Deadline</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in open_review_requests %}
|
||||
<tr>
|
||||
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}</a></td>
|
||||
<td>{{ r.team.acronym }}</td>
|
||||
<td>{{ r.type.name }}</td>
|
||||
<td>
|
||||
{{ r.deadline|date:"Y-m-d" }}
|
||||
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>You do not have any open review requests assigned.</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h2>Latest closed requests</h2>
|
||||
|
||||
{% if closed_review_requests %}
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request</th>
|
||||
<th>Team</th>
|
||||
<th>Type</th>
|
||||
<th>Deadline</th>
|
||||
<th>State</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in closed_review_requests %}
|
||||
<tr>
|
||||
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}-{% if r.requested_rev %}{{ r.requested_rev }}{% else %}{{ r.doc.rev }}{% endif %}</a></td>
|
||||
<td>{{ r.team.acronym }}</td>
|
||||
<td>{{ r.type.name }}</td>
|
||||
<td>
|
||||
{{ r.deadline|date:"Y-m-d" }}
|
||||
{% if r.due %}<span class="label label-warning">{{ r.due }} day{{ r.due|pluralize }}</span>{% endif %}
|
||||
</td>
|
||||
<td><span class="{% if r.state_id == "completed" or r.state_id == "part-completed" %}bg-success{% endif %}">{{ r.state.name }}</span></td>
|
||||
<td>{% if r.result %}{{ r.result.name }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>Did not find any closed requests assigned to you.</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h2>Review wishes</h2>
|
||||
|
||||
{% if review_wishes %}
|
||||
<p>You have indicated that you would like to review:</p>
|
||||
|
||||
<table class="table">
|
||||
{% for w in review_wishes %}
|
||||
<tr>
|
||||
<td><a href="{% url "doc_view" w.doc_id %}">{{ w.doc_id }}</a></td>
|
||||
<td>{{ w.team.acronym }}</td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input name="wish_id" value="{{ w.pk }}" type="hidden">
|
||||
<button class="btn btn-sm btn-danger" type="submit" name="action" value="delete_wish">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p>You do not have any review wishes.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if teams %}
|
||||
<p>Add a draft that you would like to review when it becomes available for review:</p>
|
||||
|
||||
<form role="form" method="post" class="form-inline">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form review_wish_form layout="inline" %}
|
||||
|
||||
{% buttons %}
|
||||
<button class="btn btn-default" type="submit" name="action" value="add_wish">Add draft</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% for t in teams %}
|
||||
<h2>Settings for {{ t }}</h2>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Can review</th>
|
||||
<td>{{ t.reviewer_settings.get_min_interval_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Skip next assignments</th>
|
||||
<td>{{ t.reviewer_settings.skip_next }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Filter regexp</th>
|
||||
<td>{{ t.reviewer_settings.filter_re|default:"(None)" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Unavailable periods</th>
|
||||
<td>
|
||||
{% if t.unavailable_periods %}
|
||||
<table class="unavailable-periods">
|
||||
{% for o in t.unavailable_periods %}
|
||||
<tr class="unavailable-period-{{ o.state }}">
|
||||
<td>{{ o.start_date }} - {{ o.end_date|default:"" }}</td>
|
||||
<td>{{ o.get_availability_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
(No periods)
|
||||
{% endif %}
|
||||
</td>
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<a class="btn btn-default" href="{% url "ietf.group.views_review.change_reviewer_settings" group_type=t.type_id acronym=t.acronym reviewer_email=t.role.email.address %}?next={{ request.get_full_path|urlencode }}">Change settings</a>
|
||||
</div>
|
||||
|
||||
|
||||
{% empty %}
|
||||
<h2>Settings</h2>
|
||||
|
||||
<p>It looks like you are not a reviewer in any active review team.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'select2/select2.min.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
|
||||
{% endblock %}
|
9
ietf/templates/review/review_request_changed.txt
Normal file
9
ietf/templates/review/review_request_changed.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% autoescape off %}
|
||||
{{ review_req.type.name }} review of: {{ review_req.doc.name }} ({% if review_req.requested_rev %}rev. {{ review_req.requested_rev }}{% else %}no specific version{% endif %})
|
||||
Deadline: {{ review_req.deadline|date:"Y-m-d" }}
|
||||
|
||||
{{ review_req_url }}
|
||||
|
||||
{{ msg|wordwrap:72 }}
|
||||
|
||||
{% endautoescape %}
|
8
ietf/templates/review/reviewer_availability_changed.txt
Normal file
8
ietf/templates/review/reviewer_availability_changed.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% autoescape off %}{% filter wordwrap:72 %}
|
||||
Reviewer availability of {{ reviewer }} in {{ team.acronym }} changed by {{ by }}.
|
||||
|
||||
{{ msg }}
|
||||
|
||||
{{ reviewer_overview_url }}
|
||||
|
||||
{% endfilter %}{% endautoescape %}
|
|
@ -400,7 +400,7 @@ def make_review_data(doc):
|
|||
p = Person.objects.get(user__username="plain")
|
||||
email = p.email_set.first()
|
||||
Role.objects.create(name_id="reviewer", person=p, email=email, group=team)
|
||||
ReviewerSettings.objects.create(team=team, person=p, frequency=14, skip_next=0)
|
||||
ReviewerSettings.objects.create(team=team, person=p, min_interval=14, skip_next=0)
|
||||
|
||||
review_req = ReviewRequest.objects.create(
|
||||
doc=doc,
|
||||
|
|
Loading…
Reference in a new issue