519 lines
20 KiB
Python
519 lines
20 KiB
Python
# generation of mails
|
|
|
|
import textwrap, datetime
|
|
|
|
from django.template.loader import render_to_string
|
|
from django.utils.html import strip_tags
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse as urlreverse
|
|
|
|
from ietf.utils.mail import send_mail, send_mail_text
|
|
from ietf.ipr.utils import iprs_from_docs, related_docs
|
|
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent, DocTagName
|
|
from ietf.doc.utils import needed_ballot_positions
|
|
from ietf.person.models import Person
|
|
from ietf.group.models import Group, Role
|
|
from ietf.doc.models import Document
|
|
|
|
def email_state_changed(request, doc, text):
|
|
to = [x.strip() for x in doc.notify.replace(';', ',').split(',')]
|
|
if not to:
|
|
return
|
|
|
|
text = strip_tags(text)
|
|
send_mail(request, to, None,
|
|
"ID Tracker State Update Notice: %s" % doc.file_tag(),
|
|
"doc/mail/state_changed_email.txt",
|
|
dict(text=text,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
|
|
|
|
def email_stream_changed(request, doc, old_stream, new_stream, text=""):
|
|
"""Email the change text to the notify group and to the stream chairs"""
|
|
to = [x.strip() for x in doc.notify.replace(';', ',').split(',')]
|
|
|
|
# These use comprehension to deal with conditions when there might be more than one chair listed for a stream
|
|
if old_stream:
|
|
to.extend([r.formatted_email() for r in Role.objects.filter(group__acronym=old_stream.slug, name='chair')])
|
|
if new_stream:
|
|
to.extend([r.formatted_email() for r in Role.objects.filter(group__acronym=new_stream.slug, name='chair')])
|
|
|
|
if not to:
|
|
return
|
|
|
|
if not text:
|
|
text = u"Stream changed to <b>%s</b> from %s" % (new_stream, old_stream)
|
|
text = strip_tags(text)
|
|
|
|
send_mail(request, to, None,
|
|
"ID Tracker Stream Change Notice: %s" % doc.file_tag(),
|
|
"doc/mail/stream_changed_email.txt",
|
|
dict(text=text,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
|
|
|
|
def email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state):
|
|
extra=extra_automation_headers(doc)
|
|
extra['Cc'] = 'iesg-secretary@ietf.org'
|
|
send_mail(request, ["IANA <iana@iana.org>", "RFC Editor <rfc-editor@rfc-editor.org>"], None,
|
|
"%s changed state from %s to %s" % (doc.name, prev_state.name, next_state.name),
|
|
"doc/mail/pulled_from_rfc_queue_email.txt",
|
|
dict(doc=doc,
|
|
prev_state=prev_state,
|
|
next_state=next_state,
|
|
comment=comment,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
|
|
extra=extra)
|
|
|
|
|
|
def email_authors(request, doc, subject, text):
|
|
to = [x.strip() for x in doc.author_list().split(',')]
|
|
if not to:
|
|
return
|
|
|
|
send_mail_text(request, to, None, subject, text)
|
|
|
|
def html_to_text(html):
|
|
return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("<br>", "\n"))
|
|
|
|
def email_ad(request, doc, ad, changed_by, text, subject=None):
|
|
if not ad or not changed_by or ad == changed_by:
|
|
return
|
|
|
|
to = ad.role_email("ad").formatted_email()
|
|
send_mail(request, to,
|
|
"DraftTracker Mail System <iesg-secretary@ietf.org>",
|
|
"%s updated by %s" % (doc.file_tag(), changed_by.plain_name()),
|
|
"doc/mail/change_notice.txt",
|
|
dict(text=html_to_text(text),
|
|
doc=doc,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
|
|
|
|
|
|
def generate_ballot_writeup(request, doc):
|
|
e = doc.latest_event(type="iana_review")
|
|
iana = e.desc if e else ""
|
|
|
|
e = WriteupDocEvent()
|
|
e.type = "changed_ballot_writeup_text"
|
|
e.by = request.user.person
|
|
e.doc = doc
|
|
e.desc = u"Ballot writeup was generated"
|
|
e.text = unicode(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana}))
|
|
e.save()
|
|
|
|
return e
|
|
|
|
def generate_last_call_announcement(request, doc):
|
|
expiration_date = datetime.date.today() + datetime.timedelta(days=14)
|
|
cc = []
|
|
if doc.group.type_id in ("individ", "area"):
|
|
group = "an individual submitter"
|
|
expiration_date += datetime.timedelta(days=14)
|
|
else:
|
|
group = "the %s WG (%s)" % (doc.group.name, doc.group.acronym)
|
|
if doc.group.list_email:
|
|
cc.append(doc.group.list_email)
|
|
|
|
doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3)
|
|
|
|
iprs = iprs_from_docs(related_docs(DocAlias.objects.get(name=doc.canonical_name())))
|
|
if iprs:
|
|
ipr_links = [ urlreverse("ietf.ipr.views.show", kwargs=dict(id=i.id)) for i in iprs]
|
|
ipr_links = [ settings.IDTRACKER_BASE_URL+url if not url.startswith("http") else url for url in ipr_links ]
|
|
else:
|
|
ipr_links = None
|
|
|
|
mail = render_to_string("doc/mail/last_call_announcement.txt",
|
|
dict(doc=doc,
|
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() + "ballot/",
|
|
expiration_date=expiration_date.strftime("%Y-%m-%d"), #.strftime("%B %-d, %Y"),
|
|
cc=", ".join("<%s>" % e for e in cc),
|
|
group=group,
|
|
docs=[ doc ],
|
|
urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ],
|
|
ipr_links=ipr_links,
|
|
)
|
|
)
|
|
|
|
e = WriteupDocEvent()
|
|
e.type = "changed_last_call_text"
|
|
e.by = request.user.person
|
|
e.doc = doc
|
|
e.desc = u"Last call announcement was generated"
|
|
e.text = unicode(mail)
|
|
e.save()
|
|
|
|
return e
|
|
|
|
|
|
DO_NOT_PUBLISH_IESG_STATES = ("nopubadw", "nopubanw")
|
|
|
|
def generate_approval_mail(request, doc):
|
|
if doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES or doc.stream_id in ('ise','irtf'):
|
|
mail = generate_approval_mail_rfc_editor(request, doc)
|
|
else:
|
|
mail = generate_approval_mail_approved(request, doc)
|
|
|
|
e = WriteupDocEvent()
|
|
e.type = "changed_ballot_approval_text"
|
|
e.by = request.user.person
|
|
e.doc = doc
|
|
e.desc = u"Ballot approval text was generated"
|
|
e.text = unicode(mail)
|
|
e.save()
|
|
|
|
return e
|
|
|
|
def generate_approval_mail_approved(request, doc):
|
|
|
|
if doc.intended_std_level_id in ("std", "ds", "ps", "bcp"):
|
|
action_type = "Protocol"
|
|
else:
|
|
action_type = "Document"
|
|
|
|
cc = []
|
|
cc.extend(settings.DOC_APPROVAL_EMAIL_CC)
|
|
|
|
# the second check catches some area working groups (like
|
|
# Transport Area Working Group)
|
|
if doc.group.type_id not in ("area", "individ", "ag") and not doc.group.name.endswith("Working Group"):
|
|
doc.group.name_with_wg = doc.group.name + " Working Group"
|
|
if doc.group.list_email:
|
|
cc.append("%s mailing list <%s>" % (doc.group.acronym, doc.group.list_email))
|
|
cc.append("%s chair <%s-chairs@ietf.org>" % (doc.group.acronym, doc.group.acronym))
|
|
else:
|
|
doc.group.name_with_wg = doc.group.name
|
|
|
|
doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3)
|
|
|
|
if doc.group.type_id in ("individ", "area"):
|
|
made_by = "This document has been reviewed in the IETF but is not the product of an IETF Working Group."
|
|
else:
|
|
made_by = "This document is the product of the %s." % doc.group.name_with_wg
|
|
|
|
responsible_directors = set([doc.ad,])
|
|
if doc.group.type_id not in ("individ","area"):
|
|
responsible_directors.update([x.person for x in Role.objects.filter(group=doc.group.parent,name='ad')])
|
|
responsible_directors = [x.plain_name() for x in responsible_directors if x]
|
|
|
|
if len(responsible_directors)>1:
|
|
contacts = "The IESG contact persons are "+", ".join(responsible_directors[:-1])+" and "+responsible_directors[-1]+"."
|
|
else:
|
|
contacts = "The IESG contact person is %s." % responsible_directors[0]
|
|
|
|
doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft"
|
|
|
|
return render_to_string("doc/mail/approval_mail.txt",
|
|
dict(doc=doc,
|
|
docs=[doc],
|
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
|
cc=",\n ".join(cc),
|
|
doc_type=doc_type,
|
|
made_by=made_by,
|
|
contacts=contacts,
|
|
action_type=action_type,
|
|
)
|
|
)
|
|
|
|
def generate_approval_mail_rfc_editor(request, doc):
|
|
disapproved = doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES
|
|
doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft"
|
|
|
|
to = []
|
|
if doc.group.type_id != "individ":
|
|
for r in doc.group.role_set.filter(name="chair").select_related():
|
|
to.append(r.formatted_email())
|
|
|
|
if doc.stream_id in ("ise", "irtf"):
|
|
# include ISE/IRTF chairs
|
|
g = Group.objects.get(acronym=doc.stream_id)
|
|
for r in g.role_set.filter(name="chair").select_related():
|
|
to.append(r.formatted_email())
|
|
|
|
if doc.stream_id == "irtf":
|
|
# include IRSG
|
|
to.append('"Internet Research Steering Group" <irsg@irtf.org>')
|
|
|
|
return render_to_string("doc/mail/approval_mail_rfc_editor.txt",
|
|
dict(doc=doc,
|
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
|
doc_type=doc_type,
|
|
disapproved=disapproved,
|
|
to=", ".join(to),
|
|
)
|
|
)
|
|
|
|
def generate_publication_request(request, doc):
|
|
group_description = ""
|
|
if doc.group and doc.group.acronym != "none":
|
|
group_description = doc.group.name_with_acronym()
|
|
|
|
e = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
|
|
consensus = e.consensus if e else None
|
|
|
|
if doc.stream_id == "irtf":
|
|
approving_body = "IRSG"
|
|
consensus_body = doc.group.acronym.upper()
|
|
else:
|
|
approving_body = str(doc.stream)
|
|
consensus_body = approving_body
|
|
|
|
return render_to_string("doc/mail/publication_request.txt",
|
|
dict(doc=doc,
|
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
|
group_description=group_description,
|
|
approving_body=approving_body,
|
|
consensus_body=consensus_body,
|
|
consensus=consensus,
|
|
)
|
|
)
|
|
|
|
def send_last_call_request(request, doc):
|
|
to = "iesg-secretary@ietf.org"
|
|
frm = '"DraftTracker Mail System" <iesg-secretary@ietf.org>'
|
|
|
|
send_mail(request, to, frm,
|
|
"Last Call: %s" % doc.file_tag(),
|
|
"doc/mail/last_call_request.txt",
|
|
dict(docs=[doc],
|
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
|
|
|
|
def email_resurrect_requested(request, doc, by):
|
|
to = "I-D Administrator <internet-drafts@ietf.org>"
|
|
|
|
if by.role_set.filter(name="secr", group__acronym="secretariat"):
|
|
e = by.role_email("secr", group="secretariat")
|
|
else:
|
|
e = by.role_email("ad")
|
|
frm = e.address
|
|
|
|
send_mail(request, to, e.formatted_email(),
|
|
"I-D Resurrection Request",
|
|
"doc/mail/resurrect_request_email.txt",
|
|
dict(doc=doc,
|
|
by=frm,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
|
|
|
|
def email_resurrection_completed(request, doc, requester):
|
|
if requester.role_set.filter(name="secr", group__acronym="secretariat"):
|
|
e = requester.role_email("secr", group="secretariat")
|
|
else:
|
|
e = requester.role_email("ad")
|
|
|
|
to = e.formatted_email()
|
|
frm = "I-D Administrator <internet-drafts-reply@ietf.org>"
|
|
send_mail(request, to, frm,
|
|
"I-D Resurrection Completed - %s" % doc.file_tag(),
|
|
"doc/mail/resurrect_completed_email.txt",
|
|
dict(doc=doc,
|
|
by=frm,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
|
|
|
|
def email_ballot_deferred(request, doc, by, telechat_date):
|
|
to = "iesg@ietf.org"
|
|
frm = "DraftTracker Mail System <iesg-secretary@ietf.org>"
|
|
send_mail(request, to, frm,
|
|
"IESG Deferred Ballot notification: %s" % doc.file_tag(),
|
|
"doc/mail/ballot_deferred_email.txt",
|
|
dict(doc=doc,
|
|
by=by,
|
|
action='deferred',
|
|
telechat_date=telechat_date))
|
|
|
|
def email_ballot_undeferred(request, doc, by, telechat_date):
|
|
to = "iesg@ietf.org"
|
|
frm = "DraftTracker Mail System <iesg-secretary@ietf.org>"
|
|
send_mail(request, to, frm,
|
|
"IESG Undeferred Ballot notification: %s" % doc.file_tag(),
|
|
"doc/mail/ballot_deferred_email.txt",
|
|
dict(doc=doc,
|
|
by=by,
|
|
action='undeferred',
|
|
telechat_date=telechat_date))
|
|
|
|
def generate_issue_ballot_mail(request, doc, ballot):
|
|
active_ads = Person.objects.filter(role__name="ad", role__group__state="active", role__group__type="area").distinct()
|
|
|
|
positions = BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position", ballot=ballot).order_by("-time", '-id').select_related('ad')
|
|
|
|
# format positions and setup discusses and comments
|
|
ad_feedback = []
|
|
seen = set()
|
|
active_ad_positions = []
|
|
inactive_ad_positions = []
|
|
for p in positions:
|
|
if p.ad in seen:
|
|
continue
|
|
|
|
seen.add(p.ad)
|
|
|
|
def formatted(val):
|
|
if val:
|
|
return "[ X ]"
|
|
else:
|
|
return "[ ]"
|
|
|
|
fmt = u"%-21s%-10s%-11s%-9s%-10s" % (
|
|
p.ad.plain_name()[:21],
|
|
formatted(p.pos_id == "yes"),
|
|
formatted(p.pos_id == "noobj"),
|
|
formatted(p.pos_id == "discuss"),
|
|
"[ R ]" if p.pos_id == "recuse" else formatted(p.pos_id == "abstain"),
|
|
)
|
|
|
|
if p.ad in active_ads:
|
|
active_ad_positions.append(fmt)
|
|
if not p.pos_id == "discuss":
|
|
p.discuss = ""
|
|
if p.comment or p.discuss:
|
|
ad_feedback.append(p)
|
|
else:
|
|
inactive_ad_positions.append(fmt)
|
|
|
|
active_ad_positions.sort()
|
|
inactive_ad_positions.sort()
|
|
ad_feedback.sort(key=lambda p: p.ad.plain_name())
|
|
|
|
e = doc.latest_event(LastCallDocEvent, type="sent_last_call")
|
|
last_call_expires = e.expires if e else None
|
|
|
|
e = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
|
|
approval_text = e.text if e else ""
|
|
|
|
e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
|
|
ballot_writeup = e.text if e else ""
|
|
|
|
return render_to_string("doc/mail/issue_ballot_mail.txt",
|
|
dict(doc=doc,
|
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
|
active_ad_positions=active_ad_positions,
|
|
inactive_ad_positions=inactive_ad_positions,
|
|
ad_feedback=ad_feedback,
|
|
last_call_expires=last_call_expires,
|
|
approval_text=approval_text,
|
|
ballot_writeup=ballot_writeup,
|
|
needed_ballot_positions=
|
|
needed_ballot_positions(doc,
|
|
doc.active_ballot().active_ad_positions().values()
|
|
),
|
|
)
|
|
)
|
|
|
|
def email_iana(request, doc, to, msg):
|
|
# fix up message and send it with extra info on doc in headers
|
|
import email
|
|
parsed_msg = email.message_from_string(msg.encode("utf-8"))
|
|
|
|
extra = {}
|
|
extra["Reply-To"] = "noreply@ietf.org"
|
|
extra["X-IETF-Draft-string"] = doc.name
|
|
extra["X-IETF-Draft-revision"] = doc.rev
|
|
|
|
send_mail_text(request, "IANA <%s>" % to,
|
|
parsed_msg["From"], parsed_msg["Subject"],
|
|
parsed_msg.get_payload(),
|
|
extra=extra)
|
|
|
|
def extra_automation_headers(doc):
|
|
extra = {}
|
|
extra["X-IETF-Draft-string"] = doc.name
|
|
extra["X-IETF-Draft-revision"] = doc.rev
|
|
|
|
return extra
|
|
|
|
def email_last_call_expired(doc):
|
|
text = "IETF Last Call has ended, and the state has been changed to\n%s." % doc.get_state("draft-iesg").name
|
|
|
|
to = [x.strip() for x in doc.notify.replace(';', ',').split(',')]
|
|
to.insert(0, "iesg@ietf.org")
|
|
|
|
send_mail(None,
|
|
to,
|
|
"DraftTracker Mail System <iesg-secretary@ietf.org>",
|
|
"Last Call Expired: %s" % doc.file_tag(),
|
|
"doc/mail/change_notice.txt",
|
|
dict(text=text,
|
|
doc=doc,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
|
|
cc="iesg-secretary@ietf.org")
|
|
|
|
def stream_state_email_recipients(doc, extra_recipients=[]):
|
|
persons = set()
|
|
res = []
|
|
for r in Role.objects.filter(group=doc.group, name__in=("chair", "delegate")).select_related("person", "email"):
|
|
res.append(r.formatted_email())
|
|
persons.add(r.person)
|
|
|
|
for email in doc.authors.all():
|
|
if email.person not in persons:
|
|
res.append(email.formatted_email())
|
|
persons.add(email.person)
|
|
|
|
for e in extra_recipients:
|
|
if e.person not in persons:
|
|
res.append(e.formatted_email())
|
|
persons.add(e.person)
|
|
|
|
return res
|
|
|
|
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""):
|
|
recipients = stream_state_email_recipients(doc)
|
|
|
|
state_type = (prev_state or new_state).type
|
|
|
|
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
|
|
u"%s changed for %s" % (state_type.label, doc.name),
|
|
'doc/mail/stream_state_changed_email.txt',
|
|
dict(doc=doc,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
|
state_type=state_type,
|
|
prev_state=prev_state,
|
|
new_state=new_state,
|
|
by=by,
|
|
comment=comment))
|
|
|
|
def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""):
|
|
extra_recipients = []
|
|
|
|
if DocTagName.objects.get(slug="sheph-u") in added_tags and doc.shepherd:
|
|
extra_recipients.append(doc.shepherd)
|
|
|
|
recipients = stream_state_email_recipients(doc, extra_recipients)
|
|
|
|
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
|
|
u"Tags changed for %s" % doc.name,
|
|
'doc/mail/stream_tags_changed_email.txt',
|
|
dict(doc=doc,
|
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
|
added=added_tags,
|
|
removed=removed_tags,
|
|
by=by,
|
|
comment=comment))
|
|
|
|
def send_review_possibly_replaces_request(request, doc):
|
|
to_email = []
|
|
|
|
if doc.stream_id == "ietf":
|
|
to_email.extend(r.formatted_email() for r in Role.objects.filter(group=doc.group, name="chair").select_related("email", "person"))
|
|
elif doc.stream_id == "iab":
|
|
to_email.append("IAB Stream <iab-stream@iab.org>")
|
|
elif doc.stream_id == "ise":
|
|
to_email.append("Independent Submission Editor <rfc-ise@rfc-editor.org>")
|
|
elif doc.stream_id == "irtf":
|
|
to_email.append("IRSG <irsg@irtf.org>")
|
|
|
|
possibly_replaces = Document.objects.filter(name__in=[alias.name for alias in doc.related_that_doc("possibly-replaces")])
|
|
other_chairs = Role.objects.filter(group__in=[other.group for other in possibly_replaces], name="chair").select_related("email", "person")
|
|
to_email.extend(r.formatted_email() for r in other_chairs)
|
|
|
|
if not to_email:
|
|
to_email.append("internet-drafts@ietf.org")
|
|
|
|
if to_email:
|
|
send_mail(request, list(set(to_email)), settings.DEFAULT_FROM_EMAIL,
|
|
'Review of suggested possible replacements for %s-%s needed' % (doc.name, doc.rev),
|
|
'doc/mail/review_possibly_replaces_request.txt', {
|
|
'doc': doc,
|
|
'possibly_replaces': doc.related_that_doc("possibly-replaces"),
|
|
'review_url': settings.IDTRACKER_BASE_URL + urlreverse("doc_review_possibly_replaces", kwargs={ "name": doc.name }),
|
|
})
|