Merged in [18719] from housley@vigilsec.com:

Automatically move the IESG document state when a ballot is issued, prevent a writeup change or re-issue of ballot if the document is already approved, and warn about issuing ballots before the IETF Last Call is finished. Fixes #3119.
 - Legacy-Id: 18746
Note: SVN reference [18719] has been migrated to Git commit 89ec802a5b
This commit is contained in:
Robert Sparks 2020-12-10 17:04:41 +00:00
commit b673e9f836
4 changed files with 135 additions and 51 deletions

View file

@ -346,7 +346,7 @@ class BallotWriteupsTests(TestCase):
self.assertTrue('aread@' in outbox[-1]['Cc']) self.assertTrue('aread@' in outbox[-1]['Cc'])
def test_edit_ballot_writeup(self): def test_edit_ballot_writeup(self):
draft = IndividualDraftFactory() draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','iesg-eva')])
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "secretary", url) login_testing_unauthorized(self, "secretary", url)
@ -372,8 +372,32 @@ class BallotWriteupsTests(TestCase):
ballot_writeup="This is a simple test.", ballot_writeup="This is a simple test.",
save_ballot_writeup="1")) save_ballot_writeup="1"))
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
draft = Document.objects.get(name=draft.name) d = Document.objects.get(name=draft.name)
self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text) self.assertTrue("This is a simple test" in d.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text)
self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg'))
def test_edit_ballot_writeup_already_approved(self):
draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','approved')])
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
self.assertTrue(q('[type=submit]:contains("Save")'))
# save
r = self.client.post(url, dict(
ballot_writeup="This is a simple test.",
save_ballot_writeup="1"))
self.assertEqual(r.status_code, 200)
msgs = [m for m in r.context['messages']]
self.assertTrue(1 == len(msgs))
self.assertTrue("Writeup not changed" in msgs[0].message)
d = Document.objects.get(name=draft.name)
self.assertTrue('approved' == d.get_state_slug('draft-iesg'))
def test_edit_ballot_rfceditornote(self): def test_edit_ballot_rfceditornote(self):
draft = IndividualDraftFactory() draft = IndividualDraftFactory()
@ -467,6 +491,41 @@ class BallotWriteupsTests(TestCase):
self.assertIn('call expires', get_payload_text(outbox[-1])) self.assertIn('call expires', get_payload_text(outbox[-1]))
self.client.logout() self.client.logout()
def test_issue_ballot_auto_state_change(self):
ad = Person.objects.get(user__username="ad")
draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','writeupw')])
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
self.assertFalse(q('[class=help-block]:contains("not completed IETF Last Call")'))
self.assertTrue(q('[type=submit]:contains("Save")'))
# save
r = self.client.post(url, dict(
ballot_writeup="This is a simple test.",
issue_ballot="1"))
self.assertEqual(r.status_code, 200)
d = Document.objects.get(name=draft.name)
self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg'))
def test_issue_ballot_warn_if_early(self):
ad = Person.objects.get(user__username="ad")
draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','lc')])
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "secretary", url)
# expect warning about issuing a ballot before IETF Last Call is done
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
self.assertTrue(q('[class=help-block]:contains("not completed IETF Last Call")'))
self.assertTrue(q('[type=submit]:contains("Save")'))
def test_edit_approval_text(self): def test_edit_approval_text(self):
ad = Person.objects.get(user__username="ad") ad = Person.objects.get(user__username="ad")

View file

@ -8,6 +8,7 @@ import datetime, json
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.template.defaultfilters import striptags from django.template.defaultfilters import striptags
@ -592,6 +593,7 @@ class BallotWriteupForm(forms.Form):
def ballot_writeupnotes(request, name): def ballot_writeupnotes(request, name):
"""Editing of ballot write-up and notes""" """Editing of ballot write-up and notes"""
doc = get_object_or_404(Document, docalias__name=name) doc = get_object_or_404(Document, docalias__name=name)
prev_state = doc.get_state("draft-iesg")
login = request.user.person login = request.user.person
@ -604,61 +606,76 @@ def ballot_writeupnotes(request, name):
if request.method == 'POST' and "save_ballot_writeup" in request.POST or "issue_ballot" in request.POST: if request.method == 'POST' and "save_ballot_writeup" in request.POST or "issue_ballot" in request.POST:
form = BallotWriteupForm(request.POST) form = BallotWriteupForm(request.POST)
if form.is_valid(): if form.is_valid():
t = form.cleaned_data["ballot_writeup"] if prev_state.slug in ['ann', 'approved', 'rfcqueue', 'pub']:
if t != existing.text: ballot_already_approved = True
e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login) messages.warning(request, "There is an approved ballot for %s. Writeup not changed." % doc.name)
e.by = login else:
e.type = "changed_ballot_writeup_text" ballot_already_approved = False
e.desc = "Ballot writeup was changed" t = form.cleaned_data["ballot_writeup"]
e.text = t if t != existing.text:
e.save() e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login)
elif existing.pk == None: e.by = login
existing.save() e.type = "changed_ballot_writeup_text"
e.desc = "Ballot writeup was changed"
e.text = t
e.save()
elif existing.pk == None:
existing.save()
if "issue_ballot" in request.POST: if "issue_ballot" in request.POST and not ballot_already_approved:
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore if prev_state.slug in ['watching', 'writeupw', 'goaheadw']:
ballot = doc.latest_event(BallotDocEvent, type="created_ballot") new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva')
if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, balloter=login, ballot=ballot): prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
# sending the ballot counts as a yes doc.set_state(new_state)
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login) doc.tags.remove(*prev_tags)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.balloter = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.balloter.plain_name())
pos.save()
# Consider mailing this position to 'iesg_ballot_saved' sce = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
if sce:
doc.save_with_history([sce])
approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") if not ballot_already_approved:
if not approval: e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
approval = generate_approval_mail(request, doc) ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
approval.save() if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, balloter=login, ballot=ballot):
# sending the ballot counts as a yes
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.balloter = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.balloter.plain_name())
pos.save()
msg = generate_issue_ballot_mail(request, doc, ballot) # Consider mailing this position to 'iesg_ballot_saved'
addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings() approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
override = {'To':addrs.to} if not approval:
if addrs.cc: approval = generate_approval_mail(request, doc)
override['CC'] = addrs.cc approval.save()
send_mail_preformatted(request, msg, override=override)
addrs = gather_address_lists('ballot_issued_iana',doc=doc).as_strings() msg = generate_issue_ballot_mail(request, doc, ballot)
override={ "To": "IANA <%s>"%settings.IANA_EVAL_EMAIL, "Bcc": None , "Reply-To": []}
if addrs.cc:
override['CC'] = addrs.cc
send_mail_preformatted(request, msg, extra=extra_automation_headers(doc), override=override)
e = DocEvent(doc=doc, rev=doc.rev, by=login) addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings()
e.by = login override = {'To':addrs.to}
e.type = "sent_ballot_announcement" if addrs.cc:
e.desc = "Ballot has been issued" override['CC'] = addrs.cc
e.save() send_mail_preformatted(request, msg, override=override)
return render(request, 'doc/ballot/ballot_issued.html', addrs = gather_address_lists('ballot_issued_iana',doc=doc).as_strings()
dict(doc=doc, override={ "To": "IANA <%s>"%settings.IANA_EVAL_EMAIL, "Bcc": None , "Reply-To": []}
back_url=doc.get_absolute_url())) if addrs.cc:
override['CC'] = addrs.cc
send_mail_preformatted(request, msg, extra=extra_automation_headers(doc), override=override)
e = DocEvent(doc=doc, rev=doc.rev, by=login)
e.by = login
e.type = "sent_ballot_announcement"
e.desc = "Ballot has been issued"
e.save()
return render(request, 'doc/ballot/ballot_issued.html',
dict(doc=doc,
back_url=doc.get_absolute_url()))
need_intended_status = "" need_intended_status = ""
if not doc.intended_std_level: if not doc.intended_std_level:
@ -668,6 +685,7 @@ def ballot_writeupnotes(request, name):
dict(doc=doc, dict(doc=doc,
back_url=doc.get_absolute_url(), back_url=doc.get_absolute_url(),
ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")), ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")),
ballot_issue_danger=bool(prev_state.slug in ['ad-eval', 'lc']),
ballot_writeup_form=form, ballot_writeup_form=form,
need_intended_status=need_intended_status, need_intended_status=need_intended_status,
)) ))

View file

@ -1903,6 +1903,9 @@ class ApiSubmitTests(TestCase):
data['xml'], author = submission_file(name, rev, group, 'xml', "test_submission.xml", author=author, email=email, title=title, year=year) data['xml'], author = submission_file(name, rev, group, 'xml', "test_submission.xml", author=author, email=email, title=title, year=year)
data['user'] = email data['user'] = email
r = self.client.post(url, data) r = self.client.post(url, data)
debug.show('url')
debug.show('r')
debug.show('r.content')
return r, author, name return r, author, name
def test_api_submit_info(self): def test_api_submit_info(self):

View file

@ -17,11 +17,15 @@
<div class="help-block"> <div class="help-block">
Technical summary, Working Group summary, document quality, personnel, IRTF note, IESG note, IANA note. This text will be appended to all announcements and messages to the IRTF or RFC Editor. Technical summary, Working Group summary, document quality, personnel, IRTF note, IESG note, IANA note. This text will be appended to all announcements and messages to the IRTF or RFC Editor.
{% if ballot_issue_danger %}
<p class="text-danger">This document has not completed IETF Last Call. Please do not issue the ballot early without good reason.</p>
{% endif %}
</div> </div>
{% buttons %} {% buttons %}
<button type="submit" class="btn btn-primary" name="save_ballot_writeup" value="Save Ballot Writeup">Save</button> <button type="submit" class="btn btn-primary" name="save_ballot_writeup" value="Save Ballot Writeup">Save</button>
<button type="submit" class="btn btn-warning" name="issue_ballot" value="Save and Issue Ballot">Save & {% if ballot_issued %}re-{% endif %}issue ballot</button> <button type="submit" class={% if ballot_issue_danger %}"btn btn-danger"{% else %}"btn btn-warning"{% endif %} name="issue_ballot" value="Save and Issue Ballot">Save & {% if ballot_issued %}re-{% endif %}issue ballot</button>
{% endbuttons %} {% endbuttons %}
</form> </form>