Skip to content

Commit 89ec802

Browse files
committed
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 ietf-tools#3119.
- Legacy-Id: 18719
1 parent 0bf56c9 commit 89ec802

3 files changed

Lines changed: 139 additions & 58 deletions

File tree

ietf/doc/tests_ballot.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def test_request_last_call(self):
346346
self.assertTrue('aread@' in outbox[-1]['Cc'])
347347

348348
def test_edit_ballot_writeup(self):
349-
draft = IndividualDraftFactory()
349+
draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','iesg-eva')])
350350
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
351351
login_testing_unauthorized(self, "secretary", url)
352352

@@ -372,8 +372,32 @@ def test_edit_ballot_writeup(self):
372372
ballot_writeup="This is a simple test.",
373373
save_ballot_writeup="1"))
374374
self.assertEqual(r.status_code, 200)
375-
draft = Document.objects.get(name=draft.name)
376-
self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text)
375+
d = Document.objects.get(name=draft.name)
376+
self.assertTrue("This is a simple test" in d.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text)
377+
self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg'))
378+
379+
def test_edit_ballot_writeup_already_approved(self):
380+
draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','approved')])
381+
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
382+
login_testing_unauthorized(self, "secretary", url)
383+
384+
# normal get
385+
r = self.client.get(url)
386+
self.assertEqual(r.status_code, 200)
387+
q = PyQuery(r.content)
388+
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
389+
self.assertTrue(q('[type=submit]:contains("Save")'))
390+
391+
# save
392+
r = self.client.post(url, dict(
393+
ballot_writeup="This is a simple test.",
394+
save_ballot_writeup="1"))
395+
self.assertEqual(r.status_code, 200)
396+
msgs = [m for m in r.context['messages']]
397+
self.assertTrue(1 == len(msgs))
398+
self.assertTrue("Writeup not changed" in msgs[0].message)
399+
d = Document.objects.get(name=draft.name)
400+
self.assertTrue('approved' == d.get_state_slug('draft-iesg'))
377401

378402
def test_edit_ballot_rfceditornote(self):
379403
draft = IndividualDraftFactory()
@@ -467,6 +491,41 @@ def test_issue_ballot(self):
467491
self.assertIn('call expires', get_payload_text(outbox[-1]))
468492
self.client.logout()
469493

494+
def test_issue_ballot_auto_state_change(self):
495+
ad = Person.objects.get(user__username="ad")
496+
draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','writeupw')])
497+
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
498+
login_testing_unauthorized(self, "secretary", url)
499+
500+
# normal get
501+
r = self.client.get(url)
502+
self.assertEqual(r.status_code, 200)
503+
q = PyQuery(r.content)
504+
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
505+
self.assertFalse(q('[class=help-block]:contains("not completed IETF Last Call")'))
506+
self.assertTrue(q('[type=submit]:contains("Save")'))
507+
508+
# save
509+
r = self.client.post(url, dict(
510+
ballot_writeup="This is a simple test.",
511+
issue_ballot="1"))
512+
self.assertEqual(r.status_code, 200)
513+
d = Document.objects.get(name=draft.name)
514+
self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg'))
515+
516+
def test_issue_ballot_warn_if_early(self):
517+
ad = Person.objects.get(user__username="ad")
518+
draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','lc')])
519+
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
520+
login_testing_unauthorized(self, "secretary", url)
521+
522+
# expect warning about issuing a ballot before IETF Last Call is done
523+
r = self.client.get(url)
524+
self.assertEqual(r.status_code, 200)
525+
q = PyQuery(r.content)
526+
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
527+
self.assertTrue(q('[class=help-block]:contains("not completed IETF Last Call")'))
528+
self.assertTrue(q('[type=submit]:contains("Save")'))
470529

471530
def test_edit_approval_text(self):
472531
ad = Person.objects.get(user__username="ad")

ietf/doc/views_ballot.py

Lines changed: 72 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from django import forms
1010
from django.conf import settings
11+
from django.contrib import messages
1112
from django.http import HttpResponse, HttpResponseRedirect, Http404
1213
from django.shortcuts import render, get_object_or_404, redirect
1314
from django.template.defaultfilters import striptags
@@ -592,6 +593,7 @@ def clean_ballot_writeup(self):
592593
def ballot_writeupnotes(request, name):
593594
"""Editing of ballot write-up and notes"""
594595
doc = get_object_or_404(Document, docalias__name=name)
596+
prev_state = doc.get_state("draft-iesg")
595597

596598
login = request.user.person
597599

@@ -604,61 +606,76 @@ def ballot_writeupnotes(request, name):
604606
if request.method == 'POST' and "save_ballot_writeup" in request.POST or "issue_ballot" in request.POST:
605607
form = BallotWriteupForm(request.POST)
606608
if form.is_valid():
607-
t = form.cleaned_data["ballot_writeup"]
608-
if t != existing.text:
609-
e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login)
610-
e.by = login
611-
e.type = "changed_ballot_writeup_text"
612-
e.desc = "Ballot writeup was changed"
613-
e.text = t
614-
e.save()
615-
elif existing.pk == None:
616-
existing.save()
617-
618-
if "issue_ballot" in request.POST:
619-
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
620-
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
621-
if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, balloter=login, ballot=ballot):
622-
# sending the ballot counts as a yes
623-
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
624-
pos.ballot = ballot
625-
pos.type = "changed_ballot_position"
626-
pos.balloter = login
627-
pos.pos_id = "yes"
628-
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.balloter.plain_name())
629-
pos.save()
630-
631-
# Consider mailing this position to 'iesg_ballot_saved'
632-
633-
approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
634-
if not approval:
635-
approval = generate_approval_mail(request, doc)
636-
approval.save()
637-
638-
msg = generate_issue_ballot_mail(request, doc, ballot)
639-
640-
addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings()
641-
override = {'To':addrs.to}
642-
if addrs.cc:
643-
override['CC'] = addrs.cc
644-
send_mail_preformatted(request, msg, override=override)
645-
646-
addrs = gather_address_lists('ballot_issued_iana',doc=doc).as_strings()
647-
override={ "To": "IANA <%s>"%settings.IANA_EVAL_EMAIL, "Bcc": None , "Reply-To": []}
648-
if addrs.cc:
649-
override['CC'] = addrs.cc
650-
send_mail_preformatted(request, msg, extra=extra_automation_headers(doc), override=override)
651-
652-
e = DocEvent(doc=doc, rev=doc.rev, by=login)
653-
e.by = login
654-
e.type = "sent_ballot_announcement"
655-
e.desc = "Ballot has been issued"
656-
e.save()
609+
if prev_state.slug in ['ann', 'approved', 'rfcqueue', 'pub']:
610+
ballot_already_approved = True
611+
messages.warning(request, "There is an approved ballot for %s. Writeup not changed." % doc.name)
612+
else:
613+
ballot_already_approved = False
614+
t = form.cleaned_data["ballot_writeup"]
615+
if t != existing.text:
616+
e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login)
617+
e.by = login
618+
e.type = "changed_ballot_writeup_text"
619+
e.desc = "Ballot writeup was changed"
620+
e.text = t
621+
e.save()
622+
elif existing.pk == None:
623+
existing.save()
624+
625+
if "issue_ballot" in request.POST and not ballot_already_approved:
626+
if prev_state.slug in ['watching', 'writeupw', 'goaheadw']:
627+
new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva')
628+
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
629+
doc.set_state(new_state)
630+
doc.tags.remove(*prev_tags)
631+
632+
sce = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
633+
if sce:
634+
doc.save_with_history([sce])
635+
636+
if not ballot_already_approved:
637+
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
638+
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
639+
if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, balloter=login, ballot=ballot):
640+
# sending the ballot counts as a yes
641+
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
642+
pos.ballot = ballot
643+
pos.type = "changed_ballot_position"
644+
pos.balloter = login
645+
pos.pos_id = "yes"
646+
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.balloter.plain_name())
647+
pos.save()
648+
649+
# Consider mailing this position to 'iesg_ballot_saved'
650+
651+
approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
652+
if not approval:
653+
approval = generate_approval_mail(request, doc)
654+
approval.save()
655+
656+
msg = generate_issue_ballot_mail(request, doc, ballot)
657+
658+
addrs = gather_address_lists('iesg_ballot_issued',doc=doc).as_strings()
659+
override = {'To':addrs.to}
660+
if addrs.cc:
661+
override['CC'] = addrs.cc
662+
send_mail_preformatted(request, msg, override=override)
663+
664+
addrs = gather_address_lists('ballot_issued_iana',doc=doc).as_strings()
665+
override={ "To": "IANA <%s>"%settings.IANA_EVAL_EMAIL, "Bcc": None , "Reply-To": []}
666+
if addrs.cc:
667+
override['CC'] = addrs.cc
668+
send_mail_preformatted(request, msg, extra=extra_automation_headers(doc), override=override)
669+
670+
e = DocEvent(doc=doc, rev=doc.rev, by=login)
671+
e.by = login
672+
e.type = "sent_ballot_announcement"
673+
e.desc = "Ballot has been issued"
674+
e.save()
657675

658-
return render(request, 'doc/ballot/ballot_issued.html',
659-
dict(doc=doc,
660-
back_url=doc.get_absolute_url()))
661-
676+
return render(request, 'doc/ballot/ballot_issued.html',
677+
dict(doc=doc,
678+
back_url=doc.get_absolute_url()))
662679

663680
need_intended_status = ""
664681
if not doc.intended_std_level:
@@ -668,6 +685,7 @@ def ballot_writeupnotes(request, name):
668685
dict(doc=doc,
669686
back_url=doc.get_absolute_url(),
670687
ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")),
688+
ballot_issue_danger=bool(prev_state.slug in ['ad-eval', 'lc']),
671689
ballot_writeup_form=form,
672690
need_intended_status=need_intended_status,
673691
))

ietf/templates/doc/ballot/writeupnotes.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ <h1>Ballot writeup and notes<br><small><a href="{% url "ietf.doc.views_doc.docum
1717

1818
<div class="help-block">
1919
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.
20+
21+
{% if ballot_issue_danger %}
22+
<p class="text-danger">This document has not completed IETF Last Call. Please do not issue the ballot early without good reason.</p>
23+
{% endif %}
2024
</div>
2125

2226
{% buttons %}
2327
<button type="submit" class="btn btn-primary" name="save_ballot_writeup" value="Save Ballot Writeup">Save</button>
24-
<button type="submit" class="btn btn-warning" name="issue_ballot" value="Save and Issue Ballot">Save & {% if ballot_issued %}re-{% endif %}issue ballot</button>
28+
<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>
2529
{% endbuttons %}
2630
</form>
2731

0 commit comments

Comments
 (0)