|
| 1 | +# ballot management (voting, commenting, ...) for IESG members |
| 2 | + |
| 3 | +import re, os |
| 4 | +from datetime import datetime, date, time, timedelta |
| 5 | +from django.http import HttpResponse, HttpResponseRedirect, Http404 |
| 6 | +from django.shortcuts import render_to_response, get_object_or_404 |
| 7 | +from django.core.urlresolvers import reverse as urlreverse |
| 8 | +from django.template.loader import render_to_string |
| 9 | +from django.template import RequestContext |
| 10 | +from django import forms |
| 11 | +from django.utils.html import strip_tags |
| 12 | + |
| 13 | +from ietf.utils.mail import send_mail_text |
| 14 | +from ietf.ietfauth.decorators import group_required |
| 15 | +from ietf.idtracker.templatetags.ietf_filters import in_group |
| 16 | +from ietf.idtracker.models import * |
| 17 | +from ietf.iesg.models import * |
| 18 | +from ietf import settings |
| 19 | +from ietf.idrfc.mails import * |
| 20 | +from ietf.idrfc.utils import * |
| 21 | + |
| 22 | + |
| 23 | +BALLOT_CHOICES = (("yes", "Yes"), |
| 24 | + ("noobj", "No Objection"), |
| 25 | + ("discuss", "Discuss"), |
| 26 | + ("abstain", "Abstain"), |
| 27 | + ("recuse", "Recuse"), |
| 28 | + ("", "No Record"), |
| 29 | + ) |
| 30 | + |
| 31 | +def position_to_ballot_choice(position): |
| 32 | + for v, label in BALLOT_CHOICES: |
| 33 | + if v and getattr(position, v): |
| 34 | + return v |
| 35 | + return "" |
| 36 | + |
| 37 | +def position_label(position_value): |
| 38 | + return dict(BALLOT_CHOICES).get(position_value, "") |
| 39 | + |
| 40 | +def get_ballot_info(ballot, area_director): |
| 41 | + pos = Position.objects.filter(ballot=ballot, ad=area_director) |
| 42 | + pos = pos[0] if pos else None |
| 43 | + |
| 44 | + discuss = IESGDiscuss.objects.filter(ballot=ballot, ad=area_director) |
| 45 | + discuss = discuss[0] if discuss else None |
| 46 | + |
| 47 | + comment = IESGComment.objects.filter(ballot=ballot, ad=area_director) |
| 48 | + comment = comment[0] if comment else None |
| 49 | + |
| 50 | + return (pos, discuss, comment) |
| 51 | + |
| 52 | +class EditPositionForm(forms.Form): |
| 53 | + position = forms.ChoiceField(choices=BALLOT_CHOICES, widget=forms.RadioSelect) |
| 54 | + discuss_text = forms.CharField(required=False, widget=forms.Textarea) |
| 55 | + comment_text = forms.CharField(required=False, widget=forms.Textarea) |
| 56 | + |
| 57 | +@group_required('Area_Director','Secretariat') |
| 58 | +def edit_position(request, name): |
| 59 | + """Vote and edit discuss and comment on Internet Draft as Area Director.""" |
| 60 | + doc = get_object_or_404(InternetDraft, filename=name) |
| 61 | + if not doc.idinternal: |
| 62 | + raise Http404() |
| 63 | + |
| 64 | + login = IESGLogin.objects.get(login_name=request.user.username) |
| 65 | + |
| 66 | + pos, discuss, comment = get_ballot_info(doc.idinternal.ballot, login) |
| 67 | + |
| 68 | + if request.method == 'POST': |
| 69 | + form = EditPositionForm(request.POST) |
| 70 | + if form.is_valid(): |
| 71 | + # save the vote |
| 72 | + clean = form.cleaned_data |
| 73 | + vote = clean['position'] |
| 74 | + if pos: |
| 75 | + # mark discuss as cleared (quirk from old system) |
| 76 | + if pos.discuss: |
| 77 | + pos.discuss = -1 |
| 78 | + else: |
| 79 | + pos = Position(ballot=doc.idinternal.ballot, ad=login) |
| 80 | + pos.discuss = 0 |
| 81 | + |
| 82 | + old_vote = position_to_ballot_choice(pos) |
| 83 | + |
| 84 | + pos.yes = pos.noobj = pos.abstain = pos.recuse = 0 |
| 85 | + if vote: |
| 86 | + setattr(pos, vote, 1) |
| 87 | + |
| 88 | + if pos.id: |
| 89 | + pos.save() |
| 90 | + if vote != old_vote: |
| 91 | + add_document_comment(request, doc, "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.ad, position_label(vote), position_label(old_vote))) |
| 92 | + elif vote: |
| 93 | + pos.save() |
| 94 | + add_document_comment(request, doc, "[Ballot Position Update] New position, %s, has been recorded" % position_label(vote)) |
| 95 | + |
| 96 | + # save discuss |
| 97 | + if (discuss and clean['discuss_text'] != discuss.text) or (clean['discuss_text'] and not discuss): |
| 98 | + if not discuss: |
| 99 | + discuss = IESGDiscuss(ballot=doc.idinternal.ballot, ad=login) |
| 100 | + |
| 101 | + discuss.text = clean['discuss_text'] |
| 102 | + discuss.date = date.today() |
| 103 | + discuss.revision = doc.revision_display() |
| 104 | + discuss.active = True |
| 105 | + discuss.save() |
| 106 | + |
| 107 | + if discuss.text: |
| 108 | + add_document_comment(request, doc, discuss.text, |
| 109 | + ballot=DocumentComment.BALLOT_DISCUSS) |
| 110 | + |
| 111 | + if pos.discuss < 1: |
| 112 | + IESGDiscuss.objects.filter(ballot=doc.idinternal.ballot, ad=pos.ad).update(active=False) |
| 113 | + |
| 114 | + # similar for comment (could share code with discuss, but |
| 115 | + # it's maybe better to coalesce them in the model instead |
| 116 | + # than doing a clever hack here) |
| 117 | + if (comment and clean['comment_text'] != comment.text) or (clean['comment_text'] and not comment): |
| 118 | + if not comment: |
| 119 | + comment = IESGComment(ballot=doc.idinternal.ballot, ad=login) |
| 120 | + |
| 121 | + comment.text = clean['comment_text'] |
| 122 | + comment.date = date.today() |
| 123 | + comment.revision = doc.revision_display() |
| 124 | + comment.active = True |
| 125 | + comment.save() |
| 126 | + |
| 127 | + if comment.text: |
| 128 | + add_document_comment(request, doc, comment.text, |
| 129 | + ballot=DocumentComment.BALLOT_COMMENT) |
| 130 | + |
| 131 | + doc.idinternal.event_date = date.today() |
| 132 | + doc.idinternal.save() |
| 133 | + if request.POST.get("send_mail"): |
| 134 | + return HttpResponseRedirect(urlreverse("doc_send_ballot_comment", kwargs=dict(name=doc.filename))) |
| 135 | + else: |
| 136 | + return HttpResponseRedirect(doc.idinternal.get_absolute_url()) |
| 137 | + else: |
| 138 | + initial = {} |
| 139 | + if pos: |
| 140 | + initial['position'] = position_to_ballot_choice(pos) |
| 141 | + |
| 142 | + if discuss: |
| 143 | + initial['discuss_text'] = discuss.text |
| 144 | + |
| 145 | + if comment: |
| 146 | + initial['comment_text'] = comment.text |
| 147 | + |
| 148 | + form = EditPositionForm(initial=initial) |
| 149 | + |
| 150 | + return render_to_response('idrfc/edit_position.html', |
| 151 | + dict(doc=doc, |
| 152 | + form=form, |
| 153 | + discuss=discuss, |
| 154 | + comment=comment), |
| 155 | + context_instance=RequestContext(request)) |
| 156 | + |
| 157 | +@group_required('Area_Director','Secretariat') |
| 158 | +def send_ballot_comment(request, name): |
| 159 | + """Email Internet Draft ballot discuss/comment for area director.""" |
| 160 | + doc = get_object_or_404(InternetDraft, filename=name) |
| 161 | + if not doc.idinternal: |
| 162 | + raise Http404() |
| 163 | + |
| 164 | + login = IESGLogin.objects.get(login_name=request.user.username) |
| 165 | + pos, discuss, comment = get_ballot_info(doc.idinternal.ballot, login) |
| 166 | + |
| 167 | + subj = [] |
| 168 | + d = "" |
| 169 | + if pos and pos.discuss == 1 and discuss and discuss.text: |
| 170 | + d = discuss.text |
| 171 | + subj.append("DISCUSS") |
| 172 | + c = "" |
| 173 | + if comment and comment.text: |
| 174 | + c = comment.text |
| 175 | + subj.append("COMMENT") |
| 176 | + |
| 177 | + subject = "%s: %s" % (" and ".join(subj), doc.file_tag()) |
| 178 | + body = render_to_string("idrfc/ballot_comment_mail.txt", |
| 179 | + dict(discuss=d, comment=c)) |
| 180 | + frm = u"%s <%s>" % login.person.email() |
| 181 | + to = "iesg@ietf.org" |
| 182 | + |
| 183 | + if request.method == 'POST': |
| 184 | + cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] |
| 185 | + if request.POST.get("cc_state_change") and doc.idinternal.state_change_notice_to: |
| 186 | + cc.extend(doc.idinternal.state_change_notice_to.split(',')) |
| 187 | + |
| 188 | + send_mail_text(request, to, frm, subject, body, cc=", ".join(cc)) |
| 189 | + |
| 190 | + return HttpResponseRedirect(doc.idinternal.get_absolute_url()) |
| 191 | + |
| 192 | + return render_to_response('idrfc/send_ballot_comment.html', |
| 193 | + dict(doc=doc, |
| 194 | + subject=subject, |
| 195 | + body=body, |
| 196 | + frm=frm, |
| 197 | + to=to, |
| 198 | + can_send=d or c), |
| 199 | + context_instance=RequestContext(request)) |
| 200 | + |
| 201 | + |
| 202 | +@group_required('Area_Director','Secretariat') |
| 203 | +def defer_ballot(request, name): |
| 204 | + """Signal post-pone of Internet Draft ballot, notifying relevant parties.""" |
| 205 | + doc = get_object_or_404(InternetDraft, filename=name) |
| 206 | + if not doc.idinternal: |
| 207 | + raise Http404() |
| 208 | + |
| 209 | + login = IESGLogin.objects.get(login_name=request.user.username) |
| 210 | + telechat_date = TelechatDates.objects.all()[0].date2 |
| 211 | + |
| 212 | + if request.method == 'POST': |
| 213 | + doc.idinternal.ballot.defer = True |
| 214 | + doc.idinternal.ballot.defer_by = login |
| 215 | + doc.idinternal.ballot.defer_date = date.today() |
| 216 | + doc.idinternal.ballot.save() |
| 217 | + |
| 218 | + doc.idinternal.change_state(IDState.objects.get(document_state_id=IDState.IESG_EVALUATION_DEFER), None) |
| 219 | + doc.idinternal.agenda = True |
| 220 | + doc.idinternal.telechat_date = telechat_date |
| 221 | + doc.idinternal.event_date = date.today() |
| 222 | + doc.idinternal.save() |
| 223 | + |
| 224 | + email_ballot_deferred(request, doc, login, telechat_date) |
| 225 | + |
| 226 | + log_state_changed(request, doc, login) |
| 227 | + |
| 228 | + return HttpResponseRedirect(doc.idinternal.get_absolute_url()) |
| 229 | + |
| 230 | + return render_to_response('idrfc/defer_ballot.html', |
| 231 | + dict(doc=doc, |
| 232 | + telechat_date=telechat_date), |
| 233 | + context_instance=RequestContext(request)) |
| 234 | + |
| 235 | +@group_required('Area_Director','Secretariat') |
| 236 | +def undefer_ballot(request, name): |
| 237 | + """Delete deferral of Internet Draft ballot.""" |
| 238 | + doc = get_object_or_404(InternetDraft, filename=name) |
| 239 | + if not doc.idinternal: |
| 240 | + raise Http404() |
| 241 | + |
| 242 | + login = IESGLogin.objects.get(login_name=request.user.username) |
| 243 | + |
| 244 | + if request.method == 'POST': |
| 245 | + doc.idinternal.ballot.defer = False |
| 246 | + doc.idinternal.ballot.save() |
| 247 | + |
| 248 | + doc.idinternal.change_state(IDState.objects.get(document_state_id=IDState.IESG_EVALUATION), None) |
| 249 | + doc.idinternal.event_date = date.today() |
| 250 | + doc.idinternal.save() |
| 251 | + |
| 252 | + log_state_changed(request, doc, login) |
| 253 | + |
| 254 | + return HttpResponseRedirect(doc.idinternal.get_absolute_url()) |
| 255 | + |
| 256 | + return render_to_response('idrfc/undefer_ballot.html', |
| 257 | + dict(doc=doc), |
| 258 | + context_instance=RequestContext(request)) |
| 259 | + |
0 commit comments