# ballot management (voting, commenting, writeups, ...) for Area # Directors and Secretariat import re, os from datetime import datetime, date, time, timedelta from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import render_to_response, get_object_or_404, redirect from django.core.urlresolvers import reverse as urlreverse from django.template.loader import render_to_string from django.template import RequestContext from django import forms from django.utils.html import strip_tags from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from ietf.utils.mail import send_mail_text, send_mail_preformatted from ietf.ietfauth.decorators import group_required from ietf.idtracker.templatetags.ietf_filters import in_group from ietf.ietfauth.decorators import has_role from mails import email_secretariat, generate_ballot_writeup, generate_issue_ballot_mail from utils import * from ietf.group.models import Group, GroupHistory, GroupEvent from ietf.group.utils import save_group_in_history from ietf.name.models import GroupBallotPositionName, GroupStateName from ietf.doc.models import * def default_action_text(wg, charter, user, action): e = WriteupDocEvent(doc=charter, by=user) e.by = user e.type = "changed_action_announcement" e.desc = "WG action text was changed" info = {} info['chairs'] = [{ 'name': x.email.person.name, 'email': x.email.address} for x in wg.role_set.filter(name="Chair")] info['secr'] = [{ 'name': x.email.person.name, 'email': x.email.address} for x in wg.role_set.filter(name="Secr")] info['techadv'] = [{ 'name': x.email.person.name, 'email': x.email.address} for x in wg.role_set.filter(name="Techadv")] info['ad'] = {'name': wg.ad.name, 'email': wg.ad.email_address().address } if wg.ad else None, info['list'] = wg.list_email if wg.list_email else None, info['list_subscribe'] = str(wg.list_subscribe) if wg.list_subscribe else None, info['list_archive'] = str(wg.list_archive) if wg.list_archive else None, filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev)) try: charter_text = open(filename, 'r') info['charter_txt'] = charter_text.read() except IOError: info['charter_txt'] = "Error: couldn't read charter text" e.text = render_to_string("wgcharter/action_text.txt", dict(wg=wg, charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), action_type=action, info=info, ) ) e.save() return e def default_review_text(wg, charter, user): e = WriteupDocEvent(doc=charter, by=user) e.by = user e.type = "changed_review_announcement" e.desc = "WG review text was changed" info = {} info['chairs'] = [{ 'name': x.person.name, 'email': x.email.address} for x in wg.role_set.filter(name="Chair")] info['secr'] = [{ 'name': x.person.name, 'email': x.email.address} for x in wg.role_set.filter(name="Secr")] info['techadv'] = [{ 'name': x.person.name, 'email': x.email.address} for x in wg.role_set.filter(name="Techadv")] info['ad'] = {'name': wg.ad.name, 'email': wg.ad.role_email("ad").address } if wg.ad else None, info['list'] = wg.list_email if wg.list_email else None, info['list_subscribe'] = wg.list_subscribe if wg.list_subscribe else None, info['list_archive'] = wg.list_archive if wg.list_archive else None, info['bydate'] = (date.today() + timedelta(weeks=1)).isoformat() filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev)) try: charter_text = open(filename, 'r') info['charter_txt'] = charter_text.read() except IOError: info['charter_txt'] = "Error: couldn't read charter text" e.text = render_to_string("wgcharter/review_text.txt", dict(wg=wg, charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), info=info, review_type="new" if wg.state_id == "proposed" else "recharter", ) ) e.save() return e BALLOT_CHOICES = (("yes", "Yes"), ("no", "No"), ("block", "Block"), ("abstain", "Abstain"), ("", "No Record"), ) 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=GroupBallotPositionName.objects.all(), widget=forms.RadioSelect, initial="norecord", required=True) block_comment = forms.CharField(required=False, label="Blocking comment", widget=forms.Textarea) comment = forms.CharField(required=False, widget=forms.Textarea) return_to_url = forms.CharField(required=False, widget=forms.HiddenInput) def clean_blocking(self): entered_blocking = self.cleaned_data["block_comment"] entered_pos = self.cleaned_data["position"] if entered_pos.slug == "block" and not entered_blocking: raise forms.ValidationError("You must enter a non-empty blocking comment") return entered_blocking @group_required('Area_Director','Secretariat') def edit_position(request, name): """Vote and edit comments on Charter as Area Director.""" try: wg = Group.objects.get(acronym=name) except ObjectDoesNotExist: wglist = GroupHistory.objects.filter(acronym=name) if wglist: return redirect('wg_edit_position', name=wglist[0].group.acronym) else: raise Http404 charter = set_or_create_charter(wg) started_process = charter.latest_event(type="started_iesg_process") if not started_process: raise Http404 ad = login = request.user.get_profile() if 'HTTP_REFERER' in request.META: return_to_url = request.META['HTTP_REFERER'] else: return_to_url = charter.get_absolute_url() # 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() from ietf.person.models import Person ad = get_object_or_404(Person, pk=ad_id) old_pos = charter.latest_event(GroupBallotPositionDocEvent, type="changed_ballot_position", ad=ad, time__gte=started_process.time) if request.method == 'POST': form = EditPositionForm(request.POST) if form.is_valid(): # save the vote clean = form.cleaned_data if clean['return_to_url']: return_to_url = clean['return_to_url'] pos = GroupBallotPositionDocEvent(doc=charter, by=login) pos.type = "changed_ballot_position" pos.ad = ad pos.pos = clean["position"] pos.comment = clean["comment"].strip() pos.comment_time = old_pos.comment_time if old_pos else None pos.block_comment = clean["block_comment"].strip() if pos.pos_id == "block" else "" pos.block_comment_time = old_pos.block_comment_time if old_pos else None changes = [] added_events = [] # possibly add discuss/comment comments to history trail # so it's easy to see 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=charter) 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_block_comment = old_pos.block_comment if old_pos else "" if pos.block_comment != old_block_comment: pos.block_comment_time = pos.time changes.append("block_comment") if pos.block_comment: e = DocEvent(doc=charter, by=login) e.by = ad # otherwise we can't see who's saying it e.type = "added_comment" e.desc = "[Ballot blocking comment]\n" + pos.block_comment 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.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.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.name) # only add new event if we actually got a change if pos.desc: if login != ad: pos.desc += u" by %s" % login.name pos.save() for e in added_events: e.save() # save them after the position is saved to get later id charter.time = pos.time charter.save() if request.POST.get("send_mail"): qstr = "?return_to_url=%s" % return_to_url if request.GET.get('ad'): qstr += "&ad=%s" % request.GET.get('ad') return HttpResponseRedirect(urlreverse("wg_send_ballot_comment", kwargs=dict(name=wg.acronym)) + qstr) else: return HttpResponseRedirect(return_to_url) else: initial = {} if old_pos: initial['position'] = old_pos.pos.slug initial['block_comment'] = old_pos.block_comment initial['comment'] = old_pos.comment if return_to_url: initial['return_to_url'] = return_to_url form = EditPositionForm(initial=initial) return render_to_response('wgcharter/edit_position.html', dict(charter=charter, wg=wg, form=form, ad=ad, return_to_url=return_to_url, old_pos=old_pos, ), context_instance=RequestContext(request)) @group_required('Area_Director','Secretariat') def send_ballot_comment(request, name): """Email Charter ballot comment for area director.""" try: wg = Group.objects.get(acronym=name) except ObjectDoesNotExist: wglist = GroupHistory.objects.filter(acronym=name) if wglist: return redirect('wg_send_ballot_comment', name=wglist[0].group.acronym) else: raise Http404 charter = set_or_create_charter(wg) started_process = charter.latest_event(type="started_iesg_process") if not started_process: raise Http404() ad = login = request.user.get_profile() return_to_url = request.GET.get('return_to_url') if not return_to_url: return_to_url = charter.get_absolute_url() if 'HTTP_REFERER' in request.META: back_url = request.META['HTTP_REFERER'] else: back_url = charter.get_absolute_url() # 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() from ietf.person.models import Person ad = get_object_or_404(Person, pk=ad_id) pos = charter.latest_event(GroupBallotPositionDocEvent, type="changed_ballot_position", ad=ad, time__gte=started_process.time) if not pos: raise Http404() subj = [] d = "" if pos.pos_id == "block" and pos.block_comment: d = pos.block_comment subj.append("BLOCKING COMMENT") c = "" if pos.comment: c = pos.comment subj.append("COMMENT") ad_name_genitive = ad.name + "'" if ad.name.endswith('s') else ad.name + "'s" subject = "%s %s on %s" % (ad_name_genitive, pos.pos.name if pos.pos else "No Position", charter.name + "-" + charter.rev) if subj: subject += ": (with %s)" % " and ".join(subj) body = render_to_string("wgcharter/ballot_comment_mail.txt", dict(block_comment=d, comment=c, ad=ad.name, charter=charter, pos=pos.pos)) frm = ad.formatted_email() to = "The IESG " if request.method == 'POST': cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] send_mail_text(request, to, frm, subject, body, cc=", ".join(cc)) return HttpResponseRedirect(return_to_url) return render_to_response('wgcharter/send_ballot_comment.html', dict(charter=charter, subject=subject, body=body, frm=frm, to=to, ad=ad, can_send=d or c, back_url=back_url, ), context_instance=RequestContext(request)) class AnnouncementTextForm(forms.Form): announcement_text = forms.CharField(widget=forms.Textarea, required=True) def clean_announcement_text(self): return self.cleaned_data["announcement_text"].replace("\r", "") @group_required('Area_Director','Secretariat') def announcement_text(request, name, ann): """Editing of announcement text""" try: wg = Group.objects.get(acronym=name) except ObjectDoesNotExist: wglist = GroupHistory.objects.filter(acronym=name) if wglist: return redirect('wg_announcement_text', name=wglist[0].group.acronym) else: raise Http404 charter = set_or_create_charter(wg) login = request.user.get_profile() if ann == "action": existing = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") elif ann == "review": existing = charter.latest_event(WriteupDocEvent, type="changed_review_announcement") if not existing: if ann == "action": if next_approved_revision(wg.charter.rev) == "01": existing = default_action_text(wg, charter, login, "Formed") else: existing = default_action_text(wg, charter, login, "Rechartered") elif ann == "review": existing = default_review_text(wg, charter, login) form = AnnouncementTextForm(initial=dict(announcement_text=existing.text)) if request.method == 'POST': form = AnnouncementTextForm(request.POST) if "save_text" in request.POST and form.is_valid(): t = form.cleaned_data['announcement_text'] if t != existing.text: e = WriteupDocEvent(doc=charter, by=login) e.by = login e.type = "changed_%s_announcement" % ann e.desc = "WG %s text was changed" % ann e.text = t e.save() charter.time = e.time charter.save() return redirect('wg_view', name=wg.acronym) if "regenerate_text" in request.POST: if ann == "action": if next_approved_revision(wg.charter.rev) == "01": e = default_action_text(wg, charter, login, "Formed") else: e = default_action_text(wg, charter, login, "Rechartered") elif ann == "review": e = default_review_text(wg, charter, login) # make sure form has the updated text form = AnnouncementTextForm(initial=dict(announcement_text=e.text)) if "send_text" in request.POST and form.is_valid(): msg = form.cleaned_data['announcement_text'] import email parsed_msg = email.message_from_string(msg.encode("utf-8")) send_mail_text(request, parsed_msg["To"], parsed_msg["From"], parsed_msg["Subject"], parsed_msg.get_payload()) return redirect('wg_view', name=wg.acronym) return render_to_response('wgcharter/announcement_text.html', dict(charter=charter, announcement=ann, back_url=charter.get_absolute_url(), announcement_text_form=form, ), context_instance=RequestContext(request)) class BallotWriteupForm(forms.Form): ballot_writeup = forms.CharField(widget=forms.Textarea, required=True) def clean_ballot_writeup(self): return self.cleaned_data["ballot_writeup"].replace("\r", "") @group_required('Area_Director','Secretariat') def ballot_writeupnotes(request, name): """Editing of ballot write-up and notes""" try: wg = Group.objects.get(acronym=name) except ObjectDoesNotExist: wglist = GroupHistory.objects.filter(acronym=name) if wglist: return redirect('wg_ballot_writeupnotes', name=wglist[0].group.acronym) else: raise Http404 charter = set_or_create_charter(wg) started_process = charter.latest_event(type="started_iesg_process") if not started_process: raise Http404() login = request.user.get_profile() approval = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") existing = charter.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") if not existing: existing = generate_ballot_writeup(request, charter) reissue = charter.latest_event(DocEvent, type="sent_ballot_announcement") 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=charter, by=login) e.by = login e.type = "changed_ballot_writeup_text" e.desc = "Ballot writeup was changed" e.text = t e.save() if "issue_ballot" in request.POST and approval: if has_role(request.user, "Area Director") and not charter.latest_event(GroupBallotPositionDocEvent, ad=login, time__gte=started_process.time): # sending the ballot counts as a yes pos = GroupBallotPositionDocEvent(doc=charter, by=login) 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.name) pos.save() msg = generate_issue_ballot_mail(request, charter) send_mail_preformatted(request, msg) e = DocEvent(doc=charter, by=login) e.by = login e.type = "sent_ballot_announcement" e.desc = "Ballot has been issued by %s" % login.name e.save() return render_to_response('wgcharter/ballot_issued.html', dict(charter=charter, back_url=charter.get_absolute_url()), context_instance=RequestContext(request)) return render_to_response('wgcharter/ballot_writeupnotes.html', dict(charter=charter, back_url=charter.get_absolute_url(), ballot_issued=bool(charter.latest_event(type="sent_ballot_announcement")), ballot_writeup_form=form, reissue=reissue, approval=approval, ), context_instance=RequestContext(request)) @group_required('Secretariat') def approve_ballot(request, name): """Approve ballot, changing state, copying charter""" try: wg = Group.objects.get(acronym=name) except ObjectDoesNotExist: wglist = GroupHistory.objects.filter(acronym=name) if wglist: return redirect('wg_approve_ballot', name=wglist[0].group.acronym) else: raise Http404 charter = set_or_create_charter(wg) login = request.user.get_profile() e = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") if not e: if next_approved_revision(wg.charter.rev) == "01": announcement= default_action_text(wg, charter, login, "Formed").text else: announcement = default_action_text(wg, charter, login, "Rechartered").text else: announcement = e.text if request.method == 'POST': new_state = GroupStateName.objects.get(slug="active") new_charter_state = State.objects.get(type="charter", slug="approved") save_charter_in_history(charter) save_group_in_history(wg) prev_state = wg.state prev_charter_state = charter.get_state() wg.state = new_state charter.set_state(new_charter_state) e = DocEvent(doc=charter, by=login) e.type = "iesg_approved" e.desc = "IESG has approved the charter" e.save() change_description = e.desc + " and WG state has been changed to %s" % new_state.name e = log_state_changed(request, charter, login, prev_state) wg.time = e.time wg.save() ch = get_charter_for_revision(wg.charter, wg.charter.rev) filename = os.path.join(charter.get_file_path(), ch.name+"-"+ch.rev+".txt") try: source = open(filename, 'rb') raw_content = source.read() new_filename = os.path.join(charter.get_file_path(), 'charter-ietf-%s-%s.txt' % (wg.acronym, next_approved_revision(ch.rev))) destination = open(new_filename, 'wb+') destination.write(raw_content) destination.close() except IOError: raise Http404("Charter text %s" % filename) charter.rev = next_approved_revision(charter.rev) charter.save() email_secretariat(request, wg, "state-%s" % new_charter_state.slug, change_description) # send announcement send_mail_preformatted(request, announcement) return HttpResponseRedirect(charter.get_absolute_url()) return render_to_response('wgcharter/approve_ballot.html', dict(charter=charter, announcement=announcement, wg=wg), context_instance=RequestContext(request))