Skip to content

Commit

Permalink
Automatically move the IESG document state when a ballot is issued, p…
Browse files Browse the repository at this point in the history
…revent 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: 18719
  • Loading branch information
russhousley committed Nov 23, 2020
1 parent 0bf56c9 commit 89ec802
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 58 deletions.
65 changes: 62 additions & 3 deletions ietf/doc/tests_ballot.py
Expand Up @@ -346,7 +346,7 @@ def test_request_last_call(self):
self.assertTrue('aread@' in outbox[-1]['Cc'])

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))
login_testing_unauthorized(self, "secretary", url)

Expand All @@ -372,8 +372,32 @@ def test_edit_ballot_writeup(self):
ballot_writeup="This is a simple test.",
save_ballot_writeup="1"))
self.assertEqual(r.status_code, 200)
draft = Document.objects.get(name=draft.name)
self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text)
d = Document.objects.get(name=draft.name)
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):
draft = IndividualDraftFactory()
Expand Down Expand Up @@ -467,6 +491,41 @@ def test_issue_ballot(self):
self.assertIn('call expires', get_payload_text(outbox[-1]))
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):
ad = Person.objects.get(user__username="ad")
Expand Down
126 changes: 72 additions & 54 deletions ietf/doc/views_ballot.py
Expand Up @@ -8,6 +8,7 @@

from django import forms
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.template.defaultfilters import striptags
Expand Down Expand Up @@ -592,6 +593,7 @@ def clean_ballot_writeup(self):
def ballot_writeupnotes(request, name):
"""Editing of ballot write-up and notes"""
doc = get_object_or_404(Document, docalias__name=name)
prev_state = doc.get_state("draft-iesg")

login = request.user.person

Expand All @@ -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:
form = BallotWriteupForm(request.POST)
if form.is_valid():
t = form.cleaned_data["ballot_writeup"]
if t != existing.text:
e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login)
e.by = login
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:
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
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()

# Consider mailing this position to 'iesg_ballot_saved'

approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
if not approval:
approval = generate_approval_mail(request, doc)
approval.save()

msg = generate_issue_ballot_mail(request, doc, ballot)

addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings()
override = {'To':addrs.to}
if addrs.cc:
override['CC'] = addrs.cc
send_mail_preformatted(request, msg, override=override)

addrs = gather_address_lists('ballot_issued_iana',doc=doc).as_strings()
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)
e.by = login
e.type = "sent_ballot_announcement"
e.desc = "Ballot has been issued"
e.save()
if prev_state.slug in ['ann', 'approved', 'rfcqueue', 'pub']:
ballot_already_approved = True
messages.warning(request, "There is an approved ballot for %s. Writeup not changed." % doc.name)
else:
ballot_already_approved = False
t = form.cleaned_data["ballot_writeup"]
if t != existing.text:
e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login)
e.by = login
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 and not ballot_already_approved:
if prev_state.slug in ['watching', 'writeupw', 'goaheadw']:
new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva')
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
doc.set_state(new_state)
doc.tags.remove(*prev_tags)

sce = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
if sce:
doc.save_with_history([sce])

if not ballot_already_approved:
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
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()

# Consider mailing this position to 'iesg_ballot_saved'

approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
if not approval:
approval = generate_approval_mail(request, doc)
approval.save()

msg = generate_issue_ballot_mail(request, doc, ballot)

addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings()
override = {'To':addrs.to}
if addrs.cc:
override['CC'] = addrs.cc
send_mail_preformatted(request, msg, override=override)

addrs = gather_address_lists('ballot_issued_iana',doc=doc).as_strings()
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)
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()))

return render(request, 'doc/ballot/ballot_issued.html',
dict(doc=doc,
back_url=doc.get_absolute_url()))

need_intended_status = ""
if not doc.intended_std_level:
Expand All @@ -668,6 +685,7 @@ def ballot_writeupnotes(request, name):
dict(doc=doc,
back_url=doc.get_absolute_url(),
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,
need_intended_status=need_intended_status,
))
Expand Down
6 changes: 5 additions & 1 deletion ietf/templates/doc/ballot/writeupnotes.html
Expand Up @@ -17,11 +17,15 @@ <h1>Ballot writeup and notes<br><small><a href="{% url "ietf.doc.views_doc.docum

<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.

{% 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>

{% buttons %}
<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 %}
</form>

Expand Down

0 comments on commit 89ec802

Please sign in to comment.