(Rocky) merge forward.

- Legacy-Id: 10157
This commit is contained in:
Robert Sparks 2015-10-09 03:16:24 +00:00
commit f0f5a51eb6
89 changed files with 4447 additions and 1336 deletions

163
2015-08-28-scrub-notify.py Normal file
View file

@ -0,0 +1,163 @@
#!/usr/bin/env python
import os, sys, re
from copy import copy
import datetime
# boilerplate
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../web/"))
sys.path = [ basedir ] + sys.path
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings")
import django
django.setup()
from django.db.models import F
from django.template import Template,Context
from ietf.doc.models import Document, DocEvent
from ietf.person.models import Person
from ietf.utils.mail import send_mail_text
def message_body_template():
return Template("""{% filter wordwrap:72 %}The Notify field for the document{{ count|pluralize }} listed at the end of this message {% if count > 1 %}were{% else %}was{% endif %} changed by removing the chair, shepherd, author, and similar addresses (in direct or alias form) to the point they could be identified.
The Datatracker now includes those addresses explicitly in each message it sends as appropriate. You can review where the datatracker sends messages for a given action in general using <https://datatracker.ietf.org/mailtoken/token>. You can review the expansions for a specific document by the new Email expansions tab on the document's page. Note that the addresses included for any given action are much more comprehensive than they were before this release.
Please review each new Notify field, and help remove any remaining addresses that will normally be copied per the configuration shown at https://datatracker.ietf.org/mailtoken/token. The field should now only contain exceptional addresses - parties you wish to be notified that aren't part of the new normal recipient set.
You can see exactly how the Notify field was changed for a given document by looking in the document's history.{% endfilter %}
{% if non_empty%}The document{{non_empty|length|pluralize }} with non-empty new Notify fields are:{% for doc in non_empty %}
https://datatracker.ietf.org{{doc.get_absolute_url}}{% endfor %}{% endif %}
{% if empty%}The document{{non_empty|length|pluralize }} with empty new Notify fields are:{% for doc in empty %}
https://datatracker.ietf.org{{doc.get_absolute_url}}{% endfor %}{% endif %}
""")
def other_addrs(addr):
person = Person.objects.filter(email__address__iexact=addr).first()
if not person:
return None
return [x.lower() for x in person.email_set.values_list('address',flat=True)]
def prep(item):
retval = item.lower()
if '<' in retval:
if not '>' in retval:
raise "Bad item: "+item
start=retval.index('<')+1
stop=retval.index('>')
retval = retval[start:stop]
return retval
def is_management(item, doc):
item = prep(item)
if any([
item == '%s.chairs@ietf.org'%doc.name,
item == '%s.ad@ietf.org'%doc.name,
item == '%s.shepherd@ietf.org'%doc.name,
item == '%s.chairs@tools.ietf.org'%doc.name,
item == '%s.ad@tools.ietf.org'%doc.name,
item == '%s.shepherd@tools.ietf.org'%doc.name,
doc.ad and item == doc.ad.email_address().lower(),
doc.shepherd and item == doc.shepherd.address.lower(),
]):
return True
if doc.group:
if any([
item == '%s-chairs@ietf.org'%doc.group.acronym,
item == '%s-ads@ietf.org'%doc.group.acronym,
item == '%s-chairs@tools.ietf.org'%doc.group.acronym,
item == '%s-ads@tools.ietf.org'%doc.group.acronym,
]):
return True
for addr in doc.group.role_set.filter(name__in=['chair','ad','delegate']).values_list('email__address',flat=True):
other = other_addrs(addr)
if item == addr.lower() or item in other:
return True
if doc.group.parent:
if item == '%s-ads@ietf.org'%doc.group.parent.acronym or item == '%s-ads@tools.ietf.org'%doc.group.parent.acronym:
return True
return False
def is_author(item, doc):
item = prep(item)
if item == '%s@ietf.org' % doc.name or item == '%s@tools.ietf.org' % doc.name:
return True
for addr in doc.authors.values_list('address',flat=True):
other = other_addrs(addr)
if item == addr.lower() or item in other:
return True
return False
msg_template = message_body_template()
by = Person.objects.get(name="(System)")
active_ads = list(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type="area"))
affected = set()
empty = dict()
non_empty = dict()
changed = 0
emptied = 0
qs = Document.objects.exclude(notify__isnull=True).exclude(notify='')
for doc in qs:
doc.notify = doc.notify.replace(';', ',')
items = set([ i.strip() for i in doc.notify.split(',') if i.strip() and '@' in i])
original_items = copy(items)
for item in original_items:
if any([
doc.group and doc.group.list_email and item.lower() == doc.group.list_email.lower(),
is_management(item,doc),
is_author(item,doc),
]):
items.discard(item)
if original_items != items:
changed += 1
if len(list(items))==0:
emptied += 1
to = []
if doc.ad and doc.ad in active_ads:
to.append(doc.ad.email_address())
if doc.group and doc.group.state_id=='active':
to.extend(doc.group.role_set.filter(name__in=['chair','ad']).values_list('email__address',flat=True))
if not to:
to = ['iesg@ietf.org']
to = ", ".join(sorted(to))
affected.add(to)
empty.setdefault(to,[])
non_empty.setdefault(to,[])
if len(list(items))==0:
empty[to].append(doc)
else:
non_empty[to].append(doc)
original_notify = doc.notify
new_notify = ', '.join(list(items))
doc.notify = new_notify
doc.time = datetime.datetime.now()
doc.save()
e = DocEvent(type="added_comment",doc=doc,time=doc.time,by=by)
e.desc = "Notify list changed from %s to %s"% (original_notify, new_notify if new_notify else '(None)')
e.save()
for a in list(affected):
txt = msg_template.render(Context({'count':len(empty[a])+len(non_empty[a]),'empty':empty[a],'non_empty':non_empty[a]}))
send_mail_text(None, to=a, frm=None, subject='Document Notify fields changed to match new Datatracker addressing defaults',txt =txt)
print "Changed",changed,"documents.",emptied,"of those had their notify field emptied"
print "Sent email to ",len(affected),"different sets of addresses"

View file

@ -1,46 +0,0 @@
#!/usr/bin/env python
#
# This script will send various milestone reminders. It's supposed to
# be run daily, and will then send reminders weekly/monthly as
# appropriate.
import datetime, os
import syslog
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings")
syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0)
import django
django.setup()
from ietf.group.mails import *
today = datetime.date.today()
MONDAY = 1
FIRST_DAY_OF_MONTH = 1
if today.isoweekday() == MONDAY:
# send milestone review reminders - ideally we'd keep track of
# exactly when we sent one last time for a group, but it's a bit
# complicated because people can change the milestones in the mean
# time, so dodge all of this by simply sending once a week only
for g in groups_with_milestones_needing_review():
mail_sent = email_milestone_review_reminder(g, grace_period=7)
if mail_sent:
syslog.syslog("Sent milestone review reminder for %s %s" % (g.acronym, g.type.name))
early_warning_days = 30
# send any milestones due reminders
for g in groups_needing_milestones_due_reminder(early_warning_days):
email_milestones_due(g, early_warning_days)
syslog.syslog("Sent milestones due reminder for %s %s" % (g.acronym, g.type.name))
if today.day == FIRST_DAY_OF_MONTH:
# send milestone overdue reminders - once a month
for g in groups_needing_milestones_overdue_reminder(grace_period=30):
email_milestones_overdue(g)
syslog.syslog("Sent milestones overdue reminder for %s %s" % (g.acronym, g.type.name))

View file

@ -7,10 +7,10 @@ from pathlib import Path
from ietf.utils.mail import send_mail
from ietf.doc.models import Document, DocEvent, State, save_document_in_history, IESG_SUBSTATE_TAGS
from ietf.person.models import Person, Email
from ietf.person.models import Person
from ietf.meeting.models import Meeting
from ietf.doc.utils import add_state_change_event
from ietf.mailtrigger.utils import gather_address_lists
def expirable_draft(draft):
@ -70,10 +70,7 @@ def send_expire_warning_for_draft(doc):
expiration = doc.expires.date()
to = [e.formatted_email() for e in doc.authors.all() if not e.address.startswith("unknown-email")]
cc = None
if doc.group.type_id in ("wg", "rg"):
cc = [e.formatted_email() for e in Email.objects.filter(role__group=doc.group, role__name="chair") if not e.address.startswith("unknown-email")]
(to,cc) = gather_address_lists('doc_expires_soon',doc=doc)
s = doc.get_state("draft-iesg")
state = s.name if s else "I-D Exists"
@ -91,21 +88,22 @@ def send_expire_warning_for_draft(doc):
cc=cc)
def send_expire_notice_for_draft(doc):
if not doc.ad or doc.get_state_slug("draft-iesg") == "dead":
if doc.get_state_slug("draft-iesg") == "dead":
return
s = doc.get_state("draft-iesg")
state = s.name if s else "I-D Exists"
request = None
to = doc.ad.role_email("ad").formatted_email()
(to,cc) = gather_address_lists('doc_expired',doc=doc)
send_mail(request, to,
"I-D Expiring System <ietf-secretariat-reply@ietf.org>",
u"I-D was expired %s" % doc.file_tag(),
"doc/draft/id_expired_email.txt",
dict(doc=doc,
state=state,
))
),
cc=cc)
def move_draft_files_to_archive(doc, rev):
def move_file(f):

View file

@ -1,5 +1,6 @@
# generation of mails
import os
import textwrap, datetime
from django.template.loader import render_to_string
@ -9,14 +10,14 @@ 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 WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
from ietf.doc.utils import needed_ballot_positions, get_document_content
from ietf.group.models import Role
from ietf.doc.models import Document
from ietf.mailtrigger.utils import gather_address_lists
def email_state_changed(request, doc, text):
to = [x.strip() for x in doc.notify.replace(';', ',').split(',')]
def email_state_changed(request, doc, text, mailtrigger_id=None):
(to,cc) = gather_address_lists(mailtrigger_id or 'doc_state_edited',doc=doc)
if not to:
return
@ -25,17 +26,17 @@ def email_state_changed(request, doc, text):
"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()))
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=cc)
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
streams = []
if old_stream:
to.extend([r.formatted_email() for r in Role.objects.filter(group__acronym=old_stream.slug, name='chair')])
streams.append(old_stream.slug)
if new_stream:
to.extend([r.formatted_email() for r in Role.objects.filter(group__acronym=new_stream.slug, name='chair')])
streams.append(new_stream.slug)
(to,cc) = gather_address_lists('doc_stream_changed',doc=doc,streams=streams)
if not to:
return
@ -48,12 +49,14 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""):
"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()))
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=cc)
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,
addrs = gather_address_lists('doc_pulled_from_rfc_queue',doc=doc)
extra['Cc'] = addrs.as_strings().cc
send_mail(request, addrs.to , 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,
@ -63,31 +66,18 @@ def email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state):
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 email_iesg_processing_document(request, doc, changes):
addrs = gather_address_lists('doc_iesg_processing_started',doc=doc)
send_mail(request, addrs.to, None,
'IESG processing details changed for %s' % doc.name,
'doc/mail/email_iesg_processing.txt',
dict(doc=doc,
changes=changes),
cc=addrs.cc)
def html_to_text(html):
return strip_tags(html.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&").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 ""
@ -104,14 +94,11 @@ def generate_ballot_writeup(request, doc):
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)
@ -122,11 +109,14 @@ def generate_last_call_announcement(request, doc):
else:
ipr_links = None
addrs = gather_address_lists('last_call_issued',doc=doc).as_strings()
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),
to=addrs.to,
cc=addrs.cc,
group=group,
docs=[ doc ],
urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ],
@ -170,16 +160,10 @@ def generate_approval_mail_approved(request, doc):
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
@ -202,11 +186,13 @@ def generate_approval_mail_approved(request, doc):
doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft"
addrs = gather_address_lists('ballot_approved_ietf_stream',doc=doc).as_strings()
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),
to = addrs.to,
cc = addrs.cc,
doc_type=doc_type,
made_by=made_by,
contacts=contacts,
@ -215,30 +201,19 @@ def generate_approval_mail_approved(request, doc):
)
def generate_approval_mail_rfc_editor(request, doc):
# This is essentially dead code - it is only exercised if the IESG ballots on some other stream's document,
# which does not happen now that we have conflict reviews.
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>')
addrs = gather_address_lists('ballot_approved_conflrev', doc=doc).as_strings()
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),
to = addrs.to,
cc = addrs.cc,
)
)
@ -268,17 +243,19 @@ def generate_publication_request(request, doc):
)
def send_last_call_request(request, doc):
to = "iesg-secretary@ietf.org"
(to, cc) = gather_address_lists('last_call_requested',doc=doc)
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()))
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
),
cc=cc)
def email_resurrect_requested(request, doc, by):
to = "I-D Administrator <internet-drafts@ietf.org>"
(to, cc) = gather_address_lists('resurrection_requested',doc=doc)
if by.role_set.filter(name="secr", group__acronym="secretariat"):
e = by.role_email("secr", group="secretariat")
@ -291,25 +268,22 @@ def email_resurrect_requested(request, doc, by):
"doc/mail/resurrect_request_email.txt",
dict(doc=doc,
by=frm,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=cc)
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()
(to, cc) = gather_address_lists('resurrection_completed',doc=doc)
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()))
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=cc)
def email_ballot_deferred(request, doc, by, telechat_date):
to = "iesg@ietf.org"
(to, cc) = gather_address_lists('ballot_deferred',doc=doc)
frm = "DraftTracker Mail System <iesg-secretary@ietf.org>"
send_mail(request, to, frm,
"IESG Deferred Ballot notification: %s" % doc.file_tag(),
@ -317,10 +291,11 @@ def email_ballot_deferred(request, doc, by, telechat_date):
dict(doc=doc,
by=by,
action='deferred',
telechat_date=telechat_date))
telechat_date=telechat_date),
cc=cc)
def email_ballot_undeferred(request, doc, by, telechat_date):
to = "iesg@ietf.org"
(to, cc) = gather_address_lists('ballot_deferred',doc=doc)
frm = "DraftTracker Mail System <iesg-secretary@ietf.org>"
send_mail(request, to, frm,
"IESG Undeferred Ballot notification: %s" % doc.file_tag(),
@ -328,69 +303,18 @@ def email_ballot_undeferred(request, doc, by, telechat_date):
dict(doc=doc,
by=by,
action='undeferred',
telechat_date=telechat_date))
telechat_date=telechat_date),
cc=cc)
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()
@ -398,7 +322,7 @@ def generate_issue_ballot_mail(request, doc, ballot):
)
)
def email_iana(request, doc, to, msg):
def email_iana(request, doc, to, msg, cc=None):
# 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"))
@ -411,7 +335,8 @@ def email_iana(request, doc, to, msg):
send_mail_text(request, "IANA <%s>" % to,
parsed_msg["From"], parsed_msg["Subject"],
parsed_msg.get_payload(),
extra=extra)
extra=extra,
cc=cc)
def extra_automation_headers(doc):
extra = {}
@ -422,45 +347,70 @@ def extra_automation_headers(doc):
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
addrs = gather_address_lists('last_call_expired',doc=doc)
to = [x.strip() for x in doc.notify.replace(';', ',').split(',')]
to.insert(0, "iesg@ietf.org")
send_mail(None,
to,
addrs.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")
cc = addrs.cc)
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)
def email_intended_status_changed(request, doc, text):
(to,cc) = gather_address_lists('doc_intended_status_changed',doc=doc)
for email in doc.authors.all():
if email.person not in persons:
res.append(email.formatted_email())
persons.add(email.person)
if not to:
return
text = strip_tags(text)
send_mail(request, to, None,
"Intended Status for %s changed to %s" % (doc.file_tag(),doc.intended_std_level),
"doc/mail/intended_status_changed_email.txt",
dict(text=text,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=cc)
for e in extra_recipients:
if e.person not in persons:
res.append(e.formatted_email())
persons.add(e.person)
def email_comment(request, doc, comment):
(to, cc) = gather_address_lists('doc_added_comment',doc=doc)
return res
send_mail(request, to, None, "Comment added to %s history"%doc.name,
"doc/mail/comment_added_email.txt",
dict(
comment=comment,
doc=doc,
by=request.user.person,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
),
cc = cc)
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""):
recipients = stream_state_email_recipients(doc)
def email_adopted(request, doc, prev_state, new_state, by, comment=""):
(to, cc) = gather_address_lists('doc_adopted_by_group',doc=doc)
state_type = (prev_state or new_state).type
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
send_mail(request, to, settings.DEFAULT_FROM_EMAIL,
u"The %s %s has adopted %s" %
(doc.group.acronym.upper(),doc.group.type_id.upper(), doc.name),
'doc/mail/doc_adopted_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),
cc=cc)
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""):
(to, cc)= gather_address_lists('doc_stream_state_edited',doc=doc)
state_type = (prev_state or new_state).type
send_mail(request, to, settings.DEFAULT_FROM_EMAIL,
u"%s changed for %s" % (state_type.label, doc.name),
'doc/mail/stream_state_changed_email.txt',
dict(doc=doc,
@ -469,17 +419,14 @@ def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=
prev_state=prev_state,
new_state=new_state,
by=by,
comment=comment))
comment=comment),
cc=cc)
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)
(to, cc) = gather_address_lists('doc_stream_state_edited',doc=doc)
recipients = stream_state_email_recipients(doc, extra_recipients)
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
send_mail(request, to, settings.DEFAULT_FROM_EMAIL,
u"Tags changed for %s" % doc.name,
'doc/mail/stream_tags_changed_email.txt',
dict(doc=doc,
@ -487,32 +434,46 @@ def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, commen
added=added_tags,
removed=removed_tags,
by=by,
comment=comment))
comment=comment),
cc=cc)
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>")
addrs = gather_address_lists('doc_replacement_suggested',doc=doc)
to = set(addrs.to)
cc = set(addrs.cc)
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)
for other_doc in possibly_replaces:
(other_to, other_cc) = gather_address_lists('doc_replacement_suggested',doc=other_doc)
to.update(other_to)
cc.update(other_cc)
if not to_email:
to_email.append("internet-drafts@ietf.org")
send_mail(request, list(to), settings.DEFAULT_FROM_EMAIL,
'Review of suggested possible replacements for %s-%s needed' % (doc.name, doc.rev),
'doc/mail/review_possibly_replaces_request.txt',
dict(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 })),
cc=list(cc),)
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 }),
})
def email_charter_internal_review(request, charter):
addrs = gather_address_lists('charter_internal_review',doc=charter,group=charter.group)
filename = '%s-%s.txt' % (charter.canonical_name(),charter.rev)
charter_text = get_document_content(
filename,
os.path.join(settings.CHARTER_PATH,filename),
split=False,
markup=False,
)
send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL,
'Internal WG Review: %s (%s)'%(charter.group.name,charter.group.acronym),
'doc/mail/charter_internal_review.txt',
dict(charter=charter,
chairs=charter.group.role_set.filter(name='chair').values_list('person__name',flat=True),
ads=charter.group.role_set.filter(name='ad').values_list('person__name',flat=True),
charter_text=charter_text,
milestones=charter.group.groupmilestone_set.filter(state="charter"),
),
cc=addrs.cc,
extra={'Reply-To':"iesg@ietf.org"},
)

View file

@ -718,8 +718,9 @@ class AddCommentTestCase(TestCase):
self.assertEqual("This is a test.", draft.latest_event().desc)
self.assertEqual("added_comment", draft.latest_event().type)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("updated" in outbox[-1]['Subject'])
self.assertTrue("Comment added" in outbox[-1]['Subject'])
self.assertTrue(draft.name in outbox[-1]['Subject'])
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
# Make sure we can also do it as IANA
self.client.login(username="iana", password="iana+password")
@ -788,14 +789,20 @@ expand-draft-ietf-ames-test.all@virtual.ietf.org ames-author@example.ames, ames
os.unlink(self.doc_alias_file.name)
def testAliases(self):
url = urlreverse('ietf.doc.views_doc.email_aliases', kwargs=dict(name="draft-ietf-mars-test"))
url = urlreverse('doc_specific_email_aliases', kwargs=dict(name="draft-ietf-mars-test"))
r = self.client.get(url)
self.assertTrue(all([x in r.content for x in ['mars-test@','mars-test.authors@','mars-test.chairs@']]))
self.assertFalse(any([x in r.content for x in ['ames-test@','ames-test.authors@','ames-test.chairs@']]))
self.assertEqual(r.status_code, 302)
url = urlreverse('ietf.doc.views_doc.email_aliases', kwargs=dict())
login_testing_unauthorized(self, "plain", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(all([x in r.content for x in ['mars-test@','mars-test.authors@','mars-test.chairs@']]))
self.assertTrue(all([x in r.content for x in ['ames-test@','ames-test.authors@','ames-test.chairs@']]))
def testExpansions(self):
url = urlreverse('ietf.doc.views_doc.document_email', kwargs=dict(name="draft-ietf-mars-test"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue('draft-ietf-mars-test.all@ietf.org' in r.content)
self.assertTrue('ballot_saved' in r.content)

View file

@ -12,7 +12,7 @@ from ietf.name.models import BallotPositionName
from ietf.iesg.models import TelechatDate
from ietf.person.models import Person
from ietf.utils.test_utils import TestCase
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
@ -147,12 +147,12 @@ class EditPositionTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form input[name="cc"]')) > 0)
self.assertTrue(len(q('form input[name="extra_cc"]')) > 0)
# send
mailbox_before = len(outbox)
r = self.client.post(url, dict(cc="test@example.com", cc_state_change="1",cc_group_list="1"))
r = self.client.post(url, dict(extra_cc="test298347@example.com", cc_choices=['doc_notify','doc_group_chairs']))
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox), mailbox_before + 1)
@ -162,16 +162,22 @@ class EditPositionTests(TestCase):
self.assertTrue(draft.name in m['Subject'])
self.assertTrue("clearer title" in str(m))
self.assertTrue("Test!" in str(m))
self.assertTrue("iesg@" in m['To'])
# cc_choice doc_group_chairs
self.assertTrue("mars-chairs@" in m['Cc'])
# cc_choice doc_notify
self.assertTrue("somebody@example.com" in m['Cc'])
self.assertTrue("test@example.com" in m['Cc'])
self.assertTrue(draft.group.list_email)
self.assertTrue(draft.group.list_email in m['Cc'])
# cc_choice doc_group_email_list was not selected
self.assertFalse(draft.group.list_email in m['Cc'])
# extra-cc
self.assertTrue("test298347@example.com" in m['Cc'])
r = self.client.post(url, dict(cc=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox), mailbox_before + 2)
m = outbox[-1]
self.assertEqual(m['Cc'],None)
self.assertTrue("iesg@" in m['To'])
self.assertFalse(m['Cc'] and draft.group.list_email in m['Cc'])
class BallotWriteupsTests(TestCase):
@ -232,9 +238,11 @@ class BallotWriteupsTests(TestCase):
send_last_call_request="1"))
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-iesg"), "lc-req")
self.assertEqual(len(outbox), mailbox_before + 3)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Last Call" in outbox[-1]['Subject'])
self.assertTrue(draft.name in outbox[-1]['Subject'])
self.assertTrue('iesg-secretary@' in outbox[-1]['To'])
self.assertTrue('aread@' in outbox[-1]['Cc'])
def test_edit_ballot_writeup(self):
draft = make_test_data()
@ -270,36 +278,8 @@ class BallotWriteupsTests(TestCase):
url = urlreverse('doc_ballot_writeupnotes', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "ad", url)
ballot = draft.latest_event(BallotDocEvent, type="created_ballot")
def create_pos(num, vote, comment="", discuss=""):
ad = Person.objects.get(name="Ad No%s" % num)
e = BallotPositionDocEvent()
e.doc = draft
e.ballot = ballot
e.by = ad
e.ad = ad
e.pos = BallotPositionName.objects.get(slug=vote)
e.type = "changed_ballot_position"
e.comment = comment
if e.comment:
e.comment_time = datetime.datetime.now()
e.discuss = discuss
if e.discuss:
e.discuss_time = datetime.datetime.now()
e.save()
# active
create_pos(1, "yes", discuss="discuss1 " * 20)
create_pos(2, "noobj", comment="comment2 " * 20)
create_pos(3, "discuss", discuss="discuss3 " * 20, comment="comment3 " * 20)
create_pos(4, "abstain")
create_pos(5, "recuse")
# inactive
create_pos(9, "yes")
mailbox_before = len(outbox)
empty_outbox()
r = self.client.post(url, dict(
ballot_writeup="This is a test.",
@ -308,15 +288,12 @@ class BallotWriteupsTests(TestCase):
draft = Document.objects.get(name=draft.name)
self.assertTrue(draft.latest_event(type="sent_ballot_announcement"))
self.assertEqual(len(outbox), mailbox_before + 2)
issue_email = outbox[-2]
self.assertTrue("Evaluation:" in issue_email['Subject'])
self.assertTrue("comment1" not in str(issue_email))
self.assertTrue("comment2" in str(issue_email))
self.assertTrue("comment3" in str(issue_email))
self.assertTrue("discuss3" in str(issue_email))
self.assertTrue("This is a test" in str(issue_email))
self.assertTrue("The IESG has approved" in str(issue_email))
self.assertEqual(len(outbox), 2)
self.assertTrue('Evaluation:' in outbox[-2]['Subject'])
self.assertTrue('iesg@' in outbox[-2]['To'])
self.assertTrue('Evaluation:' in outbox[-1]['Subject'])
self.assertTrue('drafts-eval@' in outbox[-1]['To'])
self.assertTrue('X-IETF-Draft-string' in outbox[-1])
def test_edit_approval_text(self):
draft = make_test_data()
@ -387,14 +364,20 @@ class ApproveBallotTests(TestCase):
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-iesg"), "ann")
self.assertEqual(len(outbox), mailbox_before + 4)
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("Protocol Action" in outbox[-2]['Subject'])
self.assertTrue("ietf-announce" in outbox[-2]['To'])
self.assertTrue("rfc-editor" in outbox[-2]['Cc'])
# the IANA copy
self.assertTrue("Protocol Action" in outbox[-1]['Subject'])
self.assertTrue(not outbox[-1]['CC'])
self.assertTrue('drafts-approval@icann.org' in outbox[-1]['To'])
self.assertTrue("Protocol Action" in draft.message_set.order_by("-time")[0].subject)
def test_disapprove_ballot(self):
# This tests a codepath that is not used in production
# and that has already had some drift from usefulness (it results in a
# older-style conflict review response).
draft = make_test_data()
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="nopubadw"))
@ -409,10 +392,9 @@ class ApproveBallotTests(TestCase):
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-iesg"), "dead")
self.assertEqual(len(outbox), mailbox_before + 3)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("NOT be published" in str(outbox[-1]))
class MakeLastCallTests(TestCase):
def test_make_last_call(self):
draft = make_test_data()
@ -441,11 +423,17 @@ class MakeLastCallTests(TestCase):
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-iesg"), "lc")
self.assertEqual(draft.latest_event(LastCallDocEvent, "sent_last_call").expires.strftime("%Y-%m-%d"), expire_date)
self.assertEqual(len(outbox), mailbox_before + 4)
self.assertTrue("Last Call" in outbox[-4]['Subject'])
# the IANA copy
self.assertTrue("Last Call" in outbox[-3]['Subject'])
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("Last Call" in outbox[-2]['Subject'])
self.assertTrue("ietf-announce@" in outbox[-2]['To'])
for prefix in ['draft-ietf-mars-test','mars-chairs','aread']:
self.assertTrue(prefix+"@" in outbox[-2]['Cc'])
self.assertTrue("Last Call" in outbox[-1]['Subject'])
self.assertTrue("drafts-lastcall@icann.org" in outbox[-1]['To'])
self.assertTrue("Last Call" in draft.message_set.order_by("-time")[0].subject)
class DeferUndeferTestCase(TestCase):
@ -491,11 +479,16 @@ class DeferUndeferTestCase(TestCase):
if doc.type_id in defer_states:
self.assertEqual(doc.get_state(defer_states[doc.type_id][0]).slug,defer_states[doc.type_id][1])
self.assertTrue(doc.active_defer_event())
self.assertEqual(len(outbox), mailbox_before + 3)
self.assertTrue("State Update" in outbox[-3]['Subject'])
self.assertTrue("Telechat update" in outbox[-2]['Subject'])
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue('Telechat update' in outbox[-2]['Subject'])
self.assertTrue('iesg-secretary@' in outbox[-2]['To'])
self.assertTrue('iesg@' in outbox[-2]['To'])
self.assertTrue("Deferred" in outbox[-1]['Subject'])
self.assertTrue(doc.file_tag() in outbox[-1]['Subject'])
self.assertTrue('iesg@' in outbox[-1]['To'])
# Ensure it's not possible to defer again
r = self.client.get(url)
@ -546,11 +539,13 @@ class DeferUndeferTestCase(TestCase):
if doc.type_id in undefer_states:
self.assertEqual(doc.get_state(undefer_states[doc.type_id][0]).slug,undefer_states[doc.type_id][1])
self.assertFalse(doc.active_defer_event())
self.assertEqual(len(outbox), mailbox_before + 3)
self.assertTrue("Telechat update" in outbox[-3]['Subject'])
self.assertTrue("State Update" in outbox[-2]['Subject'])
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("Telechat update" in outbox[-2]['Subject'])
self.assertTrue('iesg-secretary@' in outbox[-2]['To'])
self.assertTrue('iesg@' in outbox[-2]['To'])
self.assertTrue("Undeferred" in outbox[-1]['Subject'])
self.assertTrue(doc.file_tag() in outbox[-1]['Subject'])
self.assertTrue('iesg@' in outbox[-1]['To'])
# Ensure it's not possible to undefer again
r = self.client.get(url)

View file

@ -9,12 +9,12 @@ from django.core.urlresolvers import reverse as urlreverse
from ietf.doc.models import ( Document, State, BallotDocEvent, BallotType, NewRevisionDocEvent,
TelechatDocEvent, WriteupDocEvent )
from ietf.doc.utils_charter import next_revision, default_review_text, default_action_text
from ietf.doc.utils_charter import next_revision, default_review_text, default_action_text
from ietf.group.models import Group, GroupMilestone
from ietf.iesg.models import TelechatDate
from ietf.person.models import Person
from ietf.utils.test_utils import TestCase
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
@ -78,7 +78,8 @@ class EditCharterTests(TestCase):
for slug in ("intrev", "extrev", "iesgrev"):
s = State.objects.get(used=True, type="charter", slug=slug)
events_before = charter.docevent_set.count()
mailbox_before = len(outbox)
empty_outbox()
r = self.client.post(url, dict(charter_state=str(s.pk), message="test message"))
self.assertEqual(r.status_code, 302)
@ -96,8 +97,17 @@ class EditCharterTests(TestCase):
if slug in ("intrev", "iesgrev"):
self.assertTrue(find_event("created_ballot"))
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("state changed" in outbox[-1]['Subject'].lower())
self.assertEqual(len(outbox), 3 if slug=="intrev" else 2 )
if slug=="intrev":
self.assertTrue("Internal WG Review" in outbox[-3]['Subject'])
self.assertTrue(all([x in outbox[-3]['To'] for x in ['iab@','iesg@']]))
self.assertTrue("state changed" in outbox[-2]['Subject'].lower())
self.assertTrue("iesg-secretary@" in outbox[-2]['To'])
self.assertTrue("State Update Notice" in outbox[-1]['Subject'])
self.assertTrue("ames-chairs@" in outbox[-1]['To'])
def test_edit_telechat_date(self):
make_test_data()
@ -196,8 +206,7 @@ class EditCharterTests(TestCase):
self.assertEqual(charter.notify,newlist)
q = PyQuery(r.content)
formlist = q('form input[name=notify]')[0].value
self.assertTrue('marschairman@ietf.org' in formlist)
self.assertFalse('someone@example.com' in formlist)
self.assertEqual(formlist, None)
def test_edit_ad(self):
make_test_data()
@ -264,43 +273,101 @@ class EditCharterTests(TestCase):
self.assertEqual(f.read(),
"Windows line\nMac line\nUnix line\n" + utf_8_snippet)
def test_edit_announcement_text(self):
def test_edit_review_announcement_text(self):
draft = make_test_data()
charter = draft.group.charter
for ann in ("action", "review"):
url = urlreverse('ietf.doc.views_charter.announcement_text', kwargs=dict(name=charter.name, ann=ann))
self.client.logout()
login_testing_unauthorized(self, "secretary", url)
url = urlreverse('ietf.doc.views_charter.review_announcement_text', kwargs=dict(name=charter.name))
self.client.logout()
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=announcement_text]')), 1)
# as Secretariat, we can send
if ann == "review":
mailbox_before = len(outbox)
by = Person.objects.get(user__username="secretary")
r = self.client.post(url, dict(
announcement_text=default_review_text(draft.group, charter, by).text,
send_text="1"))
self.assertEqual(len(outbox), mailbox_before + 1)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=announcement_text]')), 1)
self.assertEqual(len(q('textarea[name=new_work_text]')), 1)
# save
r = self.client.post(url, dict(
announcement_text="This is a simple test.",
save_text="1"))
self.assertEqual(r.status_code, 302)
self.assertTrue("This is a simple test" in charter.latest_event(WriteupDocEvent, type="changed_%s_announcement" % ann).text)
by = Person.objects.get(user__username="secretary")
# test regenerate
r = self.client.post(url, dict(
announcement_text="This is a simple test.",
regenerate_text="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(draft.group.name in charter.latest_event(WriteupDocEvent, type="changed_%s_announcement" % ann).text)
(e1, e2) = default_review_text(draft.group, charter, by)
announcement_text = e1.text
new_work_text = e2.text
empty_outbox()
r = self.client.post(url, dict(
announcement_text=announcement_text,
new_work_text=new_work_text,
send_both="1"))
self.assertEqual(len(outbox), 2)
self.assertTrue(all(['WG Review' in m['Subject'] for m in outbox]))
self.assertTrue('ietf-announce@' in outbox[0]['To'])
self.assertTrue('mars-wg@' in outbox[0]['Cc'])
self.assertTrue('new-work@' in outbox[1]['To'])
empty_outbox()
r = self.client.post(url, dict(
announcement_text=announcement_text,
new_work_text=new_work_text,
send_annc_only="1"))
self.assertEqual(len(outbox), 1)
self.assertTrue('ietf-announce@' in outbox[0]['To'])
empty_outbox()
r = self.client.post(url, dict(
announcement_text=announcement_text,
new_work_text=new_work_text,
send_nw_only="1"))
self.assertEqual(len(outbox), 1)
self.assertTrue('new-work@' in outbox[0]['To'])
# save
r = self.client.post(url, dict(
announcement_text="This is a simple test.",
new_work_text="New work gets something different.",
save_text="1"))
self.assertEqual(r.status_code, 302)
self.assertTrue("This is a simple test" in charter.latest_event(WriteupDocEvent, type="changed_review_announcement").text)
self.assertTrue("New work gets something different." in charter.latest_event(WriteupDocEvent, type="changed_new_work_text").text)
# test regenerate
r = self.client.post(url, dict(
announcement_text="This is a simple test.",
new_work_text="Too simple perhaps?",
regenerate_text="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(draft.group.name in charter.latest_event(WriteupDocEvent, type="changed_review_announcement").text)
self.assertTrue(draft.group.name in charter.latest_event(WriteupDocEvent, type="changed_new_work_text").text)
def test_edit_action_announcement_text(self):
draft = make_test_data()
charter = draft.group.charter
url = urlreverse('ietf.doc.views_charter.action_announcement_text', kwargs=dict(name=charter.name))
self.client.logout()
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=announcement_text]')), 1)
# save
r = self.client.post(url, dict(
announcement_text="This is a simple test.",
save_text="1"))
self.assertEqual(r.status_code, 302)
self.assertTrue("This is a simple test" in charter.latest_event(WriteupDocEvent, type="changed_action_announcement").text)
# test regenerate
r = self.client.post(url, dict(
announcement_text="This is a simple test.",
regenerate_text="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(draft.group.name in charter.latest_event(WriteupDocEvent, type="changed_action_announcement").text)
def test_edit_ballot_writeupnotes(self):
draft = make_test_data()
@ -334,11 +401,12 @@ class EditCharterTests(TestCase):
self.assertTrue("This is a simple test" in charter.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text)
# send
mailbox_before = len(outbox)
empty_outbox()
r = self.client.post(url, dict(
ballot_writeup="This is a simple test.",
send_ballot="1"))
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertEqual(len(outbox), 1)
self.assertTrue('Evaluation' in outbox[0]['Subject'])
def test_approve(self):
make_test_data()
@ -394,7 +462,7 @@ class EditCharterTests(TestCase):
self.assertEqual(len(q('pre')), 1)
# approve
mailbox_before = len(outbox)
empty_outbox()
r = self.client.post(url, dict())
self.assertEqual(r.status_code, 302)
@ -406,9 +474,12 @@ class EditCharterTests(TestCase):
self.assertEqual(charter.rev, "01")
self.assertTrue(os.path.exists(os.path.join(self.charter_dir, "charter-ietf-%s-%s.txt" % (group.acronym, charter.rev))))
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("WG Action" in outbox[-1]['Subject'])
self.assertTrue("approved" in outbox[-2]['Subject'].lower())
self.assertEqual(len(outbox), 2)
self.assertTrue("approved" in outbox[0]['Subject'].lower())
self.assertTrue("iesg-secretary" in outbox[0]['To'])
self.assertTrue("WG Action" in outbox[1]['Subject'])
self.assertTrue("ietf-announce" in outbox[1]['To'])
self.assertTrue("ames-wg@ietf.org" in outbox[1]['Cc'])
self.assertEqual(group.groupmilestone_set.filter(state="charter").count(), 0)
self.assertEqual(group.groupmilestone_set.filter(state="active").count(), 2)

View file

@ -15,7 +15,7 @@ from ietf.group.models import Person
from ietf.iesg.models import TelechatDate
from ietf.name.models import StreamName
from ietf.utils.test_utils import TestCase
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
@ -70,7 +70,6 @@ class ConflictReviewTests(TestCase):
self.assertTrue(review_doc.latest_event(DocEvent,type="added_comment").desc.startswith("IETF conflict review requested"))
self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith("IETF conflict review initiated"))
self.assertTrue('Conflict Review requested' in outbox[-1]['Subject'])
self.assertTrue(settings.IANA_EVAL_EMAIL in outbox[-1]['To'])
# verify you can't start a review when a review is already in progress
r = self.client.post(url,dict(ad="Aread Irector",create_in_state="Needs Shepherd",notify='ipu@ietf.org'))
@ -116,10 +115,14 @@ class ConflictReviewTests(TestCase):
self.assertEquals(review_doc.notify,u'ipu@ietf.org')
doc = Document.objects.get(name='draft-imaginary-independent-submission')
self.assertTrue(doc in [x.target.document for x in review_doc.relateddocument_set.filter(relationship__slug='conflrev')])
self.assertEqual(len(outbox), messages_before + 2)
self.assertTrue('Conflict Review requested' in outbox[-1]['Subject'])
self.assertTrue(any('iesg-secretary@ietf.org' in x['To'] for x in outbox[-2:]))
self.assertTrue(any(settings.IANA_EVAL_EMAIL in x['To'] for x in outbox[-2:]))
self.assertTrue('drafts-eval@icann.org' in outbox[-1]['To'])
self.assertTrue('Conflict Review requested' in outbox[-2]['Subject'])
self.assertTrue('iesg-secretary@' in outbox[-2]['To'])
def test_change_state(self):
@ -189,9 +192,7 @@ class ConflictReviewTests(TestCase):
# Regenerate does not save!
self.assertEqual(doc.notify,newlist)
q = PyQuery(r.content)
self.assertTrue('draft-imaginary-irtf-submission@ietf.org' in q('form input[name=notify]')[0].value)
self.assertTrue('irtf-chair@ietf.org' in q('form input[name=notify]')[0].value)
self.assertTrue('foo@bar.baz.com' not in q('form input[name=notify]')[0].value)
self.assertEqual(None,q('form input[name=notify]')[0].value)
def test_edit_ad(self):
doc = Document.objects.get(name='conflict-review-imaginary-irtf-submission')
@ -281,7 +282,7 @@ class ConflictReviewTests(TestCase):
self.assertTrue( 'NOT be published' in ''.join(wrap(r.content,2**16)))
# submit
messages_before = len(outbox)
empty_outbox()
r = self.client.post(url,dict(announcement_text=default_approval_text(doc)))
self.assertEqual(r.status_code, 302)
@ -289,12 +290,15 @@ class ConflictReviewTests(TestCase):
self.assertEqual(doc.get_state_slug(),approve_type+'-sent')
self.assertFalse(doc.ballot_open("conflrev"))
self.assertEqual(len(outbox), messages_before + 1)
self.assertTrue('Results of IETF-conflict review' in outbox[-1]['Subject'])
self.assertEqual(len(outbox), 1)
self.assertTrue('Results of IETF-conflict review' in outbox[0]['Subject'])
self.assertTrue('irtf-chair' in outbox[0]['To'])
self.assertTrue('ietf-announce@' in outbox[0]['Cc'])
self.assertTrue('iana@' in outbox[0]['Cc'])
if approve_type == 'appr-noprob':
self.assertTrue( 'IESG has no problem' in ''.join(wrap(unicode(outbox[-1]),2**16)))
self.assertTrue( 'IESG has no problem' in ''.join(wrap(unicode(outbox[0]),2**16)))
else:
self.assertTrue( 'NOT be published' in ''.join(wrap(unicode(outbox[-1]),2**16)))
self.assertTrue( 'NOT be published' in ''.join(wrap(unicode(outbox[0]),2**16)))
def test_approve_reqnopub(self):

View file

@ -20,7 +20,7 @@ from ietf.meeting.models import Meeting, MeetingTypeName
from ietf.iesg.models import TelechatDate
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.utils.test_data import make_test_data
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_utils import TestCase
@ -72,10 +72,11 @@ class ChangeStateTests(TestCase):
self.assertEqual(draft.docevent_set.count(), events_before + 2)
self.assertTrue("Test comment" in draft.docevent_set.all()[0].desc)
self.assertTrue("IESG state changed" in draft.docevent_set.all()[1].desc)
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("State Update Notice" in outbox[-2]['Subject'])
self.assertTrue(draft.name in outbox[-1]['Subject'])
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("State Update Notice" in outbox[-1]['Subject'])
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
self.assertTrue('mars-chairs@' in outbox[-1]['To'])
self.assertTrue('aread@' in outbox[-1]['To'])
# check that we got a previous state now
r = self.client.get(url)
@ -101,11 +102,19 @@ class ChangeStateTests(TestCase):
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-iesg"), "review-e")
self.assertEqual(len(outbox), mailbox_before + 2 + 1)
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue(draft.name in outbox[-1]['Subject'])
self.assertTrue("changed state" in outbox[-1]['Subject'])
self.assertTrue("is no longer" in str(outbox[-1]))
self.assertTrue("Test comment" in str(outbox[-1]))
self.assertTrue("rfc-editor@" in outbox[-1]['To'])
self.assertTrue("iana@" in outbox[-1]['To'])
self.assertTrue("ID Tracker State Update Notice:" in outbox[-2]['Subject'])
self.assertTrue("aread@" in outbox[-2]['To'])
def test_change_iana_state(self):
draft = make_test_data()
@ -145,8 +154,8 @@ class ChangeStateTests(TestCase):
self.client.login(username="secretary", password="secretary+password")
url = urlreverse('doc_change_state', kwargs=dict(name=draft.name))
mailbox_before = len(outbox)
empty_outbox()
self.assertTrue(not draft.latest_event(type="changed_ballot_writeup_text"))
r = self.client.post(url, dict(state=State.objects.get(used=True, type="draft-iesg", slug="lc-req").pk))
self.assertTrue("Your request to issue" in r.content)
@ -171,8 +180,14 @@ class ChangeStateTests(TestCase):
self.assertTrue("Technical Summary" in e.text)
# mail notice
self.assertTrue(len(outbox) > mailbox_before)
self.assertTrue("Last Call:" in outbox[-1]['Subject'])
self.assertEqual(len(outbox), 2)
self.assertTrue("ID Tracker State Update" in outbox[0]['Subject'])
self.assertTrue("aread@" in outbox[0]['To'])
self.assertTrue("Last Call:" in outbox[1]['Subject'])
self.assertTrue('iesg-secretary@' in outbox[1]['To'])
self.assertTrue('aread@' in outbox[1]['Cc'])
# comment
self.assertTrue("Last call was requested" in draft.latest_event().desc)
@ -252,6 +267,8 @@ class EditInfoTests(TestCase):
self.assertEqual(draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date, TelechatDate.objects.active()[0].date)
self.assertEqual(len(outbox),mailbox_before+1)
self.assertTrue("Telechat update" in outbox[-1]['Subject'])
self.assertTrue('iesg@' in outbox[-1]['To'])
self.assertTrue('iesg-secretary@' in outbox[-1]['To'])
# change telechat
mailbox_before=len(outbox)
@ -330,7 +347,7 @@ class EditInfoTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=intended_std_level]')), 1)
self.assertTrue('@' in q('form input[name=notify]')[0].get('value'))
self.assertEqual(None,q('form input[name=notify]')[0].value)
# add
events_before = draft.docevent_set.count()
@ -356,7 +373,9 @@ class EditInfoTests(TestCase):
self.assertEqual(draft.docevent_set.count(), events_before + 3)
events = list(draft.docevent_set.order_by('time', 'id'))
self.assertEqual(events[-3].type, "started_iesg_process")
self.assertEqual(len(outbox), mailbox_before)
self.assertEqual(len(outbox), mailbox_before+1)
self.assertTrue('IESG processing' in outbox[-1]['Subject'])
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
# Redo, starting in publication requested to make sure WG state is also set
draft.unset_state('draft-iesg')
@ -431,6 +450,7 @@ class ResurrectTests(TestCase):
self.assertTrue("Resurrection" in e.desc)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Resurrection" in outbox[-1]['Subject'])
self.assertTrue('internet-drafts@' in outbox[-1]['To'])
def test_resurrect(self):
draft = make_test_data()
@ -463,6 +483,9 @@ class ResurrectTests(TestCase):
self.assertEqual(draft.get_state_slug(), "active")
self.assertTrue(draft.expires >= datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1))
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue('Resurrection Completed' in outbox[-1]['Subject'])
self.assertTrue('iesg-secretary' in outbox[-1]['To'])
self.assertTrue('aread' in outbox[-1]['To'])
class ExpireIDsTests(TestCase):
@ -524,8 +547,9 @@ class ExpireIDsTests(TestCase):
send_expire_warning_for_draft(draft)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("aread@ietf.org" in str(outbox[-1])) # author
self.assertTrue("marschairman@ietf.org" in str(outbox[-1]))
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To']) # Gets the authors
self.assertTrue('mars-chairs@ietf.org' in outbox[-1]['Cc'])
self.assertTrue('aread@' in outbox[-1]['Cc'])
def test_expire_drafts(self):
from ietf.doc.expire import get_expired_drafts, send_expire_notice_for_draft, expire_draft
@ -556,6 +580,9 @@ class ExpireIDsTests(TestCase):
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("expired" in outbox[-1]["Subject"])
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To']) # gets authors
self.assertTrue('mars-chairs@ietf.org' in outbox[-1]['Cc'])
self.assertTrue('aread@' in outbox[-1]['Cc'])
# test expiry
txt = "%s-%s.txt" % (draft.name, draft.rev)
@ -680,7 +707,9 @@ class ExpireLastCallTests(TestCase):
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Last Call Expired" in outbox[-1]["Subject"])
self.assertTrue('iesg-secretary@' in outbox[-1]['Cc'])
self.assertTrue('aread@' in outbox[-1]['To'])
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
class IndividualInfoFormsTests(TestCase):
def test_doc_change_stream(self):
@ -694,22 +723,25 @@ class IndividualInfoFormsTests(TestCase):
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# shift to ISE stream
messages_before = len(outbox)
empty_outbox()
r = self.client.post(url,dict(stream="ise",comment="7gRMTjBM"))
self.assertEqual(r.status_code,302)
self.doc = Document.objects.get(name=self.docname)
self.assertEqual(self.doc.stream_id,'ise')
self.assertEqual(len(outbox),messages_before+1)
self.assertTrue('Stream Change Notice' in outbox[-1]['Subject'])
self.assertTrue('7gRMTjBM' in str(outbox[-1]))
self.assertEqual(len(outbox), 1)
self.assertTrue('Stream Change Notice' in outbox[0]['Subject'])
self.assertTrue('rfc-ise@' in outbox[0]['To'])
self.assertTrue('iesg@' in outbox[0]['To'])
self.assertTrue('7gRMTjBM' in str(outbox[0]))
self.assertTrue('7gRMTjBM' in self.doc.latest_event(DocEvent,type='added_comment').desc)
# Would be nice to test that the stream managers were in the To header...
# shift to an unknown stream (it must be possible to throw a document out of any stream)
empty_outbox()
r = self.client.post(url,dict(stream=""))
self.assertEqual(r.status_code,302)
self.doc = Document.objects.get(name=self.docname)
self.assertEqual(self.doc.stream,None)
self.assertTrue('rfc-ise@' in outbox[0]['To'])
def test_doc_change_notify(self):
url = urlreverse('doc_change_notify', kwargs=dict(name=self.docname))
@ -734,7 +766,7 @@ class IndividualInfoFormsTests(TestCase):
# Regenerate does not save!
self.assertEqual(self.doc.notify,'TJ2APh2P@ietf.org')
q = PyQuery(r.content)
self.assertTrue('TJ2Aph2P' not in q('form input[name=notify]')[0].value)
self.assertEqual(None,q('form input[name=notify]')[0].value)
def test_doc_change_intended_status(self):
url = urlreverse('doc_change_intended_status', kwargs=dict(name=self.docname))
@ -759,7 +791,10 @@ class IndividualInfoFormsTests(TestCase):
self.doc = Document.objects.get(name=self.docname)
self.assertEqual(self.doc.intended_std_level_id,'bcp')
self.assertEqual(len(outbox),messages_before+1)
self.assertTrue('Intended Status ' in outbox[-1]['Subject'])
self.assertTrue('mars-chairs@' in outbox[-1]['To'])
self.assertTrue('ZpyQFGmA' in str(outbox[-1]))
self.assertTrue('ZpyQFGmA' in self.doc.latest_event(DocEvent,type='added_comment').desc)
def test_doc_change_telechat_date(self):
@ -773,12 +808,17 @@ class IndividualInfoFormsTests(TestCase):
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# set a date
empty_outbox()
self.assertFalse(self.doc.latest_event(TelechatDocEvent, "scheduled_for_telechat"))
telechat_date = TelechatDate.objects.active().order_by('date')[0].date
r = self.client.post(url,dict(telechat_date=telechat_date.isoformat()))
self.assertEqual(r.status_code,302)
self.doc = Document.objects.get(name=self.docname)
self.assertEqual(self.doc.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date,telechat_date)
self.assertEqual(len(outbox), 1)
self.assertTrue('Telechat update notice' in outbox[0]['Subject'])
self.assertTrue('iesg@' in outbox[0]['To'])
self.assertTrue('iesg-secretary@' in outbox[0]['To'])
# Take the doc back off any telechat
r = self.client.post(url,dict(telechat_date=""))
@ -964,7 +1004,7 @@ class IndividualInfoFormsTests(TestCase):
class SubmitToIesgTests(TestCase):
def verify_permissions(self):
def test_verify_permissions(self):
def verify_fail(username):
if username:
@ -987,7 +1027,7 @@ class SubmitToIesgTests(TestCase):
for username in ['marschairman','secretary','ad']:
verify_can_see(username)
def cancel_submission(self):
def test_cancel_submission(self):
url = urlreverse('doc_to_iesg', kwargs=dict(name=self.docname))
self.client.login(username="marschairman", password="marschairman+password")
@ -997,7 +1037,7 @@ class SubmitToIesgTests(TestCase):
doc = Document.objects.get(pk=self.doc.pk)
self.assertTrue(doc.get_state('draft-iesg')==None)
def confirm_submission(self):
def test_confirm_submission(self):
url = urlreverse('doc_to_iesg', kwargs=dict(name=self.docname))
self.client.login(username="marschairman", password="marschairman+password")
@ -1014,6 +1054,8 @@ class SubmitToIesgTests(TestCase):
self.assertTrue(doc.docevent_set.count() != docevent_count_pre)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Publication has been requested" in outbox[-1]['Subject'])
self.assertTrue("aread@" in outbox[-1]['To'])
self.assertTrue("iesg-secretary@" in outbox[-1]['Cc'])
def setUp(self):
make_test_data()
@ -1052,12 +1094,16 @@ class RequestPublicationTests(TestCase):
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-stream-iab"), "rfc-edit")
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue("Document Action" in outbox[-2]['Subject'])
self.assertTrue("Document Action" in draft.message_set.order_by("-time")[0].subject)
# the IANA copy
self.assertTrue("rfc-editor@" in outbox[-2]['To'])
self.assertTrue("Document Action" in outbox[-1]['Subject'])
self.assertTrue(not outbox[-1]['CC'])
self.assertTrue("drafts-approval@icann.org" in outbox[-1]['To'])
self.assertTrue("Document Action" in draft.message_set.order_by("-time")[0].subject)
class AdoptDraftTests(TestCase):
def test_adopt_document(self):
@ -1092,13 +1138,12 @@ class AdoptDraftTests(TestCase):
self.assertEqual(draft.group.acronym, "mars")
self.assertEqual(draft.stream_id, "ietf")
self.assertEqual(draft.docevent_set.count() - events_before, 5)
self.assertTrue('draft-ietf-mars-test@ietf.org' in draft.notify)
self.assertTrue('draft-ietf-mars-test.ad@ietf.org' in draft.notify)
self.assertTrue('draft-ietf-mars-test.shepherd@ietf.org' in draft.notify)
self.assertEqual(draft.notify,"aliens@example.mars")
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("state changed" in outbox[-1]["Subject"].lower())
self.assertTrue("marschairman@ietf.org" in unicode(outbox[-1]))
self.assertTrue("marsdelegate@ietf.org" in unicode(outbox[-1]))
self.assertTrue("has adopted" in outbox[-1]["Subject"].lower())
self.assertTrue("mars-chairs@ietf.org" in outbox[-1]['To'])
self.assertTrue("draft-ietf-mars-test@" in outbox[-1]['To'])
self.assertTrue("mars-wg@" in outbox[-1]['To'])
self.assertFalse(mars.list_email in draft.notify)
@ -1139,7 +1184,7 @@ class ChangeStreamStateTests(TestCase):
self.assertEqual(draft.docevent_set.count() - events_before, 2)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("tags changed" in outbox[-1]["Subject"].lower())
self.assertTrue("marschairman@ietf.org" in unicode(outbox[-1]))
self.assertTrue("mars-chairs@ietf.org" in unicode(outbox[-1]))
self.assertTrue("marsdelegate@ietf.org" in unicode(outbox[-1]))
self.assertTrue("plain@example.com" in unicode(outbox[-1]))
@ -1163,7 +1208,7 @@ class ChangeStreamStateTests(TestCase):
old_state = draft.get_state("draft-stream-%s" % draft.stream_id )
new_state = State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="parked")
self.assertNotEqual(old_state, new_state)
mailbox_before = len(outbox)
empty_outbox()
events_before = draft.docevent_set.count()
r = self.client.post(url,
@ -1181,10 +1226,10 @@ class ChangeStreamStateTests(TestCase):
self.assertEqual(len(reminder), 1)
due = datetime.datetime.now() + datetime.timedelta(weeks=10)
self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1))
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("state changed" in outbox[-1]["Subject"].lower())
self.assertTrue("marschairman@ietf.org" in unicode(outbox[-1]))
self.assertTrue("marsdelegate@ietf.org" in unicode(outbox[-1]))
self.assertEqual(len(outbox), 1)
self.assertTrue("state changed" in outbox[0]["Subject"].lower())
self.assertTrue("mars-chairs@ietf.org" in unicode(outbox[0]))
self.assertTrue("marsdelegate@ietf.org" in unicode(outbox[0]))
class ChangeReplacesTests(TestCase):
def setUp(self):
@ -1255,6 +1300,7 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# Post that says replacea replaces base a
empty_outbox()
RelatedDocument.objects.create(source=self.replacea, target=self.basea.docalias_set.first(),
relationship=DocRelationshipName.objects.get(slug="possibly-replaces"))
self.assertEqual(self.basea.get_state().slug,'active')
@ -1263,7 +1309,12 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(RelatedDocument.objects.filter(relationship__slug='replaces',source=self.replacea).count(),1)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertTrue(not RelatedDocument.objects.filter(relationship='possibly-replaces', source=self.replacea))
self.assertEqual(len(outbox), 1)
self.assertTrue('replacement status updated' in outbox[-1]['Subject'])
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('replace-a@' in outbox[-1]['To'])
empty_outbox()
# Post that says replaceboth replaces both base a and base b
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replaceboth.name))
self.assertEqual(self.baseb.get_state().slug,'expired')
@ -1271,18 +1322,31 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
self.assertEqual(len(outbox), 1)
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('base-b@' in outbox[-1]['To'])
self.assertTrue('replace-both@' in outbox[-1]['To'])
# Post that undoes replaceboth
empty_outbox()
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
self.assertEqual(len(outbox), 1)
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('base-b@' in outbox[-1]['To'])
self.assertTrue('replace-both@' in outbox[-1]['To'])
# Post that undoes replacea
empty_outbox()
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replacea.name))
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('replace-a@' in outbox[-1]['To'])
def test_review_possibly_replaces(self):
replaced = self.basea.docalias_set.first()

View file

@ -111,7 +111,6 @@ class StatusChangeTests(TestCase):
doc = Document.objects.get(name='status-change-imaginary-mid-review')
self.assertEquals(doc.get_state('statchg').slug,'lc-req')
self.assertEquals(len(outbox), messages_before + 1)
self.assertTrue('iesg-secretary' in outbox[-1]['To'])
self.assertTrue('Last Call:' in outbox[-1]['Subject'])
# successful change to IESG Evaluation
@ -157,9 +156,7 @@ class StatusChangeTests(TestCase):
self.assertEqual(doc.notify,newlist)
q = PyQuery(r.content)
formlist = q('form input[name=notify]')[0].value
self.assertTrue('draft-ietf-random-thing@ietf.org' in formlist)
self.assertTrue('draft-ietf-random-otherthing@ietf.org' in formlist)
self.assertFalse('foo@bar.baz.com' in formlist)
self.assertEqual(None,formlist)
def test_edit_title(self):
doc = Document.objects.get(name='status-change-imaginary-mid-review')
@ -285,7 +282,6 @@ class StatusChangeTests(TestCase):
self.assertEqual(r.status_code,200)
self.assertTrue( 'Last call requested' in ''.join(wrap(r.content,2**16)))
self.assertEqual(len(outbox), messages_before + 1)
self.assertTrue('iesg-secretary' in outbox[-1]['To'])
self.assertTrue('Last Call:' in outbox[-1]['Subject'])
self.assertTrue('Last Call Request has been submitted' in ''.join(wrap(unicode(outbox[-1]),2**16)))
@ -326,8 +322,10 @@ class StatusChangeTests(TestCase):
self.assertEqual(len(outbox), messages_before + 2)
self.assertTrue('Action:' in outbox[-1]['Subject'])
self.assertTrue('(rfc9999) to Internet Standard' in ''.join(wrap(unicode(outbox[-1])+unicode(outbox[-2]),2**16)))
self.assertTrue('ietf-announce' in outbox[-1]['To'])
self.assertTrue('rfc-editor' in outbox[-1]['Cc'])
self.assertTrue('(rfc9998) to Historic' in ''.join(wrap(unicode(outbox[-1])+unicode(outbox[-2]),2**16)))
self.assertTrue('(rfc9999) to Internet Standard' in ''.join(wrap(unicode(outbox[-1])+unicode(outbox[-2]),2**16)))
self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('The following approval message was sent'))

View file

@ -55,6 +55,7 @@ urlpatterns = patterns('',
url(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?$', views_doc.document_main, name="doc_view"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/history/$', views_doc.document_history, name="doc_history"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/writeup/$', views_doc.document_writeup, name="doc_writeup"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email/$', views_doc.document_email, name="doc_email"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/shepherdwriteup/$', views_doc.document_shepherd_writeup, name="doc_shepherd_writeup"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/references/$', views_doc.document_references, name="doc_references"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/referencedby/$', views_doc.document_referenced_by, name="doc_referenced_by"),
@ -65,7 +66,7 @@ urlpatterns = patterns('',
(r'^(?P<name>[A-Za-z0-9._+-]+)/doc.json$', views_doc.document_json),
(r'^(?P<name>[A-Za-z0-9._+-]+)/ballotpopup/(?P<ballot_id>[0-9]+)/$', views_doc.ballot_popup),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', views_doc.email_aliases),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/state/$', views_draft.change_state, name='doc_change_state'), # IESG state
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/state/(?P<state_type>iana-action|iana-review)/$', views_draft.change_iana_state, name='doc_change_iana_state'),

View file

@ -9,7 +9,8 @@ urlpatterns = patterns('',
url(r'^telechat/$', "ietf.doc.views_doc.telechat_date", name='charter_telechat_date'),
url(r'^notify/$', "ietf.doc.views_doc.edit_notify", name='charter_edit_notify'),
url(r'^ad/$', "ietf.doc.views_charter.edit_ad", name='charter_edit_ad'),
url(r'^(?P<ann>action|review)/$', "ietf.doc.views_charter.announcement_text", name="charter_edit_announcement"),
url(r'^action/$', "ietf.doc.views_charter.action_announcement_text"),
url(r'^review/$', "ietf.doc.views_charter.review_announcement_text"),
url(r'^ballotwriteupnotes/$', "ietf.doc.views_charter.ballot_writeupnotes"),
url(r'^approve/$', "ietf.doc.views_charter.approve", name='charter_approve'),
url(r'^submit/(?:(?P<option>initcharter|recharter)/)?$', "ietf.doc.views_charter.submit", name='charter_submit'),

View file

@ -5,7 +5,6 @@ import math
import datetime
from django.conf import settings
from django.db.models import Q
from django.db.models.query import EmptyQuerySet
from django.forms import ValidationError
from django.utils.html import strip_tags, escape
@ -13,29 +12,29 @@ from django.utils.html import strip_tags, escape
from ietf.doc.models import Document, DocHistory, State
from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder
from ietf.doc.models import DocEvent, BallotDocEvent, NewRevisionDocEvent, StateDocEvent
from ietf.doc.models import save_document_in_history, STATUSCHANGE_RELATIONS
from ietf.doc.models import save_document_in_history
from ietf.name.models import DocReminderTypeName, DocRelationshipName
from ietf.group.models import Role
from ietf.person.models import Email
from ietf.ietfauth.utils import has_role
from ietf.utils import draft, markup_txt
from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists
#FIXME - it would be better if this lived in ietf/doc/mails.py, but there's
#TODO FIXME - it would be better if this lived in ietf/doc/mails.py, but there's
# an import order issue to work out.
def email_update_telechat(request, doc, text):
to = set(['iesg@ietf.org','iesg-secretary@ietf.org'])
to.update(set([x.strip() for x in doc.notify.replace(';', ',').split(',')]))
(to, cc) = gather_address_lists('doc_telechat_details_changed',doc=doc)
if not to:
return
text = strip_tags(text)
send_mail(request, list(to), None,
send_mail(request, to, None,
"Telechat update notice: %s" % doc.file_tag(),
"doc/mail/update_telechat.txt",
dict(text=text,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=cc)
def get_state_types(doc):
res = []
@ -458,35 +457,19 @@ def rebuild_reference_relations(doc,filename=None):
return ret
def collect_email_addresses(emails, doc):
for author in doc.authors.all():
if author.address not in emails:
emails[author.address] = '"%s"' % (author.person.name)
if doc.group and doc.group.acronym != 'none':
for role in doc.group.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
if doc.group.type.slug == 'wg':
address = '%s-ads@ietf.org' % doc.group.acronym
if address not in emails:
emails[address] = '"%s-ads"' % (doc.group.acronym)
elif doc.group.type.slug == 'rg':
for role in doc.group.parent.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
if doc.shepherd and doc.shepherd.address not in emails:
emails[doc.shepherd.address] = u'"%s"' % (doc.shepherd.person.name or "")
def set_replaces_for_document(request, doc, new_replaces, by, email_subject, email_comment=""):
emails = {}
collect_email_addresses(emails, doc)
addrs = gather_address_lists('doc_replacement_changed',doc=doc)
to = set(addrs.to)
cc = set(addrs.cc)
relationship = DocRelationshipName.objects.get(slug='replaces')
old_replaces = doc.related_that_doc("replaces")
for d in old_replaces:
if d not in new_replaces:
collect_email_addresses(emails, d.document)
other_addrs = gather_address_lists('doc_replacement_changed',doc=d.document)
to.update(other_addrs.to)
cc.update(other_addrs.cc)
RelatedDocument.objects.filter(source=doc, target=d, relationship=relationship).delete()
if not RelatedDocument.objects.filter(target=d, relationship=relationship):
s = 'active' if d.document.expires > datetime.datetime.now() else 'expired'
@ -494,7 +477,9 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, ema
for d in new_replaces:
if d not in old_replaces:
collect_email_addresses(emails, d.document)
other_addrs = gather_address_lists('doc_replacement_changed',doc=d.document)
to.update(other_addrs.to)
cc.update(other_addrs.cc)
RelatedDocument.objects.create(source=doc, target=d, relationship=relationship)
d.document.set_state(State.objects.get(type='draft', slug='repl'))
@ -512,20 +497,16 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, ema
if email_comment:
email_desc += "\n" + email_comment
to = [
u'%s <%s>' % (emails[email], email) if emails[email] else u'<%s>' % email
for email in sorted(emails)
]
from ietf.doc.mails import html_to_text
send_mail(request, to,
send_mail(request, list(to),
"DraftTracker Mail System <iesg-secretary@ietf.org>",
email_subject,
"doc/mail/change_notice.txt",
dict(text=html_to_text(email_desc),
doc=doc,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc=list(cc))
def check_common_doc_name_rules(name):
"""Check common rules for document names for use in forms, throws
@ -543,41 +524,9 @@ def check_common_doc_name_rules(name):
raise ValidationError(errors)
def get_initial_notify(doc,extra=None):
# set change state notice to something sensible
# With the mailtrigger based changes, a document's notify should start empty
receivers = []
if doc.type.slug=='draft':
if doc.group.type_id in ("individ", "area"):
for a in doc.authors.all():
receivers.append(a.address)
else:
receivers.append("%s-chairs@%s" % (doc.group.acronym, settings.DRAFT_ALIAS_DOMAIN))
for editor in Email.objects.filter(role__name="editor", role__group=doc.group):
receivers.append(editor.address)
receivers.append("%s@%s" % (doc.name, settings.DRAFT_ALIAS_DOMAIN))
receivers.append("%s.ad@%s" % (doc.name, settings.DRAFT_ALIAS_DOMAIN))
receivers.append("%s.shepherd@%s" % (doc.name, settings.DRAFT_ALIAS_DOMAIN))
elif doc.type.slug=='charter':
receivers.extend([role.person.formatted_email() for role in doc.group.role_set.filter(name__slug__in=['ad','chair','secr','techadv'])])
else:
pass
for relation in doc.relateddocument_set.filter(Q(relationship='conflrev')|Q(relationship__in=STATUSCHANGE_RELATIONS)):
if relation.relationship.slug=='conflrev':
doc_to_review = relation.target.document
receivers.extend([x.person.formatted_email() for x in Role.objects.filter(group__acronym=doc_to_review.stream.slug,name='chair')])
receivers.append("%s@%s" % (doc_to_review.name, settings.DRAFT_ALIAS_DOMAIN))
elif relation.relationship.slug in STATUSCHANGE_RELATIONS:
affected_doc = relation.target.document
if affected_doc.notify:
receivers.extend(affected_doc.notify.split(','))
if doc.shepherd:
receivers.append(doc.shepherd.email_address())
if extra:
if isinstance(extra,basestring):
extra = extra.split(', ')

View file

@ -1,13 +1,12 @@
import re, datetime, os
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent, BallotPositionDocEvent
from ietf.person.models import Person
from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent
from ietf.utils.history import find_history_active_at
from ietf.utils.mail import send_mail_text
from ietf.utils.mail import parse_preformatted
from ietf.mailtrigger.utils import gather_address_lists
def charter_name_for_group(group):
if group.type_id == "rg":
@ -83,19 +82,6 @@ def historic_milestones_for_charter(charter, rev):
return res
def email_state_changed(request, doc, text):
to = [e.strip() for e in doc.notify.replace(';', ',').split(',')]
if not to:
return
text = strip_tags(text)
text += "\n\n"
text += "URL: %s" % (settings.IDTRACKER_BASE_URL + doc.get_absolute_url())
send_mail_text(request, to, None,
"State changed: %s-%s" % (doc.canonical_name(), doc.rev),
text)
def generate_ballot_writeup(request, doc):
e = WriteupDocEvent()
e.type = "changed_ballot_writeup_text"
@ -113,6 +99,7 @@ def default_action_text(group, charter, by):
else:
action = "Rechartered"
addrs = gather_address_lists('ballot_approved_charter',doc=charter,group=group).as_strings(compact=False)
e = WriteupDocEvent(doc=charter, by=by)
e.by = by
e.type = "changed_action_announcement"
@ -126,93 +113,68 @@ def default_action_text(group, charter, by):
techadv=group.role_set.filter(name="techadv"),
milestones=group.groupmilestone_set.filter(state="charter"),
action_type=action,
to=addrs.to,
cc=addrs.cc,
))
e.save()
return e
def derive_new_work_text(review_text,group):
addrs= gather_address_lists('charter_external_review_new_work',group=group).as_strings()
(m,_,_) = parse_preformatted(review_text,
override={'To':addrs.to,
'Cc':addrs.cc,
'From':'The IESG <iesg@ietf.org>',
'Reply_to':'<iesg@ietf.org>'})
if not addrs.cc:
del m['Cc']
return m.as_string()
def default_review_text(group, charter, by):
e = WriteupDocEvent(doc=charter, by=by)
e.by = by
e.type = "changed_review_announcement"
e.desc = "%s review text was changed" % group.type.name
e.text = render_to_string("doc/charter/review_text.txt",
now = datetime.datetime.now()
addrs=gather_address_lists('charter_external_review',group=group).as_strings(compact=False)
e1 = WriteupDocEvent(doc=charter, by=by)
e1.by = by
e1.type = "changed_review_announcement"
e1.desc = "%s review text was changed" % group.type.name
e1.text = render_to_string("doc/charter/review_text.txt",
dict(group=group,
charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(),
charter_text=read_charter_text(charter),
chairs=group.role_set.filter(name="chair"),
secr=group.role_set.filter(name="secr"),
techadv=group.role_set.filter(name="techadv"),
milestones=group.groupmilestone_set.filter(state="charter"),
review_date=(datetime.date.today() + datetime.timedelta(weeks=1)).isoformat(),
review_type="new" if group.state_id == "proposed" else "recharter",
charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(),
charter_text=read_charter_text(charter),
chairs=group.role_set.filter(name="chair"),
secr=group.role_set.filter(name="secr"),
techadv=group.role_set.filter(name="techadv"),
milestones=group.groupmilestone_set.filter(state="charter"),
review_date=(datetime.date.today() + datetime.timedelta(weeks=1)).isoformat(),
review_type="new" if group.state_id == "proposed" else "recharter",
to=addrs.to,
cc=addrs.cc,
)
)
e.save()
return e
e1.time = now
e1.save()
e2 = WriteupDocEvent(doc=charter, by=by)
e2.by = by
e2.type = "changed_new_work_text"
e2.desc = "%s review text was changed" % group.type.name
e2.text = derive_new_work_text(e1.text,group)
e2.time = now
e2.save()
return (e1,e2)
def generate_issue_ballot_mail(request, doc, ballot):
active_ads = Person.objects.filter(email__role__name="ad", email__role__group__state="active", email__role__group__type="area").distinct()
seen = []
positions = []
for p in BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position", ballot=ballot).order_by("-time", '-id').select_related('ad'):
if p.ad not in seen:
positions.append(p)
seen.append(p.ad)
# format positions and setup blocking and non-blocking 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%-6s%-6s%-8s%-7s" % (
p.ad.plain_name(),
formatted(p.pos_id == "yes"),
formatted(p.pos_id == "no"),
formatted(p.pos_id == "block"),
formatted(p.pos_id == "abstain"),
)
if p.ad in active_ads:
active_ad_positions.append(fmt)
if not p.pos or not p.pos.blocking:
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(WriteupDocEvent, type="changed_action_announcement")
approval_text = e.text if e else ""
e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
ballot_writeup = e.text if e else ""
addrs=gather_address_lists('ballot_issued',doc=doc).as_strings()
return render_to_string("doc/charter/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,
approval_text=approval_text,
ballot_writeup=ballot_writeup,
to = addrs.to,
cc = addrs.cc,
)
)

View file

@ -17,8 +17,8 @@ from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent, BallotP
BallotType, LastCallDocEvent, WriteupDocEvent, save_document_in_history, 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_ad, email_ballot_deferred, email_ballot_undeferred,
email_state_changed, extra_automation_headers, generate_last_call_announcement,
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_approval_mail )
from ietf.doc.lastcall import request_last_call
from ietf.iesg.models import TelechatDate
@ -27,6 +27,8 @@ 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"),
@ -68,8 +70,6 @@ def do_undefer_ballot(request, doc):
doc.save()
update_telechat(request, doc, login, telechat_date)
if e:
email_state_changed(request, doc, e.desc)
email_ballot_undeferred(request, doc, login.plain_name(), telechat_date)
def position_to_ballot_choice(position):
@ -284,28 +284,36 @@ def send_ballot_comment(request, name, ballot_id):
blocking_name=blocking_name,
settings=settings))
frm = ad.role_email("ad").formatted_email()
to = "The IESG <iesg@ietf.org>"
addrs = gather_address_lists('ballot_saved',doc=doc)
if request.method == 'POST':
cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()]
if request.POST.get("cc_state_change") and doc.notify:
cc.extend(doc.notify.split(','))
if request.POST.get("cc_group_list") and doc.group.list_email:
cc.append(doc.group.list_email)
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, to, frm, subject, body, cc=u", ".join(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_to_response('doc/ballot/send_ballot_comment.html',
dict(doc=doc,
subject=subject,
body=body,
frm=frm,
to=to,
to=addrs.as_strings().to,
ad=ad,
can_send=d or c,
back_url=back_url,
cc_select_form = cc_select_form,
),
context_instance=RequestContext(request))
@ -363,9 +371,6 @@ def defer_ballot(request, name):
doc.time = (e and e.time) or datetime.datetime.now()
doc.save()
if e:
email_state_changed(request, doc, e.desc)
update_telechat(request, doc, login, telechat_date)
email_ballot_deferred(request, doc, login.plain_name(), telechat_date)
@ -458,10 +463,6 @@ def lastcalltext(request, name):
doc.time = (e and e.time) or datetime.datetime.now()
doc.save()
if e:
email_state_changed(request, doc, e.desc)
email_ad(request, doc, doc.ad, login, e.desc)
request_last_call(request, doc)
return render_to_response('doc/draft/last_call_requested.html',
@ -538,12 +539,24 @@ def ballot_writeupnotes(request, name):
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)
msg = generate_issue_ballot_mail(request, doc, ballot)
send_mail_preformatted(request, msg)
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})
@ -696,23 +709,19 @@ def approve_ballot(request, name):
e.save()
change_description = e.desc + " and state has been changed to %s" % doc.get_state("draft-iesg").name
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
doc.time = (e and e.time) or datetime.datetime.now()
doc.save()
email_state_changed(request, doc, change_description)
email_ad(request, doc, doc.ad, login, change_description)
# 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": "IANA <%s>"%settings.IANA_APPROVE_EMAIL, "CC": None, "Bcc": None, "Reply-To": None})
override={ "To": addrs.to, "CC": addrs.cc, "Bcc": None, "Reply-To": None})
msg = infer_message(announcement)
msg.by = login
@ -753,8 +762,9 @@ def make_last_call(request, name):
if form.is_valid():
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": "IANA <drafts-lastcall@icann.org>", "CC": None, "Bcc": None, "Reply-To": None})
override={ "To": addrs.to, "CC": addrs.cc, "Bcc": None, "Reply-To": None})
msg = infer_message(announcement)
msg.by = login
@ -782,11 +792,6 @@ def make_last_call(request, name):
doc.time = (e and e.time) or datetime.datetime.now()
doc.save()
change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, new_state.name)
email_state_changed(request, doc, change_description)
email_ad(request, doc, doc.ad, login, change_description)
e = LastCallDocEvent(doc=doc, by=login)
e.type = "sent_last_call"
e.desc = "The following Last Call announcement was sent out:<br><br>"

View file

@ -18,17 +18,18 @@ from ietf.doc.models import ( Document, DocHistory, State, DocEvent, BallotDocEv
from ietf.doc.utils import ( add_state_change_event, close_open_ballots,
create_ballot_if_not_open, get_chartering_type )
from ietf.doc.utils_charter import ( historic_milestones_for_charter,
approved_revision, default_review_text, default_action_text, email_state_changed,
approved_revision, default_review_text, default_action_text,
generate_ballot_writeup, generate_issue_ballot_mail, next_approved_revision, next_revision )
from ietf.doc.mails import email_state_changed, email_charter_internal_review
from ietf.group.models import ChangeStateGroupEvent, MilestoneGroupEvent
from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_group_type
from ietf.ietfauth.utils import has_role, role_required
from ietf.name.models import GroupStateName
from ietf.person.models import Person
from ietf.utils.history import find_history_active_at
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.group.mails import email_iesg_secretary_re_charter
from ietf.group.mails import email_admin_re_charter
class ChangeStateForm(forms.Form):
charter_state = forms.ModelChoiceField(State.objects.filter(used=True, type="charter"), label="Charter state", empty_label=None, required=False)
@ -139,10 +140,14 @@ def change_state(request, name, option=None):
charter.time = datetime.datetime.now()
charter.save()
if message or charter_state.slug == "intrev" or charter_state.slug == "extrev":
email_iesg_secretary_re_charter(request, group, "Charter state changed to %s" % charter_state.name, message)
if charter_state.slug == 'intrev':
email_charter_internal_review(request,charter)
email_state_changed(request, charter, "State changed to %s." % charter_state)
if message or charter_state.slug == "intrev" or charter_state.slug == "extrev":
email_admin_re_charter(request, group, "Charter state changed to %s" % charter_state.name, message,'charter_state_edit_admin_needed')
# TODO - do we need a seperate set of recipients for state changes to charters vrs other kind of documents
email_state_changed(request, charter, "State changed to %s." % charter_state, 'doc_state_edited')
if charter_state.slug == "intrev" and group.type_id == "wg":
if request.POST.get("ballot_wo_extern"):
@ -202,9 +207,9 @@ def change_state(request, name, option=None):
info_msg = {}
if group.type_id == "wg":
info_msg[state_pk("infrev")] = 'The %s "%s" (%s) has been set to Informal IESG review by %s.' % (group.type.name, group.name, group.acronym, login.plain_name())
info_msg[state_pk("intrev")] = 'The %s "%s" (%s) has been set to Internal review by %s.\nPlease place it on the next IESG telechat and inform the IAB.' % (group.type.name, group.name, group.acronym, login.plain_name())
info_msg[state_pk("extrev")] = 'The %s "%s" (%s) has been set to External review by %s.\nPlease send out the external review announcement to the appropriate lists.\n\nSend the announcement to other SDOs: Yes\nAdditional recipients of the announcement: ' % (group.type.name, group.name, group.acronym, login.plain_name())
info_msg[state_pk("infrev")] = 'The proposed charter for %s "%s" (%s) has been set to Informal IESG review by %s.' % (group.type.name, group.name, group.acronym, login.plain_name())
info_msg[state_pk("intrev")] = 'The proposed charter for %s "%s" (%s) has been set to Internal review by %s.\nPlease place it on the next IESG telechat if it has not already been placed.' % (group.type.name, group.name, group.acronym, login.plain_name())
info_msg[state_pk("extrev")] = 'The proposed charter for %s "%s" (%s) has been set to External review by %s.\nPlease send out the external review announcement to the appropriate lists.\n\nSend the announcement to other SDOs: Yes\nAdditional recipients of the announcement: ' % (group.type.name, group.name, group.acronym, login.plain_name())
states_for_ballot_wo_extern = State.objects.none()
if group.type_id == "wg":
@ -265,8 +270,8 @@ def change_title(request, name, option=None):
charter.time = datetime.datetime.now()
charter.save()
if message:
email_iesg_secretary_re_charter(request, group, "Charter title changed to %s" % new_title, message)
email_state_changed(request, charter, "Title changed to %s." % new_title)
email_admin_re_charter(request, group, "Charter title changed to %s" % new_title, message,'charter_state_edit_admin_needed')
email_state_changed(request, charter, "Title changed to %s." % new_title,'doc_state_edited')
return redirect('doc_view', name=charter.name)
else:
form = ChangeTitleForm(charter=charter)
@ -422,42 +427,124 @@ def submit(request, name=None, option=None):
'name': name },
context_instance=RequestContext(request))
class AnnouncementTextForm(forms.Form):
class ActionAnnouncementTextForm(forms.Form):
announcement_text = forms.CharField(widget=forms.Textarea, required=True)
def clean_announcement_text(self):
return self.cleaned_data["announcement_text"].replace("\r", "")
class ReviewAnnouncementTextForm(forms.Form):
announcement_text = forms.CharField(widget=forms.Textarea, required=True)
new_work_text = forms.CharField(widget=forms.Textarea, required=True)
def clean_announcement_text(self):
return self.cleaned_data["announcement_text"].replace("\r", "")
@role_required('Area Director','Secretariat')
def announcement_text(request, name, ann):
"""Editing of announcement text"""
def review_announcement_text(request, name):
"""Editing of review announcement text"""
charter = get_object_or_404(Document, type="charter", name=name)
group = charter.group
login = request.user.person
if ann in ("action", "review"):
existing = charter.latest_event(WriteupDocEvent, type="changed_%s_announcement" % ann)
existing = charter.latest_event(WriteupDocEvent, type="changed_review_announcement")
existing_new_work = charter.latest_event(WriteupDocEvent, type="changed_new_work_text")
if not existing:
if ann == "action":
existing = default_action_text(group, charter, login)
elif ann == "review":
existing = default_review_text(group, charter, login)
(existing, existing_new_work) = default_review_text(group, charter, login)
if not existing:
raise Http404
form = AnnouncementTextForm(initial=dict(announcement_text=existing.text))
new_work_text = existing_new_work.text
form = ReviewAnnouncementTextForm(initial=dict(announcement_text=existing.text,new_work_text=new_work_text))
if request.method == 'POST':
form = AnnouncementTextForm(request.POST)
form = ReviewAnnouncementTextForm(request.POST)
if "save_text" in request.POST and form.is_valid():
now = datetime.datetime.now()
(e1, e2) = (None, None)
t = form.cleaned_data['announcement_text']
if t != existing.text:
e1 = WriteupDocEvent(doc=charter, by=login)
e1.by = login
e1.type = "changed_review_announcement"
e1.desc = "%s review text was changed" % (group.type.name)
e1.text = t
e1.time = now
e1.save()
t = form.cleaned_data['new_work_text']
if t != new_work_text:
e2 = WriteupDocEvent(doc=charter, by=login)
e2.by = login
e2.type = "changed_new_work_text"
e2.desc = "%s new work message text was changed" % (group.type.name)
e2.text = t
e2.time = now
e2.save()
if e1 or e2:
charter.time = now
charter.save()
if request.GET.get("next", "") == "approve":
return redirect('charter_approve', name=charter.canonical_name())
return redirect('doc_writeup', name=charter.canonical_name())
if "regenerate_text" in request.POST:
(e1, e2) = default_review_text(group, charter, login)
form = ReviewAnnouncementTextForm(initial=dict(announcement_text=e1.text,new_work_text=e2.text))
if any([x in request.POST for x in ['send_annc_only','send_nw_only','send_both']]) and form.is_valid():
if any([x in request.POST for x in ['send_annc_only','send_both']]):
parsed_msg = send_mail_preformatted(request, form.cleaned_data['announcement_text'])
messages.success(request, "The email To: '%s' with Subject: '%s' has been sent." % (parsed_msg["To"],parsed_msg["Subject"],))
if any([x in request.POST for x in ['send_nw_only','send_both']]):
parsed_msg = send_mail_preformatted(request, form.cleaned_data['new_work_text'])
messages.success(request, "The email To: '%s' with Subject: '%s' has been sent." % (parsed_msg["To"],parsed_msg["Subject"],))
return redirect('doc_writeup', name=charter.name)
return render_to_response('doc/charter/review_announcement_text.html',
dict(charter=charter,
back_url=urlreverse("doc_writeup", kwargs=dict(name=charter.name)),
announcement_text_form=form,
),
context_instance=RequestContext(request))
@role_required('Area Director','Secretariat')
def action_announcement_text(request, name):
"""Editing of action announcement text"""
charter = get_object_or_404(Document, type="charter", name=name)
group = charter.group
login = request.user.person
existing = charter.latest_event(WriteupDocEvent, type="changed_action_announcement")
if not existing:
existing = default_action_text(group, charter, login)
if not existing:
raise Http404
form = ActionAnnouncementTextForm(initial=dict(announcement_text=existing.text))
if request.method == 'POST':
form = ActionAnnouncementTextForm(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 = "%s %s text was changed" % (group.type.name, ann)
e.type = "changed_action_announcement"
e.desc = "%s action text was changed" % group.type.name
e.text = t
e.save()
@ -470,21 +557,16 @@ def announcement_text(request, name, ann):
return redirect('doc_writeup', name=charter.canonical_name())
if "regenerate_text" in request.POST:
if ann == "action":
e = default_action_text(group, charter, login)
elif ann == "review":
e = default_review_text(group, charter, login)
# make sure form has the updated text
form = AnnouncementTextForm(initial=dict(announcement_text=e.text))
e = default_action_text(group, charter, login)
form = ActionAnnouncementTextForm(initial=dict(announcement_text=e.text))
if "send_text" in request.POST and form.is_valid():
parsed_msg = send_mail_preformatted(request, form.cleaned_data['announcement_text'])
messages.success(request, "The email To: '%s' with Subject: '%s' has been sent." % (parsed_msg["To"],parsed_msg["Subject"],))
return redirect('doc_writeup', name=charter.name)
return render_to_response('doc/charter/announcement_text.html',
return render_to_response('doc/charter/action_announcement_text.html',
dict(charter=charter,
announcement=ann,
back_url=urlreverse("doc_writeup", kwargs=dict(name=charter.name)),
announcement_text_form=form,
),
@ -540,6 +622,7 @@ def ballot_writeupnotes(request, name):
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'
msg = generate_issue_ballot_mail(request, charter, ballot)
send_mail_preformatted(request, msg)
@ -643,7 +726,7 @@ def approve(request, name):
fix_charter_revision_after_approval(charter, login)
email_iesg_secretary_re_charter(request, group, "Charter state changed to %s" % new_charter_state.name, change_description)
email_admin_re_charter(request, group, "Charter state changed to %s" % new_charter_state.name, change_description,'charter_state_edit_admin_needed')
# move milestones over
milestones_to_delete = list(group.groupmilestone_set.filter(state__in=("active", "review")))

View file

@ -20,6 +20,7 @@ from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_st
from ietf.person.models import Person
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.mailtrigger.utils import gather_address_lists
class ChangeStateForm(forms.Form):
review_state = forms.ModelChoiceField(State.objects.filter(used=True, type="conflrev"), label="Conflict review state", empty_label=None, required=True)
@ -68,6 +69,7 @@ def change_state(request, name, option=None):
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 that position to 'ballot_saved'
send_conflict_eval_email(request,review)
@ -86,8 +88,11 @@ def change_state(request, name, option=None):
context_instance=RequestContext(request))
def send_conflict_review_started_email(request, review):
addrs = gather_address_lists('conflrev_requested',doc=review).as_strings(compact=False)
msg = render_to_string("doc/conflict_review/review_started.txt",
dict(frm = settings.DEFAULT_FROM_EMAIL,
to = addrs.to,
cc = addrs.cc,
by = request.user.person,
review = review,
reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target.document,
@ -96,10 +101,13 @@ def send_conflict_review_started_email(request, review):
)
if not has_role(request.user,"Secretariat"):
send_mail_preformatted(request,msg)
addrs = gather_address_lists('conflrev_requested_iana',doc=review).as_strings(compact=False)
email_iana(request,
review.relateddocument_set.get(relationship__slug='conflrev').target.document,
settings.IANA_EVAL_EMAIL,
msg)
addrs.to,
msg,
cc=addrs.cc)
def send_conflict_eval_email(request,review):
msg = render_to_string("doc/eval_email.txt",
@ -107,11 +115,17 @@ def send_conflict_eval_email(request,review):
doc_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(),
)
)
send_mail_preformatted(request,msg)
addrs = gather_address_lists('ballot_issued',doc=review).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=review).as_strings()
email_iana(request,
review.relateddocument_set.get(relationship__slug='conflrev').target.document,
settings.IANA_EVAL_EMAIL,
msg)
addrs.to,
msg,
addrs.cc)
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Conflict review response", help_text="Edit the conflict review response.", required=False)
@ -251,13 +265,16 @@ def default_approval_text(review):
receiver = 'IRTF'
else:
receiver = 'recipient'
addrs = gather_address_lists('ballot_approved_conflrev',doc=review).as_strings(compact=False)
text = render_to_string("doc/conflict_review/approval_text.txt",
dict(review=review,
review_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(),
conflictdoc = conflictdoc,
conflictdoc_url = settings.IDTRACKER_BASE_URL+conflictdoc.get_absolute_url(),
receiver=receiver,
approved_review = current_text
approved_review = current_text,
to = addrs.to,
cc = addrs.cc,
)
)

View file

@ -51,7 +51,6 @@ from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_wi
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event )
from ietf.community.models import CommunityList
from ietf.doc.mails import email_ad
from ietf.group.models import Role
from ietf.group.utils import can_manage_group_type, can_manage_materials
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
@ -59,6 +58,8 @@ from ietf.name.models import StreamName, BallotPositionName
from ietf.person.models import Email
from ietf.utils.history import find_history_active_at
from ietf.doc.forms import TelechatForm, NotifyForm
from ietf.doc.mails import email_comment
from ietf.mailtrigger.utils import gather_relevant_expansions
def render_document_top(request, doc, tab, name):
tabs = []
@ -73,6 +74,7 @@ def render_document_top(request, doc, tab, name):
if doc.type_id == "draft" or (doc.type_id == "charter" and doc.group.type_id == "wg"):
tabs.append(("IESG Writeups", "writeup", urlreverse("doc_writeup", kwargs=dict(name=name)), True))
tabs.append(("Email expansions","email",urlreverse("doc_email", kwargs=dict(name=name)), True))
tabs.append(("History", "history", urlreverse("doc_history", kwargs=dict(name=name)), True))
if name.startswith("rfc"):
@ -569,9 +571,36 @@ def document_main(request, name, rev=None):
raise Http404
def get_email_aliases(name):
if name:
pattern = re.compile('^expand-(%s)(\..*?)?@.*? +(.*)$'%name)
else:
pattern = re.compile('^expand-(.*?)(\..*?)?@.*? +(.*)$')
aliases = []
with open(settings.DRAFT_VIRTUAL_PATH,"r") as virtual_file:
for line in virtual_file.readlines():
m = pattern.match(line)
if m:
aliases.append({'doc_name':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)})
return aliases
def document_email(request,name):
doc = get_object_or_404(Document, docalias__name=name)
top = render_document_top(request, doc, "email", name)
aliases = get_email_aliases(name) if doc.type_id=='draft' else None
expansions = gather_relevant_expansions(doc=doc)
return render(request, "doc/document_email.html",
dict(doc=doc,
top=top,
aliases=aliases,
expansions=expansions,
ietf_domain=settings.IETF_DOMAIN,
)
)
def document_history(request, name):
@ -658,14 +687,14 @@ def document_writeup(request, name):
"",
[("WG Review Announcement",
text_from_writeup("changed_review_announcement"),
urlreverse("ietf.doc.views_charter.announcement_text", kwargs=dict(name=doc.name, ann="review")))]
urlreverse("ietf.doc.views_charter.review_announcement_text", kwargs=dict(name=doc.name)))]
))
sections.append(("WG Action Announcement",
"",
[("WG Action Announcement",
text_from_writeup("changed_action_announcement"),
urlreverse("ietf.doc.views_charter.announcement_text", kwargs=dict(name=doc.name, ann="action")))]
urlreverse("ietf.doc.views_charter.action_announcement_text", kwargs=dict(name=doc.name)))]
))
if doc.latest_event(BallotDocEvent, type="created_ballot"):
@ -877,9 +906,8 @@ def add_comment(request, name):
e.desc = c
e.save()
if doc.type_id == "draft":
email_ad(request, doc, doc.ad, login,
"A new comment added by %s" % login.name)
email_comment(request, doc, e)
return redirect("doc_history", name=doc.name)
else:
form = AddCommentForm()
@ -984,20 +1012,12 @@ def edit_notify(request, name):
def email_aliases(request,name=''):
doc = get_object_or_404(Document, name=name) if name else None
if name:
pattern = re.compile('^expand-(%s)(\..*?)?@.*? +(.*)$'%name)
else:
if not name:
# require login for the overview page, but not for the
# document-specific pages handled above
# document-specific pages
if not request.user.is_authenticated():
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
pattern = re.compile('^expand-(.*?)(\..*?)?@.*? +(.*)$')
aliases = []
with open(settings.DRAFT_VIRTUAL_PATH,"r") as virtual_file:
for line in virtual_file.readlines():
m = pattern.match(line)
if m:
aliases.append({'doc_name':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)})
aliases = get_email_aliases(name)
return render(request,'doc/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'doc':doc})

View file

@ -18,10 +18,11 @@ import debug # pyflakes:ignore
from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State,
StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS,
save_document_in_history )
from ietf.doc.mails import ( email_ad, email_pulled_from_rfc_queue, email_resurrect_requested,
from ietf.doc.mails import ( email_pulled_from_rfc_queue, email_resurrect_requested,
email_resurrection_completed, email_state_changed, email_stream_changed,
email_stream_state_changed, email_stream_tags_changed, extra_automation_headers,
generate_publication_request )
generate_publication_request, email_adopted, email_intended_status_changed,
email_iesg_processing_document )
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft,
get_tags_for_stream_id, nice_consensus,
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify,
@ -39,6 +40,7 @@ from ietf.person.models import Person, Email
from ietf.secr.lib.template import jsonapi
from ietf.utils.mail import send_mail, send_mail_message
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.mailtrigger.utils import gather_address_lists
class ChangeStateForm(forms.Form):
state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg"), empty_label=None, required=True)
@ -111,8 +113,7 @@ def change_state(request, name):
doc.time = e.time
doc.save()
email_state_changed(request, doc, msg)
email_ad(request, doc, doc.ad, login, msg)
email_state_changed(request, doc, msg,'doc_state_edited')
if prev_state and prev_state.slug in ("ann", "rfcqueue") and new_state.slug not in ("rfcqueue", "pub"):
@ -451,7 +452,7 @@ def change_intention(request, name):
doc.time = e.time
doc.save()
email_ad(request, doc, doc.ad, login, email_desc)
email_intended_status_changed(request, doc, email_desc)
return HttpResponseRedirect(doc.get_absolute_url())
@ -583,10 +584,11 @@ def to_iesg(request,name):
doc.save()
addrs= gather_address_lists('pubreq_iesg',doc=doc)
extra = {}
extra['Cc'] = "%s-chairs@ietf.org, iesg-secretary@ietf.org, %s" % (doc.group.acronym,doc.notify)
extra['Cc'] = addrs.as_strings().cc
send_mail(request=request,
to = doc.ad.email_address(),
to = addrs.to,
frm = login.formatted_email(),
subject = "Publication has been requested for %s-%s" % (doc.name,doc.rev),
template = "doc/submit_to_iesg_email.txt",
@ -670,8 +672,6 @@ def edit_info(request, name):
e.desc = "IESG process started in state <b>%s</b>" % doc.get_state("draft-iesg").name
e.save()
orig_ad = doc.ad
changes = []
def desc(attr, new, old):
@ -721,13 +721,14 @@ def edit_info(request, name):
e.type = "changed_document"
e.save()
# Todo - chase this
update_telechat(request, doc, login,
r['telechat_date'], r['returning_item'])
doc.time = datetime.datetime.now()
if changes and not new_document:
email_ad(request, doc, orig_ad, login, "\n".join(changes))
if changes:
email_iesg_processing_document(request, doc, changes)
doc.save()
return HttpResponseRedirect(doc.get_absolute_url())
@ -1134,7 +1135,7 @@ def request_publication(request, name):
m = Message()
m.frm = request.user.person.formatted_email()
m.to = "RFC Editor <rfc-editor@rfc-editor.org>"
(m.to, m.cc) = gather_address_lists('pubreq_rfced',doc=doc)
m.by = request.user.person
next_state = State.objects.get(used=True, type="draft-stream-%s" % doc.stream.slug, slug="rfc-edit")
@ -1164,7 +1165,7 @@ def request_publication(request, name):
send_mail_message(request, m)
# IANA copy
m.to = settings.IANA_APPROVE_EMAIL
(m.to, m.cc) = gather_address_lists('pubreq_rfced_iana',doc=doc)
send_mail_message(request, m, extra=extra_automation_headers(doc))
e = DocEvent(doc=doc, type="requested_publication", by=request.user.person)
@ -1300,7 +1301,7 @@ def adopt_draft(request, name):
update_reminder(doc, "stream-s", e, due_date)
email_stream_state_changed(request, doc, prev_state, new_state, by, comment)
email_adopted(request, doc, prev_state, new_state, by, comment)
# comment
if comment:

View file

@ -22,6 +22,7 @@ from ietf.name.models import DocRelationshipName, StdLevelName
from ietf.person.models import Person
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.mailtrigger.utils import gather_address_lists
class ChangeStateForm(forms.Form):
new_state = forms.ModelChoiceField(State.objects.filter(type="statchg", used=True), label="Status Change Evaluation State", empty_label=None, required=True)
@ -106,7 +107,11 @@ def send_status_change_eval_email(request,doc):
doc_url = settings.IDTRACKER_BASE_URL+doc.get_absolute_url(),
)
)
send_mail_preformatted(request,msg)
addrs = gather_address_lists('ballot_issued',doc=doc)
override = {'To':addrs.to }
if addrs.cc:
override['Cc'] = addrs.cc
send_mail_preformatted(request,msg,override=override)
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Status change text", help_text="Edit the status change text.", required=False)
@ -289,7 +294,8 @@ def default_approval_text(status_change,relateddoc):
else:
action = "Document Action"
addrs = gather_address_lists('ballot_approved_status_change',doc=status_change).as_strings(compact=False)
text = render_to_string("doc/status_change/approval_text.txt",
dict(status_change=status_change,
status_change_url = settings.IDTRACKER_BASE_URL+status_change.get_absolute_url(),
@ -298,6 +304,8 @@ def default_approval_text(status_change,relateddoc):
approved_text = current_text,
action=action,
newstatus=newstatus(relateddoc),
to=addrs.to,
cc=addrs.cc,
)
)

View file

@ -21,8 +21,7 @@ from ietf.group.utils import get_group_or_404
from ietf.ietfauth.utils import has_role
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Person, Email
from ietf.group.mails import ( email_iesg_secretary_re_charter, email_iesg_secretary_personnel_change,
email_interested_parties_re_changed_delegates )
from ietf.group.mails import ( email_admin_re_charter, email_personnel_change)
from ietf.utils.ordereddict import insert_after_in_ordered_dict
MAX_GROUP_DELEGATES = 3
@ -270,6 +269,7 @@ def edit(request, group_type=None, acronym=None, action="edit"):
diff('list_archive', "Mailing list archive")
personnel_change_text=""
changed_personnel = set()
# update roles
for attr, slug, title in [('ad','ad','Shepherding AD'), ('chairs', 'chair', "Chairs"), ('secretaries', 'secr', "Secretaries"), ('techadv', 'techadv', "Tech Advisors"), ('delegates', 'delegate', "Delegates")]:
new = clean[attr]
@ -291,10 +291,10 @@ def edit(request, group_type=None, acronym=None, action="edit"):
if deleted:
change_text=title + ' deleted: ' + ", ".join(x.formatted_email() for x in deleted)
personnel_change_text+=change_text+"\n"
email_interested_parties_re_changed_delegates(request, group, title, added, deleted)
changed_personnel.update(set(old)^set(new))
if personnel_change_text!="":
email_iesg_secretary_personnel_change(request, group, personnel_change_text)
email_personnel_change(request, group, personnel_change_text, changed_personnel)
# update urls
new_urls = clean['urls']
@ -372,7 +372,7 @@ def conclude(request, acronym, group_type=None):
if form.is_valid():
instructions = form.cleaned_data['instructions']
email_iesg_secretary_re_charter(request, group, "Request closing of group", instructions)
email_admin_re_charter(request, group, "Request closing of group", instructions, 'group_closure_requested')
e = GroupEvent(group=group, by=request.user.person)
e.type = "requested_close"

View file

@ -57,6 +57,7 @@ from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_
from ietf.group.utils import can_manage_materials, get_group_or_404
from ietf.utils.pipe import pipe
from ietf.settings import MAILING_LIST_INFO_URL
from ietf.mailtrigger.utils import gather_relevant_expansions
def roles(group, role_name):
return Role.objects.filter(group=group, name=role_name).select_related("email", "person")
@ -332,6 +333,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
entries.append(("About", urlreverse("group_about", kwargs=kwargs)))
if group.features.has_materials and get_group_materials(group).exists():
entries.append(("Materials", urlreverse("ietf.group.info.materials", kwargs=kwargs)))
entries.append(("Email expansions", urlreverse("ietf.group.info.email", kwargs=kwargs)))
entries.append(("History", urlreverse("ietf.group.info.history", kwargs=kwargs)))
if group.features.has_documents:
entries.append((mark_safe("Dependency graph &raquo;"), urlreverse("ietf.group.info.dependencies_pdf", kwargs=kwargs)))
@ -480,6 +482,34 @@ def group_about(request, acronym, group_type=None):
"can_manage": can_manage,
}))
def get_email_aliases(acronym, group_type):
if acronym:
pattern = re.compile('expand-(%s)(-\w+)@.*? +(.*)$'%acronym)
else:
pattern = re.compile('expand-(.*?)(-\w+)@.*? +(.*)$')
aliases = []
with open(settings.GROUP_VIRTUAL_PATH,"r") as virtual_file:
for line in virtual_file.readlines():
m = pattern.match(line)
if m:
if acronym or not group_type or Group.objects.filter(acronym=m.group(1),type__slug=group_type):
aliases.append({'acronym':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)})
return aliases
def email(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
aliases = get_email_aliases(acronym, group_type)
expansions = gather_relevant_expansions(group=group)
return render(request, 'group/email.html',
construct_group_menu_context(request, group, "email expansions", group_type, {
'expansions':expansions,
'aliases':aliases,
'group':group,
'ietf_domain':settings.IETF_DOMAIN,
}))
def history(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
@ -674,22 +704,13 @@ def dependencies_pdf(request, acronym, group_type=None):
def email_aliases(request, acronym=None, group_type=None):
group = get_group_or_404(acronym,group_type) if acronym else None
if acronym:
pattern = re.compile('expand-(%s)(-\w+)@.*? +(.*)$'%acronym)
else:
if not acronym:
# require login for the overview page, but not for the group-specific
# pages handled above
# pages
if not request.user.is_authenticated():
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
pattern = re.compile('expand-(.*?)(-\w+)@.*? +(.*)$')
aliases = []
with open(settings.GROUP_VIRTUAL_PATH,"r") as virtual_file:
for line in virtual_file.readlines():
m = pattern.match(line)
if m:
if acronym or not group_type or Group.objects.filter(acronym=m.group(1),type__slug=group_type):
aliases.append({'acronym':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)})
aliases = get_email_aliases(acronym, group_type)
return render(request,'group/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'group':group})

View file

@ -1,6 +1,5 @@
# generation of mails
import datetime
import re
@ -10,11 +9,10 @@ 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.group.models import Group
from ietf.group.utils import milestone_reviewer_for_group_type
from ietf.mailtrigger.utils import gather_address_lists
def email_iesg_secretary_re_charter(request, group, subject, text):
to = ["iesg-secretary@ietf.org"]
def email_admin_re_charter(request, group, subject, text, mailtrigger):
(to,cc) = gather_address_lists(mailtrigger,group=group)
full_subject = u"Regarding %s %s: %s" % (group.type.name, group.acronym, subject)
text = strip_tags(text)
@ -24,48 +22,18 @@ def email_iesg_secretary_re_charter(request, group, subject, text):
group=group,
group_url=settings.IDTRACKER_BASE_URL + group.about_url(),
charter_url=settings.IDTRACKER_BASE_URL + urlreverse('doc_view', kwargs=dict(name=group.charter.name)) if group.charter else "[no charter]",
)
)
),
cc=cc,
)
def email_iesg_secretary_personnel_change(request, group, text):
to = ["iesg-secretary@ietf.org"]
def email_personnel_change(request, group, text, changed_personnel):
(to, cc) = gather_address_lists('group_personnel_change',group=group,changed_personnel=changed_personnel)
full_subject = u"Personnel change for %s working group" % (group.acronym)
send_mail_text(request, to, None, full_subject,text)
def email_interested_parties_re_changed_delegates(request, group, title, added, deleted):
# Send to management and chairs
to = []
if group.ad_role():
to.append(group.ad_role().email.formatted_email())
elif group.type_id == "rg":
to.append("IRTF Chair <irtf-chair@irtf.org>")
for r in group.role_set.filter(name="chair"):
to.append(r.formatted_email())
# Send to the delegates who were added or deleted
for delegate in added:
to.append(delegate.formatted_email())
for delegate in deleted:
to.append(delegate.formatted_email())
personnel_change_text=""
if added:
change_text=title + ' added: ' + ", ".join(x.formatted_email() for x in added)
personnel_change_text+=change_text+"\n"
if deleted:
change_text=title + ' deleted: ' + ", ".join(x.formatted_email() for x in deleted)
personnel_change_text+=change_text+"\n"
if to:
full_subject = u"%s changed for %s working group" % (title, group.acronym)
send_mail_text(request, to, None, full_subject,personnel_change_text)
send_mail_text(request, to, None, full_subject, text, cc=cc)
def email_milestones_changed(request, group, changes):
def wrap_up_email(to, text):
def wrap_up_email(addrs, text):
subject = u"Milestones changed for %s %s" % (group.acronym, group.type.name)
if re.search("Added .* for review, due",text):
@ -75,123 +43,18 @@ def email_milestones_changed(request, group, changes):
text += "\n\n"
text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + group.about_url())
send_mail_text(request, to, None, subject, text)
send_mail_text(request, addrs.to, None, subject, text, cc=addrs.cc)
# first send to management and chairs
to = []
if group.ad_role():
to.append(group.ad_role().email.formatted_email())
elif group.type_id == "rg":
to.append("IRTF Chair <irtf-chair@irtf.org>")
# first send to those who should see any edits (such as management and chairs)
addrs = gather_address_lists('group_milestones_edited',group=group)
if addrs.to or addrs.cc:
wrap_up_email(addrs, u"\n\n".join(c + "." for c in changes))
for r in group.role_set.filter(name="chair"):
to.append(r.formatted_email())
if to:
wrap_up_email(to, u"\n\n".join(c + "." for c in changes))
# then send to group
if group.list_email:
review_re = re.compile("Added .* for review, due")
to = [ group.list_email ]
msg = u"\n\n".join(c + "." for c in changes if not review_re.match(c))
if msg:
wrap_up_email(to, msg)
def email_milestone_review_reminder(group, grace_period=7):
"""Email reminders about milestones needing review to management."""
to = []
if group.ad_role():
to.append(group.ad_role().email.formatted_email())
elif group.type_id == "rg":
to.append("IRTF Chair <irtf-chair@irtf.org>")
if not to:
return False
cc = [r.formatted_email() for r in group.role_set.filter(name="chair")]
now = datetime.datetime.now()
too_early = True
milestones = group.groupmilestone_set.filter(state="review")
for m in milestones:
e = m.milestonegroupevent_set.filter(type="changed_milestone").order_by("-time")[:1]
m.days_ready = (now - e[0].time).days if e else None
if m.days_ready == None or m.days_ready >= grace_period:
too_early = False
if too_early:
return False
subject = u"Reminder: Milestone%s needing review in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name)
send_mail(None, to, None,
subject,
"group/reminder_milestones_need_review.txt",
dict(group=group,
milestones=milestones,
reviewer=milestone_reviewer_for_group_type(group.type_id),
url=settings.IDTRACKER_BASE_URL + urlreverse("group_edit_milestones", kwargs=dict(group_type=group.type_id, acronym=group.acronym)),
cc=cc,
)
)
return True
def groups_with_milestones_needing_review():
return Group.objects.filter(groupmilestone__state="review").distinct()
def email_milestones_due(group, early_warning_days):
to = [r.formatted_email() for r in group.role_set.filter(name="chair")]
today = datetime.date.today()
early_warning = today + datetime.timedelta(days=early_warning_days)
milestones = group.groupmilestone_set.filter(due__in=[today, early_warning],
resolved="", state="active")
subject = u"Reminder: Milestone%s are soon due in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name)
send_mail(None, to, None,
subject,
"group/reminder_milestones_due.txt",
dict(group=group,
milestones=milestones,
today=today,
early_warning_days=early_warning_days,
url=settings.IDTRACKER_BASE_URL + group.about_url(),
))
def groups_needing_milestones_due_reminder(early_warning_days):
"""Return groups having milestones that are either
early_warning_days from being due or are due today."""
today = datetime.date.today()
return Group.objects.filter(state="active", groupmilestone__due__in=[today, today + datetime.timedelta(days=early_warning_days)], groupmilestone__resolved="", groupmilestone__state="active").distinct()
def email_milestones_overdue(group):
to = [r.formatted_email() for r in group.role_set.filter(name="chair")]
today = datetime.date.today()
milestones = group.groupmilestone_set.filter(due__lt=today, resolved="", state="active")
for m in milestones:
m.months_overdue = (today - m.due).days // 30
subject = u"Reminder: Milestone%s overdue in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name)
send_mail(None, to, None,
subject,
"group/reminder_milestones_overdue.txt",
dict(group=group,
milestones=milestones,
url=settings.IDTRACKER_BASE_URL + group.about_url(),
))
def groups_needing_milestones_overdue_reminder(grace_period=30):
cut_off = datetime.date.today() - datetime.timedelta(days=grace_period)
return Group.objects.filter(state="active", groupmilestone__due__lt=cut_off, groupmilestone__resolved="", groupmilestone__state="active").distinct()
# then send only the approved milestones to those who shouldn't be
# bothered with milestones pending approval
review_re = re.compile("Added .* for review, due")
addrs = gather_address_lists('group_approved_milestones_edited',group=group)
msg = u"\n\n".join(c + "." for c in changes if not review_re.match(c))
if (addrs.to or addrs.cc) and msg:
wrap_up_email(addrs, msg)

View file

@ -13,17 +13,14 @@ from django.core.urlresolvers import reverse as urlreverse
from django.core.urlresolvers import NoReverseMatch
from ietf.doc.models import Document, DocAlias, DocEvent, State
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions, MilestoneGroupEvent
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
from ietf.group.utils import save_group_in_history
from ietf.name.models import DocTagName, GroupStateName, GroupTypeName
from ietf.person.models import Person, Email
from ietf.utils.test_utils import TestCase
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.group.mails import ( email_milestone_review_reminder, email_milestones_due,
email_milestones_overdue, groups_needing_milestones_due_reminder,
groups_needing_milestones_overdue_reminder, groups_with_milestones_needing_review )
class GroupPagesTests(TestCase):
def setUp(self):
@ -482,6 +479,7 @@ class GroupEditTests(TestCase):
area = group.parent
ad = Person.objects.get(name="Aread Irector")
state = GroupStateName.objects.get(slug="bof")
empty_outbox()
r = self.client.post(url,
dict(name="Mars Not Special Interest Group",
acronym="mars",
@ -512,6 +510,10 @@ class GroupEditTests(TestCase):
self.assertEqual(group.groupurl_set.all()[0].url, "http://mars.mars")
self.assertEqual(group.groupurl_set.all()[0].name, "MARS site")
self.assertTrue(os.path.exists(os.path.join(self.charter_dir, "%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev))))
self.assertEqual(len(outbox), 1)
self.assertTrue('Personnel change' in outbox[0]['Subject'])
for prefix in ['ad1','ad2','aread','marschairman','marsdelegate']:
self.assertTrue(prefix+'@' in outbox[0]['To'])
def test_initial_charter(self):
make_test_data()
@ -551,6 +553,7 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(instructions="Test instructions"))
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue('iesg-secretary@' in outbox[-1]['To'])
# the WG remains active until the Secretariat takes action
group = Group.objects.get(acronym=group.acronym)
self.assertEqual(group.state_id, "active")
@ -653,6 +656,11 @@ class MilestoneTests(TestCase):
self.assertTrue("Added milestone" in m.milestonegroupevent_set.all()[0].desc)
self.assertEqual(len(outbox),mailbox_before+2)
self.assertFalse(any('Review Required' in x['Subject'] for x in outbox[-2:]))
self.assertTrue('Milestones changed' in outbox[-2]['Subject'])
self.assertTrue('mars-chairs@' in outbox[-2]['To'])
self.assertTrue('aread@' in outbox[-2]['To'])
self.assertTrue('Milestones changed' in outbox[-1]['Subject'])
self.assertTrue('mars-wg@' in outbox[-1]['To'])
def test_add_milestone_as_chair(self):
m1, m2, group = self.create_test_milestones()
@ -826,137 +834,6 @@ class MilestoneTests(TestCase):
self.assertEqual(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add
def test_send_review_needed_reminders(self):
make_test_data()
group = Group.objects.get(acronym="mars")
person = Person.objects.get(user__username="marschairman")
m1 = GroupMilestone.objects.create(group=group,
desc="Test 1",
due=datetime.date.today(),
resolved="",
state_id="review")
MilestoneGroupEvent.objects.create(
group=group, type="changed_milestone",
by=person, desc='Added milestone "%s"' % m1.desc, milestone=m1,
time=datetime.datetime.now() - datetime.timedelta(seconds=60))
# send
mailbox_before = len(outbox)
for g in groups_with_milestones_needing_review():
email_milestone_review_reminder(g)
self.assertEqual(len(outbox), mailbox_before) # too early to send reminder
# add earlier added milestone
m2 = GroupMilestone.objects.create(group=group,
desc="Test 2",
due=datetime.date.today(),
resolved="",
state_id="review")
MilestoneGroupEvent.objects.create(
group=group, type="changed_milestone",
by=person, desc='Added milestone "%s"' % m2.desc, milestone=m2,
time=datetime.datetime.now() - datetime.timedelta(days=10))
# send
mailbox_before = len(outbox)
for g in groups_with_milestones_needing_review():
email_milestone_review_reminder(g)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue(group.acronym in outbox[-1]["Subject"])
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
def test_send_milestones_due_reminders(self):
make_test_data()
group = Group.objects.get(acronym="mars")
early_warning_days = 30
# due dates here aren't aligned on the last day of the month,
# but everything should still work
m1 = GroupMilestone.objects.create(group=group,
desc="Test 1",
due=datetime.date.today(),
resolved="Done",
state_id="active")
m2 = GroupMilestone.objects.create(group=group,
desc="Test 2",
due=datetime.date.today() + datetime.timedelta(days=early_warning_days - 10),
resolved="",
state_id="active")
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_due_reminder(early_warning_days):
email_milestones_due(g, early_warning_days)
self.assertEqual(len(outbox), mailbox_before) # none found
m1.resolved = ""
m1.save()
m2.due = datetime.date.today() + datetime.timedelta(days=early_warning_days)
m2.save()
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_due_reminder(early_warning_days):
email_milestones_due(g, early_warning_days)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue(group.acronym in outbox[-1]["Subject"])
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
def test_send_milestones_overdue_reminders(self):
make_test_data()
group = Group.objects.get(acronym="mars")
# due dates here aren't aligned on the last day of the month,
# but everything should still work
m1 = GroupMilestone.objects.create(group=group,
desc="Test 1",
due=datetime.date.today() - datetime.timedelta(days=200),
resolved="Done",
state_id="active")
m2 = GroupMilestone.objects.create(group=group,
desc="Test 2",
due=datetime.date.today() - datetime.timedelta(days=10),
resolved="",
state_id="active")
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_overdue_reminder(grace_period=30):
email_milestones_overdue(g)
self.assertEqual(len(outbox), mailbox_before) # none found
m1.resolved = ""
m1.save()
m2.due = self.last_day_of_month(datetime.date.today() - datetime.timedelta(days=300))
m2.save()
# send
mailbox_before = len(outbox)
for g in groups_needing_milestones_overdue_reminder(grace_period=30):
email_milestones_overdue(g)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue(group.acronym in outbox[-1]["Subject"])
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
class CustomizeWorkflowTests(TestCase):
def test_customize_workflow(self):
make_test_data()
@ -1049,27 +926,42 @@ expand-ames-chairs@virtual.ietf.org mars_chair@ietf
def tearDown(self):
os.unlink(self.group_alias_file.name)
def testEmailAliases(self):
def testAliases(self):
url = urlreverse('old_group_email_aliases', kwargs=dict(acronym="mars"))
r = self.client.get(url)
self.assertEqual(r.status_code, 302)
for testdict in [dict(acronym="mars"),dict(acronym="mars",group_type="wg")]:
url = urlreverse('ietf.group.info.email_aliases', kwargs=testdict)
r = self.client.get(url)
url = urlreverse('old_group_email_aliases', kwargs=testdict)
r = self.client.get(url,follow=True)
self.assertTrue(all([x in r.content for x in ['mars-ads@','mars-chairs@']]))
self.assertFalse(any([x in r.content for x in ['ames-ads@','ames-chairs@']]))
url = urlreverse('ietf.group.info.email_aliases', kwargs=dict())
login_testing_unauthorized(self, "plain", url)
r = self.client.get(url)
self.assertTrue(r.status_code,200)
self.assertTrue(all([x in r.content for x in ['mars-ads@','mars-chairs@','ames-ads@','ames-chairs@']]))
url = urlreverse('ietf.group.info.email_aliases', kwargs=dict(group_type="wg"))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertTrue('mars-ads@' in r.content)
url = urlreverse('ietf.group.info.email_aliases', kwargs=dict(group_type="rg"))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertFalse('mars-ads@' in r.content)
def testExpansions(self):
url = urlreverse('ietf.group.info.email', kwargs=dict(acronym="mars"))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertTrue('Email Aliases' in r.content)
self.assertTrue('mars-ads@ietf.org' in r.content)
self.assertTrue('group_personnel_change' in r.content)
class AjaxTests(TestCase):
def test_group_menu_data(self):

View file

@ -15,4 +15,3 @@ urlpatterns = patterns('',
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/', include('ietf.group.urls_info_details')),
)

View file

@ -1,4 +1,5 @@
from django.conf.urls import patterns
from django.conf.urls import patterns, url
from django.views.generic import RedirectView
urlpatterns = patterns('',
(r'^$', 'ietf.group.info.group_home', None, "group_home"),
@ -7,6 +8,7 @@ urlpatterns = patterns('',
(r'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
(r'^history/$','ietf.group.info.history'),
(r'^email/$', 'ietf.group.info.email'),
(r'^deps/dot/$', 'ietf.group.info.dependencies_dot'),
(r'^deps/pdf/$', 'ietf.group.info.dependencies_pdf'),
(r'^init-charter/', 'ietf.group.edit.submit_initial_charter'),
@ -19,5 +21,5 @@ urlpatterns = patterns('',
(r'^materials/$', 'ietf.group.info.materials', None, "group_materials"),
(r'^materials/new/$', 'ietf.doc.views_material.choose_material_type'),
(r'^materials/new/(?P<doc_type>[\w-]+)/$', 'ietf.doc.views_material.edit_material', { 'action': "new" }, "group_new_material"),
(r'^/email-aliases/$', 'ietf.group.info.email_aliases'),
url(r'^email-aliases/$', RedirectView.as_view(pattern_name='ietf.group.info.email',permanent=False),name='old_group_email_aliases'),
)

View file

@ -5,13 +5,13 @@ from dateutil.tz import tzoffset
import os
import pytz
import re
from django.conf import settings
from django.template.loader import render_to_string
from ietf.ipr.models import IprEvent
from ietf.message.models import Message
from ietf.person.models import Person
from ietf.utils.log import log
from ietf.mailtrigger.utils import get_base_ipr_request_address
# ----------------------------------------------------------------
# Date Functions
@ -89,7 +89,7 @@ def get_pseudo_submitter(ipr):
def get_reply_to():
"""Returns a new reply-to address for use with an outgoing message. This is an
address with "plus addressing" using a random string. Guaranteed to be unique"""
local,domain = settings.IPR_EMAIL_TO.split('@')
local,domain = get_base_ipr_request_address().split('@')
while True:
rand = base64.urlsafe_b64encode(os.urandom(12))
address = "{}+{}@{}".format(local,rand,domain)
@ -129,6 +129,8 @@ def get_update_submitter_emails(ipr):
else:
email_to_iprs[email] = [related.target]
# TODO: This has not been converted to use mailtrigger. It is complicated.
# When converting it, it will need something like ipr_submitter_ietfer_or_holder perhaps
for email in email_to_iprs:
context = dict(
to_email=email,
@ -167,7 +169,7 @@ def process_response_email(msg):
to = message.get('To')
# exit if this isn't a response we're interested in (with plus addressing)
local,domain = settings.IPR_EMAIL_TO.split('@')
local,domain = get_base_ipr_request_address().split('@')
if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to):
return None
@ -193,4 +195,4 @@ def process_response_email(msg):
)
log(u"Received IPR email from %s" % ietf_message.frm)
return ietf_message
return ietf_message

View file

@ -92,6 +92,18 @@ class IprDisclosureBase(models.Model):
else:
return None
def recursively_updates(self,disc_set=None):
"""Returns the set of disclosures updated directly or transitively by this disclosure"""
if disc_set == None:
disc_set = set()
new_candidates = set([y.target.get_child() for y in self.updates])
unseen = new_candidates - disc_set
disc_set.update(unseen)
for disc in unseen:
disc_set.update(disc.recursively_updates(disc_set))
return disc_set
class HolderIprDisclosure(IprDisclosureBase):
ietfer_name = models.CharField(max_length=255, blank=True) # "Whose Personal Belief Triggered..."
ietfer_contact_email = models.EmailField(blank=True)

View file

@ -3,7 +3,6 @@ import urllib
from pyquery import PyQuery
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from ietf.doc.models import DocAlias
@ -15,7 +14,8 @@ from ietf.ipr.utils import get_genitive, get_ipr_summary
from ietf.message.models import Message
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.utils.test_data import make_test_data
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.mailtrigger.utils import gather_address_lists
class IprTests(TestCase):
@ -251,6 +251,7 @@ class IprTests(TestCase):
self.assertTrue(len(q("form .has-error")) > 0)
# successful post
empty_outbox()
r = self.client.post(url, {
"holder_legal_name": "Test Legal",
"holder_contact_name": "Test Holder",
@ -262,6 +263,9 @@ class IprTests(TestCase):
})
self.assertEqual(r.status_code, 200)
self.assertTrue("Your IPR disclosure has been submitted" in r.content)
self.assertEqual(len(outbox),1)
self.assertTrue('New IPR Submission' in outbox[0]['Subject'])
self.assertTrue('ietf-ipr@' in outbox[0]['To'])
iprs = IprDisclosureBase.objects.filter(title__icontains="General License Statement")
self.assertEqual(len(iprs), 1)
@ -277,6 +281,7 @@ class IprTests(TestCase):
url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" })
# successful post
empty_outbox()
r = self.client.post(url, {
"holder_legal_name": "Test Legal",
"holder_contact_name": "Test Holder",
@ -305,6 +310,9 @@ class IprTests(TestCase):
self.assertEqual(ipr.holder_legal_name, "Test Legal")
self.assertEqual(ipr.state.slug, 'pending')
self.assertTrue(isinstance(ipr.get_child(),HolderIprDisclosure))
self.assertEqual(len(outbox),1)
self.assertTrue('New IPR Submission' in outbox[0]['Subject'])
self.assertTrue('ietf-ipr@' in outbox[0]['To'])
def test_new_thirdparty(self):
"""Add a new third-party disclosure. Note: submitter does not need to be logged in.
@ -313,6 +321,7 @@ class IprTests(TestCase):
url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "third-party" })
# successful post
empty_outbox()
r = self.client.post(url, {
"holder_legal_name": "Test Legal",
"ietfer_name": "Test Participant",
@ -338,6 +347,9 @@ class IprTests(TestCase):
self.assertEqual(ipr.holder_legal_name, "Test Legal")
self.assertEqual(ipr.state.slug, "pending")
self.assertTrue(isinstance(ipr.get_child(),ThirdPartyIprDisclosure))
self.assertEqual(len(outbox),1)
self.assertTrue('New IPR Submission' in outbox[0]['Subject'])
self.assertTrue('ietf-ipr@' in outbox[0]['To'])
def test_update(self):
draft = make_test_data()
@ -345,6 +357,7 @@ class IprTests(TestCase):
url = urlreverse("ietf.ipr.views.new", kwargs={ "type": "specific" })
# successful post
empty_outbox()
r = self.client.post(url, {
"updates": str(original_ipr.pk),
"holder_legal_name": "Test Legal",
@ -374,6 +387,9 @@ class IprTests(TestCase):
self.assertEqual(ipr.state.slug, 'pending')
self.assertTrue(ipr.relatedipr_source_set.filter(target=original_ipr))
self.assertEqual(len(outbox),1)
self.assertTrue('New IPR Submission' in outbox[0]['Subject'])
self.assertTrue('ietf-ipr@' in outbox[0]['To'])
def test_addcomment(self):
make_test_data()
@ -484,12 +500,15 @@ I would like to revoke this declaration.
name = 'form-%d-type' % i
data[name] = q('form input[name=%s]'%name).val()
text_name = 'form-%d-text' % i
data[text_name] = q('form textarea[name=%s]'%text_name).text()
data[text_name] = q('form textarea[name=%s]'%text_name).html().strip()
# Do not try to use
#data[text_name] = q('form textarea[name=%s]'%text_name).text()
# .text does not work - the field will likely contain <> characters
r = self.client.post(url, data )
self.assertEqual(r.status_code,302)
self.assertEqual(len(outbox),len_before+2)
self.assertTrue('george@acme.com' in outbox[len_before]['To'])
self.assertTrue('aread@ietf.org' in outbox[len_before+1]['To'])
self.assertTrue('draft-ietf-mars-test@ietf.org' in outbox[len_before+1]['To'])
self.assertTrue('mars-wg@ietf.org' in outbox[len_before+1]['Cc'])
def test_process_response_email(self):
@ -506,6 +525,7 @@ I would like to revoke this declaration.
reply_to=get_reply_to(),
body='Testing.',
response_due=yesterday.isoformat())
empty_outbox()
r = self.client.post(url,data,follow=True)
#print r.content
self.assertEqual(r.status_code,200)
@ -513,13 +533,17 @@ I would like to revoke this declaration.
self.assertEqual(q.count(),1)
event = q[0].msgevents.first()
self.assertTrue(event.response_past_due())
self.assertEqual(len(outbox), 1)
self.assertTrue('joe@test.com' in outbox[0]['To'])
# test process response uninteresting message
addrs = gather_address_lists('ipr_disclosure_submitted').as_strings()
message_string = """To: {}
Cc: {}
From: joe@test.com
Date: {}
Subject: test
""".format(settings.IPR_EMAIL_TO,datetime.datetime.now().ctime())
""".format(addrs.to, addrs.cc, datetime.datetime.now().ctime())
result = process_response_email(message_string)
self.assertIsNone(result)

View file

@ -16,8 +16,7 @@ from django.template.loader import render_to_string
from ietf.doc.models import DocAlias
from ietf.group.models import Role, Group
from ietf.ietfauth.utils import role_required, has_role
from ietf.ipr.mail import (message_from_message, get_reply_to, get_update_submitter_emails,
get_update_cc_addrs)
from ietf.ipr.mail import (message_from_message, get_reply_to, get_update_submitter_emails)
from ietf.ipr.fields import select2_id_ipr_title_json
from ietf.ipr.forms import (HolderIprDisclosureForm, GenericDisclosureForm,
ThirdPartyIprDisclosureForm, DraftForm, SearchForm, MessageModelForm,
@ -35,6 +34,7 @@ from ietf.person.models import Person
from ietf.secr.utils.document import get_rfc_num, is_draft
from ietf.utils.draft_search import normalize_draftname
from ietf.utils.mail import send_mail, send_mail_message
from ietf.mailtrigger.utils import gather_address_lists
# ----------------------------------------------------------------
# Globals
@ -79,13 +79,12 @@ def get_document_emails(ipr):
else:
cc_list = get_wg_email_list(doc.group)
author_emails = ','.join([a.address for a in authors])
(to_list,cc_list) = gather_address_lists('ipr_posted_on_doc',doc=doc)
author_names = ', '.join([a.person.name for a in authors])
cc_list += ", ipr-announce@ietf.org"
context = dict(
doc_info=doc_info,
to_email=author_emails,
to_email=to_list,
to_name=author_names,
cc_email=cc_list,
ipr=ipr)
@ -98,16 +97,15 @@ def get_posted_emails(ipr):
"""Return a list of messages suitable to initialize a NotifyFormset for
the notify view when a new disclosure is posted"""
messages = []
# NOTE 1000+ legacy iprs have no submitter_email
# add submitter message
if True:
context = dict(
to_email=ipr.submitter_email,
to_name=ipr.submitter_name,
cc_email=get_update_cc_addrs(ipr),
ipr=ipr)
text = render_to_string('ipr/posted_submitter_email.txt',context)
messages.append(text)
addrs = gather_address_lists('ipr_posting_confirmation',ipr=ipr).as_strings(compact=False)
context = dict(
to_email=addrs.to,
to_name=ipr.submitter_name,
cc_email=addrs.cc,
ipr=ipr)
text = render_to_string('ipr/posted_submitter_email.txt',context)
messages.append(text)
# add email to related document authors / parties
if ipr.iprdocrel_set.all():
@ -377,9 +375,11 @@ def email(request, id):
else:
reply_to = get_reply_to()
addrs = gather_address_lists('ipr_disclosure_followup',ipr=ipr).as_strings(compact=False)
initial = {
'to': ipr.submitter_email,
'frm': settings.IPR_EMAIL_TO,
'to': addrs.to,
'cc': addrs.cc,
'frm': settings.IPR_EMAIL_FROM,
'subject': 'Regarding {}'.format(ipr.title),
'reply_to': reply_to,
}
@ -474,10 +474,12 @@ def new(request, type, updates=None):
desc="Disclosure Submitted")
# send email notification
send_mail(request, settings.IPR_EMAIL_TO, ('IPR Submitter App', 'ietf-ipr@ietf.org'),
(to, cc) = gather_address_lists('ipr_disclosure_submitted')
send_mail(request, to, ('IPR Submitter App', 'ietf-ipr@ietf.org'),
'New IPR Submission Notification',
"ipr/new_update_email.txt",
{"ipr": disclosure,})
{"ipr": disclosure,},
cc=cc)
return render(request, "ipr/submitted.html")

View file

@ -7,16 +7,12 @@ from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import send_mail_text
from ietf.liaisons.utils import role_persons_with_fixed_email
from ietf.group.models import Role
from ietf.mailtrigger.utils import gather_address_lists
def send_liaison_by_email(request, liaison):
subject = u'New Liaison Statement, "%s"' % (liaison.title)
from_email = settings.LIAISON_UNIVERSAL_FROM
to_email = liaison.to_contact.split(',')
cc = liaison.cc.split(',')
if liaison.technical_contact:
cc += liaison.technical_contact.split(',')
if liaison.response_contact:
cc += liaison.response_contact.split(',')
(to_email, cc) = gather_address_lists('liaison_statement_posted',liaison=liaison)
bcc = ['statements@ietf.org']
body = render_to_string('liaisons/liaison_mail.txt', dict(
liaison=liaison,
@ -42,13 +38,13 @@ def notify_pending_by_email(request, liaison):
# to_email.append('%s <%s>' % person.email())
subject = u'New Liaison Statement, "%s" needs your approval' % (liaison.title)
from_email = settings.LIAISON_UNIVERSAL_FROM
(to, cc) = gather_address_lists('liaison_approval_requested',liaison=liaison)
body = render_to_string('liaisons/pending_liaison_mail.txt', dict(
liaison=liaison,
url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_approval_detail", kwargs=dict(object_id=liaison.pk)),
referenced_url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_detail", kwargs=dict(object_id=liaison.related_to.pk)) if liaison.related_to else None,
))
# send_mail_text(request, to_email, from_email, subject, body)
send_mail_text(request, ['statements@ietf.org'], from_email, subject, body)
send_mail_text(request, to, from_email, subject, body, cc=cc)
def send_sdo_reminder(sdo):
roles = Role.objects.filter(name="liaiman", group=sdo)
@ -58,7 +54,7 @@ def send_sdo_reminder(sdo):
manager_role = roles[0]
subject = 'Request for update of list of authorized individuals'
to_email = manager_role.email.address
(to_email,cc) = gather_address_lists('liaison_manager_update_request',group=sdo)
name = manager_role.person.plain_name()
authorized_list = role_persons_with_fixed_email(sdo, "auth")
@ -68,7 +64,7 @@ def send_sdo_reminder(sdo):
individuals=authorized_list,
))
send_mail_text(None, to_email, settings.LIAISON_UNIVERSAL_FROM, subject, body)
send_mail_text(None, to_email, settings.LIAISON_UNIVERSAL_FROM, subject, body, cc=cc)
return body
@ -95,12 +91,7 @@ def possibly_send_deadline_reminder(liaison):
days_msg = 'expires %s' % PREVIOUS_DAYS[days_to_go]
from_email = settings.LIAISON_UNIVERSAL_FROM
to_email = liaison.to_contact.split(',')
cc = liaison.cc.split(',')
if liaison.technical_contact:
cc += liaison.technical_contact.split(',')
if liaison.response_contact:
cc += liaison.response_contact.split(',')
(to_email, cc) = gather_address_lists('liaison_deadline_soon',liaison=liaison)
bcc = 'statements@ietf.org'
body = render_to_string('liaisons/liaison_deadline_mail.txt',
dict(liaison=liaison,

View file

@ -333,6 +333,8 @@ class LiaisonManagementTests(TestCase):
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Liaison Statement" in outbox[-1]["Subject"])
self.assertTrue('marschairman@' in outbox[-1]['To'])
self.assertTrue('cc@' in outbox[-1]['Cc'])
def test_add_outgoing_liaison(self):
make_test_data()
@ -407,6 +409,7 @@ class LiaisonManagementTests(TestCase):
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Liaison Statement" in outbox[-1]["Subject"])
self.assertTrue('statements@ietf.org' in outbox[-1]['To'])
# try adding statement to non-predefined organization
r = self.client.post(url,
@ -441,6 +444,7 @@ class LiaisonManagementTests(TestCase):
send_sdo_reminder(Group.objects.filter(type="sdo")[0])
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("authorized individuals" in outbox[-1]["Subject"])
self.assertTrue('zrk@ulm.mars' in outbox[-1]['To'])
def test_send_liaison_deadline_reminder(self):
make_test_data()

View file

17
ietf/mailtrigger/admin.py Normal file
View file

@ -0,0 +1,17 @@
from django.contrib import admin
from ietf.mailtrigger.models import MailTrigger, Recipient
class RecipientAdmin(admin.ModelAdmin):
list_display = [ 'slug', 'desc', 'template', 'has_code', ]
def has_code(self, obj):
return hasattr(obj,'gather_%s'%obj.slug)
has_code.boolean = True
admin.site.register(Recipient, RecipientAdmin)
class MailTriggerAdmin(admin.ModelAdmin):
list_display = [ 'slug', 'desc', ]
filter_horizontal = [ 'to', 'cc', ]
admin.site.register(MailTrigger, MailTriggerAdmin)

31
ietf/mailtrigger/forms.py Normal file
View file

@ -0,0 +1,31 @@
from django import forms
from ietf.mailtrigger.models import MailTrigger
class CcSelectForm(forms.Form):
expansions = dict()
cc_choices = forms.MultipleChoiceField(
label='Cc',
choices=[],
widget=forms.CheckboxSelectMultiple(),
)
def __init__(self, mailtrigger_slug, mailtrigger_context, *args, **kwargs):
super(CcSelectForm,self).__init__(*args,**kwargs)
mailtrigger = MailTrigger.objects.get(slug=mailtrigger_slug)
for r in mailtrigger.cc.all():
self.expansions[r.slug] = r.gather(**mailtrigger_context)
non_empty_expansions = [x for x in self.expansions if self.expansions[x]]
self.fields['cc_choices'].initial = non_empty_expansions
self.fields['cc_choices'].choices = [(t,'%s: %s'%(t,", ".join(self.expansions[t]))) for t in non_empty_expansions]
def get_selected_addresses(self):
if self.is_valid():
addrs = []
for t in self.cleaned_data['cc_choices']:
addrs.extend(self.expansions[t])
return addrs
else:
raise forms.ValidationError('Cannot get selected addresses from an invalid form.')

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='MailTrigger',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('desc', models.TextField(blank=True)),
],
options={
'ordering': ['slug'],
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Recipient',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('desc', models.TextField(blank=True)),
('template', models.TextField(null=True, blank=True)),
],
options={
'ordering': ['slug'],
},
bases=(models.Model,),
),
migrations.AddField(
model_name='mailtrigger',
name='cc',
field=models.ManyToManyField(related_name='used_in_cc', null=True, to='mailtrigger.Recipient', blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='mailtrigger',
name='to',
field=models.ManyToManyField(related_name='used_in_to', null=True, to='mailtrigger.Recipient', blank=True),
preserve_default=True,
),
]

View file

@ -0,0 +1,849 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def make_recipients(apps):
Recipient=apps.get_model('mailtrigger','Recipient')
rc = Recipient.objects.create
rc(slug='iesg',
desc='The IESG',
template='The IESG <iesg@ietf.org>')
rc(slug='iab',
desc='The IAB',
template='The IAB <iab@iab.org>')
rc(slug='ietf_announce',
desc='The IETF Announce list',
template='IETF-Announce <ietf-announce@ietf.org>')
rc(slug='rfc_editor',
desc='The RFC Editor',
template='<rfc-editor@rfc-editor.org>')
rc(slug='iesg_secretary',
desc='The Secretariat',
template='<iesg-secretary@ietf.org>')
rc(slug='ietf_secretariat',
desc='The Secretariat',
template='<ietf-secretariat-reply@ietf.org>')
rc(slug='doc_authors',
desc="The document's authors",
template='{% if doc.type_id == "draft" %}<{{doc.name}}@ietf.org>{% endif %}')
rc(slug='doc_notify',
desc="The addresses in the document's notify field",
template='{{doc.notify}}')
rc(slug='doc_group_chairs',
desc="The document's group chairs (if the document is assigned to a working or research group)",
template=None)
rc(slug='doc_group_delegates',
desc="The document's group delegates (if the document is assigned to a working or research group)",
template=None)
rc(slug='doc_affecteddoc_authors',
desc="The authors of the subject documents of a conflict-review or status-change",
template=None)
rc(slug='doc_affecteddoc_group_chairs',
desc="The chairs of groups of the subject documents of a conflict-review or status-change",
template=None)
rc(slug='doc_affecteddoc_notify',
desc="The notify field of the subject documents of a conflict-review or status-change",
template=None)
rc(slug='doc_shepherd',
desc="The document's shepherd",
template='{% if doc.shepherd %}<{{doc.shepherd.address}}>{% endif %}' )
rc(slug='doc_ad',
desc="The document's responsible Area Director",
template='{% if doc.ad %}<{{doc.ad.email_address}}>{% endif %}' )
rc(slug='doc_group_mail_list',
desc="The list address of the document's group",
template=None )
rc(slug='doc_stream_manager',
desc="The manager of the document's stream",
template=None )
rc(slug='stream_managers',
desc="The managers of any related streams",
template=None )
rc(slug='conflict_review_stream_manager',
desc="The stream manager of a document being reviewed for IETF stream conflicts",
template=None )
rc(slug='conflict_review_steering_group',
desc="The steering group (e.g. IRSG) of a document being reviewed for IETF stream conflicts",
template = None)
rc(slug='iana_approve',
desc="IANA's draft approval address",
template='IANA <drafts-approval@icann.org>')
rc(slug='iana_last_call',
desc="IANA's draft last call address",
template='IANA <drafts-lastcall@icann.org>')
rc(slug='iana_eval',
desc="IANA's draft evaluation address",
template='IANA <drafts-eval@icann.org>')
rc(slug='iana',
desc="IANA",
template='<iana@iana.org>')
rc(slug='group_mail_list',
desc="The group's mailing list",
template='{% if group.list_email %}<{{ group.list_email }}>{% endif %}')
rc(slug='group_steering_group',
desc="The group's steering group (IESG or IRSG)",
template=None)
rc(slug='group_chairs',
desc="The group's chairs",
template="{% if group and group.acronym %}<{{group.acronym}}-chairs@ietf.org>{% endif %}")
rc(slug='group_responsible_directors',
desc="The group's responsible AD(s) or IRTF chair",
template=None)
rc(slug='doc_group_responsible_directors',
desc="The document's group's responsible AD(s) or IRTF chair",
template=None)
rc(slug='internet_draft_requests',
desc="The internet drafts ticketing system",
template='<internet-drafts@ietf.org>')
rc(slug='submission_submitter',
desc="The person that submitted a draft",
template='{{submission.submitter}}')
rc(slug='submission_authors',
desc="The authors of a submitted draft",
template=None)
rc(slug='submission_group_chairs',
desc="The chairs of a submitted draft belonging to a group",
template=None)
rc(slug='submission_confirmers',
desc="The people who can confirm a draft submission",
template=None)
rc(slug='submission_group_mail_list',
desc="The people who can confirm a draft submission",
template=None)
rc(slug='doc_non_ietf_stream_manager',
desc="The document's stream manager if the document is not in the IETF stream",
template=None)
rc(slug='rfc_editor_if_doc_in_queue',
desc="The RFC Editor if a document is in the RFC Editor queue",
template=None)
rc(slug='doc_discussing_ads',
desc="Any ADs holding an active DISCUSS position on a given document",
template=None)
rc(slug='group_changed_personnel',
desc="Any personnel who were added or deleted when a group's personnel changes",
template='{{ changed_personnel | join:", " }}')
rc(slug='session_requests',
desc="The session request ticketing system",
template='<session-request@ietf.org>')
rc(slug='session_requester',
desc="The person that requested a meeting slot for a given group",
template=None)
rc(slug='logged_in_person',
desc="The person currently logged into the datatracker who initiated a given action",
template='{% if person and person.email_address %}<{{ person.email_address }}>{% endif %}')
rc(slug='ipr_requests',
desc="The ipr disclosure handling system",
template='<ietf-ipr@ietf.org>')
rc(slug='ipr_submitter',
desc="The submitter of an IPR disclosure",
template='{% if ipr.submitter_email %}{{ ipr.submitter_email }}{% endif %}')
rc(slug='ipr_updatedipr_contacts',
desc="The submitter (or ietf participant if the submitter is not available) "
"of all IPR disclosures updated directly by this disclosure, without recursing "
"to what the updated disclosures might have updated.",
template=None)
rc(slug='ipr_updatedipr_holders',
desc="The holders of all IPR disclosures updated by disclosure and disclosures updated by those and so on.",
template=None)
rc(slug='ipr_announce',
desc="The IETF IPR announce list",
template='ipr-announce@ietf.org')
rc(slug='doc_ipr_group_or_ad',
desc="Leadership for a document that has a new IPR disclosure",
template=None)
rc(slug='liaison_to_contact',
desc="The addresses captured in the To field of the liaison statement form",
template='{{liaison.to_contact}}')
rc(slug='liaison_cc',
desc="The addresses captured in the Cc field of the liaison statement form",
template='{{liaison.cc}}')
rc(slug='liaison_technical_contact',
desc="The addresses captured in the technical contact field of the liaison statement form",
template='{{liaison.technical_contact}}')
rc(slug='liaison_response_contact',
desc="The addresses captured in the response contact field of the liaison statement form",
template='{{liaison.response_contact}}')
rc(slug='liaison_statements_list',
desc="The IETF liaison statement ticketing system",
template='<statements@ietf.org>')
rc(slug='liaison_manager',
desc="The assigned liaison manager for an external group ",
template=None)
rc(slug='nominator',
desc="The person that submitted a nomination to nomcom",
template='{{nominator}}')
rc(slug='nominee',
desc="The person nominated for a position",
template='{{nominee}}')
rc(slug='nomcom_chair',
desc="The chair of a given nomcom",
template='{{nomcom.group.get_chair.email.address}}')
rc(slug='commenter',
desc="The person providing a comment to nomcom",
template='{{commenter}}')
rc(slug='new_work',
desc="The IETF New Work list",
template='<new-work@ietf.org>')
def make_mailtriggers(apps):
Recipient=apps.get_model('mailtrigger','Recipient')
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
def mt_factory(slug,desc,to_slugs,cc_slugs=[]):
# Try to protect ourselves from typos
all_slugs = to_slugs[:]
all_slugs.extend(cc_slugs)
for recipient_slug in all_slugs:
try:
Recipient.objects.get(slug=recipient_slug)
except Recipient.DoesNotExist:
print "****Some rule tried to use",recipient_slug
raise
m = MailTrigger.objects.create(slug=slug, desc=desc)
m.to = Recipient.objects.filter(slug__in=to_slugs)
m.cc = Recipient.objects.filter(slug__in=cc_slugs)
mt_factory(slug='ballot_saved',
desc="Recipients when a new ballot position "
"(with discusses, other blocking positions, "
"or comments) is saved",
to_slugs=['iesg'],
cc_slugs=['doc_notify',
'doc_group_mail_list',
'doc_authors',
'doc_group_chairs',
'doc_shepherd',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
'conflict_review_stream_manager',
]
)
mt_factory(slug='ballot_deferred',
desc="Recipients when a ballot is deferred to "
"or undeferred from a future telechat",
to_slugs=['iesg',
'iesg_secretary',
'doc_group_chairs',
'doc_notify',
'doc_authors',
'doc_shepherd',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
'conflict_review_stream_manager',
],
)
mt_factory(slug='ballot_approved_ietf_stream',
desc="Recipients when an IETF stream document ballot is approved",
to_slugs=['ietf_announce'],
cc_slugs=['iesg',
'doc_notify',
'doc_ad',
'doc_authors',
'doc_shepherd',
'doc_group_mail_list',
'doc_group_chairs',
'rfc_editor',
],
)
mt_factory(slug='ballot_approved_ietf_stream_iana',
desc="Recipients for IANA message when an IETF stream document ballot is approved",
to_slugs=['iana_approve'])
mt_factory(slug='ballot_approved_conflrev',
desc="Recipients when a conflict review ballot is approved",
to_slugs=['conflict_review_stream_manager',
'conflict_review_steering_group',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
'doc_notify',
],
cc_slugs=['iesg',
'ietf_announce',
'iana',
],
)
mt_factory(slug='ballot_approved_charter',
desc="Recipients when a charter is approved",
to_slugs=['ietf_announce',],
cc_slugs=['group_mail_list',
'group_steering_group',
'group_chairs',
'doc_notify',
],
)
mt_factory(slug='ballot_approved_status_change',
desc="Recipients when a status change is approved",
to_slugs=['ietf_announce',],
cc_slugs=['iesg',
'rfc_editor',
'doc_notify',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
],
)
mt_factory(slug='ballot_issued',
desc="Recipients when a ballot is issued",
to_slugs=['iesg',])
mt_factory(slug='ballot_issued_iana',
desc="Recipients for IANA message when a ballot is issued",
to_slugs=['iana_eval',])
mt_factory(slug='last_call_requested',
desc="Recipients when AD requests a last call",
to_slugs=['iesg_secretary',],
cc_slugs=['doc_ad',
'doc_shepherd',
'doc_notify',
],
)
mt_factory(slug='last_call_issued',
desc="Recipients when a last call is issued",
to_slugs=['ietf_announce',],
cc_slugs=['doc_ad',
'doc_shepherd',
'doc_authors',
'doc_notify',
'doc_group_mail_list',
'doc_group_chairs',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
]
)
mt_factory(slug='last_call_issued_iana',
desc="Recipients for IANA message when a last call is issued",
to_slugs=['iana_last_call'])
mt_factory(slug='last_call_expired',
desc="Recipients when a last call has expired",
to_slugs=['doc_ad',
'doc_notify',
'doc_authors',
'doc_shepherd',
],
cc_slugs=['iesg_secretary',],
)
mt_factory(slug='pubreq_iesg',
desc="Recipients when a draft is submitted to the IESG",
to_slugs=['doc_ad',],
cc_slugs=['iesg_secretary',
'doc_notify',
'doc_shepherd',
'doc_group_chairs',
],
)
mt_factory(slug='pubreq_rfced',
desc="Recipients when a non-IETF stream manager requests publication",
to_slugs=['rfc_editor',])
mt_factory(slug='pubreq_rfced_iana',
desc="Recipients for IANA message when a non-IETF stream manager "
"requests publication",
to_slugs=['iana_approve',])
mt_factory(slug='charter_internal_review',
desc="Recipients for message noting that internal review has "
"started on a charter",
to_slugs=['iesg',
'iab',
])
mt_factory(slug='charter_external_review',
desc="Recipients for a charter external review",
to_slugs=['ietf_announce',],
cc_slugs=['group_mail_list',],
)
mt_factory(slug='charter_external_review_new_work',
desc="Recipients for a message to new-work about a charter review",
to_slugs=['new_work',])
mt_factory(slug='conflrev_requested',
desc="Recipients for a stream manager's request for an IETF conflict review",
to_slugs=['iesg_secretary'],
cc_slugs=['iesg',
'doc_notify',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
],
)
mt_factory(slug='conflrev_requested_iana',
desc="Recipients for IANA message when a stream manager requests "
"an IETF conflict review",
to_slugs=['iana_eval',])
mt_factory(slug='doc_stream_changed',
desc="Recipients for notification when a document's stream changes",
to_slugs=['doc_authors',
'stream_managers',
'doc_notify',
])
mt_factory(slug='doc_stream_state_edited',
desc="Recipients when the stream state of a document is manually edited",
to_slugs=['doc_group_chairs',
'doc_group_delegates',
'doc_shepherd',
'doc_authors',
])
mt_factory(slug='group_milestones_edited',
desc="Recipients when any of a group's milestones are edited",
to_slugs=['group_responsible_directors',
'group_chairs',
])
mt_factory(slug='group_approved_milestones_edited',
desc="Recipients when the set of approved milestones for a group are edited",
to_slugs=['group_mail_list',
])
mt_factory(slug='doc_state_edited',
desc="Recipients when a document's state is manually edited",
to_slugs=['doc_notify',
'doc_ad',
'doc_authors',
'doc_shepherd',
'doc_group_chairs',
'doc_affecteddoc_authors',
'doc_group_responsible_directors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
])
mt_factory(slug='doc_iana_state_changed',
desc="Recipients when IANA state information for a document changes ",
to_slugs=['doc_notify',
'doc_ad',
'doc_authors',
'doc_shepherd',
'doc_group_chairs',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
])
mt_factory(slug='doc_telechat_details_changed',
desc="Recipients when a document's telechat date or other "
"telechat specific details are changed",
to_slugs=['iesg',
'iesg_secretary',
'doc_notify',
'doc_authors',
'doc_shepherd',
'doc_group_chairs',
'doc_affecteddoc_authors',
'doc_affecteddoc_group_chairs',
'doc_affecteddoc_notify',
])
mt_factory(slug='doc_pulled_from_rfc_queue',
desc="Recipients when a document is taken out of the RFC's editor queue "
"before publication",
to_slugs=['iana',
'rfc_editor',
],
cc_slugs=['iesg_secretary',
'iesg',
'doc_notify',
'doc_authors',
'doc_shepherd',
'doc_group_chairs',
],
)
mt_factory(slug='doc_replacement_changed',
desc="Recipients when what a document replaces or is replaced by changes",
to_slugs=['doc_authors',
'doc_notify',
'doc_shepherd',
'doc_group_chairs',
'doc_group_responsible_directors',
])
mt_factory(slug='charter_state_edit_admin_needed',
desc="Recipients for message to adminstrators when a charter state edit "
"needs followon administrative action",
to_slugs=['iesg_secretary'])
mt_factory(slug='group_closure_requested',
desc="Recipients for message requesting closure of a group",
to_slugs=['iesg_secretary'])
mt_factory(slug='doc_expires_soon',
desc="Recipients for notification of impending expiration of a document",
to_slugs=['doc_authors'],
cc_slugs=['doc_notify',
'doc_shepherd',
'doc_group_chairs',
'doc_group_responsible_directors',
],
)
mt_factory(slug='doc_expired',
desc="Recipients for notification of a document's expiration",
to_slugs=['doc_authors'],
cc_slugs=['doc_notify',
'doc_shepherd',
'doc_group_chairs',
'doc_group_responsible_directors',
],
)
mt_factory(slug='resurrection_requested',
desc="Recipients of a request to change the state of a draft away from 'Dead'",
to_slugs=['internet_draft_requests',])
mt_factory(slug='resurrection_completed',
desc="Recipients when a draft resurrection request has been completed",
to_slugs=['iesg_secretary',
'doc_ad',
])
mt_factory(slug='sub_manual_post_requested',
desc="Recipients for a manual post request for a draft submission",
to_slugs=['internet_draft_requests',],
cc_slugs=['submission_submitter',
'submission_authors',
'submission_group_chairs',
],
)
mt_factory(slug='sub_chair_approval_requested',
desc="Recipients for a message requesting group chair approval of "
"a draft submission",
to_slugs=['submission_group_chairs',])
mt_factory(slug='sub_confirmation_requested',
desc="Recipients for a message requesting confirmation of a draft submission",
to_slugs=['submission_confirmers',])
mt_factory(slug='sub_management_url_requested',
desc="Recipients for a message with the full URL for managing a draft submission",
to_slugs=['submission_confirmers',])
mt_factory(slug='sub_announced',
desc="Recipients for the announcement of a successfully submitted draft",
to_slugs=['ietf_announce',
],
cc_slugs=['submission_group_mail_list',
],
)
mt_factory(slug='sub_announced_to_authors',
desc="Recipients for the announcement to the authors of a successfully "
"submitted draft",
to_slugs=['submission_authors',
'submission_confirmers',
])
mt_factory(slug='sub_new_version',
desc="Recipients for notification of a new version of an existing document",
to_slugs=['doc_notify',
'doc_ad',
'doc_non_ietf_stream_manager',
'rfc_editor_if_doc_in_queue',
'doc_discussing_ads',
])
mt_factory(slug='group_personnel_change',
desc="Recipients for a message noting changes in a group's personnel",
to_slugs=['iesg_secretary',
'group_responsible_directors',
'group_chairs',
'group_changed_personnel',
])
mt_factory(slug='session_requested',
desc="Recipients for a normal meeting session request",
to_slugs=['session_requests', ],
cc_slugs=['group_mail_list',
'group_chairs',
'group_responsible_directors',
'logged_in_person',
],
)
mt_factory(slug='session_requested_long',
desc="Recipients for a meeting session request for more than 2 sessions",
to_slugs=['group_responsible_directors', ],
cc_slugs=['session_requests',
'group_chairs',
'logged_in_person',
],
)
mt_factory(slug='session_request_cancelled',
desc="Recipients for a message cancelling a session request",
to_slugs=['session_requests', ],
cc_slugs=['group_mail_list',
'group_chairs',
'group_responsible_directors',
'logged_in_person',
],
)
mt_factory(slug='session_request_not_meeting',
desc="Recipients for a message noting a group plans to not meet",
to_slugs=['session_requests', ],
cc_slugs=['group_mail_list',
'group_chairs',
'group_responsible_directors',
'logged_in_person',
],
)
mt_factory(slug='session_scheduled',
desc="Recipients for details when a session has been scheduled",
to_slugs=['session_requester',
'group_chairs',
],
cc_slugs=['group_mail_list',
'group_responsible_directors',
],
)
mt_factory(slug='ipr_disclosure_submitted',
desc="Recipients when an IPR disclosure is submitted",
to_slugs=['ipr_requests', ])
mt_factory(slug='ipr_disclosure_followup',
desc="Recipients when the secretary follows up on an IPR disclosure submission",
to_slugs=['ipr_submitter', ],)
mt_factory(slug='ipr_posting_confirmation',
desc="Recipients for a message confirming that a disclosure has been posted",
to_slugs=['ipr_submitter', ],
cc_slugs=['ipr_updatedipr_contacts',
'ipr_updatedipr_holders',
],
)
mt_factory(slug='ipr_posted_on_doc',
desc="Recipients when an IPR disclosure calls out a given document",
to_slugs=['doc_authors', ],
cc_slugs=['doc_ipr_group_or_ad',
'ipr_announce',
],
)
mt_factory(slug='liaison_statement_posted',
desc="Recipient for a message when a new liaison statement is posted",
to_slugs=['liaison_to_contact', ],
cc_slugs=['liaison_cc',
'liaison_technical_contact',
'liaison_response_contact',
],
)
mt_factory(slug='liaison_approval_requested',
desc="Recipients for a message that a pending liaison statement needs approval",
to_slugs=['liaison_statements_list',
])
mt_factory(slug='liaison_deadline_soon',
desc="Recipients for a message about a liaison statement deadline that is "
"approaching.",
to_slugs=['liaison_to_contact',
],
cc_slugs=['liaison_cc',
'liaison_technical_contact',
'liaison_response_contact',
],
)
mt_factory(slug='liaison_manager_update_request',
desc="Recipients for a message requesting an updated list of authorized individuals",
to_slugs=['liaison_manager', ])
mt_factory(slug='nomination_received',
desc="Recipients for a message noting a new nomination has been received",
to_slugs=['nomcom_chair', ])
mt_factory(slug='nomination_receipt_requested',
desc="Recipients for a message confirming a nomination was made",
to_slugs=['nominator', ])
mt_factory(slug='nomcom_comment_receipt_requested',
desc="Recipients for a message confirming a comment was made",
to_slugs=['commenter', ])
mt_factory(slug='nomination_created_person',
desc="Recipients for a message noting that a nomination caused a "
"new Person record to be created in the datatracker",
to_slugs=['ietf_secretariat',
'nomcom_chair',
],
)
mt_factory(slug='nomination_new_nominee',
desc="Recipients the first time a person is nominated for a position, "
"asking them to accept or decline the nomination",
to_slugs=['nominee', ])
mt_factory(slug='nomination_accept_reminder',
desc="Recipeints of message reminding a nominee to accept or decline a nomination",
to_slugs=['nominee', ])
mt_factory(slug='nomcom_questionnaire',
desc="Recipients for the questionairre that nominees should complete",
to_slugs=['nominee', ])
mt_factory(slug='nomcom_questionnaire_reminder',
desc="Recipients for a message reminding a nominee to return a "
"completed questionairre response",
to_slugs=['nominee', ])
mt_factory(slug='doc_replacement_suggested',
desc="Recipients for suggestions that this doc replaces or is replace by "
"some other document",
to_slugs=['doc_group_chairs',
'doc_group_responsible_directors',
'doc_non_ietf_stream_manager',
'iesg_secretary',
])
mt_factory(slug='doc_adopted_by_group',
desc="Recipients for notification that a document has been adopted by a group",
to_slugs=['doc_authors',
'doc_group_chairs',
'doc_group_mail_list',
],
cc_slugs=['doc_ad',
'doc_shepherd',
'doc_notify',
],
)
mt_factory(slug='doc_added_comment',
desc="Recipients for a message when a new comment is manually entered into the document's history",
to_slugs=['doc_authors',
'doc_group_chairs',
'doc_shepherd',
'doc_group_responsible_directors',
'doc_non_ietf_stream_manager',
])
mt_factory(slug='doc_intended_status_changed',
desc="Recipients for a message when a document's intended "
"publication status changes",
to_slugs=['doc_authors',
'doc_group_chairs',
'doc_shepherd',
'doc_group_responsible_directors',
'doc_non_ietf_stream_manager',
])
mt_factory(slug='doc_iesg_processing_started',
desc="Recipients for a message when the IESG begins processing a document ",
to_slugs=['doc_authors',
'doc_ad',
'doc_shepherd',
'doc_group_chairs',
])
def forward(apps, schema_editor):
make_recipients(apps)
make_mailtriggers(apps)
def reverse(apps, schema_editor):
Recipient=apps.get_model('mailtrigger','Recipient')
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
Recipient.objects.all().delete()
MailTrigger.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('mailtrigger', '0001_initial'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

262
ietf/mailtrigger/models.py Normal file
View file

@ -0,0 +1,262 @@
# Copyright The IETF Trust 2015, All Rights Reserved
from django.db import models
from django.template import Template, Context
from ietf.group.models import Role
class MailTrigger(models.Model):
slug = models.CharField(max_length=32, primary_key=True)
desc = models.TextField(blank=True)
to = models.ManyToManyField('Recipient', null=True, blank=True, related_name='used_in_to')
cc = models.ManyToManyField('Recipient', null=True, blank=True, related_name='used_in_cc')
class Meta:
ordering = ["slug"]
def __unicode__(self):
return self.slug
class Recipient(models.Model):
slug = models.CharField(max_length=32, primary_key=True)
desc = models.TextField(blank=True)
template = models.TextField(null=True, blank=True)
class Meta:
ordering = ["slug"]
def __unicode__(self):
return self.slug
def gather(self, **kwargs):
retval = []
if hasattr(self,'gather_%s'%self.slug):
retval.extend(eval('self.gather_%s(**kwargs)'%self.slug))
if self.template:
rendering = Template('{%% autoescape off %%}%s{%% endautoescape %%}'%self.template).render(Context(kwargs))
if rendering:
retval.extend([x.strip() for x in rendering.split(',')])
retval = list(set(retval))
return retval
def gather_doc_group_chairs(self, **kwargs):
addrs = []
if 'doc' in kwargs:
doc=kwargs['doc']
if doc.group and doc.group.type.slug in ['wg','rg']:
addrs.append('%s-chairs@ietf.org'%doc.group.acronym)
return addrs
def gather_doc_group_delegates(self, **kwargs):
addrs = []
if 'doc' in kwargs:
doc=kwargs['doc']
if doc.group and doc.group.type.slug in ['wg','rg']:
addrs.extend(doc.group.role_set.filter(name='delegate').values_list('email__address',flat=True))
return addrs
def gather_doc_group_mail_list(self, **kwargs):
addrs = []
if 'doc' in kwargs:
doc=kwargs['doc']
if doc.group.type.slug in ['wg','rg']:
if doc.group.list_email:
addrs.append(doc.group.list_email)
return addrs
def gather_doc_affecteddoc_authors(self, **kwargs):
addrs = []
if 'doc' in kwargs:
for reldoc in kwargs['doc'].related_that_doc(['conflrev','tohist','tois','tops']):
addrs.extend(Recipient.objects.get(slug='doc_authors').gather(**{'doc':reldoc.document}))
return addrs
def gather_doc_affecteddoc_group_chairs(self, **kwargs):
addrs = []
if 'doc' in kwargs:
for reldoc in kwargs['doc'].related_that_doc(['conflrev','tohist','tois','tops']):
addrs.extend(Recipient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc.document}))
return addrs
def gather_doc_affecteddoc_notify(self, **kwargs):
addrs = []
if 'doc' in kwargs:
for reldoc in kwargs['doc'].related_that_doc(['conflrev','tohist','tois','tops']):
addrs.extend(Recipient.objects.get(slug='doc_notify').gather(**{'doc':reldoc.document}))
return addrs
def gather_conflict_review_stream_manager(self, **kwargs):
addrs = []
if 'doc' in kwargs:
for reldoc in kwargs['doc'].related_that_doc(['conflrev']):
addrs.extend(Recipient.objects.get(slug='doc_stream_manager').gather(**{'doc':reldoc.document}))
return addrs
def gather_conflict_review_steering_group(self,**kwargs):
addrs = []
if 'doc' in kwargs:
for reldoc in kwargs['doc'].related_that_doc(['conflrev']):
if reldoc.document.stream_id=='irsg':
addrs.append('"Internet Research Steering Group" <irsg@ietf.org>')
return addrs
def gather_group_steering_group(self,**kwargs):
addrs = []
sg_map = dict( wg='"The IESG" <iesg@ietf.org>', rg='"Internet Research Steering Group" <irsg@ietf.org>' )
if 'group' in kwargs and kwargs['group'].type_id in sg_map:
addrs.append(sg_map[kwargs['group'].type_id])
return addrs
def gather_stream_managers(self, **kwargs):
addrs = []
manager_map = dict(ise = '<rfc-ise@rfc-editor.org>',
irtf = '<irtf-chair@irtf.org>',
ietf = '<iesg@ietf.org>',
iab = '<iab-chair@iab.org>')
if 'streams' in kwargs:
for stream in kwargs['streams']:
if stream in manager_map:
addrs.append(manager_map[stream])
return addrs
def gather_doc_stream_manager(self, **kwargs):
addrs = []
if 'doc' in kwargs:
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':[kwargs['doc'].stream_id]}))
return addrs
def gather_doc_non_ietf_stream_manager(self, **kwargs):
addrs = []
if 'doc' in kwargs:
doc = kwargs['doc']
if doc.stream_id and doc.stream_id != 'ietf':
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':[doc.stream_id,]}))
return addrs
def gather_group_responsible_directors(self, **kwargs):
addrs = []
if 'group' in kwargs:
group = kwargs['group']
if not group.acronym=='none':
addrs.extend(group.role_set.filter(name='ad').values_list('email__address',flat=True))
if group.type_id=='rg':
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':['irtf']}))
return addrs
def gather_doc_group_responsible_directors(self, **kwargs):
addrs = []
if 'doc' in kwargs:
group = kwargs['doc'].group
if group and not group.acronym=='none':
addrs.extend(Recipient.objects.get(slug='group_responsible_directors').gather(**{'group':group}))
return addrs
def gather_submission_authors(self, **kwargs):
addrs = []
if 'submission' in kwargs:
submission = kwargs['submission']
addrs.extend(["%s <%s>" % (author["name"], author["email"]) for author in submission.authors_parsed() if author["email"]])
return addrs
def gather_submission_group_chairs(self, **kwargs):
addrs = []
if 'submission' in kwargs:
submission = kwargs['submission']
if submission.group:
addrs.extend(Recipient.objects.get(slug='group_chairs').gather(**{'group':submission.group}))
return addrs
def gather_submission_confirmers(self, **kwargs):
"""If a submitted document is revising an existing document, the confirmers
are the authors of that existing document. Otherwise, the confirmers
are the authors and submitter of the submitted document."""
addrs=[]
if 'submission' in kwargs:
submission = kwargs['submission']
doc=submission.existing_document()
if doc:
addrs.extend([i.author.formatted_email() for i in doc.documentauthor_set.all() if not i.author.invalid_address()])
else:
addrs.extend([u"%s <%s>" % (author["name"], author["email"]) for author in submission.authors_parsed() if author["email"]])
if submission.submitter_parsed()["email"]:
addrs.append(submission.submitter)
return addrs
def gather_submission_group_mail_list(self, **kwargs):
addrs=[]
if 'submission' in kwargs:
submission = kwargs['submission']
if submission.group:
addrs.extend(Recipient.objects.get(slug='group_mail_list').gather(**{'group':submission.group}))
return addrs
def gather_rfc_editor_if_doc_in_queue(self, **kwargs):
addrs=[]
if 'doc' in kwargs:
doc = kwargs['doc']
if doc.get_state_slug("draft-rfceditor") is not None:
addrs.extend(Recipient.objects.get(slug='rfc_editor').gather(**{}))
return addrs
def gather_doc_discussing_ads(self, **kwargs):
addrs=[]
if 'doc' in kwargs:
doc = kwargs['doc']
active_ballot = doc.active_ballot()
if active_ballot:
for ad, pos in active_ballot.active_ad_positions().iteritems():
if pos and pos.pos_id == "discuss":
addrs.append(ad.role_email("ad").address)
return addrs
def gather_ipr_updatedipr_contacts(self, **kwargs):
addrs=[]
if 'ipr' in kwargs:
ipr = kwargs['ipr']
for rel in ipr.updates:
if rel.target.submitter_email:
addrs.append(rel.target.submitter_email)
elif hasattr(rel.target,'ietfer_email') and rel.target.ietfer_email:
addrs.append(rel.target.ietfer_email)
return addrs
def gather_ipr_updatedipr_holders(self, **kwargs):
addrs=[]
if 'ipr' in kwargs:
ipr = kwargs['ipr']
for disc in ipr.recursively_updates():
if hasattr(ipr,'holder_contact_email') and ipr.holder_contact_email:
addrs.append(ipr.holder_contact_email)
return addrs
def gather_doc_ipr_group_or_ad(self, **kwargs):
"""A document's group email list if the document is a group document,
otherwise, the document's AD if the document is active, otherwise
the IETF chair"""
addrs=[]
if 'doc' in kwargs:
doc=kwargs['doc']
if doc.group and doc.group.acronym == 'none':
if doc.ad and doc.get_state_slug('draft')=='active':
addrs.extend(Recipient.objects.get(slug='doc_ad').gather(**kwargs))
else:
addrs.extend(Role.objects.filter(group__acronym='gen',name='ad').values_list('email__address',flat=True))
else:
addrs.extend(Recipient.objects.get(slug='doc_group_mail_list').gather(**kwargs))
return addrs
def gather_liaison_manager(self, **kwargs):
addrs=[]
if 'group' in kwargs:
group=kwargs['group']
addrs.extend(group.role_set.filter(name='liaiman').values_list('email__address',flat=True))
return addrs
def gather_session_requester(self, **kwargs):
addrs=[]
if 'session' in kwargs:
session = kwargs['session']
addrs.append(session.requested_by.role_email('chair').address)
return addrs

View file

@ -0,0 +1,35 @@
# Autogenerated by the makeresources management command 2015-08-06 11:00 PDT
from tastypie.resources import ModelResource
from tastypie.fields import ToOneField, ToManyField # pyflakes:ignore
from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore
from ietf import api
from ietf.mailtrigger.models import * # pyflakes:ignore
class RecipientResource(ModelResource):
class Meta:
queryset = Recipient.objects.all()
#resource_name = 'recipient'
filtering = {
"slug": ALL,
"desc": ALL,
"template": ALL,
}
api.mailtrigger.register(RecipientResource())
class MailTriggerResource(ModelResource):
to = ToManyField(RecipientResource, 'to', null=True)
cc = ToManyField(RecipientResource, 'cc', null=True)
class Meta:
queryset = MailTrigger.objects.all()
#resource_name = 'mailtrigger'
filtering = {
"slug": ALL,
"desc": ALL,
"to": ALL_WITH_RELATIONS,
"cc": ALL_WITH_RELATIONS,
}
api.mailtrigger.register(MailTriggerResource())

34
ietf/mailtrigger/tests.py Normal file
View file

@ -0,0 +1,34 @@
from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.test_utils import TestCase
from ietf.utils.test_data import make_test_data
class EventMailTests(TestCase):
def setUp(self):
make_test_data()
def test_show_triggers(self):
url = urlreverse('ietf.mailtrigger.views.show_triggers')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue('ballot_saved' in r.content)
url = urlreverse('ietf.mailtrigger.views.show_triggers',kwargs=dict(mailtrigger_slug='ballot_saved'))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue('ballot_saved' in r.content)
def test_show_recipients(self):
url = urlreverse('ietf.mailtrigger.views.show_recipients')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue('doc_group_mail_list' in r.content)
url = urlreverse('ietf.mailtrigger.views.show_recipients',kwargs=dict(recipient_slug='doc_group_mail_list'))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue('doc_group_mail_list' in r.content)

11
ietf/mailtrigger/urls.py Normal file
View file

@ -0,0 +1,11 @@
from django.conf.urls import patterns, url
from django.views.generic import RedirectView
from django.core.urlresolvers import reverse_lazy
urlpatterns = patterns('ietf.mailtrigger.views',
url(r'^$', RedirectView.as_view(url=reverse_lazy('mailtrigger_show_triggers'), permanent=True)),
url(r'^name/$', 'show_triggers', name='mailtrigger_show_triggers' ),
url(r'^name/(?P<mailtrigger_slug>[-\w]+)/$', 'show_triggers' ),
url(r'^recipient/$', 'show_recipients' ),
url(r'^recipient/(?P<recipient_slug>[-\w]+)/$', 'show_recipients' ),
)

86
ietf/mailtrigger/utils.py Normal file
View file

@ -0,0 +1,86 @@
from collections import namedtuple
from ietf.mailtrigger.models import MailTrigger, Recipient
from ietf.submit.models import Submission
class AddrLists(namedtuple('AddrLists',['to','cc'])):
__slots__ = ()
def as_strings(self,compact=True):
separator = ", " if compact else ",\n "
to_string = separator.join(self.to)
cc_string = separator.join(self.cc)
return namedtuple('AddrListsAsStrings',['to','cc'])(to=to_string,cc=cc_string)
def gather_address_lists(slug, **kwargs):
mailtrigger = MailTrigger.objects.get(slug=slug)
to = set()
for recipient in mailtrigger.to.all():
to.update(recipient.gather(**kwargs))
to.discard('')
cc = set()
for recipient in mailtrigger.cc.all():
cc.update(recipient.gather(**kwargs))
cc.discard('')
return AddrLists(to=list(to),cc=list(cc))
def gather_relevant_expansions(**kwargs):
def starts_with(prefix):
return MailTrigger.objects.filter(slug__startswith=prefix).values_list('slug',flat=True)
relevant = set()
if 'doc' in kwargs:
doc = kwargs['doc']
relevant.update(['doc_state_edited','doc_telechat_details_changed','ballot_deferred','ballot_saved'])
if doc.type_id in ['draft','statchg']:
relevant.update(starts_with('last_call_'))
if doc.type_id == 'draft':
relevant.update(starts_with('doc_'))
relevant.update(starts_with('resurrection_'))
relevant.update(['ipr_posted_on_doc',])
if doc.stream_id == 'ietf':
relevant.update(['ballot_approved_ietf_stream','pubreq_iesg'])
else:
relevant.update(['pubreq_rfced'])
last_submission = Submission.objects.filter(name=doc.name,state='posted').order_by('-rev').first()
if last_submission and 'submission' not in kwargs:
kwargs['submission'] = last_submission
if doc.type_id == 'conflrev':
relevant.update(['conflrev_requested','ballot_approved_conflrev'])
if doc.type_id == 'charter':
relevant.update(['charter_external_review','ballot_approved_charter'])
if 'group' in kwargs:
relevant.update(starts_with('group_'))
relevant.update(starts_with('milestones_'))
relevant.update(starts_with('session_'))
relevant.update(['charter_external_review',])
if 'submission' in kwargs:
relevant.update(starts_with('sub_'))
rule_list = []
for mailtrigger in MailTrigger.objects.filter(slug__in=relevant):
addrs = gather_address_lists(mailtrigger.slug,**kwargs)
if addrs.to or addrs.cc:
rule_list.append((mailtrigger.slug,mailtrigger.desc,addrs.to,addrs.cc))
return sorted(rule_list)
def get_base_ipr_request_address():
return Recipient.objects.get(slug='ipr_requests').gather()[0]

26
ietf/mailtrigger/views.py Normal file
View file

@ -0,0 +1,26 @@
# Copyright The IETF Trust 2015, All Rights Reserved
from inspect import getsourcelines
from django.shortcuts import render, get_object_or_404
from ietf.mailtrigger.models import MailTrigger, Recipient
def show_triggers(request, mailtrigger_slug=None):
mailtriggers = MailTrigger.objects.all()
if mailtrigger_slug:
get_object_or_404(MailTrigger,slug=mailtrigger_slug)
mailtriggers = mailtriggers.filter(slug=mailtrigger_slug)
return render(request,'mailtrigger/trigger.html',{'mailtrigger_slug':mailtrigger_slug,
'mailtriggers':mailtriggers})
def show_recipients(request, recipient_slug=None):
recipients = Recipient.objects.all()
if recipient_slug:
get_object_or_404(Recipient,slug=recipient_slug)
recipients = recipients.filter(slug=recipient_slug)
for recipient in recipients:
fname = 'gather_%s'%recipient.slug
if hasattr(recipient,fname):
recipient.code = ''.join(getsourcelines(getattr(recipient,fname))[0])
return render(request,'mailtrigger/recipient.html',{'recipient_slug':recipient_slug,
'recipients':recipients})

File diff suppressed because it is too large Load diff

View file

@ -41,5 +41,9 @@ objects += ietf.doc.models.StateType.objects.all()
objects += ietf.doc.models.State.objects.all()
objects += ietf.doc.models.BallotType.objects.all()
import ietf.mailtrigger.models
objects += ietf.mailtrigger.models.Recipient.objects.all()
objects += ietf.mailtrigger.models.MailTrigger.objects.all()
output("names", objects)

View file

@ -20,6 +20,7 @@ from ietf.person.models import Email
from ietf.person.fields import SearchableEmailField
from ietf.utils.fields import MultiEmailField
from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists
ROLODEX_URL = getattr(settings, 'ROLODEX_URL', None)
@ -413,12 +414,12 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
if author:
subject = 'Nomination receipt'
from_email = settings.NOMCOM_FROM_EMAIL
to_email = author.address
(to_email, cc) = gather_address_lists('nomination_receipt_requested',nominator=author.address)
context = {'nominee': nominee.email.person.name,
'comments': comments,
'position': position.name}
path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context)
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
return nomination
@ -531,12 +532,12 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm):
if author:
subject = "NomCom comment confirmation"
from_email = settings.NOMCOM_FROM_EMAIL
to_email = author.address
(to_email, cc) = gather_address_lists('nomcom_comment_receipt_requested',commenter=author.address)
context = {'nominee': self.nominee.email.person.name,
'comments': comments,
'position': self.position.name}
path = nomcom_template_path + FEEDBACK_RECEIPT_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context)
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
class Meta:
model = Feedback

View file

@ -14,7 +14,7 @@ from django.contrib.auth.models import User
import debug # pyflakes:ignore
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.person.models import Email, Person
from ietf.group.models import Group
@ -466,19 +466,60 @@ class NomcomViewsTest(TestCase):
def test_public_nominate(self):
login_testing_unauthorized(self, COMMUNITY_USER, self.public_nominate_url)
return self.nominate_view(public=True)
self.client.logout()
messages_before = len(outbox)
self.nominate_view(public=True,confirmation=True)
self.assertEqual(len(outbox), messages_before + 4)
self.assertTrue('New person' in outbox[-4]['Subject'])
self.assertTrue('nomcomchair' in outbox[-4]['To'])
self.assertTrue('secretariat' in outbox[-4]['To'])
self.assertEqual('IETF Nomination Information', outbox[-3]['Subject'])
self.assertTrue('nominee' in outbox[-3]['To'])
self.assertEqual('Nomination Information', outbox[-2]['Subject'])
self.assertTrue('nomcomchair' in outbox[-2]['To'])
self.assertEqual('Nomination receipt', outbox[-1]['Subject'])
self.assertTrue('plain' in outbox[-1]['To'])
self.assertTrue(u'Comments with accents äöå' in unicode(outbox[-1].get_payload(decode=True),"utf-8","replace"))
# Nominate the same person for the same position again without asking for confirmation
messages_before = len(outbox)
self.nominate_view(public=True)
self.assertEqual(len(outbox), messages_before + 1)
self.assertEqual('Nomination Information', outbox[-1]['Subject'])
self.assertTrue('nomcomchair' in outbox[-1]['To'])
def test_private_nominate(self):
self.access_member_url(self.private_nominate_url)
return self.nominate_view(public=False)
self.client.logout()
def test_public_nominate_with_automatic_questionnaire(self):
nomcom = get_nomcom_by_year(self.year)
nomcom.send_questionnaire = True
nomcom.save()
login_testing_unauthorized(self, COMMUNITY_USER, self.public_nominate_url)
empty_outbox()
self.nominate_view(public=True)
self.assertEqual(len(outbox), 4)
# test_public_nominate checks the other messages
self.assertTrue('Questionnaire' in outbox[2]['Subject'])
self.assertTrue('nominee@' in outbox[2]['To'])
def nominate_view(self, *args, **kwargs):
public = kwargs.pop('public', True)
nominee_email = kwargs.pop('nominee_email', u'nominee@example.com')
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
position_name = kwargs.pop('position', 'IAOC')
confirmation = kwargs.pop('confirmation', False)
if public:
nominate_url = self.public_nominate_url
@ -511,7 +552,8 @@ class NomcomViewsTest(TestCase):
'candidate_email': candidate_email,
'candidate_phone': candidate_phone,
'position': position.id,
'comments': comments}
'comments': comments,
'confirmation': confirmation}
if not public:
test_data['nominator_email'] = nominator_email
@ -599,8 +641,20 @@ class NomcomViewsTest(TestCase):
def test_public_feedback(self):
login_testing_unauthorized(self, COMMUNITY_USER, self.public_feedback_url)
return self.feedback_view(public=True)
self.client.logout()
empty_outbox()
self.feedback_view(public=True,confirmation=True)
# feedback_view does a nomination internally: there is a lot of email related to that - tested elsewhere
# We're interested in the confirmation receipt here
self.assertEqual(len(outbox),4)
self.assertEqual('NomCom comment confirmation', outbox[3]['Subject'])
self.assertTrue('plain' in outbox[3]['To'])
self.assertTrue(u'Comments with accents äöå' in unicode(outbox[3].get_payload(decode=True),"utf-8","replace"))
empty_outbox()
self.feedback_view(public=True)
self.assertEqual(len(outbox),1)
self.assertFalse('confirmation' in outbox[0]['Subject'])
def test_private_feedback(self):
self.access_member_url(self.private_feedback_url)
@ -612,6 +666,7 @@ class NomcomViewsTest(TestCase):
nominee_email = kwargs.pop('nominee_email', u'nominee@example.com')
nominator_email = kwargs.pop('nominator_email', "%s%s" % (COMMUNITY_USER, EMAIL_DOMAIN))
position_name = kwargs.pop('position', 'IAOC')
confirmation = kwargs.pop('confirmation', False)
self.nominate_view(public=public,
nominee_email=nominee_email,
@ -645,7 +700,8 @@ class NomcomViewsTest(TestCase):
test_data = {'comments': comments,
'position_name': position.name,
'nominee_name': nominee.email.person.name,
'nominee_email': nominee.email.address}
'nominee_email': nominee.email.address,
'confirmation': confirmation}
if public:
test_data['nominator_email'] = nominator_email
@ -854,6 +910,8 @@ class ReminderTest(TestCase):
response = self.client.post(url, test_data)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(outbox), messages_before + 2)
self.assertTrue('nominee1@' in outbox[-2]['To'])
self.assertTrue('nominee2@' in outbox[-1]['To'])
def test_remind_questionnaire_view(self):
url = reverse('nomcom_send_reminder_mail', kwargs={'year': NOMCOM_YEAR,'type':'questionnaire'})
@ -863,4 +921,5 @@ class ReminderTest(TestCase):
response = self.client.post(url, test_data)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(outbox), messages_before + 1)
self.assertTrue('nominee1@' in outbox[-1]['To'])

View file

@ -18,6 +18,7 @@ from django.utils.encoding import smart_str
from ietf.dbtemplate.models import DBTemplate
from ietf.person.models import Email, Person
from ietf.mailtrigger.utils import gather_address_lists
from ietf.utils.pipe import pipe
from ietf.utils import unaccent
from ietf.utils.mail import send_mail_text, send_mail
@ -207,7 +208,7 @@ def send_accept_reminder_to_nominee(nominee_position):
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_ACCEPT_REMINDER_TEMPLATE
nominee = nominee_position.nominee
to_email = nominee.email.address
(to_email, cc) = gather_address_lists('nomination_accept_reminder',nominee=nominee.email.address)
hash = get_hash_nominee_position(today, nominee_position.id)
accept_url = reverse('nomcom_process_nomination_status',
@ -233,7 +234,7 @@ def send_accept_reminder_to_nominee(nominee_position):
body = render_to_string(mail_path, context)
path = '%s%d/%s' % (nomcom_template_path, position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body)
send_mail_text(None, to_email, from_email, subject, body, cc=cc)
def send_questionnaire_reminder_to_nominee(nominee_position):
subject = 'Reminder: please complete the Nomcom questionnaires for your nomination.'
@ -244,7 +245,7 @@ def send_questionnaire_reminder_to_nominee(nominee_position):
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
mail_path = nomcom_template_path + NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE
nominee = nominee_position.nominee
to_email = nominee.email.address
(to_email,cc) = gather_address_lists('nomcom_questionnaire_reminder',nominee=nominee.email.address)
context = {'nominee': nominee,
'position': position,
@ -253,7 +254,7 @@ def send_questionnaire_reminder_to_nominee(nominee_position):
body = render_to_string(mail_path, context)
path = '%s%d/%s' % (nomcom_template_path, position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body)
send_mail_text(None, to_email, from_email, subject, body, cc=cc)
def send_reminder_to_nominees(nominees,type):
addrs = []
@ -274,8 +275,6 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
from ietf.nomcom.models import Nominee, NomineePosition
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
nomcom_chair = nomcom.group.get_chair()
nomcom_chair_mail = nomcom_chair and nomcom_chair.email.address or None
# Create person and email if candidate email does't exist and send email
email, created_email = Email.objects.get_or_create(address=candidate_email)
@ -296,20 +295,18 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
# send email to secretariat and nomcomchair to warn about the new person
subject = 'New person is created'
from_email = settings.NOMCOM_FROM_EMAIL
to_email = [settings.NOMCOM_ADMIN_EMAIL]
(to_email, cc) = gather_address_lists('nomination_created_person',nomcom=nomcom)
context = {'email': email.address,
'fullname': email.person.name,
'person_id': email.person.id}
path = nomcom_template_path + INEXISTENT_PERSON_TEMPLATE
if nomcom_chair_mail:
to_email.append(nomcom_chair_mail)
send_mail(None, to_email, from_email, subject, path, context)
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
if nominee_position_created:
# send email to nominee
subject = 'IETF Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL
to_email = email.address
(to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=email.address)
domain = Site.objects.get_current().domain
today = datetime.date.today().strftime('%Y%m%d')
hash = get_hash_nominee_position(today, nominee_position.id)
@ -335,13 +332,13 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
'decline_url': decline_url}
path = nomcom_template_path + NOMINEE_EMAIL_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context)
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
# send email to nominee with questionnaire
if nomcom.send_questionnaire:
subject = '%s Questionnaire' % position
from_email = settings.NOMCOM_FROM_EMAIL
to_email = email.address
(to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=email.address)
context = {'nominee': email.person.name,
'position': position.name}
path = '%s%d/%s' % (nomcom_template_path,
@ -350,12 +347,12 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
path = '%s%d/%s' % (nomcom_template_path,
position.id, QUESTIONNAIRE_TEMPLATE)
body += '\n\n%s' % render_to_string(path, context)
send_mail_text(None, to_email, from_email, subject, body)
send_mail_text(None, to_email, from_email, subject, body, cc=cc)
# send emails to nomcom chair
subject = 'Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL
to_email = nomcom_chair_mail
(to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom)
context = {'nominee': email.person.name,
'nominee_email': email.address,
'position': position.name}
@ -368,7 +365,7 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
'nominator_email': ''})
path = nomcom_template_path + NOMINATION_EMAIL_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context)
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
return nominee

View file

@ -25,8 +25,8 @@ from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm,
from ietf.secr.proceedings.views import build_choices, handle_upload_file, make_directories
from ietf.secr.sreq.forms import GroupSelectForm
from ietf.secr.sreq.views import get_initial_session
from ietf.secr.utils.mail import get_cc_list
from ietf.secr.utils.meeting import get_session, get_timeslot
from ietf.mailtrigger.utils import gather_address_lists
# prep for agenda changes
@ -188,9 +188,7 @@ def send_notifications(meeting, groups, person):
now = datetime.datetime.now()
for group in groups:
sessions = group.session_set.filter(meeting=meeting)
to_email = sessions[0].requested_by.role_email('chair').address
# TODO confirm list, remove requested_by from cc, add session-request@ietf.org?
cc_list = get_cc_list(group)
addrs = gather_address_lists('session_scheduled',group=group,session=sessions[0])
from_email = ('"IETF Secretariat"','agenda@ietf.org')
if len(sessions) == 1:
subject = '%s - Requested session has been scheduled for IETF %s' % (group.acronym, meeting.number)
@ -223,12 +221,12 @@ def send_notifications(meeting, groups, person):
context['login'] = sessions[0].requested_by
send_mail(None,
to_email,
addrs.to,
from_email,
subject,
template,
context,
cc=cc_list)
cc=addrs.cc)
# create sent_notification event
GroupEvent.objects.create(group=group,time=now,type='sent_notification',

View file

@ -5,6 +5,7 @@ from ietf.group.models import Group
#from ietf.meeting.models import Session
#from ietf.utils.test_data import make_test_data
from ietf.meeting.test_data import make_meeting_test_data as make_test_data
from ietf.utils.mail import outbox, empty_outbox
from pyquery import PyQuery
@ -108,6 +109,8 @@ class NotMeetingCase(TestCase):
url = reverse('sessions_no_session',kwargs={'acronym':group.acronym})
self.client.login(username="secretary", password="secretary+password")
empty_outbox()
r = self.client.get(url,follow=True)
# If the view invoked by that get throws an exception (such as an integrity error),
# the traceback from this test will talk about a TransactionManagementError and
@ -121,6 +124,10 @@ class NotMeetingCase(TestCase):
self.assertEqual(r.status_code, 200)
self.assertTrue('is already marked as not meeting' in r.content)
self.assertEqual(len(outbox),1)
self.assertTrue('Not having a session' in outbox[0]['Subject'])
self.assertTrue('session-request@' in outbox[0]['To'])
class RetrievePreviousCase(TestCase):
pass

View file

@ -14,13 +14,14 @@ from ietf.name.models import SessionStatusName, ConstraintName
from ietf.secr.sreq.forms import SessionForm, GroupSelectForm, ToolStatusForm
from ietf.secr.utils.decorators import check_permissions
from ietf.secr.utils.group import groups_by_session
from ietf.secr.utils.mail import get_ad_email_list, get_chair_email_list, get_cc_list
from ietf.utils.mail import send_mail
from ietf.person.models import Person
from ietf.mailtrigger.utils import gather_address_lists
# -------------------------------------------------
# Globals
# -------------------------------------------------
#TODO: DELETE
SESSION_REQUEST_EMAIL = 'session-request@ietf.org'
AUTHORIZED_ROLES=('WG Chair','WG Secretary','RG Chair','IAB Group Chair','Area Director','Secretariat','Team Chair','IRTF Chair')
@ -112,8 +113,7 @@ def send_notification(group,meeting,login,session,action):
session argument is a dictionary of fields from the session request form
action argument is a string [new|update].
'''
to_email = SESSION_REQUEST_EMAIL
cc_list = get_cc_list(group, login)
(to_email, cc_list) = gather_address_lists('session_requested',group=group,person=login)
from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org')
subject = '%s - New Meeting Session Request for IETF %s' % (group.acronym, meeting.number)
template = 'sreq/session_request_notification.txt'
@ -136,11 +136,7 @@ def send_notification(group,meeting,login,session,action):
# change headers TO=ADs, CC=session-request, submitter and cochairs
if session.get('length_session3',None):
context['session']['num_session'] = 3
to_email = get_ad_email_list(group)
cc_list = get_chair_email_list(group)
cc_list.append(SESSION_REQUEST_EMAIL)
if login.role_email(role_name='wg').address not in cc_list:
cc_list.append(login.role_email(role_name='wg').address)
(to_email, cc_list) = gather_address_lists('session_requested_long',group=group,person=login)
subject = '%s - Request for meeting session approval for IETF %s' % (group.acronym, meeting.number)
template = 'sreq/session_approval_notification.txt'
#status_text = 'the %s Directors for approval' % group.parent
@ -211,8 +207,7 @@ def cancel(request, acronym):
session.scheduledsession_set.all().delete()
# send notifitcation
to_email = SESSION_REQUEST_EMAIL
cc_list = get_cc_list(group, login)
(to_email, cc_list) = gather_address_lists('session_request_cancelled',group=group,person=login)
from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org')
subject = '%s - Cancelling a meeting request for IETF %s' % (group.acronym, meeting.number)
send_mail(request, to_email, from_email, subject, 'sreq/session_cancel_notification.txt',
@ -628,8 +623,7 @@ def no_session(request, acronym):
session_save(session)
# send notification
to_email = SESSION_REQUEST_EMAIL
cc_list = get_cc_list(group, login)
(to_email, cc_list) = gather_address_lists('session_request_not_meeting',group=group,person=login)
from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org')
subject = '%s - Not having a session at IETF %s' % (group.acronym, meeting.number)
send_mail(request, to_email, from_email, subject, 'sreq/not_meeting_notification.txt',

View file

@ -10,7 +10,7 @@ from ietf.doc.models import DocEvent, Document, BallotDocEvent, BallotPositionDo
from ietf.doc.utils import get_document_content, add_state_change_event
from ietf.person.models import Person
from ietf.doc.lastcall import request_last_call
from ietf.doc.mails import email_ad, email_state_changed
from ietf.doc.mails import email_state_changed
from ietf.iesg.models import TelechatDate, TelechatAgendaItem, Telechat
from ietf.iesg.agenda import agenda_data, get_doc_section
from ietf.ietfauth.utils import role_required
@ -261,8 +261,7 @@ def doc_detail(request, date, name):
doc.time = (e and e.time) or datetime.datetime.now()
doc.save()
email_state_changed(request, doc, e.desc)
email_ad(request, doc, doc.ad, login, e.desc)
email_state_changed(request, doc, e.desc, 'doc_state_edited')
if new_state.slug == "lc-req":
request_last_call(request, doc)

View file

@ -1,34 +0,0 @@
def get_ad_email_list(group):
'''
This function takes a group and returns the Area Director email as a list.
NOTE: we still have custom logic here for IRTF groups, where the "Area Director"
is the chair of the parent group, 'irtf'.
'''
emails = []
if group.type.slug == 'wg':
emails.append('%s-ads@ietf.org' % group.acronym)
elif group.type.slug == 'rg' and group.parent:
emails.append(group.parent.role_set.filter(name='chair')[0].email.address)
return emails
def get_cc_list(group, person=None):
'''
This function takes a Group and Person. It returns a list of emails for the ads and chairs of
the group and the person's email if it isn't already in the list.
Per Pete Resnick, at IETF 80 meeting, session request notifications
should go to chairs,ads lists not individuals.
'''
emails = []
emails.extend(get_ad_email_list(group))
emails.extend(get_chair_email_list(group))
if person and person.email_address() not in emails:
emails.append(person.email_address())
return emails
def get_chair_email_list(group):
'''
This function takes a group and returns chair email(s) as a list.
'''
return [ r.email.address for r in group.role_set.filter(name='chair') ]

View file

@ -258,6 +258,7 @@ INSTALLED_APPS = (
'ietf.ipr',
'ietf.liaisons',
'ietf.mailinglists',
'ietf.mailtrigger',
'ietf.meeting',
'ietf.message',
'ietf.name',
@ -421,11 +422,9 @@ CACHES = {
}
}
IPR_EMAIL_TO = 'ietf-ipr@ietf.org'
DOC_APPROVAL_EMAIL_CC = ["RFC Editor <rfc-editor@rfc-editor.org>", ]
IPR_EMAIL_FROM = 'ietf-ipr@ietf.org'
IANA_EVAL_EMAIL = "drafts-eval@icann.org"
IANA_APPROVE_EMAIL = "drafts-approval@icann.org"
# Put real password in settings_local.py
IANA_SYNC_PASSWORD = "secret"
@ -448,7 +447,6 @@ LIAISON_ATTACH_URL = '/documents/LIAISON/'
ROLODEX_URL = ""
NOMCOM_PUBLIC_KEYS_DIR = '/a/www/nomcom/public_keys/'
NOMCOM_FROM_EMAIL = 'nomcom-chair@ietf.org'
NOMCOM_ADMIN_EMAIL = DEFAULT_FROM_EMAIL
OPENSSL_COMMAND = '/usr/bin/openssl'
DAYS_TO_EXPIRE_NOMINATION_LINK = ''
DEFAULT_FEEDBACK_TYPE = 'offtopic'
@ -456,7 +454,6 @@ NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina']
# ID Submission Tool settings
IDSUBMIT_FROM_EMAIL = 'IETF I-D Submission Tool <idsubmission@ietf.org>'
IDSUBMIT_TO_EMAIL = 'internet-drafts@ietf.org'
IDSUBMIT_ANNOUNCE_FROM_EMAIL = 'internet-drafts@ietf.org'
IDSUBMIT_ANNOUNCE_LIST_EMAIL = 'i-d-announce@ietf.org'

View file

@ -6,76 +6,68 @@ from django.template.loader import render_to_string
from ietf.utils.mail import send_mail, send_mail_message
from ietf.doc.models import Document
from ietf.person.models import Person
from ietf.group.models import Role
from ietf.message.models import Message
from ietf.utils.accesstoken import generate_access_token
def submission_confirmation_email_list(submission):
try:
doc = Document.objects.get(name=submission.name)
email_list = [i.author.formatted_email() for i in doc.documentauthor_set.all() if not i.author.invalid_address()]
except Document.DoesNotExist:
email_list = [u'"%s" <%s>' % (author["name"], author["email"])
for author in submission.authors_parsed() if author["email"]]
if submission.submitter_parsed()["email"] and submission.submitter not in email_list:
email_list.append(submission.submitter)
return email_list
from ietf.mailtrigger.utils import gather_address_lists
def send_submission_confirmation(request, submission):
subject = 'Confirm submission of I-D %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
to_email = submission_confirmation_email_list(submission)
(to_email, cc) = gather_address_lists('sub_confirmation_requested',submission=submission)
confirm_url = settings.IDTRACKER_BASE_URL + urlreverse('submit_confirm_submission', kwargs=dict(submission_id=submission.pk, auth_token=generate_access_token(submission.auth_key)))
status_url = settings.IDTRACKER_BASE_URL + urlreverse('submit_submission_status_by_hash', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
send_mail(request, to_email, from_email, subject, 'submit/confirm_submission.txt', {
'submission': submission,
'confirm_url': confirm_url,
'status_url': status_url,
})
send_mail(request, to_email, from_email, subject, 'submit/confirm_submission.txt',
{
'submission': submission,
'confirm_url': confirm_url,
'status_url': status_url,
},
cc=cc)
return to_email
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_full_url(request, submission):
subject = 'Full URL for managing submission of draft %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
to_email = submission_confirmation_email_list(submission)
(to_email, cc) = gather_address_lists('sub_management_url_requested',submission=submission)
url = settings.IDTRACKER_BASE_URL + urlreverse('submit_submission_status_by_hash', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
send_mail(request, to_email, from_email, subject, 'submit/full_url.txt', {
'submission': submission,
'url': url,
})
send_mail(request, to_email, from_email, subject, 'submit/full_url.txt',
{
'submission': submission,
'url': url,
},
cc=cc)
return to_email
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_approval_request_to_group(request, submission):
subject = 'New draft waiting for approval: %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
to_email = [r.formatted_email() for r in Role.objects.filter(group=submission.group, name="chair").select_related("email", "person")]
(to_email,cc) = gather_address_lists('sub_chair_approval_requested',submission=submission)
if not to_email:
return to_email
send_mail(request, to_email, from_email, subject, 'submit/approval_request.txt', {
'submission': submission,
'domain': Site.objects.get_current().domain,
})
return to_email
send_mail(request, to_email, from_email, subject, 'submit/approval_request.txt',
{
'submission': submission,
'domain': Site.objects.get_current().domain,
},
cc=cc)
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_manual_post_request(request, submission, errors):
subject = u'Manual Post Requested for %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
to_email = settings.IDSUBMIT_TO_EMAIL
cc = [submission.submitter]
cc += [u'"%s" <%s>' % (author["name"], author["email"])
for author in submission.authors_parsed() if author["email"]]
if submission.group:
cc += [r.formatted_email() for r in Role.objects.filter(group=submission.group, name="chair").select_related("email", "person")]
cc = list(set(cc))
(to_email,cc) = gather_address_lists('sub_manual_post_requested',submission=submission)
send_mail(request, to_email, from_email, subject, 'submit/manual_post_request.txt', {
'submission': submission,
'url': settings.IDTRACKER_BASE_URL + urlreverse('submit_submission_status', kwargs=dict(submission_id=submission.pk)),
@ -93,9 +85,7 @@ def announce_to_lists(request, submission):
pass
m.subject = 'I-D Action: %s-%s.txt' % (submission.name, submission.rev)
m.frm = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
m.to = settings.IDSUBMIT_ANNOUNCE_LIST_EMAIL
if submission.group and submission.group.list_email:
m.cc = submission.group.list_email
(m.to, m.cc) = gather_address_lists('sub_announced',submission=submission)
m.body = render_to_string('submit/announce_to_lists.txt',
dict(submission=submission,
settings=settings))
@ -106,39 +96,18 @@ def announce_to_lists(request, submission):
def announce_new_version(request, submission, draft, state_change_msg):
to_email = []
if draft.notify:
to_email.append(draft.notify)
if draft.ad:
to_email.append(draft.ad.role_email("ad").address)
if draft.stream_id == "iab":
to_email.append("IAB Stream <iab-stream@iab.org>")
elif draft.stream_id == "ise":
to_email.append("Independent Submission Editor <rfc-ise@rfc-editor.org>")
elif draft.stream_id == "irtf":
to_email.append("IRSG <irsg@irtf.org>")
# if it has been sent to the RFC Editor, keep them in the loop
if draft.get_state_slug("draft-rfceditor") is not None:
to_email.append("RFC Editor <rfc-editor@rfc-editor.org>")
active_ballot = draft.active_ballot()
if active_ballot:
for ad, pos in active_ballot.active_ad_positions().iteritems():
if pos and pos.pos_id == "discuss":
to_email.append(ad.role_email("ad").address)
(to_email,cc) = gather_address_lists('sub_new_version',doc=draft,submission=submission)
if to_email:
subject = 'New Version Notification - %s-%s.txt' % (submission.name, submission.rev)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
send_mail(request, to_email, from_email, subject, 'submit/announce_new_version.txt',
{'submission': submission,
'msg': state_change_msg})
'msg': state_change_msg},
cc=cc)
def announce_to_authors(request, submission):
authors = [u'"%s" <%s>' % (author["name"], author["email"]) for author in submission.authors_parsed() if author["email"]]
to_email = list(set(submission_confirmation_email_list(submission) + authors))
(to_email, cc) = gather_address_lists('sub_announced_to_authors',submission=submission)
from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL
subject = 'New Version Notification for %s-%s.txt' % (submission.name, submission.rev)
if submission.group:
@ -149,4 +118,5 @@ def announce_to_authors(request, submission):
group = 'Individual Submission'
send_mail(request, to_email, from_email, subject, 'submit/announce_to_authors.txt',
{'submission': submission,
'group': group})
'group': group},
cc=cc)

View file

@ -3,6 +3,7 @@ import datetime
from django.db import models
from ietf.doc.models import Document
from ietf.person.models import Person
from ietf.group.models import Group
from ietf.name.models import DraftSubmissionStateName
@ -62,6 +63,8 @@ class Submission(models.Model):
def access_token(self):
return generate_access_token(self.access_key)
def existing_document(self):
return Document.objects.filter(name=self.name).first()
class SubmissionEvent(models.Model):
submission = models.ForeignKey(Submission)

View file

@ -221,8 +221,8 @@ class SubmitTests(TestCase):
self.assertTrue("review" in outbox[-1]["Subject"].lower())
self.assertTrue(name in unicode(outbox[-1]))
self.assertTrue(sug_replaced_alias.name in unicode(outbox[-1]))
self.assertTrue("ameschairman" in outbox[-1]["To"].lower())
self.assertTrue("marschairman" in outbox[-1]["To"].lower())
self.assertTrue("ames-chairs@" in outbox[-1]["To"].lower())
self.assertTrue("mars-chairs@" in outbox[-1]["To"].lower())
def test_submit_new_wg_txt(self):
self.submit_new_wg(["txt"])
@ -324,6 +324,7 @@ class SubmitTests(TestCase):
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
self.assertTrue((u"I-D Action: %s" % name) in draft.message_set.order_by("-time")[0].subject)
self.assertTrue("Author Name" in unicode(outbox[-3]))
self.assertTrue("ietf-announce@" in outbox[-3]['To'])
self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
self.assertTrue(name in unicode(outbox[-2]))
self.assertTrue("mars" in unicode(outbox[-2]))
@ -648,6 +649,9 @@ class SubmitTests(TestCase):
self.assertTrue("Full URL for managing submission" in outbox[-1]["Subject"])
self.assertTrue(name in outbox[-1]["Subject"])
# This could use a test on an 01 from a new author to make sure the logic on
# who gets the management url behaves as expected
def test_submit_all_file_types(self):
make_test_data()
@ -710,7 +714,7 @@ class SubmitTests(TestCase):
url = urlreverse('submit_upload_submission')
# set meeting to today so we're in blackout period
meeting = Meeting.get_current_meeting()
meeting.date = datetime.datetime.today()
meeting.date = datetime.datetime.utcnow()
meeting.save()
# regular user, no access

View file

@ -16,7 +16,7 @@ from ietf.doc.utils import prettify_std_name
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
from ietf.submit.forms import SubmissionUploadForm, NameEmailForm, EditSubmissionForm, PreapprovalForm, ReplacesForm
from ietf.submit.mail import send_full_url, send_approval_request_to_group, send_submission_confirmation, submission_confirmation_email_list, send_manual_post_request
from ietf.submit.mail import send_full_url, send_approval_request_to_group, send_submission_confirmation, send_manual_post_request
from ietf.submit.models import Submission, Preapproval, DraftSubmissionStateName
from ietf.submit.utils import approvable_submissions_for_user, preapprovals_for_user, recently_approved_by_user
from ietf.submit.utils import check_idnits, found_idnits, validate_submission, create_submission_event
@ -24,6 +24,7 @@ from ietf.submit.utils import post_submission, cancel_submission, rename_submiss
from ietf.utils.accesstoken import generate_random_key, generate_access_token
from ietf.utils.draft import Draft
from ietf.utils.log import log
from ietf.mailtrigger.utils import gather_address_lists
def upload_submission(request):
@ -185,7 +186,9 @@ def submission_status(request, submission_id, access_token=None):
can_force_post = is_secretariat and submission.state.next_states.filter(slug="posted")
show_send_full_url = not key_matched and not is_secretariat and submission.state_id not in ("cancel", "posted")
confirmation_list = submission_confirmation_email_list(submission)
addrs = gather_address_lists('sub_confirmation_requested',submission=submission)
confirmation_list = addrs.to
confirmation_list.extend(addrs.cc)
requires_group_approval = (submission.rev == '00' and submission.group and submission.group.type_id in ("wg", "rg", "ietf", "irtf", "iab", "iana", "rfcedtyp") and not Preapproval.objects.filter(name=submission.name).exists())
@ -210,7 +213,7 @@ def submission_status(request, submission_id, access_token=None):
action = request.POST.get('action')
if action == "autopost" and submission.state_id == "uploaded":
if not can_edit:
return HttpResponseForbidden("You do not have permission to perfom this action")
return HttpResponseForbidden("You do not have permission to perform this action")
submitter_form = NameEmailForm(request.POST, prefix="submitter")
replaces_form = ReplacesForm(request.POST, name=submission.name)

View file

@ -8,7 +8,7 @@ import urllib2
from django.utils.http import urlquote
from django.conf import settings
from ietf.doc.mails import email_ad, email_state_changed
from ietf.doc.mails import email_state_changed
from ietf.doc.models import Document, DocEvent, State, StateDocEvent, StateType, save_document_in_history
from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person
@ -205,8 +205,7 @@ def update_history_with_changes(changes, send_email=True):
doc.set_state(state)
if send_email and (state != prev_state):
email_state_changed(None, doc, "IANA %s state changed to %s" % (kind, state.name))
email_ad(None, doc, doc.ad, system, "IANA %s state changed to %s" % (kind, state.name))
email_state_changed(None, doc, "IANA %s state changed to %s" % (kind, state.name),'doc_iana_state_changed')
if doc.time < timestamp:
doc.time = timestamp

View file

@ -12,7 +12,7 @@ from ietf.doc.models import Document, DocAlias, DocEvent, DeletedEvent, DocTagNa
from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person
from ietf.sync import iana, rfceditor
from ietf.utils.mail import outbox
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.utils.test_utils import TestCase
@ -71,7 +71,7 @@ class IANASyncTests(TestCase):
# check sorting
self.assertEqual(changes[0]["time"], "2011-10-09 11:00:00")
mailbox_before = len(outbox)
empty_outbox()
added_events, warnings = iana.update_history_with_changes(changes)
self.assertEqual(len(added_events), 3)
@ -81,7 +81,9 @@ class IANASyncTests(TestCase):
e = draft.latest_event(StateDocEvent, type="changed_state", state_type="draft-iana-action")
self.assertEqual(e.desc, "IANA Action state changed to <b>Waiting on RFC Editor</b> from In Progress")
# self.assertEqual(e.time, datetime.datetime(2011, 10, 9, 5, 0)) # check timezone handling
self.assertEqual(len(outbox), mailbox_before + 3 * 2)
self.assertEqual(len(outbox), 3 )
for m in outbox:
self.assertTrue('aread@' in m['To'])
# make sure it doesn't create duplicates
added_events, warnings = iana.update_history_with_changes(changes)

View file

@ -25,30 +25,16 @@
<input class="form-control" type="text" placeholder="{{ to }}" disabled>
</div>
<div class="form-group">
{% bootstrap_form cc_select_form %}
</div>
<div class="form-group">
<label>Cc</label>
<input class="form-control" type="email" name="cc">
<label>Additional Cc Addresses</label>
<input class="form-control" type="email" name="extra_cc">
<div class="help-block">Separate email addresses with commas.</div>
</div>
{% if doc.notify %}
<div class="checkbox">
<label>
<input type="checkbox" name="cc_state_change" value="1" checked>
<b>Cc:</b> {{ doc.notify }}
</label>
</div>
{% endif %}
{% if doc.group.list_email %}
<div class="checkbox">
<label>
<input type="checkbox" name="cc_group_list" value="1" checked>
<b>Cc:</b> {{ doc.group.list_email }}
</label>
</div>
{% endif %}
<div class="form-group">
<label>Subject</label>
<input class="form-control" type="text" placeholder="{{ subject }}" disabled>

View file

@ -5,11 +5,11 @@
{% load bootstrap3 %}
{% load ietf_filters %}
{% block title %}WG {{ announcement }} announcement writeup for {{ charter.chartered_group.acronym }}{% endblock %}
{% block title %}WG Action announcement }} announcement writeup for {{ charter.chartered_group.acronym }}{% endblock %}
{% block content %}
{% origin %}
<h1>WG {{ announcement }} announcement writeup<br><small>{{ charter.chartered_group.acronym }}</small></h1>
<h1>WG Action announcement writeup<br><small>{{ charter.chartered_group.acronym }}</small></h1>
{% bootstrap_messages %}
@ -22,11 +22,7 @@
<button type="submit" class="btn btn-warning" name="regenerate_text" value="Reenerate"">Regenerate</button>
{% if user|has_role:"Secretariat" %}
{% if announcement == "action" %}
<a type="submit" class="btn btn-default" href="{% url "charter_approve" name=charter.canonical_name %}">Charter approval page</a>
{% else %}
<input type="submit" type="submit" class="btn btn-primary" name="send_text" value="Send WG {{ announcement }} announcement" />
{% endif %}
{% endif %}
<a class="btn btn-default pull-right" href="{{ back_url }}">Back</a>

View file

@ -1,6 +1,6 @@
{% load ietf_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: IETF-Announce <ietf-announce@ietf.org>{% if group.list_email %}
Cc: {{ group.acronym }} {{ group.type.name }} <{{ group.list_email }}> {% endif %}
To: {{ to }}{% if cc %}
Cc: {{ cc }} {% endif %}
Subject: WG Action: {{ action_type }} {{ group.name }} ({{ group.acronym }})
{% filter wordwrap:73 %}{% if action_type == "Formed" %}A new IETF working group has been formed in the {{ group.parent.name }}.{% endif %}{% if action_type == "Rechartered" %}The {{ group.name }} ({{ group.acronym }}) working group in the {{ group.parent.name }} of the IETF has been rechartered.{% endif %} For additional information please contact the Area Directors or the {{ group.type.name }} Chair{{ chairs|pluralize}}.

View file

@ -16,7 +16,7 @@
{% buttons %}
<button type="submit" class="btn btn-primary">Send announcement, close ballot & update revision</button>
<a class="btn btn-warning" href="{% url "charter_edit_announcement" name=charter.canonical_name ann="action" %}?next=approve">Edit/regenerate announcement</a>
<a class="btn btn-warning" href="{% url "ietf.doc.views_charter.action_announcement_text" name=charter.canonical_name %}?next=approve">Edit/regenerate announcement</a>
<a class="btn btn-default pull-right" href="{% url "doc_view" name=charter.name %}">Back</a>
{% endbuttons %}
</form>

View file

@ -1,38 +1,10 @@
{% autoescape off %}To: Internet Engineering Steering Group <iesg@ietf.org>
From: IESG Secretary <iesg-secretary@ietf.org>
{% autoescape off %}To: {{ to }} {% if cc %}
Cc: {{ cc }}
{% endif %}From: IESG Secretary <iesg-secretary@ietf.org>
Reply-To: IESG Secretary <iesg-secretary@ietf.org>
Subject: Evaluation: {{ doc.name }}
{% filter wordwrap:73 %}Evaluation for {{ doc.title }} can be found at {{ doc_url }}
{% endfilter %}
Please return the full line with your position.
Yes No Block Abstain
{% for fmt in active_ad_positions %}{{ fmt }}
{% endfor %}{% if inactive_ad_positions %}
{% for fmt in inactive_ad_positions %}{{ fmt }}
{% endfor %}{% endif %}
No "Block" positions, are needed for approval.
BLOCKING AND NON-BLOCKING COMMENTS
==================================
{% filter wordwrap:79 %}{% for p in ad_feedback %}{{ p.ad }}:
{% if p.discuss %}Blocking comment [{{ p.time }}]:
{{ p.discuss }}
{% endif %}{% if p.comment %}Comment [{{ p.time }}]:
{{ p.comment }}
{% endif %}
{% endfor %}{% endfilter %}
---- following is a DRAFT of message to be sent AFTER approval ---
{{ approval_text }}
---- ballot text ----
{{ ballot_writeup }}
{% endautoescape%}

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% load ietf_filters %}
{% block title %}WG Review announcement }} announcement writeup for {{ charter.chartered_group.acronym }}{% endblock %}
{% block content %}
{% origin %}
<h1>WG Review announcement writeup<br><small>{{ charter.chartered_group.acronym }}</small></h1>
{% bootstrap_messages %}
<form method="post">
{% csrf_token %}
{% bootstrap_form announcement_text_form %}
{% buttons %}
<button type="submit" class="btn btn-primary" name="save_text" value="Save">Submit</button>
<button type="submit" class="btn btn-warning" name="regenerate_text" value="Regenerate"">Regenerate</button>
{% if user|has_role:"Secretariat" %}
<input type="submit" type="submit" class="btn btn-default" name="send_annc_only" value="Send only to IETF-Announce" />
<input type="submit" type="submit" class="btn btn-default" name="send_nw_only" value="Send only to New-Work" />
<input type="submit" type="submit" class="btn btn-default" name="send_both" value="Send to both" />
{% endif %}
<a class="btn btn-default pull-right" href="{{ back_url }}">Back</a>
{% endbuttons %}
</form>
{% endblock%}

View file

@ -1,6 +1,6 @@
{% load ietf_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: IETF-Announce <ietf-announce@ietf.org>{% if group.list_email %}
Cc: {{ group.acronym }} {{ group.type.name }} <{{ group.list_email }}> {% endif %}
To: {{ to }}{% if cc %}
Cc: {{ cc }} {% endif %}
Subject: WG Review: {{ group.name }} ({{ group.acronym }})
{% filter wordwrap:73 %}{% if review_type == "new" %}A new IETF working group has been proposed in the {{ group.parent.name }}.{% endif %}{% if review_type == "recharter" %}The {{ group.name }} ({{group.acronym}}) working group in the {{ group.parent.name }} of the IETF is undergoing rechartering.{% endif %} The IESG has not made any determination yet. The following draft charter was submitted, and is provided for informational purposes only. Please send your comments to the IESG mailing list (iesg at ietf.org) by {{ review_date }}.

View file

@ -1,6 +1,6 @@
{% load mail_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: {{ review.notify }}
Cc: The IESG <iesg@ietf.org>, <iana@iana.org>, <ietf-announce@ietf.org>
To: {{ to }}
Cc: {{ cc }}
Subject: Results of IETF-conflict review for {{conflictdoc.canonical_name}}-{{conflictdoc.rev}}
{% filter wordwrap:73 %}The IESG has completed a review of {{conflictdoc.canonical_name}}-{{conflictdoc.rev}} consistent with RFC5742.

View file

@ -1,5 +1,6 @@
{% load mail_filters %}{% autoescape off %}To: IESG Secretary <iesg-secretary@ietf.org>
From: {{ frm }}
{% load mail_filters %}{% autoescape off %}To: {{to}}{% if cc %}
Cc: {{cc}}
{% endif %}From: {{ frm }}
Subject: Conflict Review requested for {{reviewed_doc.name}}
{{ by.name }} has requested a conflict review for:

View file

@ -0,0 +1,49 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters %}
{% load future %}
{% block title %}Email expansions for {{ doc.name }}-{{ doc.rev }}{% endblock %}
{% block content %}
{% origin %}
{{ top|safe }}
{% if aliases %}
<h2>Email Aliases</h2>
<table class="table table-condensed table-striped ietf">
<tbody>
{% for alias in aliases %}
<tr>
<td>{{ doc.name }}{{ alias.alias_type|default:''}}@{{ietf_domain}}</td>
<td>{{ alias.expansion }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2>Recipient Expansions</h2>
<table class="table table-condensed table-striped ietf">
<thead>
<tr>
<th>Mail Trigger</th>
<th>To</th>
<th>Cc</th>
</tr>
</thead>
<tbody>
{% for trigger,desc,to,cc in expansions %}
<tr>
<td><a href="{% url 'ietf.mailtrigger.views.show_triggers' trigger %}"
title="{{desc}}">{{trigger}}</a></td>
<td> {{to|join:', '}}</td>
<td> {{cc|join:', '}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}

View file

@ -1,5 +1,6 @@
{% load mail_filters %}{% autoescape off %}To: Internet Engineering Steering Group <iesg@ietf.org>
From: IESG Secretary <iesg-secretary@ietf.org>
{% load mail_filters %}{% autoescape off %}To: {{to}} {% if cc %}
Cc: {{cc}}
{%endif%}From: IESG Secretary <iesg-secretary@ietf.org>
Reply-To: IESG Secretary <iesg-secretary@ietf.org>
Subject: Evaluation: {{doc.title}}

View file

@ -1,5 +1,5 @@
{% load ietf_filters %}{%load mail_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: IETF-Announce <ietf-announce@ietf.org>{% if cc %}
To: {{ to }}{% if cc %}
Cc: {{ cc }}{% endif %}
Subject: {{ action_type }} Action: '{{ doc.title|clean_whitespace }}' to {{ doc|std_level_prompt }} ({{ doc.filename_with_rev }})

View file

@ -1,6 +1,6 @@
{% load mail_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: {{to}}
Cc: The IESG <iesg@ietf.org>, <iana@iana.org>, <ietf-announce@ietf.org>
Cc: {{cc}}
Subject: Results of IETF-conflict review for {{ doc.file_tag }}
{% filter wordwrap:73 %}
The IESG has completed a review of <{{ doc.name }}> consistent with RFC5742. This review is applied to all non-IETF streams.

View file

@ -0,0 +1,28 @@
{% autoescape off %}{% filter wordwrap:73 %}
A new IETF working group is being considered in the {{charter.group.parent.name}}. The draft charter for this working group is provided below for your review and comment.
Review time is one week.
The IETF Secretariat
{{charter.group.name}} ({{ charter.group.acronym }})
--------------------------------------------------
Current Status: {{ charter.group.state.name }} {% if charter.group.state_id != 'bof' %}Working Group{% endif %}
Chairs : {{ chairs|join:', '|default:'TBD' }}
Area Director: {{ ads|join:', '|default:'TBD' }}
Mailing List: {{ charter.group.list_email|default:'TBD' }}
{{ charter.name }}-{{ charter.rev }}
{{ charter_text }}
Proposed milestones
{%if milestones %}{% for milestone in milestones reversed %}{{ milestone.due|date:"M Y" }} {{ milestone.desc }}
{% endfor %}{% else %}TBD{% endif %}
{% endfilter %}{% endautoescape%}

View file

@ -0,0 +1,12 @@
{% autoescape off %}
Please DO NOT reply to this email.
{{by}} added the following comment to the history of {{doc.name}}
{{ comment.desc }}
The document can be found at
I-D: {{ doc.file_tag|safe }}
ID Tracker URL: {{ url }}
{% endautoescape%}

View file

@ -0,0 +1,10 @@
{% autoescape off %}{% filter wordwrap:73 %}
The {{ doc.group.acronym|upper }} {{ doc.group.type_id|upper }} has adopted {{ doc }} (entered by {{by}})
{% if prev_state %}The document was previously in state {{prev_state.name}}
{% endif %}The document is available at {{ url }}
{% if comment %}
Comment:
{{ comment }}{% endif %}{% endfilter %}{% endautoescape %}

View file

@ -0,0 +1,15 @@
{% autoescape off %}
Please DO NOT reply to this email.
The IESG is processing {{doc.name}}.
The following changes have been made:
{% for change in changes %}{{change}}
{% endfor %}
The document can be found here:
I-D: {{ doc.file_tag|safe }}
ID Tracker URL: {{ url }}
{% endautoescape%}

View file

@ -0,0 +1,5 @@
{% autoescape off %}{{ text }}
The document can be found at
ID Tracker URL: {{ url }}
{% endautoescape %}

View file

@ -1,5 +1,4 @@
{% load mail_filters %}{% autoescape off %}To: Internet Engineering Steering Group <iesg@ietf.org>
From: IESG Secretary <iesg-secretary@ietf.org>
{% load mail_filters %}{% autoescape off %}From: IESG Secretary <iesg-secretary@ietf.org>
Reply-To: IESG Secretary <iesg-secretary@ietf.org>
Subject: Evaluation: {{ doc.file_tag }} to {{ doc|std_level_prompt }}
@ -8,32 +7,6 @@ Subject: Evaluation: {{ doc.file_tag }} to {{ doc|std_level_prompt }}
{% if last_call_expires %}Last call to expire on: {{ last_call_expires }}
{% endif %}{% endfilter %}
Please return the full line with your position.
Yes No-Objection Discuss Abstain
{% for fmt in active_ad_positions %}{{ fmt }}
{% endfor %}{% if inactive_ad_positions %}
{% for fmt in inactive_ad_positions %}{{ fmt }}
{% endfor %}{% endif %}
{% filter wordwrap:73 %}{{ needed_ballot_positions }}{% endfilter %}
DISCUSSES AND COMMENTS
======================
{% filter wordwrap:79 %}{% for pos in ad_feedback %}{{ pos.ad }}:
{% if pos.discuss %}Discuss [{{ pos.discuss_time|date:"Y-m-d" }}]:
{{ pos.discuss }}
{% endif %}{% if pos.comment %}Comment [{{ pos.comment_time|date:"Y-m-d" }}]:
{{ pos.comment }}
{% endif %}
{% endfor %}{% endfilter %}
---- following is a DRAFT of message to be sent AFTER approval ---
{{ approval_text }}{% if ballot_writeup %}
{{ ballot_writeup }}
{% endif %}
{% endautoescape%}

View file

@ -1,5 +1,5 @@
{% load ietf_filters %}{% load mail_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: IETF-Announce <ietf-announce@ietf.org>{% if cc %}
To: {{ to }}{% if cc %}
CC: {{ cc }}{% endif %}
Reply-To: ietf@ietf.org
Sender: <iesg-secretary@ietf.org>

View file

@ -1,6 +1,6 @@
{% load mail_filters %}{% autoescape off %}From: The IESG <iesg-secretary@ietf.org>
To: IETF-Announce <ietf-announce@ietf.org>
Cc: RFC Editor <rfc-editor@rfc-editor.org>, {{status_change.notify}}
To: {{ to }}{% if cc %}
Cc: {{ cc }}{% endif %}
Subject: {{action}}: {{relateddoc.target.document.title}} to {{newstatus}}
{% filter wordwrap:73 %}The IESG has approved changing the status of the following document:

View file

@ -0,0 +1,47 @@
{% extends "group/group_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters %}
{% load future %}
{% block group_content %}
{% origin %}
{% if aliases %}
<h2>Email Aliases</h2>
<table class="table table-condensed table-striped ietf">
<tbody>
{% for alias in aliases %}
<tr>
<td>{{ group.acronym }}{{ alias.alias_type|default:''}}@{{ietf_domain}}</td>
<td>{{ alias.expansion }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h2>Recipient Expansions</h2>
<table class="table table-condensed table-striped ietf">
<thead>
<tr>
<th>Mail Trigger</th>
<th>To</th>
<th>Cc</th>
</tr>
</thead>
<tbody>
{% for trigger,desc,to,cc in expansions %}
<tr>
<td><a href="{% url 'ietf.mailtrigger.views.show_triggers' trigger %}"
title="{{desc}}">{{trigger}}</a></td>
<td> {{to|join:', '}}</td>
<td> {{cc|join:', '}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -1,7 +0,0 @@
{% autoescape off %}{% filter wordwrap:73 %}This is a reminder that milestones in "{{ group.name }}" are soon due.
{% for m in milestones %}"{{ m.desc }}" is due {% if m.due == today %}today!{% else %}in {{ early_warning_days }} days.{% endif %}
{% endfor %}
URL: {{ url }}
{% endfilter %}{% endautoescape %}

View file

@ -1,10 +0,0 @@
{% autoescape off %}{% filter wordwrap:73 %}{{ milestones|length }} new milestone{{ milestones|pluralize }} in "{{ group.name }}" {% if milestones|length > 1 %}need{% else %}needs{%endif %} review by the {{ reviewer }}:
{% for m in milestones %}"{{ m.desc }}"{% if m.days_ready != None %}
Waiting for {{ m.days_ready }} day{{ m.days_ready|pluralize }}.{% endif %}
{% endfor %}
Go here to either accept or reject the new milestones:
{{ url }}
{% endfilter %}{% endautoescape %}

View file

@ -1,7 +0,0 @@
{% autoescape off %}{% filter wordwrap:73 %}This is a reminder that milestones in "{{ group.name }}" are overdue.
{% for m in milestones %}"{{ m.desc }}" is overdue{% if m.months_overdue > 0 %} with {{ m.months_overdue }} month{{ m.months_overdue|pluralize }}{% endif %}!
{% endfor %}
URL: {{ url }}
{% endfilter %}{% endautoescape %}

View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Mail Recipients{% endblock %}
{% block content %}
{% origin %}
<h1>Mail Recipients</h1>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Recipient</th>
<th>Triggers</th>
<th>Template</th>
<th>Code</th>
</tr>
</thead>
<tbody>
{% for recipient in recipients %}
<tr>
<td><span title="{{recipient.desc}}">{{recipient.slug}}</span></td>
<td>
{% for mailtrigger in recipient.used_in_to.all %}
<a href="{% url 'ietf.mailtrigger.views.show_triggers' mailtrigger.slug %}" title="{{mailtrigger.desc}}">{{mailtrigger.slug}}</a>{% if not forloop.last %}, {%endif%}
{% endfor %}{% if recipient.used_in_to.exists and recipient.used_in_cc.exists %},{% endif %}
{% for mailtrigger in recipient.used_in_cc.all %}
<a href="{% url 'ietf.mailtrigger.views.show_triggers' mailtrigger.slug %}" title="{{mailtrigger.desc}}">{{mailtrigger.slug}}</a>{% if not forloop.last %}, {%endif%}
{% endfor %}
</td>
<td>{{recipient.template}}</td>
<td>{% if recipient.code %}<pre>{{recipient.code}}</pre>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,41 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Mail Triggers{% endblock %}
{% block content %}
{% origin %}
<h1>Mail Triggers</h1>
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Trigger</th>
<th>Recipients</th>
</tr>
</thead>
<tbody>
{% for mailtrigger in mailtriggers %}
<tr>
<td><span title="{{mailtrigger.desc}}">{{mailtrigger.slug}}</span></td>
<td>To:
{% for recipient in mailtrigger.to.all %}
{% comment %}<span title="{{recipient.desc}}">{{recipient.slug}}</span>{% endcomment %}
<a href="{% url 'ietf.mailtrigger.views.show_recipients' recipient.slug %}" title="{{recipient.desc}}">{{recipient.slug}}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% if mailtrigger.cc.exists %}
<br/>Cc:
{% for recipient in mailtrigger.cc.all %}
{% comment %}<span title="{{recipient.desc}}">{{recipient.slug}}</span>{% endcomment %}
<a href="{% url 'ietf.mailtrigger.views.show_recipients' recipient.slug %}" title="{{recipient.desc}}">{{recipient.slug}}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -35,6 +35,7 @@ urlpatterns = patterns('',
(r'^accounts/settings/', include('ietf.cookies.urls')),
(r'^doc/', include('ietf.doc.urls')),
(r'^drafts/', include('ietf.doc.redirect_drafts_urls')),
(r'^mailtrigger/',include('ietf.mailtrigger.urls')),
(r'^feed/', include('ietf.feed_urls')),
(r'^group/', include('ietf.group.urls')),
(r'^help/', include('ietf.help.urls')),