Skip to content

Commit 7cbe36f

Browse files
committed
Implement completing a review with tests. One can currently
enter/upload content or retrieve it from an IETF mailarch archive through integrated searching support. Support for partial completion. - Legacy-Id: 11360
1 parent b790781 commit 7cbe36f

20 files changed

Lines changed: 1391 additions & 384 deletions

ietf/doc/tests_review.py

Lines changed: 288 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
# -*- coding: utf-8 -*-
22

3-
import datetime
3+
import datetime, os, shutil, json
4+
import tarfile, tempfile, mailbox
5+
import email.mime.multipart, email.mime.text, email.utils
6+
from StringIO import StringIO
47

58
from django.core.urlresolvers import reverse as urlreverse
9+
from django.conf import settings
610

711
from pyquery import PyQuery
812

913
import debug # pyflakes:ignore
1014

1115
from ietf.review.models import ReviewRequest, Reviewer
16+
import ietf.review.mailarch
1217
from ietf.person.models import Person
1318
from ietf.group.models import Group, Role
1419
from ietf.name.models import ReviewResultName, ReviewRequestStateName
@@ -17,7 +22,6 @@
1722
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects
1823
from ietf.utils.mail import outbox, empty_outbox
1924

20-
2125
def make_review_data(doc):
2226
team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
2327
team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
@@ -39,9 +43,28 @@ def make_review_data(doc):
3943
p = Person.objects.get(user__username="marschairman")
4044
role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team)
4145

46+
p = Person.objects.get(user__username="secretary")
47+
role = Role.objects.create(name_id="secretary", person=p, email=p.email_set.first(), group=team)
48+
4249
return review_req
4350

4451
class ReviewTests(TestCase):
52+
def setUp(self):
53+
self.review_dir = os.path.abspath("tmp-review-dir")
54+
if not os.path.exists(self.review_dir):
55+
os.mkdir(self.review_dir)
56+
57+
self.old_document_path_pattern = settings.DOCUMENT_PATH_PATTERN
58+
settings.DOCUMENT_PATH_PATTERN = self.review_dir + "/{doc.type_id}/"
59+
60+
self.review_subdir = os.path.join(self.review_dir, "review")
61+
if not os.path.exists(self.review_subdir):
62+
os.mkdir(self.review_subdir)
63+
64+
def tearDown(self):
65+
shutil.rmtree(self.review_dir)
66+
settings.DOCUMENT_PATH_PATTERN = self.old_document_path_pattern
67+
4568
def test_request_review(self):
4669
doc = make_test_data()
4770
review_req = make_review_data(doc)
@@ -229,3 +252,266 @@ def test_reject_reviewer_assignment(self):
229252
self.assertEqual(len(outbox), 1)
230253
self.assertTrue("Test message" in unicode(outbox[0]))
231254

255+
def make_test_mbox_tarball(self, review_req):
256+
mbox_path = os.path.join(self.review_dir, "testmbox.tar.gz")
257+
with tarfile.open(mbox_path, "w:gz") as tar:
258+
with tempfile.NamedTemporaryFile(dir=self.review_dir, suffix=".mbox") as tmp:
259+
mbox = mailbox.mbox(tmp.name)
260+
261+
# plain text
262+
msg = email.mime.text.MIMEText("Hello,\n\nI have reviewed the document and did not find any problems.\n\nJohn Doe")
263+
msg["From"] = "johndoe@example.com"
264+
msg["To"] = review_req.team.list_email
265+
msg["Subject"] = "Review of {}-01".format(review_req.doc.name)
266+
msg["Message-ID"] = email.utils.make_msgid()
267+
msg["Archived-At"] = "<https://www.example.com/testmessage>"
268+
msg["Date"] = email.utils.formatdate()
269+
270+
mbox.add(msg)
271+
272+
# plain text + HTML
273+
msg = email.mime.multipart.MIMEMultipart('alternative')
274+
msg["From"] = "johndoe2@example.com"
275+
msg["To"] = review_req.team.list_email
276+
msg["Subject"] = "Review of {}".format(review_req.doc.name)
277+
msg["Message-ID"] = email.utils.make_msgid()
278+
msg["Archived-At"] = "<https://www.example.com/testmessage2>"
279+
280+
msg.attach(email.mime.text.MIMEText("Hi!,\r\nLooks OK!\r\n-John", "plain"))
281+
msg.attach(email.mime.text.MIMEText("<html><body><p>Hi!,</p><p>Looks OK!</p><p>-John</p></body></html>", "html"))
282+
mbox.add(msg)
283+
284+
tmp.flush()
285+
286+
tar.add(os.path.relpath(tmp.name))
287+
288+
return mbox_path
289+
290+
def test_search_mail_archive(self):
291+
doc = make_test_data()
292+
review_req = make_review_data(doc)
293+
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
294+
review_req.save()
295+
review_req.team.list_email = "{}@ietf.org".format(review_req.team.acronym)
296+
review_req.team.save()
297+
298+
# test URL construction
299+
query_urls = ietf.review.mailarch.construct_query_urls(review_req)
300+
self.assertTrue(review_req.doc.name in query_urls["query_data_url"])
301+
302+
# test parsing
303+
mbox_path = self.make_test_mbox_tarball(review_req)
304+
305+
try:
306+
# mock URL generator and point it to local file - for this
307+
# to work, the module (and not the function) must be
308+
# imported in the view
309+
real_fn = ietf.review.mailarch.construct_query_urls
310+
ietf.review.mailarch.construct_query_urls = lambda review_req, query=None: { "query_data_url": "file://" + os.path.abspath(mbox_path) }
311+
312+
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "request_id": review_req.pk })
313+
login_testing_unauthorized(self, "secretary", url)
314+
315+
r = self.client.get(url)
316+
self.assertEqual(r.status_code, 200)
317+
messages = json.loads(r.content)["messages"]
318+
self.assertEqual(len(messages), 2)
319+
320+
self.assertEqual(messages[0]["url"], "https://www.example.com/testmessage")
321+
self.assertTrue("John Doe" in messages[0]["content"])
322+
self.assertEqual(messages[0]["subject"], "Review of {}-01".format(review_req.doc.name))
323+
324+
self.assertEqual(messages[1]["url"], "https://www.example.com/testmessage2")
325+
self.assertTrue("Looks OK" in messages[1]["content"])
326+
self.assertTrue("<html>" not in messages[1]["content"])
327+
self.assertEqual(messages[1]["subject"], "Review of {}".format(review_req.doc.name))
328+
finally:
329+
ietf.review.mailarch.construct_query_urls = real_fn
330+
331+
def setup_complete_review_test(self):
332+
doc = make_test_data()
333+
review_req = make_review_data(doc)
334+
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
335+
review_req.save()
336+
review_req.team.list_email = "{}@ietf.org".format(review_req.team.acronym)
337+
for r in ReviewResultName.objects.filter(slug__in=("issues", "ready")):
338+
review_req.team.reviewresultname_set.add(r)
339+
review_req.team.save()
340+
341+
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": doc.name, "request_id": review_req.pk })
342+
343+
return review_req, url
344+
345+
def test_complete_review_upload_content(self):
346+
review_req, url = self.setup_complete_review_test()
347+
348+
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
349+
350+
# get
351+
r = self.client.get(url)
352+
self.assertEqual(r.status_code, 200)
353+
354+
# faulty post
355+
r = self.client.post(url, data={
356+
"result": "ready",
357+
"state": "completed",
358+
"reviewed_rev": "abc",
359+
"review_submission": "upload",
360+
"review_content": "",
361+
"review_url": "",
362+
"review_file": "",
363+
})
364+
self.assertEqual(r.status_code, 200)
365+
q = PyQuery(r.content)
366+
self.assertTrue(q("[name=reviewed_rev]").closest(".form-group").filter(".has-error"))
367+
self.assertTrue(q("[name=review_file]").closest(".form-group").filter(".has-error"))
368+
369+
# complete by uploading file
370+
empty_outbox()
371+
372+
test_file = StringIO("This is a review\nwith two lines")
373+
test_file.name = "unnamed"
374+
375+
r = self.client.post(url, data={
376+
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
377+
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
378+
"reviewed_rev": review_req.doc.rev,
379+
"review_submission": "upload",
380+
"review_content": "",
381+
"review_url": "",
382+
"review_file": test_file,
383+
})
384+
self.assertEqual(r.status_code, 302)
385+
386+
review_req = reload_db_objects(review_req)
387+
self.assertEqual(review_req.state_id, "completed")
388+
self.assertEqual(review_req.result_id, "ready")
389+
self.assertEqual(review_req.reviewed_rev, review_req.doc.rev)
390+
self.assertTrue(review_req.team.acronym.lower() in review_req.review.name)
391+
self.assertTrue(review_req.doc.rev in review_req.review.name)
392+
393+
with open(os.path.join(self.review_subdir, review_req.review.name + "-" + review_req.review.rev + ".txt")) as f:
394+
self.assertEqual(f.read(), "This is a review\nwith two lines")
395+
396+
self.assertEqual(len(outbox), 1)
397+
self.assertTrue(review_req.team.list_email in outbox[0]["To"])
398+
self.assertTrue("This is a review" in unicode(outbox[0]))
399+
400+
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
401+
402+
def test_complete_review_enter_content(self):
403+
review_req, url = self.setup_complete_review_test()
404+
405+
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
406+
407+
# complete by uploading file
408+
empty_outbox()
409+
410+
r = self.client.post(url, data={
411+
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
412+
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
413+
"reviewed_rev": review_req.doc.rev,
414+
"review_submission": "enter",
415+
"review_content": "This is a review\nwith two lines",
416+
"review_url": "",
417+
"review_file": "",
418+
})
419+
self.assertEqual(r.status_code, 302)
420+
421+
review_req = reload_db_objects(review_req)
422+
self.assertEqual(review_req.state_id, "completed")
423+
424+
with open(os.path.join(self.review_subdir, review_req.review.name + "-" + review_req.review.rev + ".txt")) as f:
425+
self.assertEqual(f.read(), "This is a review\nwith two lines")
426+
427+
self.assertEqual(len(outbox), 1)
428+
self.assertTrue(review_req.team.list_email in outbox[0]["To"])
429+
self.assertTrue("This is a review" in unicode(outbox[0]))
430+
431+
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
432+
433+
def test_complete_review_link_to_mailing_list(self):
434+
review_req, url = self.setup_complete_review_test()
435+
436+
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
437+
438+
# complete by uploading file
439+
empty_outbox()
440+
441+
r = self.client.post(url, data={
442+
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
443+
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
444+
"reviewed_rev": review_req.doc.rev,
445+
"review_submission": "link",
446+
"review_content": "This is a review\nwith two lines",
447+
"review_url": "http://example.com/testreview/",
448+
"review_file": "",
449+
})
450+
self.assertEqual(r.status_code, 302)
451+
452+
review_req = reload_db_objects(review_req)
453+
self.assertEqual(review_req.state_id, "completed")
454+
455+
with open(os.path.join(self.review_subdir, review_req.review.name + "-" + review_req.review.rev + ".txt")) as f:
456+
self.assertEqual(f.read(), "This is a review\nwith two lines")
457+
458+
self.assertEqual(len(outbox), 0)
459+
self.assertTrue("http://example.com" in review_req.review.external_url)
460+
461+
def test_partially_complete_review(self):
462+
review_req, url = self.setup_complete_review_test()
463+
464+
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
465+
466+
# partially complete
467+
empty_outbox()
468+
469+
r = self.client.post(url, data={
470+
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
471+
"state": ReviewRequestStateName.objects.get(slug="part-completed").pk,
472+
"reviewed_rev": review_req.doc.rev,
473+
"review_submission": "enter",
474+
"review_content": "This is a review\nwith two lines",
475+
})
476+
self.assertEqual(r.status_code, 302)
477+
478+
review_req = reload_db_objects(review_req)
479+
self.assertEqual(review_req.state_id, "part-completed")
480+
self.assertTrue(review_req.doc.rev in review_req.review.name)
481+
482+
self.assertEqual(len(outbox), 2)
483+
self.assertTrue("secretary" in outbox[0]["To"])
484+
self.assertTrue("partially" in outbox[0]["Subject"].lower())
485+
self.assertTrue("new review request" in unicode(outbox[0]))
486+
487+
self.assertTrue(review_req.team.list_email in outbox[1]["To"])
488+
self.assertTrue("partial review" in outbox[1]["Subject"].lower())
489+
self.assertTrue("This is a review" in unicode(outbox[1]))
490+
491+
first_review = review_req.review
492+
first_reviewer = review_req.reviewer
493+
494+
495+
# complete
496+
review_req = ReviewRequest.objects.get(state="requested", doc=review_req.doc, team=review_req.team)
497+
self.assertEqual(review_req.reviewer, None)
498+
review_req.reviewer = first_reviewer # same reviewer, so we can test uniquification
499+
review_req.save()
500+
501+
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": review_req.doc.name, "request_id": review_req.pk })
502+
503+
r = self.client.post(url, data={
504+
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
505+
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
506+
"reviewed_rev": review_req.doc.rev,
507+
"review_submission": "enter",
508+
"review_content": "This is another review\nwith\nthree lines",
509+
})
510+
self.assertEqual(r.status_code, 302)
511+
512+
review_req = reload_db_objects(review_req)
513+
self.assertEqual(review_req.state_id, "completed")
514+
self.assertTrue(review_req.doc.rev in review_req.review.name)
515+
second_review = review_req.review
516+
self.assertTrue(first_review.name != second_review.name)
517+
self.assertTrue(second_review.name.endswith("-2")) # uniquified

ietf/doc/urls_review.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
url(r'^(?P<request_id>[0-9]+)/withdraw/$', views_review.withdraw_request),
88
url(r'^(?P<request_id>[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
99
url(r'^(?P<request_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
10+
url(r'^(?P<request_id>[0-9]+)/complete/$', views_review.complete_review),
11+
url(r'^(?P<request_id>[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
1012
)
1113

ietf/doc/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ietf.doc.models import save_document_in_history
1717
from ietf.name.models import DocReminderTypeName, DocRelationshipName
1818
from ietf.group.models import Role
19-
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
19+
from ietf.ietfauth.utils import has_role
2020
from ietf.utils import draft, markup_txt
2121
from ietf.utils.mail import send_mail
2222
from ietf.mailtrigger.utils import gather_address_lists

0 commit comments

Comments
 (0)