datatracker/ietf/doc/views_ballot.py
Henrik Levkowetz aa5e61d958 Updated all urlpatterns to use ietf.utils.urls.url() instead of django's,
in order to autogenerate dotted path url pattern names.  Updated a number
of url reverses to use dotted path, and removed explicit url pattern names
as needed.

Changed some imports to prevent import of ietf.urls before django
initialization was complete.


Changed 3 cases of form classes being curried to functions; django 1.10
didn't accept that.

Started converting old-style middleware classes to new-style middleware
functions (incomplete).

Tweaked a nomcom decorator to preserve function names and attributes, like
a good decorator should.

Replaced the removed django templatetag 'removetags' with our own version
which uses bleach, and does sanitizing in addition to removing explicitly
mentionied html tags.

Rewrote the filename argument handling in a management command which had
broken with the upgrade.
 - Legacy-Id: 12818
2017-02-11 14:43:01 +00:00

914 lines
38 KiB
Python

# ballot management (voting, commenting, writeups, ...) for Area
# Directors and Secretariat
import datetime, json
from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.core.urlresolvers import reverse as urlreverse
from django.template.loader import render_to_string
from django import forms
from django.conf import settings
import debug # pyflakes:ignore
from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent, BallotPositionDocEvent,
BallotType, LastCallDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS )
from ietf.doc.utils import ( add_state_change_event, close_ballot, close_open_ballots,
create_ballot_if_not_open, update_telechat )
from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred,
extra_automation_headers, generate_last_call_announcement,
generate_issue_ballot_mail, generate_ballot_writeup, generate_ballot_rfceditornote,
generate_approval_mail )
from ietf.doc.lastcall import request_last_call
from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_stream
from ietf.message.utils import infer_message
from ietf.name.models import BallotPositionName
from ietf.person.models import Person
from ietf.utils.mail import send_mail_text, send_mail_preformatted
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.forms import CcSelectForm
BALLOT_CHOICES = (("yes", "Yes"),
("noobj", "No Objection"),
("discuss", "Discuss"),
("abstain", "Abstain"),
("recuse", "Recuse"),
("", "No Record"),
)
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def do_undefer_ballot(request, doc):
'''
Helper function to perform undefer of ballot. Takes the Request object, for use in
logging, and the Document object.
'''
by = request.user.person
telechat_date = TelechatDate.objects.active().order_by("date")[0].date
new_state = doc.get_state()
prev_tags = []
new_tags = []
if doc.type_id == 'draft':
new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva')
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
elif doc.type_id in ['conflrev','statchg']:
new_state = State.objects.get(used=True, type=doc.type_id, slug='iesgeval')
prev_state = doc.get_state(new_state.type_id if new_state else None)
doc.set_state(new_state)
doc.tags.remove(*prev_tags)
events = []
state_change_event = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
if state_change_event:
events.append(state_change_event)
e = update_telechat(request, doc, by, telechat_date)
if e:
events.append(e)
if events:
doc.save_with_history(events)
email_ballot_undeferred(request, doc, by.plain_name(), telechat_date)
def position_to_ballot_choice(position):
for v, label in BALLOT_CHOICES:
if v and getattr(position, v):
return v
return ""
def position_label(position_value):
return dict(BALLOT_CHOICES).get(position_value, "")
# -------------------------------------------------
class EditPositionForm(forms.Form):
position = forms.ModelChoiceField(queryset=BallotPositionName.objects.all(), widget=forms.RadioSelect, initial="norecord", required=True)
discuss = forms.CharField(required=False, widget=forms.Textarea, strip=False)
comment = forms.CharField(required=False, widget=forms.Textarea, strip=False)
def __init__(self, *args, **kwargs):
ballot_type = kwargs.pop("ballot_type")
super(EditPositionForm, self).__init__(*args, **kwargs)
self.fields['position'].queryset = ballot_type.positions.order_by('order')
def clean_discuss(self):
entered_discuss = self.cleaned_data["discuss"]
entered_pos = self.cleaned_data.get("position", "norecord")
if entered_pos.blocking and not entered_discuss:
raise forms.ValidationError("You must enter a non-empty discuss")
return entered_discuss
@role_required('Area Director','Secretariat')
def edit_position(request, name, ballot_id):
"""Vote and edit discuss and comment on document as Area Director."""
doc = get_object_or_404(Document, docalias__name=name)
ballot = get_object_or_404(BallotDocEvent, type="created_ballot", pk=ballot_id, doc=doc)
ad = login = request.user.person
if 'ballot_edit_return_point' in request.session:
return_to_url = request.session['ballot_edit_return_point']
else:
return_to_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id))
# if we're in the Secretariat, we can select an AD to act as stand-in for
if has_role(request.user, "Secretariat"):
ad_id = request.GET.get('ad')
if not ad_id:
raise Http404
ad = get_object_or_404(Person, pk=ad_id)
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
if request.method == 'POST':
if not has_role(request.user, "Secretariat") and not ad.role_set.filter(name="ad", group__type="area", group__state="active"):
# prevent pre-ADs from voting
return HttpResponseForbidden("Must be a proper Area Director in an active area to cast ballot")
form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
if form.is_valid():
# save the vote
clean = form.cleaned_data
pos = BallotPositionDocEvent(doc=doc, by=login)
pos.type = "changed_ballot_position"
pos.ballot = ballot
pos.ad = ad
pos.pos = clean["position"]
pos.comment = clean["comment"].rstrip()
pos.comment_time = old_pos.comment_time if old_pos else None
pos.discuss = clean["discuss"].rstrip()
if not pos.pos.blocking:
pos.discuss = ""
pos.discuss_time = old_pos.discuss_time if old_pos else None
changes = []
added_events = []
# possibly add discuss/comment comments to history trail
# so it's easy to see what's happened
old_comment = old_pos.comment if old_pos else ""
if pos.comment != old_comment:
pos.comment_time = pos.time
changes.append("comment")
if pos.comment:
e = DocEvent(doc=doc)
e.by = ad # otherwise we can't see who's saying it
e.type = "added_comment"
e.desc = "[Ballot comment]\n" + pos.comment
added_events.append(e)
old_discuss = old_pos.discuss if old_pos else ""
if pos.discuss != old_discuss:
pos.discuss_time = pos.time
changes.append("discuss")
if pos.pos.blocking:
e = DocEvent(doc=doc, by=login)
e.by = ad # otherwise we can't see who's saying it
e.type = "added_comment"
e.desc = "[Ballot %s]\n" % pos.pos.name.lower()
e.desc += pos.discuss
added_events.append(e)
# figure out a description
if not old_pos and pos.pos.slug != "norecord":
pos.desc = u"[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
elif old_pos and pos.pos != old_pos.pos:
pos.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.ad.plain_name(), pos.pos.name, old_pos.pos.name)
if not pos.desc and changes:
pos.desc = u"Ballot %s text updated for %s" % (u" and ".join(changes), ad.plain_name())
# only add new event if we actually got a change
if pos.desc:
if login != ad:
pos.desc += u" by %s" % login.plain_name()
pos.save()
for e in added_events:
e.save() # save them after the position is saved to get later id for sorting order
if request.POST.get("send_mail"):
qstr=""
if request.GET.get('ad'):
qstr += "?ad=%s" % request.GET.get('ad')
return HttpResponseRedirect(urlreverse("doc_send_ballot_comment", kwargs=dict(name=doc.name, ballot_id=ballot_id)) + qstr)
elif request.POST.get("Defer"):
return redirect("doc_defer_ballot", name=doc)
elif request.POST.get("Undefer"):
return redirect("doc_undefer_ballot", name=doc)
else:
return HttpResponseRedirect(return_to_url)
else:
initial = {}
if old_pos:
initial['position'] = old_pos.pos.slug
initial['discuss'] = old_pos.discuss
initial['comment'] = old_pos.comment
form = EditPositionForm(initial=initial, ballot_type=ballot.ballot_type)
blocking_positions = dict((p.pk, p.name) for p in form.fields["position"].queryset.all() if p.blocking)
ballot_deferred = doc.active_defer_event()
return render(request, 'doc/ballot/edit_position.html',
dict(doc=doc,
form=form,
ad=ad,
return_to_url=return_to_url,
old_pos=old_pos,
ballot_deferred=ballot_deferred,
ballot = ballot,
show_discuss_text=old_pos and old_pos.pos.blocking,
blocking_positions=json.dumps(blocking_positions),
))
@role_required('Area Director','Secretariat')
def send_ballot_comment(request, name, ballot_id):
"""Email document ballot position discuss/comment for Area Director."""
doc = get_object_or_404(Document, docalias__name=name)
ballot = get_object_or_404(BallotDocEvent, type="created_ballot", pk=ballot_id, doc=doc)
ad = request.user.person
if 'ballot_edit_return_point' in request.session:
return_to_url = request.session['ballot_edit_return_point']
else:
return_to_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id))
if 'HTTP_REFERER' in request.META:
back_url = request.META['HTTP_REFERER']
else:
back_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id))
# if we're in the Secretariat, we can select an AD to act as stand-in for
if not has_role(request.user, "Area Director"):
ad_id = request.GET.get('ad')
if not ad_id:
raise Http404
ad = get_object_or_404(Person, pk=ad_id)
pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
if not pos:
raise Http404
subj = []
d = ""
blocking_name = "DISCUSS"
if pos.pos.blocking and pos.discuss:
d = pos.discuss
blocking_name = pos.pos.name.upper()
subj.append(blocking_name)
c = ""
if pos.comment:
c = pos.comment
subj.append("COMMENT")
ad_name_genitive = ad.plain_name() + "'" if ad.plain_name().endswith('s') else ad.plain_name() + "'s"
subject = "%s %s on %s" % (ad_name_genitive, pos.pos.name if pos.pos else "No Position", doc.name + "-" + doc.rev)
if subj:
subject += ": (with %s)" % " and ".join(subj)
body = render_to_string("doc/ballot/ballot_comment_mail.txt",
dict(discuss=d,
comment=c,
ad=ad.plain_name(),
doc=doc,
pos=pos.pos,
blocking_name=blocking_name,
settings=settings))
frm = ad.role_email("ad").formatted_email()
addrs = gather_address_lists('ballot_saved',doc=doc)
if request.method == 'POST':
cc = []
cc_select_form = CcSelectForm(data=request.POST,mailtrigger_slug='ballot_saved',mailtrigger_context={'doc':doc})
if cc_select_form.is_valid():
cc.extend(cc_select_form.get_selected_addresses())
extra_cc = [x.strip() for x in request.POST.get("extra_cc","").split(',') if x.strip()]
if extra_cc:
cc.extend(extra_cc)
send_mail_text(request, addrs.to, frm, subject, body, cc=u", ".join(cc))
return HttpResponseRedirect(return_to_url)
else:
cc_select_form = CcSelectForm(mailtrigger_slug='ballot_saved',mailtrigger_context={'doc':doc})
return render(request, 'doc/ballot/send_ballot_comment.html',
dict(doc=doc,
subject=subject,
body=body,
frm=frm,
to=addrs.as_strings().to,
ad=ad,
can_send=d or c,
back_url=back_url,
cc_select_form = cc_select_form,
))
@role_required('Secretariat')
def clear_ballot(request, name):
"""Clear all positions and discusses on every open ballot for a document."""
doc = get_object_or_404(Document, name=name)
if request.method == 'POST':
by = request.user.person
for t in BallotType.objects.filter(doc_type=doc.type_id):
close_ballot(doc, by, t.slug)
create_ballot_if_not_open(doc, by, t.slug)
if doc.get_state('draft-iesg').slug == 'defer':
do_undefer_ballot(request,doc)
return redirect("doc_view", name=doc.name)
return render(request, 'doc/ballot/clear_ballot.html',
dict(doc=doc,
back_url=doc.get_absolute_url()))
@role_required('Area Director','Secretariat')
def defer_ballot(request, name):
"""Signal post-pone of ballot, notifying relevant parties."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.type_id not in ('draft','conflrev','statchg'):
raise Http404
interesting_state = dict(draft='draft-iesg',conflrev='conflrev',statchg='statchg')
state = doc.get_state(interesting_state[doc.type_id])
if not state or state.slug=='defer' or not doc.telechat_date():
raise Http404
login = request.user.person
telechat_date = TelechatDate.objects.active().order_by("date")[1].date
if request.method == 'POST':
new_state = doc.get_state()
prev_tags = []
new_tags = []
if doc.type_id == 'draft':
new_state = State.objects.get(used=True, type="draft-iesg", slug='defer')
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
elif doc.type_id in ['conflrev','statchg']:
new_state = State.objects.get(used=True, type=doc.type_id, slug='defer')
prev_state = doc.get_state(new_state.type_id if new_state else None)
doc.set_state(new_state)
doc.tags.remove(*prev_tags)
events = []
state_change_event = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
if state_change_event:
events.append(state_change_event)
e = update_telechat(request, doc, login, telechat_date)
if e:
events.append(e)
doc.save_with_history(events)
email_ballot_deferred(request, doc, login.plain_name(), telechat_date)
return HttpResponseRedirect(doc.get_absolute_url())
return render(request, 'doc/ballot/defer_ballot.html',
dict(doc=doc,
telechat_date=telechat_date,
back_url=doc.get_absolute_url()))
@role_required('Area Director','Secretariat')
def undefer_ballot(request, name):
"""undo deferral of ballot ballot."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.type_id not in ('draft','conflrev','statchg'):
raise Http404
if doc.type_id == 'draft' and not doc.get_state("draft-iesg"):
raise Http404
interesting_state = dict(draft='draft-iesg',conflrev='conflrev',statchg='statchg')
state = doc.get_state(interesting_state[doc.type_id])
if not state or state.slug!='defer':
raise Http404
telechat_date = TelechatDate.objects.active().order_by("date")[0].date
if request.method == 'POST':
do_undefer_ballot(request,doc)
return HttpResponseRedirect(doc.get_absolute_url())
return render(request, 'doc/ballot/undefer_ballot.html',
dict(doc=doc,
telechat_date=telechat_date,
back_url=doc.get_absolute_url()))
class LastCallTextForm(forms.Form):
last_call_text = forms.CharField(widget=forms.Textarea, required=True, strip=False)
def clean_last_call_text(self):
lines = self.cleaned_data["last_call_text"].split("\r\n")
for l, next in zip(lines, lines[1:]):
if l.startswith('Subject:') and next.strip():
raise forms.ValidationError("Subject line appears to have a line break, please make sure there is no line breaks in the subject line and that it is followed by an empty line.")
return self.cleaned_data["last_call_text"].replace("\r", "")
@role_required('Area Director','Secretariat')
def lastcalltext(request, name):
"""Editing of the last call text"""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404
login = request.user.person
existing = doc.latest_event(WriteupDocEvent, type="changed_last_call_text")
if not existing:
existing = generate_last_call_announcement(request, doc)
form = LastCallTextForm(initial=dict(last_call_text=existing.text))
if request.method == 'POST':
if "save_last_call_text" in request.POST or "send_last_call_request" in request.POST:
form = LastCallTextForm(request.POST)
if form.is_valid():
t = form.cleaned_data['last_call_text']
if t != existing.text:
e = WriteupDocEvent(doc=doc, by=login)
e.by = login
e.type = "changed_last_call_text"
e.desc = "Last call announcement was changed"
e.text = t
e.save()
elif existing.pk == None:
existing.save()
if "send_last_call_request" in request.POST:
prev_state = doc.get_state("draft-iesg")
new_state = State.objects.get(used=True, type="draft-iesg", slug='lc-req')
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
doc.set_state(new_state)
doc.tags.remove(*prev_tags)
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
if e:
doc.save_with_history([e])
request_last_call(request, doc)
return render(request, 'doc/draft/last_call_requested.html',
dict(doc=doc))
if "regenerate_last_call_text" in request.POST:
e = generate_last_call_announcement(request, doc)
e.save()
# make sure form has the updated text
form = LastCallTextForm(initial=dict(last_call_text=e.text))
s = doc.get_state("draft-iesg")
can_request_last_call = s.order < 27
can_make_last_call = s.order < 20
need_intended_status = ""
if not doc.intended_std_level:
need_intended_status = doc.file_tag()
return render(request, 'doc/ballot/lastcalltext.html',
dict(doc=doc,
back_url=doc.get_absolute_url(),
last_call_form=form,
can_request_last_call=can_request_last_call,
can_make_last_call=can_make_last_call,
need_intended_status=need_intended_status,
))
class BallotWriteupForm(forms.Form):
ballot_writeup = forms.CharField(widget=forms.Textarea, required=True, strip=False)
def clean_ballot_writeup(self):
return self.cleaned_data["ballot_writeup"].replace("\r", "")
@role_required('Area Director','Secretariat')
def ballot_writeupnotes(request, name):
"""Editing of ballot write-up and notes"""
doc = get_object_or_404(Document, docalias__name=name)
login = request.user.person
existing = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
if not existing:
existing = generate_ballot_writeup(request, doc)
form = BallotWriteupForm(initial=dict(ballot_writeup=existing.text))
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, 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:
create_ballot_if_not_open(doc, login, "approve")
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, ad=login, ballot=ballot):
# sending the ballot counts as a yes
pos = BallotPositionDocEvent(doc=doc, by=login)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.ad = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
pos.save()
# Consider mailing this position to '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('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": None}
if addrs.cc:
override['CC'] = addrs.cc
send_mail_preformatted(request, msg, extra=extra_automation_headers(doc),
override={ "To": "IANA <%s>"%settings.IANA_EVAL_EMAIL, "CC": None, "Bcc": None , "Reply-To": None})
e = DocEvent(doc=doc, 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 = ""
if not doc.intended_std_level:
need_intended_status = doc.file_tag()
return render(request, 'doc/ballot/writeupnotes.html',
dict(doc=doc,
back_url=doc.get_absolute_url(),
ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")),
ballot_writeup_form=form,
need_intended_status=need_intended_status,
))
class BallotRfcEditorNoteForm(forms.Form):
rfc_editor_note = forms.CharField(widget=forms.Textarea, label="RFC Editor Note", required=True, strip=False)
def clean_rfc_editor_note(self):
return self.cleaned_data["rfc_editor_note"].replace("\r", "")
@role_required('Area Director','Secretariat','IAB Chair','IRTF Chair','ISE')
def ballot_rfceditornote(request, name):
"""Editing of RFC Editor Note"""
doc = get_object_or_404(Document, docalias__name=name)
if not is_authorized_in_doc_stream(request.user, doc):
return HttpResponseForbidden("You do not have the necessary permissions to change the RFC Editor Note for this document")
login = request.user.person
existing = doc.latest_event(WriteupDocEvent, type="changed_rfc_editor_note_text")
if not existing or (existing.text == ""):
existing = generate_ballot_rfceditornote(request, doc)
form = BallotRfcEditorNoteForm(auto_id=False, initial=dict(rfc_editor_note=existing.text))
if request.method == 'POST' and "save_ballot_rfceditornote" in request.POST:
form = BallotRfcEditorNoteForm(request.POST)
if form.is_valid():
t = form.cleaned_data["rfc_editor_note"]
if t != existing.text:
e = WriteupDocEvent(doc=doc, by=login)
e.by = login
e.type = "changed_rfc_editor_note_text"
e.desc = "RFC Editor Note was changed"
e.text = t.rstrip()
e.save()
if request.method == 'POST' and "clear_ballot_rfceditornote" in request.POST:
e = WriteupDocEvent(doc=doc, by=login)
e.by = login
e.type = "changed_rfc_editor_note_text"
e.desc = "RFC Editor Note was cleared"
e.text = ""
e.save()
# make sure form shows a blank RFC Editor Note
form = BallotRfcEditorNoteForm(initial=dict(rfc_editor_note=" "))
return render(request, 'doc/ballot/rfceditornote.html',
dict(doc=doc,
back_url=doc.get_absolute_url(),
ballot_rfceditornote_form=form,
))
class ApprovalTextForm(forms.Form):
approval_text = forms.CharField(widget=forms.Textarea, required=True, strip=False)
def clean_approval_text(self):
return self.cleaned_data["approval_text"].replace("\r", "")
@role_required('Area Director','Secretariat')
def ballot_approvaltext(request, name):
"""Editing of approval text"""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404
login = request.user.person
existing = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
if not existing:
existing = generate_approval_mail(request, doc)
form = ApprovalTextForm(initial=dict(approval_text=existing.text))
if request.method == 'POST':
if "save_approval_text" in request.POST:
form = ApprovalTextForm(request.POST)
if form.is_valid():
t = form.cleaned_data['approval_text']
if t != existing.text:
e = WriteupDocEvent(doc=doc, by=login)
e.by = login
e.type = "changed_ballot_approval_text"
e.desc = "Ballot approval text was changed"
e.text = t
e.save()
elif existing.pk == None:
existing.save()
if "regenerate_approval_text" in request.POST:
e = generate_approval_mail(request, doc)
e.save()
# make sure form has the updated text
form = ApprovalTextForm(initial=dict(approval_text=e.text))
can_announce = doc.get_state("draft-iesg").order > 19
need_intended_status = ""
if not doc.intended_std_level:
need_intended_status = doc.file_tag()
return render(request, 'doc/ballot/approvaltext.html',
dict(doc=doc,
back_url=doc.get_absolute_url(),
approval_text_form=form,
can_announce=can_announce,
need_intended_status=need_intended_status,
))
@role_required('Secretariat')
def approve_ballot(request, name):
"""Approve ballot, sending out announcement, changing state."""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404
login = request.user.person
approval_mail_event = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
if not approval_mail_event:
approval_mail_event = generate_approval_mail(request, doc)
approval_text = approval_mail_event.text
ballot_writeup_event = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
if not ballot_writeup_event:
ballot_writeup_event = generate_ballot_writeup(request, doc)
ballot_writeup = ballot_writeup_event.text
error_duplicate_rfc_editor_note = False
e = doc.latest_event(WriteupDocEvent, type="changed_rfc_editor_note_text")
if e and (e.text != ""):
if "RFC Editor Note" in ballot_writeup:
error_duplicate_rfc_editor_note = True
ballot_writeup += "\n\n" + e.text
if error_duplicate_rfc_editor_note:
return render(request, 'doc/draft/rfceditor_note_duplicate_error.html', {'doc': doc})
if "NOT be published" in approval_text:
action = "do_not_publish"
elif "To: RFC Editor" in approval_text:
action = "to_rfc_editor"
else:
action = "to_announcement_list"
# NOTE: according to Michelle Cotton <michelle.cotton@icann.org>
# (as per 2011-10-24) IANA is scraping these messages for
# information so would like to know beforehand if the format
# changes
announcement = approval_text + "\n\n" + ballot_writeup
if request.method == 'POST':
if action == "do_not_publish":
new_state = State.objects.get(used=True, type="draft-iesg", slug="dead")
else:
new_state = State.objects.get(used=True, type="draft-iesg", slug="ann")
prev_state = doc.get_state("draft-iesg")
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
events = []
if approval_mail_event.pk == None:
approval_mail_event.save()
if ballot_writeup_event.pk == None:
ballot_writeup_event.save()
if new_state.slug == "ann" and new_state.slug != prev_state.slug and not request.POST.get("skiprfceditorpost"):
# start by notifying the RFC Editor
import ietf.sync.rfceditor
response, error = ietf.sync.rfceditor.post_approved_draft(settings.RFC_EDITOR_SYNC_NOTIFICATION_URL, doc.name)
if error:
return render(request, 'doc/draft/rfceditor_post_approved_draft_failed.html',
dict(name=doc.name,
response=response,
error=error))
doc.set_state(new_state)
doc.tags.remove(*prev_tags)
# fixup document
close_open_ballots(doc, login)
e = DocEvent(doc=doc, by=login)
if action == "do_not_publish":
e.type = "iesg_disapproved"
e.desc = "Do Not Publish note has been sent to the RFC Editor"
else:
e.type = "iesg_approved"
e.desc = "IESG has approved the document"
e.save()
events.append(e)
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
if e:
events.append(e)
doc.save_with_history(events)
# send announcement
send_mail_preformatted(request, announcement)
if action == "to_announcement_list":
addrs = gather_address_lists('ballot_approved_ietf_stream_iana').as_strings(compact=False)
send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc),
override={ "To": addrs.to, "CC": addrs.cc, "Bcc": None, "Reply-To": None})
msg = infer_message(announcement)
msg.by = login
msg.save()
msg.related_docs.add(doc)
return HttpResponseRedirect(doc.get_absolute_url())
return render(request, 'doc/ballot/approve_ballot.html',
dict(doc=doc,
action=action,
announcement=announcement))
class MakeLastCallForm(forms.Form):
last_call_sent_date = forms.DateField(required=True)
last_call_expiration_date = forms.DateField(required=True)
@role_required('Secretariat')
def make_last_call(request, name):
"""Make last call for Internet Draft, sending out announcement."""
doc = get_object_or_404(Document, docalias__name=name)
if not (doc.get_state("draft-iesg") or doc.get_state("statchg")):
raise Http404
login = request.user.person
announcement_event = doc.latest_event(WriteupDocEvent, type="changed_last_call_text")
if not announcement_event:
if doc.type_id != 'draft':
raise Http404
announcement_event = generate_last_call_announcement(request, doc)
announcement = announcement_event.text
if request.method == 'POST':
form = MakeLastCallForm(request.POST)
if form.is_valid():
if announcement_event.pk == None:
announcement_event.save()
send_mail_preformatted(request, announcement)
if doc.type.slug == 'draft':
addrs = gather_address_lists('last_call_issued_iana',doc=doc).as_strings(compact=False)
send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc),
override={ "To": addrs.to, "CC": addrs.cc, "Bcc": None, "Reply-To": None})
msg = infer_message(announcement)
msg.by = login
msg.save()
msg.related_docs.add(doc)
new_state = doc.get_state()
prev_tags = []
new_tags = []
events = []
if doc.type.slug == 'draft':
new_state = State.objects.get(used=True, type="draft-iesg", slug='lc')
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
elif doc.type.slug == 'statchg':
new_state = State.objects.get(used=True, type="statchg", slug='in-lc')
prev_state = doc.get_state(new_state.type_id)
doc.set_state(new_state)
doc.tags.remove(*prev_tags)
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
if e:
events.append(e)
e = LastCallDocEvent(doc=doc, by=login)
e.type = "sent_last_call"
e.desc = "The following Last Call announcement was sent out:<br><br>"
e.desc += announcement
if form.cleaned_data['last_call_sent_date'] != e.time.date():
e.time = datetime.datetime.combine(form.cleaned_data['last_call_sent_date'], e.time.time())
e.expires = form.cleaned_data['last_call_expiration_date']
e.save()
events.append(e)
# update IANA Review state
if doc.type.slug == 'draft':
prev_state = doc.get_state("draft-iana-review")
if not prev_state:
next_state = State.objects.get(used=True, type="draft-iana-review", slug="need-rev")
doc.set_state(next_state)
e = add_state_change_event(doc, login, prev_state, next_state)
if e:
events.append(e)
doc.save_with_history(events)
return HttpResponseRedirect(doc.get_absolute_url())
else:
initial = {}
initial["last_call_sent_date"] = datetime.date.today()
if doc.type.slug == 'draft':
# This logic is repeated in the code that edits last call text - why?
expire_days = 14
if doc.group.type_id in ("individ", "area"):
expire_days = 28
templ = 'doc/draft/make_last_call.html'
else:
expire_days=28
templ = 'doc/status_change/make_last_call.html'
initial["last_call_expiration_date"] = datetime.date.today() + datetime.timedelta(days=expire_days)
form = MakeLastCallForm(initial=initial)
return render(request, templ,
dict(doc=doc,
form=form,
announcement=announcement,
))