Skip to content

Commit a3fb6a4

Browse files
fix: add review team setting allowing reviewers to reject assignments past the deadline (ietf-tools#5418)
* Added a new review team setting allow_reviewer_to_reject_after_deadline that will allow rejecting review requests, even after the deadline is past. Also modified that the secretary, or whoever manages the reviews is always allowed to reject the review regardless of the deadline as he/she could change the deadline anyways. * Fixed but in view_reviews (wrong variable name), added more test cases to the test_reviews.py for different reject cases. * test: More thoroughly exercise assignment rejection * chore: Renumber migration * test: Unrelated user cannot reject assignments --------- Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
1 parent 4d6a2cf commit a3fb6a4

5 files changed

Lines changed: 140 additions & 8 deletions

File tree

ietf/doc/tests_review.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,50 @@ def test_reject_reviewer_assignment(self):
418418
r = self.client.get(req_url)
419419
self.assertEqual(r.status_code, 200)
420420
self.assertContains(r, reject_url)
421+
422+
# anonymous user should not be able to reject
423+
self.client.logout()
424+
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
425+
self.assertEqual(r.status_code, 302) # forwards to login page
426+
assignment = reload_db_objects(assignment)
427+
self.assertEqual(assignment.state_id, "accepted")
428+
429+
# unrelated person should not be able to reject
430+
other_person = PersonFactory()
431+
login_testing_unauthorized(self, other_person.user.username, reject_url)
432+
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
433+
self.assertEqual(r.status_code, 403)
434+
assignment = reload_db_objects(assignment)
435+
self.assertEqual(assignment.state_id, "accepted")
436+
437+
# Check that user can reject it
438+
login_testing_unauthorized(self, assignment.reviewer.person.user.username, reject_url)
439+
r = self.client.get(reject_url)
440+
self.assertEqual(r.status_code, 200)
441+
self.assertContains(r, escape(assignment.reviewer.person.name))
442+
self.assertNotContains(r, 'can not be rejected')
443+
self.assertContains(r, '<button type="submit"')
444+
445+
# reject
446+
empty_outbox()
447+
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
448+
self.assertEqual(r.status_code, 302)
449+
450+
assignment = reload_db_objects(assignment)
451+
self.assertEqual(assignment.state_id, "rejected")
452+
self.assertNotEqual(assignment.completed_on,None)
453+
e = doc.latest_event()
454+
self.assertEqual(e.type, "closed_review_assignment")
455+
self.assertTrue("rejected" in e.desc)
456+
self.assertEqual(len(outbox), 1)
457+
self.assertNotIn(assignment.reviewer.address, outbox[0]["To"])
458+
self.assertIn("<reviewsecretary@example.com>", outbox[0]["To"])
459+
self.assertTrue("Test message" in get_payload_text(outbox[0]))
421460
self.client.logout()
422461

423-
# get reject page
462+
# Secretary can also reject it
463+
assignment.state_id = 'assigned'
464+
assignment.save()
424465
login_testing_unauthorized(self, "reviewsecretary", reject_url)
425466
r = self.client.get(reject_url)
426467
self.assertEqual(r.status_code, 200)
@@ -444,12 +485,17 @@ def test_reject_reviewer_assignment(self):
444485
self.assertNotIn("<reviewsecretary@example.com>", outbox[0]["To"])
445486
self.assertTrue("Test message" in get_payload_text(outbox[0]))
446487

447-
# try again, but now with an expired review request, which should not be allowed (#2277)
488+
# try again, but now with an expired review request,
489+
# which should not be allowed (#2277)
448490
assignment.state_id = 'assigned'
449491
assignment.save()
450492
review_req.deadline = datetime.date(2019, 1, 1)
451493
review_req.save()
494+
self.client.logout()
452495

496+
# Login as reviewer to do this test, so it should fail, as the
497+
# request is past deadline
498+
login_testing_unauthorized(self, assignment.reviewer.person.user.username, reject_url)
453499
r = self.client.get(reject_url)
454500
self.assertEqual(r.status_code, 200)
455501
self.assertContains(r, escape(assignment.reviewer.person.name))
@@ -461,10 +507,67 @@ def test_reject_reviewer_assignment(self):
461507
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
462508
self.assertEqual(r.status_code, 200)
463509
self.assertContains(r, 'can not be rejected')
510+
self.client.logout()
511+
512+
# Change settings so that even the reviewer should
513+
# be allowed to reject the request even after past deadline
514+
m = apps.get_model('review', 'ReviewTeamSettings')
515+
for row in m.objects.all():
516+
if row.group.upcase_acronym == review_team.upcase_acronym:
517+
row.allow_reviewer_to_reject_after_deadline = True
518+
row.save(update_fields=['allow_reviewer_to_reject_after_deadline'])
519+
520+
# Test again as user
521+
login_testing_unauthorized(self, assignment.reviewer.person.user.username, reject_url)
522+
r = self.client.get(reject_url)
523+
self.assertEqual(r.status_code, 200)
524+
self.assertContains(r, escape(assignment.reviewer.person.name))
525+
self.assertNotContains(r, 'can not be rejected')
526+
self.assertContains(r, '<button type="submit"')
527+
528+
# actually reject
529+
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
530+
self.assertEqual(r.status_code, 302)
464531

465532
assignment = reload_db_objects(assignment)
466-
self.assertEqual(assignment.state_id, "assigned")
467-
self.assertEqual(len(outbox), 0)
533+
self.assertEqual(assignment.state_id, "rejected")
534+
self.assertNotEqual(len(outbox), 0)
535+
self.client.logout()
536+
537+
# Log in as secretary and that should still allow rejecting the review
538+
assignment.state_id = 'assigned'
539+
assignment.save()
540+
login_testing_unauthorized(self, "reviewsecretary", reject_url)
541+
r = self.client.get(reject_url)
542+
self.assertEqual(r.status_code, 200)
543+
self.assertContains(r, escape(assignment.reviewer.person.name))
544+
self.assertNotContains(r, 'can not be rejected')
545+
self.assertContains(r, '<button type="submit"')
546+
547+
# actually reject
548+
empty_outbox()
549+
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
550+
self.assertEqual(r.status_code, 302)
551+
552+
assignment = reload_db_objects(assignment)
553+
self.assertEqual(assignment.state_id, "rejected")
554+
self.assertNotEqual(len(outbox), 0)
555+
556+
# Revert the setting of allow_reviewer_to_reject_after_deadline
557+
# This should not affect the secretary's ability to reject.
558+
m = apps.get_model('review', 'ReviewTeamSettings')
559+
for row in m.objects.all():
560+
if row.group.upcase_acronym == review_team.upcase_acronym:
561+
row.allow_reviewer_to_reject_after_deadline = False
562+
row.save(update_fields=['allow_reviewer_to_reject_after_deadline'])
563+
assignment.state_id = 'assigned'
564+
assignment.save()
565+
r = self.client.get(reject_url)
566+
self.assertEqual(r.status_code, 200)
567+
self.assertContains(r, escape(assignment.reviewer.person.name))
568+
self.assertNotContains(r, 'can not be rejected')
569+
self.assertContains(r, '<button type="submit"')
570+
468571

469572
def make_test_mbox_tarball(self, review_req):
470573
mbox_path = os.path.join(self.review_dir, "testmbox.tar.gz")

ietf/doc/views_review.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,13 @@ class RejectReviewerAssignmentForm(forms.Form):
354354
def reject_reviewer_assignment(request, name, assignment_id):
355355
doc = get_object_or_404(Document, name=name)
356356
review_assignment = get_object_or_404(ReviewAssignment, pk=assignment_id, state__in=["assigned", "accepted"])
357-
review_request_past_deadline = review_assignment.review_request.deadline < date_today(DEADLINE_TZINFO)
358357

358+
allow_reject_request = True
359+
# Only check deadline if the group does not allow rejecting always
360+
if not review_assignment.review_request.team.reviewteamsettings.allow_reviewer_to_reject_after_deadline:
361+
if review_assignment.review_request.deadline < date_today(DEADLINE_TZINFO):
362+
allow_reject_request = False
363+
359364
if not review_assignment.reviewer:
360365
return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk)
361366

@@ -365,7 +370,12 @@ def reject_reviewer_assignment(request, name, assignment_id):
365370
if not (is_reviewer or can_manage_request):
366371
permission_denied(request, "You do not have permission to perform this action")
367372

368-
if request.method == "POST" and request.POST.get("action") == "reject" and not review_request_past_deadline:
373+
# Secretary or whoever can manage review request, has permission
374+
# to reject requests even if the deadline is in the past
375+
if can_manage_request:
376+
allow_reject_request = True
377+
378+
if request.method == "POST" and request.POST.get("action") == "reject" and allow_reject_request:
369379
form = RejectReviewerAssignmentForm(request.POST)
370380
if form.is_valid():
371381
# reject the assignment
@@ -406,7 +416,7 @@ def reject_reviewer_assignment(request, name, assignment_id):
406416
'review_req': review_assignment.review_request,
407417
'assignments': review_assignment.review_request.reviewassignment_set.all(),
408418
'form': form,
409-
'review_request_past_deadline': review_request_past_deadline,
419+
'allow_reject_request': allow_reject_request,
410420
})
411421

412422
@login_required
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.28 on 2023-03-25 06:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('review', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='reviewteamsettings',
15+
name='allow_reviewer_to_reject_after_deadline',
16+
field=models.BooleanField(default=False, verbose_name='Allow reviewer to reject request after deadline.'),
17+
),
18+
]

ietf/review/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class ReviewTeamSettings(models.Model):
191191
"""Holds configuration specific to groups that are review teams"""
192192
group = OneToOneField(Group)
193193
autosuggest = models.BooleanField(default=True, verbose_name="Automatically suggest possible review requests")
194+
allow_reviewer_to_reject_after_deadline = models.BooleanField(default=False, verbose_name="Allow reviewer to reject request after deadline.")
194195
reviewer_queue_policy = models.ForeignKey(ReviewerQueuePolicyName, default='RotateAlphabetically', on_delete=models.PROTECT)
195196
review_types = models.ManyToManyField(ReviewTypeName, default=get_default_review_types)
196197
review_results = models.ManyToManyField(ReviewResultName, default=get_default_review_results, related_name='reviewteamsettings_review_results_set')

ietf/templates/doc/review/reject_reviewer_assignment.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ <h1>
1010
<small class="text-body-secondary">{{ review_req.doc.name }}</small>
1111
</h1>
1212
{% include "doc/review/request_info.html" %}
13-
{% if not review_request_past_deadline %}
13+
{% if allow_reject_request %}
1414
<p class="alert alert-danger my-3">
1515
Do you want to reject this assignment?
1616
</p>

0 commit comments

Comments
 (0)