Merge review-tracker branch with new branch from trunk
- Legacy-Id: 11364
This commit is contained in:
commit
7b95f46ecb
|
@ -703,7 +703,11 @@ EVENT_TYPES = [
|
|||
|
||||
# RFC Editor
|
||||
("rfc_editor_received_announcement", "Announcement was received by RFC Editor"),
|
||||
("requested_publication", "Publication at RFC Editor requested")
|
||||
("requested_publication", "Publication at RFC Editor requested"),
|
||||
|
||||
# review
|
||||
("requested_review", "Requested review"),
|
||||
("changed_review_request", "Changed review request"),
|
||||
]
|
||||
|
||||
class DocEvent(models.Model):
|
||||
|
|
517
ietf/doc/tests_review.py
Normal file
517
ietf/doc/tests_review.py
Normal file
|
@ -0,0 +1,517 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime, os, shutil, json
|
||||
import tarfile, tempfile, mailbox
|
||||
import email.mime.multipart, email.mime.text, email.utils
|
||||
from StringIO import StringIO
|
||||
|
||||
from django.core.urlresolvers import reverse as urlreverse
|
||||
from django.conf import settings
|
||||
|
||||
from pyquery import PyQuery
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.review.models import ReviewRequest, Reviewer
|
||||
import ietf.review.mailarch
|
||||
from ietf.person.models import Person
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.name.models import ReviewResultName, ReviewRequestStateName
|
||||
from ietf.utils.test_utils import TestCase
|
||||
from ietf.utils.test_data import make_test_data
|
||||
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects
|
||||
from ietf.utils.mail import outbox, empty_outbox
|
||||
|
||||
def make_review_data(doc):
|
||||
team = Group.objects.create(state_id="active", acronym="reviewteam", name="Review Team", type_id="team")
|
||||
team.reviewresultname_set.add(ReviewResultName.objects.filter(slug__in=["issues", "ready-issues", "ready", "not-ready"]))
|
||||
|
||||
p = Person.objects.get(user__username="plain")
|
||||
role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team)
|
||||
Reviewer.objects.create(team=team, person=p, frequency=14, skip_next=0)
|
||||
|
||||
review_req = ReviewRequest.objects.create(
|
||||
doc=doc,
|
||||
team=team,
|
||||
type_id="early",
|
||||
deadline=datetime.datetime.now() + datetime.timedelta(days=20),
|
||||
state_id="ready",
|
||||
reviewer=role,
|
||||
reviewed_rev="01",
|
||||
)
|
||||
|
||||
p = Person.objects.get(user__username="marschairman")
|
||||
role = Role.objects.create(name_id="reviewer", person=p, email=p.email_set.first(), group=team)
|
||||
|
||||
p = Person.objects.get(user__username="secretary")
|
||||
role = Role.objects.create(name_id="secretary", person=p, email=p.email_set.first(), group=team)
|
||||
|
||||
return review_req
|
||||
|
||||
class ReviewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.review_dir = os.path.abspath("tmp-review-dir")
|
||||
if not os.path.exists(self.review_dir):
|
||||
os.mkdir(self.review_dir)
|
||||
|
||||
self.old_document_path_pattern = settings.DOCUMENT_PATH_PATTERN
|
||||
settings.DOCUMENT_PATH_PATTERN = self.review_dir + "/{doc.type_id}/"
|
||||
|
||||
self.review_subdir = os.path.join(self.review_dir, "review")
|
||||
if not os.path.exists(self.review_subdir):
|
||||
os.mkdir(self.review_subdir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.review_dir)
|
||||
settings.DOCUMENT_PATH_PATTERN = self.old_document_path_pattern
|
||||
|
||||
def test_request_review(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_team = review_req.team
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
|
||||
login_testing_unauthorized(self, "secretary", url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
deadline_date = datetime.date.today() + datetime.timedelta(days=10)
|
||||
|
||||
# post request
|
||||
r = self.client.post(url, {
|
||||
"type": "early",
|
||||
"team": review_team.pk,
|
||||
"deadline_date": deadline_date.isoformat(),
|
||||
"requested_rev": "01"
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
req = ReviewRequest.objects.get(doc=doc, state="requested")
|
||||
self.assertEqual(req.deadline.date(), deadline_date)
|
||||
self.assertEqual(req.deadline.time(), datetime.time(23, 59, 59))
|
||||
self.assertEqual(req.team, review_team)
|
||||
self.assertEqual(req.requested_rev, "01")
|
||||
self.assertEqual(doc.latest_event().type, "requested_review")
|
||||
|
||||
def test_doc_page(self):
|
||||
# FIXME: fill in
|
||||
pass
|
||||
|
||||
def test_review_request(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(review_req.team.acronym.upper() in unicontent(r))
|
||||
|
||||
def test_withdraw_request(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
|
||||
review_req.save()
|
||||
|
||||
withdraw_url = urlreverse('ietf.doc.views_review.withdraw_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
|
||||
|
||||
# follow link
|
||||
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.get(req_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(withdraw_url in unicontent(r))
|
||||
self.client.logout()
|
||||
|
||||
# get withdraw page
|
||||
login_testing_unauthorized(self, "secretary", withdraw_url)
|
||||
r = self.client.get(withdraw_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# withdraw
|
||||
empty_outbox()
|
||||
r = self.client.post(withdraw_url, { "action": "withdraw" })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "withdrawn")
|
||||
e = doc.latest_event()
|
||||
self.assertEqual(e.type, "changed_review_request")
|
||||
self.assertTrue("Withdrew" in e.desc)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue("withdrawn" in unicode(outbox[0]))
|
||||
|
||||
def test_assign_reviewer(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="requested")
|
||||
review_req.reviewer = None
|
||||
review_req.save()
|
||||
|
||||
assign_url = urlreverse('ietf.doc.views_review.assign_reviewer', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
|
||||
|
||||
# follow link
|
||||
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.get(req_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(assign_url in unicontent(r))
|
||||
self.client.logout()
|
||||
|
||||
# get assign page
|
||||
login_testing_unauthorized(self, "secretary", assign_url)
|
||||
r = self.client.get(assign_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# assign
|
||||
empty_outbox()
|
||||
reviewer = Role.objects.filter(name="reviewer", group=review_req.team).first()
|
||||
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "requested")
|
||||
self.assertEqual(review_req.reviewer, reviewer)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue("assigned" in unicode(outbox[0]))
|
||||
|
||||
# re-assign
|
||||
empty_outbox()
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
|
||||
review_req.save()
|
||||
reviewer = Role.objects.filter(name="reviewer", group=review_req.team).exclude(pk=reviewer.pk).first()
|
||||
r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "requested") # check that state is reset
|
||||
self.assertEqual(review_req.reviewer, reviewer)
|
||||
self.assertEqual(len(outbox), 2)
|
||||
self.assertTrue("cancelled your assignment" in unicode(outbox[0]))
|
||||
self.assertTrue("assigned" in unicode(outbox[1]))
|
||||
|
||||
def test_accept_reviewer_assignment(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="requested")
|
||||
review_req.save()
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
username = review_req.reviewer.person.user.username
|
||||
self.client.login(username=username, password=username + "+password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue(q("[name=action][value=accept]"))
|
||||
|
||||
# accept
|
||||
r = self.client.post(url, { "action": "accept" })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "accepted")
|
||||
|
||||
def test_reject_reviewer_assignment(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
|
||||
review_req.save()
|
||||
|
||||
reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
|
||||
|
||||
# follow link
|
||||
req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.get(req_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(reject_url in unicontent(r))
|
||||
self.client.logout()
|
||||
|
||||
# get reject page
|
||||
login_testing_unauthorized(self, "secretary", reject_url)
|
||||
r = self.client.get(reject_url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
|
||||
|
||||
# reject
|
||||
empty_outbox()
|
||||
r = self.client.post(reject_url, { "action": "reject", "message_to_secretary": "Test message" })
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "rejected")
|
||||
e = doc.latest_event()
|
||||
self.assertEqual(e.type, "changed_review_request")
|
||||
self.assertTrue("rejected" in e.desc)
|
||||
self.assertEqual(ReviewRequest.objects.filter(doc=review_req.doc, team=review_req.team, state="requested").count(), 1)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue("Test message" in unicode(outbox[0]))
|
||||
|
||||
def make_test_mbox_tarball(self, review_req):
|
||||
mbox_path = os.path.join(self.review_dir, "testmbox.tar.gz")
|
||||
with tarfile.open(mbox_path, "w:gz") as tar:
|
||||
with tempfile.NamedTemporaryFile(dir=self.review_dir, suffix=".mbox") as tmp:
|
||||
mbox = mailbox.mbox(tmp.name)
|
||||
|
||||
# plain text
|
||||
msg = email.mime.text.MIMEText("Hello,\n\nI have reviewed the document and did not find any problems.\n\nJohn Doe")
|
||||
msg["From"] = "johndoe@example.com"
|
||||
msg["To"] = review_req.team.list_email
|
||||
msg["Subject"] = "Review of {}-01".format(review_req.doc.name)
|
||||
msg["Message-ID"] = email.utils.make_msgid()
|
||||
msg["Archived-At"] = "<https://www.example.com/testmessage>"
|
||||
msg["Date"] = email.utils.formatdate()
|
||||
|
||||
mbox.add(msg)
|
||||
|
||||
# plain text + HTML
|
||||
msg = email.mime.multipart.MIMEMultipart('alternative')
|
||||
msg["From"] = "johndoe2@example.com"
|
||||
msg["To"] = review_req.team.list_email
|
||||
msg["Subject"] = "Review of {}".format(review_req.doc.name)
|
||||
msg["Message-ID"] = email.utils.make_msgid()
|
||||
msg["Archived-At"] = "<https://www.example.com/testmessage2>"
|
||||
|
||||
msg.attach(email.mime.text.MIMEText("Hi!,\r\nLooks OK!\r\n-John", "plain"))
|
||||
msg.attach(email.mime.text.MIMEText("<html><body><p>Hi!,</p><p>Looks OK!</p><p>-John</p></body></html>", "html"))
|
||||
mbox.add(msg)
|
||||
|
||||
tmp.flush()
|
||||
|
||||
tar.add(os.path.relpath(tmp.name))
|
||||
|
||||
return mbox_path
|
||||
|
||||
def test_search_mail_archive(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
|
||||
review_req.save()
|
||||
review_req.team.list_email = "{}@ietf.org".format(review_req.team.acronym)
|
||||
review_req.team.save()
|
||||
|
||||
# test URL construction
|
||||
query_urls = ietf.review.mailarch.construct_query_urls(review_req)
|
||||
self.assertTrue(review_req.doc.name in query_urls["query_data_url"])
|
||||
|
||||
# test parsing
|
||||
mbox_path = self.make_test_mbox_tarball(review_req)
|
||||
|
||||
try:
|
||||
# mock URL generator and point it to local file - for this
|
||||
# to work, the module (and not the function) must be
|
||||
# imported in the view
|
||||
real_fn = ietf.review.mailarch.construct_query_urls
|
||||
ietf.review.mailarch.construct_query_urls = lambda review_req, query=None: { "query_data_url": "file://" + os.path.abspath(mbox_path) }
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.search_mail_archive', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
login_testing_unauthorized(self, "secretary", url)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
messages = json.loads(r.content)["messages"]
|
||||
self.assertEqual(len(messages), 2)
|
||||
|
||||
self.assertEqual(messages[0]["url"], "https://www.example.com/testmessage")
|
||||
self.assertTrue("John Doe" in messages[0]["content"])
|
||||
self.assertEqual(messages[0]["subject"], "Review of {}-01".format(review_req.doc.name))
|
||||
|
||||
self.assertEqual(messages[1]["url"], "https://www.example.com/testmessage2")
|
||||
self.assertTrue("Looks OK" in messages[1]["content"])
|
||||
self.assertTrue("<html>" not in messages[1]["content"])
|
||||
self.assertEqual(messages[1]["subject"], "Review of {}".format(review_req.doc.name))
|
||||
finally:
|
||||
ietf.review.mailarch.construct_query_urls = real_fn
|
||||
|
||||
def setup_complete_review_test(self):
|
||||
doc = make_test_data()
|
||||
review_req = make_review_data(doc)
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
|
||||
review_req.save()
|
||||
review_req.team.list_email = "{}@ietf.org".format(review_req.team.acronym)
|
||||
for r in ReviewResultName.objects.filter(slug__in=("issues", "ready")):
|
||||
review_req.team.reviewresultname_set.add(r)
|
||||
review_req.team.save()
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": doc.name, "request_id": review_req.pk })
|
||||
|
||||
return review_req, url
|
||||
|
||||
def test_complete_review_upload_content(self):
|
||||
review_req, url = self.setup_complete_review_test()
|
||||
|
||||
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# faulty post
|
||||
r = self.client.post(url, data={
|
||||
"result": "ready",
|
||||
"state": "completed",
|
||||
"reviewed_rev": "abc",
|
||||
"review_submission": "upload",
|
||||
"review_content": "",
|
||||
"review_url": "",
|
||||
"review_file": "",
|
||||
})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue(q("[name=reviewed_rev]").closest(".form-group").filter(".has-error"))
|
||||
self.assertTrue(q("[name=review_file]").closest(".form-group").filter(".has-error"))
|
||||
|
||||
# complete by uploading file
|
||||
empty_outbox()
|
||||
|
||||
test_file = StringIO("This is a review\nwith two lines")
|
||||
test_file.name = "unnamed"
|
||||
|
||||
r = self.client.post(url, data={
|
||||
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
|
||||
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
|
||||
"reviewed_rev": review_req.doc.rev,
|
||||
"review_submission": "upload",
|
||||
"review_content": "",
|
||||
"review_url": "",
|
||||
"review_file": test_file,
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "completed")
|
||||
self.assertEqual(review_req.result_id, "ready")
|
||||
self.assertEqual(review_req.reviewed_rev, review_req.doc.rev)
|
||||
self.assertTrue(review_req.team.acronym.lower() in review_req.review.name)
|
||||
self.assertTrue(review_req.doc.rev in review_req.review.name)
|
||||
|
||||
with open(os.path.join(self.review_subdir, review_req.review.name + "-" + review_req.review.rev + ".txt")) as f:
|
||||
self.assertEqual(f.read(), "This is a review\nwith two lines")
|
||||
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue(review_req.team.list_email in outbox[0]["To"])
|
||||
self.assertTrue("This is a review" in unicode(outbox[0]))
|
||||
|
||||
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
|
||||
|
||||
def test_complete_review_enter_content(self):
|
||||
review_req, url = self.setup_complete_review_test()
|
||||
|
||||
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
|
||||
|
||||
# complete by uploading file
|
||||
empty_outbox()
|
||||
|
||||
r = self.client.post(url, data={
|
||||
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
|
||||
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
|
||||
"reviewed_rev": review_req.doc.rev,
|
||||
"review_submission": "enter",
|
||||
"review_content": "This is a review\nwith two lines",
|
||||
"review_url": "",
|
||||
"review_file": "",
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "completed")
|
||||
|
||||
with open(os.path.join(self.review_subdir, review_req.review.name + "-" + review_req.review.rev + ".txt")) as f:
|
||||
self.assertEqual(f.read(), "This is a review\nwith two lines")
|
||||
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue(review_req.team.list_email in outbox[0]["To"])
|
||||
self.assertTrue("This is a review" in unicode(outbox[0]))
|
||||
|
||||
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
|
||||
|
||||
def test_complete_review_link_to_mailing_list(self):
|
||||
review_req, url = self.setup_complete_review_test()
|
||||
|
||||
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
|
||||
|
||||
# complete by uploading file
|
||||
empty_outbox()
|
||||
|
||||
r = self.client.post(url, data={
|
||||
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
|
||||
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
|
||||
"reviewed_rev": review_req.doc.rev,
|
||||
"review_submission": "link",
|
||||
"review_content": "This is a review\nwith two lines",
|
||||
"review_url": "http://example.com/testreview/",
|
||||
"review_file": "",
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "completed")
|
||||
|
||||
with open(os.path.join(self.review_subdir, review_req.review.name + "-" + review_req.review.rev + ".txt")) as f:
|
||||
self.assertEqual(f.read(), "This is a review\nwith two lines")
|
||||
|
||||
self.assertEqual(len(outbox), 0)
|
||||
self.assertTrue("http://example.com" in review_req.review.external_url)
|
||||
|
||||
def test_partially_complete_review(self):
|
||||
review_req, url = self.setup_complete_review_test()
|
||||
|
||||
login_testing_unauthorized(self, review_req.reviewer.person.user.username, url)
|
||||
|
||||
# partially complete
|
||||
empty_outbox()
|
||||
|
||||
r = self.client.post(url, data={
|
||||
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
|
||||
"state": ReviewRequestStateName.objects.get(slug="part-completed").pk,
|
||||
"reviewed_rev": review_req.doc.rev,
|
||||
"review_submission": "enter",
|
||||
"review_content": "This is a review\nwith two lines",
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "part-completed")
|
||||
self.assertTrue(review_req.doc.rev in review_req.review.name)
|
||||
|
||||
self.assertEqual(len(outbox), 2)
|
||||
self.assertTrue("secretary" in outbox[0]["To"])
|
||||
self.assertTrue("partially" in outbox[0]["Subject"].lower())
|
||||
self.assertTrue("new review request" in unicode(outbox[0]))
|
||||
|
||||
self.assertTrue(review_req.team.list_email in outbox[1]["To"])
|
||||
self.assertTrue("partial review" in outbox[1]["Subject"].lower())
|
||||
self.assertTrue("This is a review" in unicode(outbox[1]))
|
||||
|
||||
first_review = review_req.review
|
||||
first_reviewer = review_req.reviewer
|
||||
|
||||
|
||||
# complete
|
||||
review_req = ReviewRequest.objects.get(state="requested", doc=review_req.doc, team=review_req.team)
|
||||
self.assertEqual(review_req.reviewer, None)
|
||||
review_req.reviewer = first_reviewer # same reviewer, so we can test uniquification
|
||||
review_req.save()
|
||||
|
||||
url = urlreverse('ietf.doc.views_review.complete_review', kwargs={ "name": review_req.doc.name, "request_id": review_req.pk })
|
||||
|
||||
r = self.client.post(url, data={
|
||||
"result": ReviewResultName.objects.get(teams=review_req.team, slug="ready").pk,
|
||||
"state": ReviewRequestStateName.objects.get(slug="completed").pk,
|
||||
"reviewed_rev": review_req.doc.rev,
|
||||
"review_submission": "enter",
|
||||
"review_content": "This is another review\nwith\nthree lines",
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
review_req = reload_db_objects(review_req)
|
||||
self.assertEqual(review_req.state_id, "completed")
|
||||
self.assertTrue(review_req.doc.rev in review_req.review.name)
|
||||
second_review = review_req.review
|
||||
self.assertTrue(first_review.name != second_review.name)
|
||||
self.assertTrue(second_review.name.endswith("-2")) # uniquified
|
|
@ -73,6 +73,7 @@ urlpatterns = patterns('',
|
|||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/ballot/$', views_doc.document_ballot, name="doc_ballot"),
|
||||
(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?doc.json$', views_doc.document_json),
|
||||
(r'^(?P<name>[A-Za-z0-9._+-]+)/ballotpopup/(?P<ballot_id>[0-9]+)/$', views_doc.ballot_popup),
|
||||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/reviewrequest/', include("ietf.doc.urls_review")),
|
||||
|
||||
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'),
|
||||
|
||||
|
|
13
ietf/doc/urls_review.py
Normal file
13
ietf/doc/urls_review.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.conf.urls import patterns, url
|
||||
from ietf.doc import views_review
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', views_review.request_review),
|
||||
url(r'^(?P<request_id>[0-9]+)/$', views_review.review_request),
|
||||
url(r'^(?P<request_id>[0-9]+)/withdraw/$', views_review.withdraw_request),
|
||||
url(r'^(?P<request_id>[0-9]+)/assignreviewer/$', views_review.assign_reviewer),
|
||||
url(r'^(?P<request_id>[0-9]+)/rejectreviewerassignment/$', views_review.reject_reviewer_assignment),
|
||||
url(r'^(?P<request_id>[0-9]+)/complete/$', views_review.complete_review),
|
||||
url(r'^(?P<request_id>[0-9]+)/searchmailarchive/$', views_review.search_mail_archive),
|
||||
)
|
||||
|
|
@ -89,7 +89,6 @@ def can_adopt_draft(user, doc):
|
|||
group__state="active",
|
||||
person__user=user).exists())
|
||||
|
||||
|
||||
def two_thirds_rule( recused=0 ):
|
||||
# For standards-track, need positions from 2/3 of the non-recused current IESG.
|
||||
active = Role.objects.filter(name="ad",group__type="area",group__state="active").count()
|
||||
|
|
|
@ -343,14 +343,6 @@ class UploadForm(forms.Form):
|
|||
def clean_txt(self):
|
||||
return get_cleaned_text_file_content(self.cleaned_data["txt"])
|
||||
|
||||
def save(self, group, rev):
|
||||
filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (group.charter.canonical_name(), rev))
|
||||
with open(filename, 'wb') as destination:
|
||||
if self.cleaned_data['txt']:
|
||||
destination.write(self.cleaned_data['txt'])
|
||||
else:
|
||||
destination.write(self.cleaned_data['content'].encode("utf-8"))
|
||||
|
||||
@login_required
|
||||
def submit(request, name=None, option=None):
|
||||
if not name.startswith('charter-'):
|
||||
|
@ -390,7 +382,12 @@ def submit(request, name=None, option=None):
|
|||
e.save()
|
||||
|
||||
# Save file on disk
|
||||
form.save(group, charter.rev)
|
||||
filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (charter.canonical_name(), charter.rev))
|
||||
with open(filename, 'wb') as destination:
|
||||
if form.cleaned_data['txt']:
|
||||
destination.write(form.cleaned_data['txt'])
|
||||
else:
|
||||
destination.write(form.cleaned_data['content'].encode("utf-8"))
|
||||
|
||||
if option in ['initcharter','recharter'] and charter.ad == None:
|
||||
charter.ad = getattr(group.ad_role(),'person',None)
|
||||
|
|
|
@ -48,7 +48,7 @@ from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDo
|
|||
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
|
||||
can_adopt_draft, get_chartering_type, get_document_content, get_tags_for_stream_id,
|
||||
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
||||
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus)
|
||||
get_initial_notify, make_notify_changed_event, crawl_history, default_consensus )
|
||||
from ietf.community.utils import augment_docs_with_tracking_info
|
||||
from ietf.group.models import Role
|
||||
from ietf.group.utils import can_manage_group, can_manage_materials
|
||||
|
@ -57,10 +57,12 @@ from ietf.name.models import StreamName, BallotPositionName
|
|||
from ietf.person.models import Email
|
||||
from ietf.utils.history import find_history_active_at
|
||||
from ietf.doc.forms import TelechatForm, NotifyForm
|
||||
from ietf.doc.mails import email_comment
|
||||
from ietf.doc.mails import email_comment
|
||||
from ietf.mailtrigger.utils import gather_relevant_expansions
|
||||
from ietf.meeting.models import Session
|
||||
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions
|
||||
from ietf.review.models import ReviewRequest
|
||||
from ietf.review.utils import can_request_review_of_doc
|
||||
|
||||
def render_document_top(request, doc, tab, name):
|
||||
tabs = []
|
||||
|
@ -279,8 +281,8 @@ def document_main(request, name, rev=None):
|
|||
can_edit_stream_info = is_authorized_in_doc_stream(request.user, doc)
|
||||
can_edit_shepherd_writeup = can_edit_stream_info or user_is_person(request.user, doc.shepherd and doc.shepherd.person) or has_role(request.user, ["Area Director"])
|
||||
can_edit_notify = can_edit_shepherd_writeup
|
||||
can_edit_consensus = False
|
||||
|
||||
can_edit_consensus = False
|
||||
consensus = nice_consensus(default_consensus(doc))
|
||||
if doc.stream_id == "ietf" and iesg_state:
|
||||
show_in_states = set(IESG_BALLOT_ACTIVE_STATES)
|
||||
|
@ -294,6 +296,8 @@ def document_main(request, name, rev=None):
|
|||
e = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
|
||||
consensus = nice_consensus(e and e.consensus)
|
||||
|
||||
can_request_review = can_request_review_of_doc(request.user, doc)
|
||||
|
||||
# mailing list search archive
|
||||
search_archive = "www.ietf.org/mail-archive/web/"
|
||||
if doc.stream_id == "ietf" and group.type_id == "wg" and group.list_archive:
|
||||
|
@ -353,6 +357,8 @@ def document_main(request, name, rev=None):
|
|||
published = doc.latest_event(type="published_rfc")
|
||||
started_iesg_process = doc.latest_event(type="started_iesg_process")
|
||||
|
||||
review_requests = ReviewRequest.objects.filter(doc=doc).exclude(state__in=["withdrawn", "rejected"])
|
||||
|
||||
return render_to_response("doc/document_draft.html",
|
||||
dict(doc=doc,
|
||||
group=group,
|
||||
|
@ -374,6 +380,7 @@ def document_main(request, name, rev=None):
|
|||
can_edit_consensus=can_edit_consensus,
|
||||
can_edit_replaces=can_edit_replaces,
|
||||
can_view_possibly_replaces=can_view_possibly_replaces,
|
||||
can_request_review=can_request_review,
|
||||
|
||||
rfc_number=rfc_number,
|
||||
draft_name=draft_name,
|
||||
|
@ -412,6 +419,7 @@ def document_main(request, name, rev=None):
|
|||
search_archive=search_archive,
|
||||
actions=actions,
|
||||
presentations=presentations,
|
||||
review_requests=review_requests,
|
||||
),
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
@ -563,6 +571,24 @@ def document_main(request, name, rev=None):
|
|||
),
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
if doc.type_id == "review":
|
||||
basename = "{}-{}.txt".format(doc.name, doc.rev)
|
||||
pathname = os.path.join(doc.get_file_path(), basename)
|
||||
content = get_document_content(basename, pathname, split=False)
|
||||
|
||||
review_req = ReviewRequest.objects.filter(review=doc.name).first()
|
||||
|
||||
return render(request, "doc/document_review.html",
|
||||
dict(doc=doc,
|
||||
top=top,
|
||||
content=content,
|
||||
revisions=revisions,
|
||||
latest_rev=latest_rev,
|
||||
snapshot=snapshot,
|
||||
review_req=review_req,
|
||||
))
|
||||
|
||||
raise Http404
|
||||
|
||||
|
||||
|
|
503
ietf/doc/views_review.py
Normal file
503
ietf/doc/views_review.py
Normal file
|
@ -0,0 +1,503 @@
|
|||
import datetime, os, email.utils
|
||||
|
||||
from django.contrib.sites.models import Site
|
||||
from django.http import HttpResponseForbidden, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django import forms
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.html import mark_safe
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from ietf.doc.models import Document, NewRevisionDocEvent, DocEvent, State, DocAlias
|
||||
from ietf.ietfauth.utils import is_authorized_in_doc_stream, user_is_person
|
||||
from ietf.name.models import ReviewRequestStateName, ReviewResultName, DocTypeName
|
||||
from ietf.group.models import Role
|
||||
from ietf.review.models import ReviewRequest
|
||||
from ietf.review.utils import (active_review_teams, assign_review_request_to_reviewer,
|
||||
can_request_review_of_doc, can_manage_review_requests_for_team,
|
||||
email_about_review_request, make_new_review_request_from_existing)
|
||||
from ietf.review import mailarch
|
||||
from ietf.utils.fields import DatepickerDateField
|
||||
from ietf.utils.text import skip_prefix
|
||||
from ietf.utils.textupload import get_cleaned_text_file_content
|
||||
from ietf.utils.mail import send_mail
|
||||
|
||||
def clean_doc_revision(doc, rev):
|
||||
if rev:
|
||||
rev = rev.rjust(2, "0")
|
||||
|
||||
if not NewRevisionDocEvent.objects.filter(doc=doc, rev=rev).exists():
|
||||
raise forms.ValidationError("Could not find revision \"{}\" of the document.".format(rev))
|
||||
|
||||
return rev
|
||||
|
||||
class RequestReviewForm(forms.ModelForm):
|
||||
deadline_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" })
|
||||
deadline_time = forms.TimeField(widget=forms.TextInput(attrs={ 'placeholder': "HH:MM" }), help_text="If time is not specified, end of day is assumed", required=False)
|
||||
|
||||
class Meta:
|
||||
model = ReviewRequest
|
||||
fields = ('type', 'team', 'deadline', 'requested_rev')
|
||||
|
||||
def __init__(self, user, doc, *args, **kwargs):
|
||||
super(RequestReviewForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.doc = doc
|
||||
|
||||
self.fields['type'].widget = forms.RadioSelect(choices=[t for t in self.fields['type'].choices if t[0]])
|
||||
|
||||
f = self.fields["team"]
|
||||
f.queryset = active_review_teams()
|
||||
if not is_authorized_in_doc_stream(user, doc): # user is a reviewer
|
||||
f.queryset = f.queryset.filter(role__name="reviewer", role__person__user=user)
|
||||
if len(f.queryset) < 6:
|
||||
f.widget = forms.RadioSelect(choices=[t for t in f.choices if t[0]])
|
||||
|
||||
self.fields["deadline"].required = False
|
||||
self.fields["requested_rev"].label = "Document revision"
|
||||
|
||||
def clean_deadline_date(self):
|
||||
v = self.cleaned_data.get('deadline_date')
|
||||
if v < datetime.date.today():
|
||||
raise forms.ValidationError("Select a future date.")
|
||||
return v
|
||||
|
||||
def clean_requested_rev(self):
|
||||
return clean_doc_revision(self.doc, self.cleaned_data.get("requested_rev"))
|
||||
|
||||
def clean(self):
|
||||
deadline_date = self.cleaned_data.get('deadline_date')
|
||||
deadline_time = self.cleaned_data.get('deadline_time', None)
|
||||
|
||||
if deadline_date:
|
||||
if deadline_time is None:
|
||||
deadline_time = datetime.time(23, 59, 59)
|
||||
|
||||
self.cleaned_data["deadline"] = datetime.datetime.combine(deadline_date, deadline_time)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
@login_required
|
||||
def request_review(request, name):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
|
||||
if not can_request_review_of_doc(request.user, doc):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
if request.method == "POST":
|
||||
form = RequestReviewForm(request.user, doc, request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
review_req = form.save(commit=False)
|
||||
review_req.doc = doc
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="requested", used=True)
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="requested_review",
|
||||
doc=doc,
|
||||
by=request.user.person,
|
||||
desc="Requested {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
|
||||
time=review_req.time,
|
||||
)
|
||||
|
||||
return redirect('doc_view', name=doc.name)
|
||||
|
||||
else:
|
||||
form = RequestReviewForm(request.user, doc)
|
||||
|
||||
return render(request, 'doc/review/request_review.html', {
|
||||
'doc': doc,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
def review_request(request, name, request_id):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
review_req = get_object_or_404(ReviewRequest, pk=request_id)
|
||||
|
||||
is_reviewer = review_req.reviewer and user_is_person(request.user, review_req.reviewer.person)
|
||||
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
|
||||
|
||||
can_withdraw_request = (review_req.state_id in ["requested", "accepted"]
|
||||
and (is_authorized_in_doc_stream(request.user, doc)
|
||||
or can_manage_request))
|
||||
|
||||
can_assign_reviewer = (review_req.state_id in ["requested", "accepted"]
|
||||
and can_manage_request)
|
||||
|
||||
can_accept_reviewer_assignment = (review_req.state_id == "requested"
|
||||
and review_req.reviewer
|
||||
and (is_reviewer or can_manage_request))
|
||||
|
||||
can_reject_reviewer_assignment = (review_req.state_id in ["requested", "accepted"]
|
||||
and review_req.reviewer
|
||||
and (is_reviewer or can_manage_request))
|
||||
|
||||
can_complete_review = (review_req.state_id in ["requested", "accepted"]
|
||||
and review_req.reviewer
|
||||
and (is_reviewer or can_manage_request))
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "accept" and can_accept_reviewer_assignment:
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="accepted")
|
||||
review_req.save()
|
||||
|
||||
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
|
||||
|
||||
return render(request, 'doc/review/review_request.html', {
|
||||
'doc': doc,
|
||||
'review_req': review_req,
|
||||
'can_withdraw_request': can_withdraw_request,
|
||||
'can_reject_reviewer_assignment': can_reject_reviewer_assignment,
|
||||
'can_assign_reviewer': can_assign_reviewer,
|
||||
'can_accept_reviewer_assignment': can_accept_reviewer_assignment,
|
||||
'can_complete_review': can_complete_review,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def withdraw_request(request, name, request_id):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
|
||||
|
||||
if not is_authorized_in_doc_stream(request.user, doc):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "withdraw":
|
||||
prev_state = review_req.state
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="withdrawn")
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
doc=doc,
|
||||
by=request.user.person,
|
||||
desc="Withdrew request for {} review by {}".format(review_req.type.name, review_req.team.acronym.upper()),
|
||||
)
|
||||
|
||||
if prev_state.slug != "requested":
|
||||
email_about_review_request(
|
||||
request, review_req,
|
||||
"Withdrew review request for %s" % review_req.doc.name,
|
||||
"Review request has been withdrawn by %s." % request.user.person,
|
||||
by=request.user.person, notify_secretary=False, notify_reviewer=True)
|
||||
|
||||
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
|
||||
|
||||
return render(request, 'doc/review/withdraw_request.html', {
|
||||
'doc': doc,
|
||||
'review_req': review_req,
|
||||
})
|
||||
|
||||
class PersonEmailLabeledRoleModelChoiceField(forms.ModelChoiceField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not "queryset" in kwargs:
|
||||
kwargs["queryset"] = Role.objects.select_related("person", "email")
|
||||
super(PersonEmailLabeledRoleModelChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
def label_from_instance(self, role):
|
||||
return u"{} <{}>".format(role.person.name, role.email.address)
|
||||
|
||||
class AssignReviewerForm(forms.Form):
|
||||
reviewer = PersonEmailLabeledRoleModelChoiceField(widget=forms.RadioSelect, empty_label="(None)", required=False)
|
||||
|
||||
def __init__(self, review_req, *args, **kwargs):
|
||||
super(AssignReviewerForm, self).__init__(*args, **kwargs)
|
||||
f = self.fields["reviewer"]
|
||||
f.queryset = f.queryset.filter(name="reviewer", group=review_req.team)
|
||||
if review_req.reviewer:
|
||||
f.initial = review_req.reviewer_id
|
||||
|
||||
@login_required
|
||||
def assign_reviewer(request, name, request_id):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
|
||||
|
||||
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
|
||||
|
||||
if not can_manage_request:
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "assign":
|
||||
form = AssignReviewerForm(review_req, request.POST)
|
||||
if form.is_valid():
|
||||
reviewer = form.cleaned_data["reviewer"]
|
||||
assign_review_request_to_reviewer(request, review_req, reviewer)
|
||||
|
||||
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
|
||||
else:
|
||||
form = AssignReviewerForm(review_req)
|
||||
|
||||
return render(request, 'doc/review/assign_reviewer.html', {
|
||||
'doc': doc,
|
||||
'review_req': review_req,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
class RejectReviewerAssignmentForm(forms.Form):
|
||||
message_to_secretary = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional explanation of rejection, will be emailed to team secretary if filled in")
|
||||
|
||||
@login_required
|
||||
def reject_reviewer_assignment(request, name, request_id):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
|
||||
|
||||
if not review_req.reviewer:
|
||||
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
|
||||
|
||||
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
|
||||
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
|
||||
|
||||
if not (is_reviewer or can_manage_request):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
if request.method == "POST" and request.POST.get("action") == "reject":
|
||||
form = RejectReviewerAssignmentForm(request.POST)
|
||||
if form.is_valid():
|
||||
# reject the request
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="rejected")
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Assignment of request for {} review by {} to {} was rejected".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.reviewer.person,
|
||||
),
|
||||
)
|
||||
|
||||
# make a new unassigned review request
|
||||
new_review_req = make_new_review_request_from_existing(review_req)
|
||||
new_review_req.save()
|
||||
|
||||
msg = render_to_string("doc/mail/reviewer_assignment_rejected.txt", {
|
||||
"by": request.user.person,
|
||||
"message_to_secretary": form.cleaned_data.get("message_to_secretary")
|
||||
})
|
||||
|
||||
email_about_review_request(request, review_req, "Reviewer assignment rejected", msg, by=request.user.person, notify_secretary=True, notify_reviewer=True)
|
||||
|
||||
return redirect(review_request, name=new_review_req.doc.name, request_id=new_review_req.pk)
|
||||
else:
|
||||
form = RejectReviewerAssignmentForm()
|
||||
|
||||
return render(request, 'doc/review/reject_reviewer_assignment.html', {
|
||||
'doc': doc,
|
||||
'review_req': review_req,
|
||||
'form': form,
|
||||
})
|
||||
|
||||
class CompleteReviewForm(forms.Form):
|
||||
state = forms.ModelChoiceField(queryset=ReviewRequestStateName.objects.filter(slug__in=("completed", "part-completed")).order_by("-order"), widget=forms.RadioSelect, initial="completed")
|
||||
reviewed_rev = forms.CharField(label="Reviewed revision", max_length=4)
|
||||
result = forms.ModelChoiceField(queryset=ReviewResultName.objects.filter(used=True), widget=forms.RadioSelect, empty_label=None)
|
||||
ACTIONS = [
|
||||
("enter", "Enter review content (automatically posts to {mailing_list})"),
|
||||
("upload", "Upload review content in text file (automatically posts to {mailing_list})"),
|
||||
("link", "Link to review message already sent to {mailing_list}"),
|
||||
]
|
||||
review_submission = forms.ChoiceField(choices=ACTIONS, widget=forms.RadioSelect)
|
||||
|
||||
review_url = forms.URLField(label="Link to message", required=False)
|
||||
review_file = forms.FileField(label="Text file to upload", required=False)
|
||||
review_content = forms.CharField(widget=forms.Textarea, required=False)
|
||||
|
||||
def __init__(self, review_req, *args, **kwargs):
|
||||
self.review_req = review_req
|
||||
|
||||
super(CompleteReviewForm, self).__init__(*args, **kwargs)
|
||||
|
||||
doc = self.review_req.doc
|
||||
|
||||
known_revisions = NewRevisionDocEvent.objects.filter(doc=doc).order_by("-time").values_list("rev", flat=True)
|
||||
|
||||
self.fields["state"].choices = [
|
||||
(slug, "{} - extra reviewer is to be assigned".format(label)) if slug == "part-completed" else (slug, label)
|
||||
for slug, label in self.fields["state"].choices
|
||||
]
|
||||
|
||||
self.fields["reviewed_rev"].help_text = mark_safe(
|
||||
" ".join("<a class=\"rev label label-default\">{}</a>".format(r)
|
||||
for r in known_revisions))
|
||||
|
||||
self.fields["result"].queryset = self.fields["result"].queryset.filter(teams=review_req.team)
|
||||
self.fields["review_submission"].choices = [
|
||||
(k, label.format(mailing_list=review_req.team.list_email or "[error: team has no mailing list set]"))
|
||||
for k, label in self.fields["review_submission"].choices
|
||||
]
|
||||
|
||||
def clean_reviewed_rev(self):
|
||||
return clean_doc_revision(self.review_req.doc, self.cleaned_data.get("reviewed_rev"))
|
||||
|
||||
def clean_review_content(self):
|
||||
return self.cleaned_data["review_content"].replace("\r", "")
|
||||
|
||||
def clean_review_file(self):
|
||||
return get_cleaned_text_file_content(self.cleaned_data["review_file"])
|
||||
|
||||
def clean(self):
|
||||
def require_field(f):
|
||||
if not self.cleaned_data.get(f):
|
||||
self.add_error(f, ValidationError("You must fill in this field."))
|
||||
|
||||
submission_method = self.cleaned_data.get("review_submission")
|
||||
if submission_method == "enter":
|
||||
require_field("review_content")
|
||||
elif submission_method == "upload":
|
||||
require_field("review_file")
|
||||
elif submission_method == "link":
|
||||
require_field("review_url")
|
||||
require_field("review_content")
|
||||
|
||||
@login_required
|
||||
def complete_review(request, name, request_id):
|
||||
doc = get_object_or_404(Document, name=name)
|
||||
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
|
||||
|
||||
if not review_req.reviewer:
|
||||
return redirect(review_request, name=review_req.doc.name, request_id=review_req.pk)
|
||||
|
||||
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
|
||||
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
|
||||
|
||||
if not (is_reviewer or can_manage_request):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
if request.method == "POST":
|
||||
form = CompleteReviewForm(review_req, request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
review_submission = form.cleaned_data['review_submission']
|
||||
|
||||
# create review doc
|
||||
for i in range(1, 100):
|
||||
name_components = [
|
||||
"review",
|
||||
review_req.team.acronym,
|
||||
review_req.type.slug,
|
||||
review_req.reviewer.person.ascii_parts()[3],
|
||||
skip_prefix(review_req.doc.name, "draft-"),
|
||||
form.cleaned_data["reviewed_rev"],
|
||||
]
|
||||
if i > 1:
|
||||
name_components.append(str(i))
|
||||
|
||||
name = "-".join(c for c in name_components if c).lower()
|
||||
if not Document.objects.filter(name=name).exists():
|
||||
review = Document.objects.create(name=name)
|
||||
break
|
||||
|
||||
review.type = DocTypeName.objects.get(slug="review")
|
||||
review.rev = "00"
|
||||
review.title = "Review of {}-{}".format(review_req.doc.name, review_req.reviewed_rev)
|
||||
review.group = review_req.team
|
||||
if review_submission == "link":
|
||||
review.external_url = form.cleaned_data['review_url']
|
||||
review.save()
|
||||
review.set_state(State.objects.get(type="review", slug="active"))
|
||||
DocAlias.objects.create(document=review, name=review.name)
|
||||
|
||||
NewRevisionDocEvent.objects.create(
|
||||
type="new_revision",
|
||||
doc=review,
|
||||
by=request.user.person,
|
||||
rev=review.rev,
|
||||
desc='New revision available',
|
||||
time=review.time,
|
||||
)
|
||||
|
||||
# save file on disk
|
||||
if review_submission == "upload":
|
||||
encoded_content = form.cleaned_data['review_file']
|
||||
else:
|
||||
encoded_content = form.cleaned_data['review_content'].encode("utf-8")
|
||||
|
||||
filename = os.path.join(review.get_file_path(), '{}-{}.txt'.format(review.name, review.rev))
|
||||
with open(filename, 'wb') as destination:
|
||||
destination.write(encoded_content)
|
||||
|
||||
# close review request
|
||||
review_req.state = form.cleaned_data["state"]
|
||||
review_req.reviewed_rev = form.cleaned_data["reviewed_rev"]
|
||||
review_req.result = form.cleaned_data["result"]
|
||||
review_req.review = review
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Request for {} review by {} {}".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.state.name,
|
||||
),
|
||||
)
|
||||
|
||||
if review_req.state_id == "part-completed":
|
||||
new_review_req = make_new_review_request_from_existing(review_req)
|
||||
new_review_req.save()
|
||||
|
||||
subject = "Review of {}-{} completed partially".format(review_req.doc.name, review_req.reviewed_rev)
|
||||
|
||||
msg = render_to_string("doc/mail/partially_completed_review.txt", {
|
||||
"domain": Site.objects.get_current().domain,
|
||||
"by": request.user.person,
|
||||
"new_review_req": new_review_req,
|
||||
})
|
||||
|
||||
email_about_review_request(request, review_req, subject, msg, request.user.person, notify_secretary=True, notify_reviewer=False)
|
||||
|
||||
if review_submission != "link" and review_req.team.list_email:
|
||||
# email the review
|
||||
subject = "{} of {}-{}".format("Partial review" if review_req.state_id == "part-completed" else "Review", review_req.doc.name, review_req.reviewed_rev)
|
||||
msg = send_mail(request, [(review_req.team.name, review_req.team.list_email)], None,
|
||||
subject,
|
||||
"doc/mail/completed_review.txt", {
|
||||
"review_req": review_req,
|
||||
"content": encoded_content.decode("utf-8"),
|
||||
})
|
||||
|
||||
list_name = mailarch.list_name_from_email(review_req.team.list_email)
|
||||
if list_name:
|
||||
review.external_url = mailarch.construct_message_url(list_name, email.utils.unquote(msg["Message-ID"]))
|
||||
review.save()
|
||||
|
||||
return redirect("doc_view", name=review_req.review.name)
|
||||
else:
|
||||
form = CompleteReviewForm(review_req)
|
||||
|
||||
mail_archive_query_urls = mailarch.construct_query_urls(review_req)
|
||||
|
||||
return render(request, 'doc/review/complete_review.html', {
|
||||
'doc': doc,
|
||||
'review_req': review_req,
|
||||
'form': form,
|
||||
'mail_archive_query_urls': mail_archive_query_urls,
|
||||
})
|
||||
|
||||
def search_mail_archive(request, name, request_id):
|
||||
#doc = get_object_or_404(Document, name=name)
|
||||
review_req = get_object_or_404(ReviewRequest, pk=request_id, state__in=["requested", "accepted"])
|
||||
|
||||
is_reviewer = user_is_person(request.user, review_req.reviewer.person)
|
||||
can_manage_request = can_manage_review_requests_for_team(request.user, review_req.team)
|
||||
|
||||
if not (is_reviewer or can_manage_request):
|
||||
return HttpResponseForbidden("You do not have permission to perform this action")
|
||||
|
||||
res = mailarch.construct_query_urls(review_req, query=request.GET.get("query"))
|
||||
if not res:
|
||||
return JsonResponse({ "error": "Couldn't do lookup in mail archive - don't know where to look"})
|
||||
|
||||
MAX_RESULTS = 30
|
||||
|
||||
try:
|
||||
res["messages"] = mailarch.retrieve_messages(res["query_data_url"])[:MAX_RESULTS]
|
||||
except Exception as e:
|
||||
res["error"] = "Retrieval from mail archive failed: {}".format(unicode(e))
|
||||
# raise # useful when debugging
|
||||
|
||||
return JsonResponse(res)
|
||||
|
|
@ -3,7 +3,8 @@ from ietf.name.models import (GroupTypeName, GroupStateName, RoleName, StreamNam
|
|||
DocRelationshipName, DocTypeName, DocTagName, StdLevelName, IntendedStdLevelName,
|
||||
DocReminderTypeName, BallotPositionName, SessionStatusName, TimeSlotTypeName,
|
||||
ConstraintName, NomineePositionStateName, FeedbackTypeName, DBTemplateTypeName,
|
||||
DraftSubmissionStateName, RoomResourceName)
|
||||
DraftSubmissionStateName, RoomResourceName,
|
||||
ReviewRequestStateName, ReviewTypeName, ReviewResultName)
|
||||
|
||||
|
||||
class NameAdmin(admin.ModelAdmin):
|
||||
|
@ -35,3 +36,6 @@ admin.site.register(FeedbackTypeName, NameAdmin)
|
|||
admin.site.register(DBTemplateTypeName, NameAdmin)
|
||||
admin.site.register(DraftSubmissionStateName, NameAdmin)
|
||||
admin.site.register(RoomResourceName, NameAdmin)
|
||||
admin.site.register(ReviewRequestStateName, NameAdmin)
|
||||
admin.site.register(ReviewTypeName, NameAdmin)
|
||||
admin.site.register(ReviewResultName, NameAdmin)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# simple script for exporting name related base data for the tests
|
||||
|
||||
# boiler plate
|
||||
import os, sys
|
||||
import django
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('group', '0008_auto_20160505_0523'),
|
||||
('name', '0010_new_liaison_names'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReviewRequestStateName',
|
||||
fields=[
|
||||
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('desc', models.TextField(blank=True)),
|
||||
('used', models.BooleanField(default=True)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['order'],
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReviewResultName',
|
||||
fields=[
|
||||
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('desc', models.TextField(blank=True)),
|
||||
('used', models.BooleanField(default=True)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('teams', models.ManyToManyField(help_text=b"Which teams this result can be set for. This also implicitly defines which teams are review teams - if there are no possible review results defined for a given team, it can't be a review team.", to='group.Group', blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['order'],
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReviewTypeName',
|
||||
fields=[
|
||||
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('desc', models.TextField(blank=True)),
|
||||
('used', models.BooleanField(default=True)),
|
||||
('order', models.IntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['order'],
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
62
ietf/name/migrations/0012_insert_review_name_data.py
Normal file
62
ietf/name/migrations/0012_insert_review_name_data.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def insert_initial_review_data(apps, schema_editor):
|
||||
ReviewRequestStateName = apps.get_model("name", "ReviewRequestStateName")
|
||||
ReviewRequestStateName.objects.get_or_create(slug="requested", name="Requested", order=1)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="accepted", name="Accepted", order=2)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="rejected", name="Rejected", order=3)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="withdrawn", name="Withdrawn", order=4)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="overtaken", name="Overtaken By Events", order=5)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="noresponse", name="No Response", order=6)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="part-completed", name="Partially Completed", order=6)
|
||||
ReviewRequestStateName.objects.get_or_create(slug="completed", name="Completed", order=8)
|
||||
|
||||
ReviewTypeName = apps.get_model("name", "ReviewTypeName")
|
||||
ReviewTypeName.objects.get_or_create(slug="early", name="Early", order=1)
|
||||
ReviewTypeName.objects.get_or_create(slug="lc", name="Last Call", order=2)
|
||||
ReviewTypeName.objects.get_or_create(slug="telechat", name="Telechat", order=3)
|
||||
|
||||
ReviewResultName = apps.get_model("name", "ReviewResultName")
|
||||
ReviewResultName.objects.get_or_create(slug="serious-issues", name="Serious Issues", order=1)
|
||||
ReviewResultName.objects.get_or_create(slug="issues", name="Has Issues", order=2)
|
||||
ReviewResultName.objects.get_or_create(slug="nits", name="Has Nits", order=3)
|
||||
|
||||
ReviewResultName.objects.get_or_create(slug="not-ready", name="Not Ready", order=4)
|
||||
ReviewResultName.objects.get_or_create(slug="right-track", name="On the Right Track", order=5)
|
||||
ReviewResultName.objects.get_or_create(slug="almost-ready", name="Almost Ready", order=6)
|
||||
|
||||
ReviewResultName.objects.get_or_create(slug="ready-issues", name="Ready with Issues", order=7)
|
||||
ReviewResultName.objects.get_or_create(slug="ready-nits", name="Ready with Nits", order=8)
|
||||
ReviewResultName.objects.get_or_create(slug="ready", name="Ready", order=9)
|
||||
|
||||
RoleName = apps.get_model("name", "RoleName")
|
||||
RoleName.objects.get_or_create(slug="reviewer", name="Reviewer", order=max(r.order for r in RoleName.objects.exclude(slug="reviewer")) + 1)
|
||||
|
||||
DocTypeName = apps.get_model("name", "DocTypeName")
|
||||
DocTypeName.objects.get_or_create(slug="review", name="Review")
|
||||
|
||||
StateType = apps.get_model("doc", "StateType")
|
||||
review_state_type, _ = StateType.objects.get_or_create(slug="review", label="Review")
|
||||
|
||||
State = apps.get_model("doc", "State")
|
||||
State.objects.get_or_create(type=review_state_type, slug="active", name="Active", order=1)
|
||||
State.objects.get_or_create(type=review_state_type, slug="deleted", name="Deleted", order=2)
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0011_reviewrequeststatename_reviewresultname_reviewtypename'),
|
||||
('group', '0001_initial'),
|
||||
('doc', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(insert_initial_review_data, noop),
|
||||
]
|
|
@ -14,7 +14,7 @@ class NameModel(models.Model):
|
|||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ['order']
|
||||
ordering = ['order', 'name']
|
||||
|
||||
class GroupStateName(NameModel):
|
||||
"""BOF, Proposed, Active, Dormant, Concluded, Abandoned"""
|
||||
|
@ -87,3 +87,14 @@ class LiaisonStatementEventTypeName(NameModel):
|
|||
"Submitted, Modified, Approved, Posted, Killed, Resurrected, MsgIn, MsgOut, Comment"
|
||||
class LiaisonStatementTagName(NameModel):
|
||||
"Action Required, Action Taken"
|
||||
class ReviewRequestStateName(NameModel):
|
||||
"""Requested, Accepted, Rejected, Withdrawn, Overtaken By Events,
|
||||
No Response, Partially Completed, Completed"""
|
||||
class ReviewTypeName(NameModel):
|
||||
"""Early Review, Last Call, Telechat"""
|
||||
class ReviewResultName(NameModel):
|
||||
"""Almost ready, Has issues, Has nits, Not Ready,
|
||||
On the right track, Ready, Ready with issues,
|
||||
Ready with nits, Serious Issues"""
|
||||
teams = models.ManyToManyField("group.Group", help_text="Which teams this result can be set for. This also implicitly defines which teams are review teams - if there are no possible review results defined for a given team, it can't be a review team.", blank=True)
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte
|
|||
IprEventTypeName, GroupMilestoneStateName, SessionStatusName, DocReminderTypeName,
|
||||
ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName,
|
||||
LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName,
|
||||
BallotPositionName, DBTemplateTypeName, NomineePositionStateName)
|
||||
BallotPositionName, DBTemplateTypeName, NomineePositionStateName,
|
||||
ReviewRequestStateName, ReviewTypeName, ReviewResultName)
|
||||
|
||||
|
||||
class TimeSlotTypeNameResource(ModelResource):
|
||||
|
@ -413,3 +414,46 @@ class NomineePositionStateNameResource(ModelResource):
|
|||
}
|
||||
api.name.register(NomineePositionStateNameResource())
|
||||
|
||||
class ReviewRequestStateNameResource(ModelResource):
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = ReviewRequestStateName.objects.all()
|
||||
#resource_name = 'reviewrequeststatename'
|
||||
filtering = {
|
||||
"slug": ALL,
|
||||
"name": ALL,
|
||||
"desc": ALL,
|
||||
"used": ALL,
|
||||
"order": ALL,
|
||||
}
|
||||
api.name.register(ReviewRequestStateNameResource())
|
||||
|
||||
class ReviewTypeNameResource(ModelResource):
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = ReviewTypeName.objects.all()
|
||||
#resource_name = 'reviewtypename'
|
||||
filtering = {
|
||||
"slug": ALL,
|
||||
"name": ALL,
|
||||
"desc": ALL,
|
||||
"used": ALL,
|
||||
"order": ALL,
|
||||
}
|
||||
api.name.register(ReviewTypeNameResource())
|
||||
|
||||
class ReviewResultNameResource(ModelResource):
|
||||
class Meta:
|
||||
cache = SimpleCache()
|
||||
queryset = ReviewResultName.objects.all()
|
||||
#resource_name = 'reviewresultname'
|
||||
filtering = {
|
||||
"slug": ALL,
|
||||
"name": ALL,
|
||||
"desc": ALL,
|
||||
"used": ALL,
|
||||
"order": ALL,
|
||||
"teams": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.name.register(ReviewResultNameResource())
|
||||
|
||||
|
|
0
ietf/review/__init__.py
Normal file
0
ietf/review/__init__.py
Normal file
95
ietf/review/mailarch.py
Normal file
95
ietf/review/mailarch.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# various utilities for working with the mailarch mail archive at
|
||||
# mailarchive.ietf.org
|
||||
|
||||
import datetime, tarfile, mailbox, tempfile, hashlib, base64, email.utils
|
||||
import urllib
|
||||
import urllib2, contextlib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
def list_name_from_email(list_email):
|
||||
if not list_email.endswith("@ietf.org"):
|
||||
return None
|
||||
|
||||
return list_email[:-len("@ietf.org")]
|
||||
|
||||
def hash_list_message_id(list_name, msgid):
|
||||
# hash in mailarch is computed similar to
|
||||
# https://www.mail-archive.com/faq.html#listserver except the list
|
||||
# name (without "@ietf.org") is used instead of the full address,
|
||||
# and rightmost "=" signs are (optionally) stripped
|
||||
sha = hashlib.sha1(msgid)
|
||||
sha.update(list_name)
|
||||
return base64.urlsafe_b64encode(sha.digest()).rstrip("=")
|
||||
|
||||
def construct_query_urls(review_req, query=None):
|
||||
list_name = list_name_from_email(review_req.team.list_email)
|
||||
if not list_name:
|
||||
return None
|
||||
|
||||
if not query:
|
||||
query = review_req.doc.name
|
||||
|
||||
encoded_query = "?" + urllib.urlencode({
|
||||
"qdr": "c", # custom time frame
|
||||
"start_date": (datetime.date.today() - datetime.timedelta(days=180)).isoformat(),
|
||||
"email_list": list_name,
|
||||
"q": "subject:({})".format(query),
|
||||
"as": "1", # this is an advanced search
|
||||
})
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"query_url": settings.MAILING_LIST_ARCHIVE_URL + "/arch/search/" + encoded_query,
|
||||
"query_data_url": settings.MAILING_LIST_ARCHIVE_URL + "/arch/export/mbox/" + encoded_query,
|
||||
}
|
||||
|
||||
def construct_message_url(list_name, msgid):
|
||||
return "{}/arch/msg/{}/{}".format(settings.MAILING_LIST_ARCHIVE_URL, list_name, hash_list_message_id(list_name, msgid))
|
||||
|
||||
def retrieve_messages_from_mbox(mbox_fileobj):
|
||||
"""Return selected content in message from mbox from mailarch."""
|
||||
res = []
|
||||
with tempfile.NamedTemporaryFile(suffix=".mbox") as mbox_file:
|
||||
# mailbox.mbox needs a path, so we need to put the contents
|
||||
# into a file
|
||||
mbox_data = mbox_fileobj.read()
|
||||
mbox_file.write(mbox_data)
|
||||
mbox_file.flush()
|
||||
|
||||
mbox = mailbox.mbox(mbox_file.name, create=False)
|
||||
for msg in mbox:
|
||||
content = u""
|
||||
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
charset = part.get_content_charset() or "utf-8"
|
||||
content += part.get_payload(decode=True).decode(charset, "ignore")
|
||||
|
||||
res.append({
|
||||
"from": msg["From"],
|
||||
"subject": msg["Subject"],
|
||||
"content": content.replace("\r\n", "\n").replace("\r", "\n").strip("\n"),
|
||||
"message_id": email.utils.unquote(msg["Message-ID"]),
|
||||
"url": email.utils.unquote(msg["Archived-At"]),
|
||||
"date": msg["Date"],
|
||||
})
|
||||
|
||||
return res
|
||||
|
||||
def retrieve_messages(query_data_url):
|
||||
"""Retrieve and return selected content from mailarch."""
|
||||
res = []
|
||||
|
||||
with contextlib.closing(urllib2.urlopen(query_data_url, timeout=15)) as fileobj:
|
||||
content_type = fileobj.info()["Content-type"]
|
||||
if not content_type.startswith("application/x-tar"):
|
||||
raise Exception("Export failed - this usually means no matches were found")
|
||||
|
||||
with tarfile.open(fileobj=fileobj, mode='r|*') as tar:
|
||||
for entry in tar:
|
||||
if entry.isfile():
|
||||
mbox_fileobj = tar.extractfile(entry)
|
||||
res.extend(retrieve_messages_from_mbox(mbox_fileobj))
|
||||
|
||||
return res
|
52
ietf/review/migrations/0001_initial.py
Normal file
52
ietf/review/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('group', '0008_auto_20160505_0523'),
|
||||
('name', '0012_insert_review_name_data'),
|
||||
('doc', '0012_auto_20160207_0537'),
|
||||
('person', '0006_auto_20160503_0937'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Reviewer',
|
||||
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')),
|
||||
('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(help_text=b'Skip the next N review assignments')),
|
||||
('person', models.ForeignKey(to='person.Person')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReviewRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('deadline', models.DateTimeField()),
|
||||
('requested_rev', models.CharField(help_text=b'Fill in if a specific revision is to be reviewed, e.g. 02', max_length=16, verbose_name=b'requested revision', blank=True)),
|
||||
('reviewed_rev', models.CharField(max_length=16, verbose_name=b'reviewed revision', blank=True)),
|
||||
('doc', models.ForeignKey(related_name='review_request_set', to='doc.Document')),
|
||||
('result', models.ForeignKey(blank=True, to='name.ReviewResultName', null=True)),
|
||||
('review', models.OneToOneField(null=True, blank=True, to='doc.Document')),
|
||||
('reviewer', models.ForeignKey(blank=True, to='group.Role', null=True)),
|
||||
('state', models.ForeignKey(to='name.ReviewRequestStateName')),
|
||||
('team', models.ForeignKey(to='group.Group')),
|
||||
('type', models.ForeignKey(to='name.ReviewTypeName')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
0
ietf/review/migrations/__init__.py
Normal file
0
ietf/review/migrations/__init__.py
Normal file
46
ietf/review/models.py
Normal file
46
ietf/review/models.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from django.db import models
|
||||
|
||||
from ietf.doc.models import Document
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.person.models import Person
|
||||
from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResultName
|
||||
|
||||
class Reviewer(models.Model):
|
||||
"""Keeps track of admin data associated with the reviewer in the
|
||||
particular team. There will be one record for each combination of
|
||||
reviewer and team."""
|
||||
team = models.ForeignKey(Group)
|
||||
person = models.ForeignKey(Person)
|
||||
frequency = models.IntegerField(help_text="Can review every N days", default=30)
|
||||
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(help_text="Skip the next N review assignments")
|
||||
|
||||
class ReviewRequest(models.Model):
|
||||
"""Represents a request for a review and the process it goes through.
|
||||
There should be one ReviewRequest entered for each combination of
|
||||
document, rev, and reviewer."""
|
||||
state = models.ForeignKey(ReviewRequestStateName)
|
||||
|
||||
# Fields filled in on the initial record creation - these
|
||||
# constitute the request part.
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
type = models.ForeignKey(ReviewTypeName)
|
||||
doc = models.ForeignKey(Document, related_name='review_request_set')
|
||||
team = models.ForeignKey(Group, limit_choices_to=~models.Q(reviewresultname=None))
|
||||
deadline = models.DateTimeField()
|
||||
requested_rev = models.CharField(verbose_name="requested revision", max_length=16, blank=True, help_text="Fill in if a specific revision is to be reviewed, e.g. 02")
|
||||
|
||||
# Fields filled in as reviewer is assigned and as the review is
|
||||
# uploaded. Once these are filled in and we progress beyond the
|
||||
# states requested/assigned, any changes to the assignment happens
|
||||
# by closing down the current request and making a new one,
|
||||
# copying the request-part fields above.
|
||||
reviewer = models.ForeignKey(Role, blank=True, null=True)
|
||||
|
||||
review = models.OneToOneField(Document, blank=True, null=True)
|
||||
reviewed_rev = models.CharField(verbose_name="reviewed revision", max_length=16, blank=True)
|
||||
result = models.ForeignKey(ReviewResultName, blank=True, null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s review on %s by %s %s" % (self.type, self.doc, self.team, self.state)
|
65
ietf/review/resources.py
Normal file
65
ietf/review/resources.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
# Autogenerated by the makeresources management command 2016-06-14 04:21 PDT
|
||||
from tastypie.resources import ModelResource
|
||||
from tastypie.fields import ToManyField # pyflakes:ignore
|
||||
from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore
|
||||
from tastypie.cache import SimpleCache
|
||||
|
||||
from ietf import api
|
||||
from ietf.api import ToOneField # pyflakes:ignore
|
||||
|
||||
from ietf.review.models import * # pyflakes:ignore
|
||||
|
||||
|
||||
from ietf.person.resources import PersonResource
|
||||
from ietf.group.resources import GroupResource
|
||||
class ReviewerResource(ModelResource):
|
||||
team = ToOneField(GroupResource, 'team')
|
||||
person = ToOneField(PersonResource, 'person')
|
||||
class Meta:
|
||||
queryset = Reviewer.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'reviewer'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"frequency": ALL,
|
||||
"unavailable_until": ALL,
|
||||
"filter_re": ALL,
|
||||
"skip_next": ALL,
|
||||
"team": ALL_WITH_RELATIONS,
|
||||
"person": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.review.register(ReviewerResource())
|
||||
|
||||
from ietf.doc.resources import DocumentResource
|
||||
from ietf.group.resources import RoleResource, GroupResource
|
||||
from ietf.name.resources import ReviewRequestStateNameResource, ReviewResultNameResource, ReviewTypeNameResource
|
||||
class ReviewRequestResource(ModelResource):
|
||||
state = ToOneField(ReviewRequestStateNameResource, 'state')
|
||||
type = ToOneField(ReviewTypeNameResource, 'type')
|
||||
doc = ToOneField(DocumentResource, 'doc')
|
||||
team = ToOneField(GroupResource, 'team')
|
||||
reviewer = ToOneField(RoleResource, 'reviewer', null=True)
|
||||
review = ToOneField(DocumentResource, 'review', null=True)
|
||||
result = ToOneField(ReviewResultNameResource, 'result', null=True)
|
||||
class Meta:
|
||||
queryset = ReviewRequest.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'reviewrequest'
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"time": ALL,
|
||||
"deadline": ALL,
|
||||
"requested_rev": ALL,
|
||||
"reviewed_rev": ALL,
|
||||
"state": ALL_WITH_RELATIONS,
|
||||
"type": ALL_WITH_RELATIONS,
|
||||
"doc": ALL_WITH_RELATIONS,
|
||||
"team": ALL_WITH_RELATIONS,
|
||||
"reviewer": ALL_WITH_RELATIONS,
|
||||
"review": ALL_WITH_RELATIONS,
|
||||
"result": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.review.register(ReviewRequestResource())
|
||||
|
94
ietf/review/utils.py
Normal file
94
ietf/review/utils.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
from django.contrib.sites.models import Site
|
||||
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.doc.models import DocEvent
|
||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream
|
||||
from ietf.review.models import ReviewRequestStateName, ReviewRequest
|
||||
from ietf.utils.mail import send_mail
|
||||
|
||||
def active_review_teams():
|
||||
# if there's a ReviewResultName defined, it's a review team
|
||||
return Group.objects.filter(state="active").exclude(reviewresultname=None)
|
||||
|
||||
def can_request_review_of_doc(user, doc):
|
||||
if not user.is_authenticated():
|
||||
return False
|
||||
|
||||
return is_authorized_in_doc_stream(user, doc)
|
||||
|
||||
def can_manage_review_requests_for_team(user, team):
|
||||
if not user.is_authenticated():
|
||||
return False
|
||||
|
||||
return Role.objects.filter(name__in=["secretary", "delegate"], person__user=user, group=team).exists() or has_role(user, "Secretariat")
|
||||
|
||||
def make_new_review_request_from_existing(review_req):
|
||||
obj = ReviewRequest()
|
||||
obj.time = review_req.time
|
||||
obj.type = review_req.type
|
||||
obj.doc = review_req.doc
|
||||
obj.team = review_req.team
|
||||
obj.deadline = review_req.deadline
|
||||
obj.requested_rev = review_req.requested_rev
|
||||
obj.state = ReviewRequestStateName.objects.get(slug="requested")
|
||||
return obj
|
||||
|
||||
def email_about_review_request(request, review_req, subject, msg, by, notify_secretary, notify_reviewer):
|
||||
"""Notify possibly both secretary and reviewer about change, skipping
|
||||
a party if the change was done by that party."""
|
||||
|
||||
def extract_email_addresses(roles):
|
||||
if any(r.person == by for r in roles if r):
|
||||
return []
|
||||
else:
|
||||
return [r.formatted_email() for r in roles if r]
|
||||
|
||||
to = []
|
||||
|
||||
if notify_secretary:
|
||||
to += extract_email_addresses(Role.objects.filter(name__in=["secretary", "delegate"], group=review_req.team).distinct())
|
||||
if notify_reviewer:
|
||||
to += extract_email_addresses([review_req.reviewer])
|
||||
|
||||
if not to:
|
||||
return
|
||||
|
||||
send_mail(request, list(set(to)), None, subject, "doc/mail/review_request_changed.txt", {
|
||||
"domain": Site.objects.get_current().domain,
|
||||
"review_req": review_req,
|
||||
"msg": msg,
|
||||
})
|
||||
|
||||
def assign_review_request_to_reviewer(request, review_req, reviewer):
|
||||
assert review_req.state_id in ("requested", "accepted")
|
||||
|
||||
if reviewer == review_req.reviewer:
|
||||
return
|
||||
|
||||
if review_req.reviewer:
|
||||
email_about_review_request(
|
||||
request, review_req,
|
||||
"Unassigned from review of %s" % review_req.doc.name,
|
||||
"%s has cancelled your assignment to the review." % request.user.person,
|
||||
by=request.user.person, notify_secretary=False, notify_reviewer=True)
|
||||
|
||||
review_req.state = ReviewRequestStateName.objects.get(slug="requested")
|
||||
review_req.reviewer = reviewer
|
||||
review_req.save()
|
||||
|
||||
DocEvent.objects.create(
|
||||
type="changed_review_request",
|
||||
doc=review_req.doc,
|
||||
by=request.user.person,
|
||||
desc="Request for {} review by {} is assigned to {}".format(
|
||||
review_req.type.name,
|
||||
review_req.team.acronym.upper(),
|
||||
review_req.reviewer.person if review_req.reviewer else "(None)",
|
||||
),
|
||||
)
|
||||
|
||||
email_about_review_request(
|
||||
request, review_req,
|
||||
"Assigned to review of %s" % review_req.doc.name,
|
||||
"%s has assigned you to review the document." % request.user.person,
|
||||
by=request.user.person, notify_secretary=False, notify_reviewer=True)
|
|
@ -298,6 +298,7 @@ INSTALLED_APPS = (
|
|||
'ietf.person',
|
||||
'ietf.redirects',
|
||||
'ietf.release',
|
||||
'ietf.review',
|
||||
'ietf.submit',
|
||||
'ietf.sync',
|
||||
'ietf.utils',
|
||||
|
@ -428,6 +429,7 @@ MEETING_RECORDINGS_DIR = '/a/www/audio'
|
|||
|
||||
# Mailing list info URL for lists hosted on the IETF servers
|
||||
MAILING_LIST_INFO_URL = "https://www.ietf.org/mailman/listinfo/%(list_addr)s"
|
||||
MAILING_LIST_ARCHIVE_URL = "https://mailarchive.ietf.org"
|
||||
|
||||
# Liaison Statement Tool settings (one is used in DOC_HREFS below)
|
||||
LIAISON_UNIVERSAL_FROM = 'Liaison Statement Management Tool <lsmt@' + IETF_DOMAIN + '>'
|
||||
|
|
|
@ -460,6 +460,18 @@ label#list-feeds {
|
|||
margin-left: 3em;
|
||||
}
|
||||
|
||||
/* Review flow */
|
||||
|
||||
form.complete-review .mail-archive-search .query-input {
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
form.complete-review .mail-archive-search .results .list-group {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.photo-name {
|
||||
height: 3em;
|
||||
}
|
||||
|
|
131
ietf/static/ietf/js/complete-review.js
Normal file
131
ietf/static/ietf/js/complete-review.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
$(document).ready(function () {
|
||||
var form = $("form.complete-review");
|
||||
|
||||
var reviewedRev = form.find("[name=reviewed_rev]");
|
||||
reviewedRev.closest(".form-group").find("a.rev").on("click", function (e) {
|
||||
e.preventDefault();
|
||||
reviewedRev.val($(this).text());
|
||||
});
|
||||
|
||||
// mail archive search functionality
|
||||
var mailArchiveSearchTemplate = form.find(".template .mail-archive-search").parent().html();
|
||||
var mailArchiveSearchResultTemplate = form.find(".template .mail-archive-search-result").parent().html();
|
||||
|
||||
form.find("[name=review_url]").closest(".form-group").before(mailArchiveSearchTemplate);
|
||||
|
||||
var mailArchiveSearch = form.find(".mail-archive-search");
|
||||
|
||||
var retrievingData = null;
|
||||
|
||||
function searchMailArchive() {
|
||||
if (retrievingData)
|
||||
return;
|
||||
|
||||
var queryInput = mailArchiveSearch.find(".query-input");
|
||||
if (queryInput.length == 0 || !$.trim(queryInput.val()))
|
||||
return;
|
||||
|
||||
mailArchiveSearch.find(".search").prop("disabled", true);
|
||||
mailArchiveSearch.find(".error").addClass("hidden");
|
||||
mailArchiveSearch.find(".retrieving").removeClass("hidden");
|
||||
mailArchiveSearch.find(".results").addClass("hidden");
|
||||
|
||||
retrievingData = $.ajax({
|
||||
url: searchMailArchiveUrl,
|
||||
method: "GET",
|
||||
data: {
|
||||
query: queryInput.val()
|
||||
},
|
||||
dataType: "json",
|
||||
timeout: 20 * 1000
|
||||
}).then(function (data) {
|
||||
retrievingData = null;
|
||||
mailArchiveSearch.find(".search").prop("disabled", false);
|
||||
mailArchiveSearch.find(".retrieving").addClass("hidden");
|
||||
|
||||
var err = data.error;
|
||||
if (!err && (!data.messages || !data.messages.length))
|
||||
err = "No messages matching document name found in archive";
|
||||
|
||||
if (err) {
|
||||
var errorDiv = mailArchiveSearch.find(".error");
|
||||
errorDiv.removeClass("hidden");
|
||||
errorDiv.find(".content").text(err);
|
||||
if (data.query && data.query_url && data.query_data_url) {
|
||||
errorDiv.find(".try-yourself .query").text(data.query);
|
||||
errorDiv.find(".try-yourself .query-url").prop("href", data.query_url);
|
||||
errorDiv.find(".try-yourself .query-data-url").prop("href", data.query_data_url);
|
||||
errorDiv.find(".try-yourself").removeClass("hidden");
|
||||
}
|
||||
}
|
||||
else {
|
||||
mailArchiveSearch.find(".results").removeClass("hidden");
|
||||
|
||||
var results = mailArchiveSearch.find(".results .list-group");
|
||||
results.children().remove();
|
||||
|
||||
for (var i = 0; i < data.messages.length; ++i) {
|
||||
var msg = data.messages[i];
|
||||
var row = $(mailArchiveSearchResultTemplate).attr("title", "Click to fill in link and content from this message");
|
||||
row.find(".subject").text(msg.subject);
|
||||
row.find(".date").text(msg.date);
|
||||
row.data("url", msg.url);
|
||||
row.data("content", msg.content);
|
||||
results.append(row);
|
||||
}
|
||||
}
|
||||
}, function () {
|
||||
retrievingData = null;
|
||||
mailArchiveSearch.find(".search").prop("disabled", false);
|
||||
mailArchiveSearch.find(".retrieving").addClass("hidden");
|
||||
|
||||
var errorDiv = mailArchiveSearch.find(".error");
|
||||
errorDiv.removeClass("hidden");
|
||||
errorDiv.find(".content").text("Error trying to retrieve data from mailing list archive.");
|
||||
});
|
||||
}
|
||||
|
||||
mailArchiveSearch.find(".search").on("click", function () {
|
||||
searchMailArchive();
|
||||
});
|
||||
|
||||
mailArchiveSearch.find(".results").on("click", ".mail-archive-search-result", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var row = $(this);
|
||||
if (!row.is(".mail-archive-search-result"))
|
||||
row = row.closest(".mail-archive-search-result");
|
||||
|
||||
form.find("[name=review_url]").val(row.data("url"));
|
||||
form.find("[name=review_content]").val(row.data("content"));
|
||||
});
|
||||
|
||||
|
||||
// review submission selection
|
||||
form.find("[name=review_submission]").on("click change", function () {
|
||||
var val = form.find("[name=review_submission]:checked").val();
|
||||
|
||||
var shouldBeVisible = {
|
||||
"enter": ['[name="review_content"]'],
|
||||
"upload": ['[name="review_file"]'],
|
||||
"link": [".mail-archive-search", '[name="review_url"]', '[name="review_content"]']
|
||||
};
|
||||
|
||||
for (var v in shouldBeVisible) {
|
||||
for (var i in shouldBeVisible[v]) {
|
||||
var selector = shouldBeVisible[v][i];
|
||||
var row = form.find(selector);
|
||||
if (!row.is(".form-group"))
|
||||
row = row.closest(".form-group");
|
||||
|
||||
if ($.inArray(selector, shouldBeVisible[val]) != -1)
|
||||
row.show();
|
||||
else
|
||||
row.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (val == "link")
|
||||
searchMailArchive();
|
||||
}).trigger("change");
|
||||
});
|
|
@ -192,6 +192,32 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
{% if review_requests or can_request_review %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Reviews</th>
|
||||
<td class="edit"></td>
|
||||
<td>
|
||||
{% for r in review_requests %}
|
||||
<div>
|
||||
{% if r.state_id == "completed" or r.state_id == "part-completed" %}
|
||||
<a href="{% url "doc_view" r.review.name %}">{{ r.team.acronym|upper }} {{ r.type.name }} Review{% if r.reviewed_rev and r.reviewed_rev != doc.rev %} (of -{{ r.reviewed_rev }}){% endif %}: {{ r.result.name }}</a>
|
||||
{% else %}
|
||||
<a href="{% url "ietf.doc.views_review.review_request" doc.name r.pk %}">{{ r.team.acronym|upper }} {{ r.type.name }} Review ({{ r.state.name }})</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if can_request_review %}
|
||||
<div>
|
||||
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.request_review" doc.name %}"><span class="fa fa-check-circle-o"></span> Request review</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if conflict_reviews %}
|
||||
<tr>
|
||||
<th></th>
|
||||
|
|
81
ietf/templates/doc/document_review.html
Normal file
81
ietf/templates/doc/document_review.html
Normal file
|
@ -0,0 +1,81 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
|
||||
{% block title %}{{ doc.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
{{ top|safe }}
|
||||
|
||||
{% include "doc/revisions_list.html" %}
|
||||
|
||||
<table class="table table-condensed">
|
||||
<thead id="message-row">
|
||||
<tr>
|
||||
{% if doc.rev != latest_rev %}
|
||||
<th colspan="3" class="alert-warning">The information below is for an old version of the document</th>
|
||||
{% else %}
|
||||
<th colspan="3"></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="meta">
|
||||
<tr>
|
||||
<th class="col-md-1">Team</th>
|
||||
<td class="edit col-md-1"></td>
|
||||
<td class="col-md-10">
|
||||
{{ doc.group.name }}
|
||||
<a href="{{ doc.group.about_url }}">({{ doc.group.acronym }})</a>
|
||||
|
||||
{% if snapshot %}
|
||||
<span class="label label-warning">Snapshot</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td class="edit"></td>
|
||||
<td>{{ doc.title }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td class="edit"></td>
|
||||
<td>{{ doc.get_state.name }}</td>
|
||||
</tr>
|
||||
|
||||
{% if review_req %}
|
||||
<tr>
|
||||
<th>Review result</th>
|
||||
<td class="edit"></td>
|
||||
<td><a href="{% url "ietf.doc.views_review.review_request" review_req.doc.name review_req.pk %}">{{ review_req.result.name }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.external_url %}
|
||||
<tr>
|
||||
<th>Posted at</th>
|
||||
<td class="edit"></td>
|
||||
<td><a href="{{ doc.external_url }}">{{ doc.external_url }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<th>Last updated</th>
|
||||
<td class="edit"></td>
|
||||
<td>{{ doc.time|date:"Y-m-d" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>{{ doc.type.name }}<br><small>{{ doc.name }}</small></h2>
|
||||
|
||||
{% if doc.rev and content != None %}
|
||||
{{ content|fill:"80"|safe|linebreaksbr|keep_spacing|sanitize_html|safe }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
7
ietf/templates/doc/mail/completed_review.txt
Normal file
7
ietf/templates/doc/mail/completed_review.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% autoescape off %}{% filter wordwrap:70 %}{% if review_req.state_id == "part-completed" %}Review is partially done. Another review request has been registered for completing it.
|
||||
|
||||
{% endif %}Reviewer: {{ review_req.reviewer.person }}
|
||||
Review result: {{ review_req.result.name }}
|
||||
|
||||
{{ content }}
|
||||
{% endfilter %}{% endautoescape %}
|
6
ietf/templates/doc/mail/partially_completed_review.txt
Normal file
6
ietf/templates/doc/mail/partially_completed_review.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% autoescape off %}Review was partially completed by {{ by }}.
|
||||
|
||||
A new review request has been registered for completing the review:
|
||||
|
||||
https://{{ domain }}{% url "ietf.doc.views_review.review_request" name=new_review_req.doc.name request_id=new_review_req.pk %}
|
||||
{% endautoescape %}
|
7
ietf/templates/doc/mail/review_request_changed.txt
Normal file
7
ietf/templates/doc/mail/review_request_changed.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% autoescape off %}
|
||||
{{ review_req.type.name }} review of: {{ review_req.doc.name }}{% if review_req.requested_rev %}-{{ review_req.requested_rev }}{% endif %}
|
||||
https://{{ domain }}{% url "ietf.doc.views_review.review_request" name=review_req.doc.name request_id=review_req.pk %}
|
||||
|
||||
{{ msg|wordwrap:72 }}
|
||||
|
||||
{% endautoescape %}
|
6
ietf/templates/doc/mail/reviewer_assignment_rejected.txt
Normal file
6
ietf/templates/doc/mail/reviewer_assignment_rejected.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% autoescape off %}Reviewer assignment rejected by {{ by }}.{% if message_to_secretary %}
|
||||
|
||||
Explanation:
|
||||
|
||||
{{ message_to_secretary }}
|
||||
{% endif %}{% endautoescape %}
|
22
ietf/templates/doc/review/assign_reviewer.html
Normal file
22
ietf/templates/doc/review/assign_reviewer.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}Assign reviewer for {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Assign reviewer<br><small>{{ review_req.doc.name }}</small></h1>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary" name="action" value="assign">Assign reviewer</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
81
ietf/templates/doc/review/complete_review.html
Normal file
81
ietf/templates/doc/review/complete_review.html
Normal file
|
@ -0,0 +1,81 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}Complete review of {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Complete review<br><small>{{ review_req.doc.name }}</small></h1>
|
||||
|
||||
<p>The review findings should be made available here and the review
|
||||
posted to the mailing list. If you enter the findings below, the
|
||||
system will post the review for you. If you already have posted
|
||||
the review, you can try to let the system find a link to the
|
||||
archive and retrieve the email body.</p>
|
||||
|
||||
<form class="complete-review form-horizontal" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Complete review</button>
|
||||
{% endbuttons %}
|
||||
|
||||
<div class="template" style="display:none">
|
||||
{% if mail_archive_query_urls %}
|
||||
<div class="mail-archive-search form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<p class="form-inline">
|
||||
Search mail archive subjects for:
|
||||
<input class="query-input form-control input-sm" value="{{ mail_archive_query_urls.query }}">
|
||||
<button type="button" class="search btn btn-default btn-sm">Search</button>
|
||||
</p>
|
||||
|
||||
<div class="retrieving hidden">
|
||||
<span class="fa fa-spin fa-circle-o-notch"></span>
|
||||
Searching...
|
||||
</div>
|
||||
|
||||
<div class="results hidden">
|
||||
<p>Select one of the following messages to automatically pre-fill link and content:</p>
|
||||
<div class="list-group">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error alert alert-warning hidden">
|
||||
<p>
|
||||
<span class="content"></span>
|
||||
<span class="hidden try-yourself">(searched for <a class="query-url" href="">"<span class="query"></span>"</a>, corresponding <a class="query-data-url" href="">export</a>).</span>
|
||||
You have to fill in link and content yourself.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mail-archive-search">
|
||||
<small class="text-muted">Mailing list does not have a recognized ietf.org archive. Auto-searching disabled.</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="template" style="display:none">
|
||||
<button type="button" class="mail-archive-search-result list-group-item">
|
||||
<span class="date badge"></span>
|
||||
<span class="subject"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
var possibleRevisions = {{ possible_revisions_as_json|safe }};
|
||||
var searchMailArchiveUrl = "{% url "ietf.doc.views_review.search_mail_archive" name=review_req.doc.name request_id=review_req.pk %}";
|
||||
</script>
|
||||
<script src="{% static 'ietf/js/complete-review.js' %}"></script>
|
||||
{% endblock %}
|
24
ietf/templates/doc/review/reject_reviewer_assignment.html
Normal file
24
ietf/templates/doc/review/reject_reviewer_assignment.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}Reject review assignment for {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Reject review assignment<br><small>{{ review_req.doc.name }}</small></h1>
|
||||
|
||||
<p>{{ review_req.reviewer.person }} is currently assigned to do the review. Do you want to reject this assignment?</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary" name="action" value="reject">Reject assignment</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
34
ietf/templates/doc/review/request_review.html
Normal file
34
ietf/templates/doc/review/request_review.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Request review of {{ doc.name }} {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Request review<br><small>{{ doc.name }}</small></h1>
|
||||
|
||||
<p>Submit a request to have the document reviewed.</p>
|
||||
|
||||
<form class="form-horizontal" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.type layout="horizontal" %}
|
||||
{% bootstrap_field form.team layout="horizontal" %}
|
||||
{% bootstrap_field form.deadline_date layout="horizontal" %}
|
||||
{% bootstrap_field form.deadline_time layout="horizontal" %}
|
||||
{% bootstrap_field form.requested_rev layout="horizontal" %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Request review</button>
|
||||
<a class="btn btn-default pull-right" href="{% url "doc_view" name=doc.canonical_name %}">Back</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
{% endblock %}
|
135
ietf/templates/doc/review/review_request.html
Normal file
135
ietf/templates/doc/review/review_request.html
Normal file
|
@ -0,0 +1,135 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}Review request for {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Review request<br><small>{{ review_req.doc.name }}</small></h1>
|
||||
|
||||
<table class="table table-condensed">
|
||||
<tbody class="meta">
|
||||
<tr>
|
||||
<th>Request</th>
|
||||
<th>Review of</th>
|
||||
<td>
|
||||
{% if review_req.requested_rev %}
|
||||
<a href="{% url "doc_view" name=review_req.doc.name rev=review_req.requested_rev %}">{{ review_req.doc.name }}-<b>{{ review_req.requested_rev }}</b></a>
|
||||
{% else %}
|
||||
<a href="{% url "doc_view" name=review_req.doc.name %}">{{ review_req.doc.name }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_withdraw_request %}
|
||||
<a class="btn btn-danger btn-xs" href="{% url "ietf.doc.views_review.withdraw_request" name=doc.name request_id=review_req.pk %}"><span class="fa fa-ban"></span> Withdraw request</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Type</th>
|
||||
<td>{{ review_req.type.name }} Review</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Team</th>
|
||||
<td>{{ review_req.team.acronym|upper }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Deadline</th>
|
||||
<td>
|
||||
{% if review_req.deadline|date:"H:i" != "23:59" %}
|
||||
{{ review_req.deadline|date:"Y-m-d H:i" }}
|
||||
{% else %}
|
||||
{{ review_req.deadline|date:"Y-m-d" }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Requested</th>
|
||||
<td>{{ review_req.time|date:"Y-m-d" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody class="meta">
|
||||
<tr>
|
||||
<th>Review</th>
|
||||
<th>State</th>
|
||||
<td>{{ review_req.state.name }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Reviewer</th>
|
||||
<td>
|
||||
{% if review_req.reviewer %}
|
||||
{{ review_req.reviewer.person }}
|
||||
{% else %}
|
||||
None assigned yet
|
||||
{% endif %}
|
||||
|
||||
{% if can_accept_reviewer_assignment %}
|
||||
<form style="display:inline" method="post" action="{% url "ietf.doc.views_review.review_request" name=doc.name request_id=review_req.pk %}">{% csrf_token %}<button class="btn btn-default btn-xs" type="submit" name="action" value="accept"><span class="fa fa-check"></span> Accept</button></form>
|
||||
{% endif %}
|
||||
|
||||
{% if can_reject_reviewer_assignment %}
|
||||
<a class="btn btn-warning btn-xs" href="{% url "ietf.doc.views_review.reject_reviewer_assignment" name=doc.name request_id=review_req.pk %}"><span class="fa fa-ban"></span> Reject</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_assign_reviewer %}
|
||||
<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_review.assign_reviewer" name=doc.name request_id=review_req.pk %}"><span class="fa fa-user"></span> {% if review_req.reviewer %}Reassign{% else %}Assign{% endif %} reviewer</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Review</th>
|
||||
<td>
|
||||
{% if review_req.review %}
|
||||
<a href="{{ review_req.review.get_absolute_url }}">{{ review_req.review.name }}</a>
|
||||
{% else %}
|
||||
Not completed yet
|
||||
{% endif %}
|
||||
|
||||
{% if can_complete_review %}
|
||||
<a class="btn btn-primary btn-xs" href="{% url "ietf.doc.views_review.complete_review" name=doc.name request_id=review_req.pk %}"><span class="fa fa-pencil-square-o"></span> Complete review</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% if review_req.review and review_req.review.external_url %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Posted at</th>
|
||||
<td>
|
||||
<a href="{{ review_req.review.external_url }}">{{ review_req.review.external_url }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if review_req.reviewed_rev %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Reviewed rev.</th>
|
||||
<td><a href="{% url "doc_view" name=review_req.doc.name rev=review_req.reviewed_rev %}">{{ review_req.reviewed_rev }}</a> {% if review_req.reviewed_rev != review_req.doc.rev %}(currently at {{ review_req.doc.rev }}){% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if review_req.result %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Review result</th>
|
||||
<td>{{ review_req.result.name }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
22
ietf/templates/doc/review/withdraw_request.html
Normal file
22
ietf/templates/doc/review/withdraw_request.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin bootstrap3 static %}
|
||||
|
||||
{% block title %}Withdraw review request for {{ review_req.doc.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Withdraw review request<br><small>{{ review_req.doc.name }}</small></h1>
|
||||
|
||||
<p>Do you want to withdraw the review request?</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% buttons %}
|
||||
<a class="btn btn-default" href="{% url "ietf.doc.views_review.review_request" name=doc.canonical_name request_id=review_req.pk %}">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary" name="action" value="withdraw">Withdraw request</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -187,7 +187,7 @@ def encode_message(txt):
|
|||
def send_mail_text(request, to, frm, subject, txt, cc=None, extra=None, toUser=False, bcc=None):
|
||||
"""Send plain text message."""
|
||||
msg = encode_message(txt)
|
||||
send_mail_mime(request, to, frm, subject, msg, cc, extra, toUser, bcc)
|
||||
return send_mail_mime(request, to, frm, subject, msg, cc, extra, toUser, bcc)
|
||||
|
||||
def condition_message(to, frm, subject, msg, cc, extra):
|
||||
if isinstance(frm, tuple):
|
||||
|
@ -284,6 +284,8 @@ def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=F
|
|||
build_warning_message(request, e)
|
||||
send_error_email(e)
|
||||
|
||||
return msg
|
||||
|
||||
def parse_preformatted(preformatted, extra={}, override={}):
|
||||
"""Parse preformatted string containing mail with From:, To:, ...,"""
|
||||
msg = message_from_string(preformatted.encode("utf-8"))
|
||||
|
@ -323,8 +325,8 @@ def send_mail_message(request, message, extra={}):
|
|||
if message.reply_to:
|
||||
e['Reply-to'] = message.reply_to
|
||||
|
||||
send_mail_text(request, message.to, message.frm, message.subject,
|
||||
message.body, cc=message.cc, bcc=message.bcc, extra=e)
|
||||
return send_mail_text(request, message.to, message.frm, message.subject,
|
||||
message.body, cc=message.cc, bcc=message.bcc, extra=e)
|
||||
|
||||
def exception_components(e):
|
||||
# See if it's a non-smtplib exception that we faked
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.contrib.auth.models import User
|
|||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.models import Document, DocAlias, State, DocumentAuthor, BallotType, DocEvent, BallotDocEvent, RelatedDocument
|
||||
from ietf.doc.models import Document, DocAlias, State, DocumentAuthor, BallotType, DocEvent, BallotDocEvent, RelatedDocument, NewRevisionDocEvent
|
||||
from ietf.group.models import Group, GroupHistory, Role, RoleHistory
|
||||
from ietf.iesg.models import TelechatDate
|
||||
from ietf.ipr.models import HolderIprDisclosure, IprDocRel, IprDisclosureStateName, IprLicenseTypeName
|
||||
|
@ -247,11 +247,12 @@ def make_test_data():
|
|||
desc="Started IESG process",
|
||||
)
|
||||
|
||||
DocEvent.objects.create(
|
||||
NewRevisionDocEvent.objects.create(
|
||||
type="new_revision",
|
||||
by=ad,
|
||||
doc=draft,
|
||||
desc="New revision available",
|
||||
rev="01",
|
||||
)
|
||||
|
||||
BallotDocEvent.objects.create(
|
||||
|
|
|
@ -255,7 +255,7 @@ def canonicalize_sitemap(s):
|
|||
|
||||
def login_testing_unauthorized(test_case, username, url, password=None):
|
||||
r = test_case.client.get(url)
|
||||
test_case.assertTrue(r.status_code in (302, 403))
|
||||
test_case.assertIn(r.status_code, (302, 403))
|
||||
if r.status_code == 302:
|
||||
test_case.assertTrue("/accounts/login" in r['Location'])
|
||||
if not password:
|
||||
|
@ -272,6 +272,17 @@ def unicontent(r):
|
|||
encoding = 'utf-8'
|
||||
return r.content.decode(encoding)
|
||||
|
||||
def reload_db_objects(*objects):
|
||||
"""Rerequest the given arguments from the database so they're refreshed, to be used like
|
||||
|
||||
foo, bar = reload_objects(foo, bar)"""
|
||||
|
||||
t = tuple(o.__class__.objects.get(pk=o.pk) for o in objects)
|
||||
if len(objects) == 1:
|
||||
return t[0]
|
||||
else:
|
||||
return t
|
||||
|
||||
class ReverseLazyTest(django.test.TestCase):
|
||||
def test_redirect_with_lazy_reverse(self):
|
||||
response = self.client.get('/ipr/update/')
|
||||
|
|
11
ietf/utils/text.py
Normal file
11
ietf/utils/text.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
def skip_prefix(text, prefix):
|
||||
if text.startswith(prefix):
|
||||
return text[len(prefix):]
|
||||
else:
|
||||
return text
|
||||
|
||||
def skip_suffix(text, prefix):
|
||||
if text.endswith(prefix):
|
||||
return text[:-len(prefix)]
|
||||
else:
|
||||
return text
|
|
@ -1,18 +1,18 @@
|
|||
import re
|
||||
|
||||
import django.forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
def get_cleaned_text_file_content(uploaded_file):
|
||||
"""Read uploaded file, try to fix up encoding to UTF-8 and
|
||||
transform line endings into Unix style, then return the content as
|
||||
a UTF-8 string. Errors are reported as
|
||||
django.forms.ValidationError exceptions."""
|
||||
django.core.exceptions.ValidationError exceptions."""
|
||||
|
||||
if not uploaded_file:
|
||||
return u""
|
||||
|
||||
if uploaded_file.size and uploaded_file.size > 10 * 1000 * 1000:
|
||||
raise django.forms.ValidationError("Text file too large (size %s)." % uploaded_file.size)
|
||||
raise ValidationError("Text file too large (size %s)." % uploaded_file.size)
|
||||
|
||||
content = "".join(uploaded_file.chunks())
|
||||
|
||||
|
@ -29,18 +29,18 @@ def get_cleaned_text_file_content(uploaded_file):
|
|||
filetype = m.from_buffer(content)
|
||||
|
||||
if not filetype.startswith("text"):
|
||||
raise django.forms.ValidationError("Uploaded file does not appear to be a text file.")
|
||||
raise ValidationError("Uploaded file does not appear to be a text file.")
|
||||
|
||||
match = re.search("charset=([\w-]+)", filetype)
|
||||
if not match:
|
||||
raise django.forms.ValidationError("File has unknown encoding.")
|
||||
raise ValidationError("File has unknown encoding.")
|
||||
|
||||
encoding = match.group(1)
|
||||
if "ascii" not in encoding:
|
||||
try:
|
||||
content = content.decode(encoding)
|
||||
except Exception as e:
|
||||
raise django.forms.ValidationError("Error decoding file (%s). Try submitting with UTF-8 encoding or remove non-ASCII characters." % str(e))
|
||||
raise ValidationError("Error decoding file (%s). Try submitting with UTF-8 encoding or remove non-ASCII characters." % str(e))
|
||||
|
||||
# turn line-endings into Unix style
|
||||
content = content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
|
Loading…
Reference in a new issue