diff --git a/2015-08-28-scrub-notify.py b/2015-08-28-scrub-notify.py new file mode 100644 index 000000000..7cacaaaab --- /dev/null +++ b/2015-08-28-scrub-notify.py @@ -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 . 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" + diff --git a/ietf/bin/send-milestone-reminders b/ietf/bin/send-milestone-reminders deleted file mode 100755 index 2df17f4c4..000000000 --- a/ietf/bin/send-milestone-reminders +++ /dev/null @@ -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)) diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index b2a62ff91..4e215abc6 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -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 ", 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): diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 6184458d8..ab7d336c6 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -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 ", "RFC Editor "], 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("<", "<").replace(">", ">").replace("&", "&").replace("
", "\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 ", - "%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" ') + 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" ' 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 " + (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 " 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 " 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 " 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 ", "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 ") - elif doc.stream_id == "ise": - to_email.append("Independent Submission Editor ") - elif doc.stream_id == "irtf": - to_email.append("IRSG ") + 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"}, + ) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 53b023bf9..06dd3f83c 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -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) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 5ba9fa867..1589df22c 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -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) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index ac99b7552..2ea147f36 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -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) diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py index 6de5ac13f..34edd19d8 100644 --- a/ietf/doc/tests_conflict_review.py +++ b/ietf/doc/tests_conflict_review.py @@ -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): diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 44a9b0b61..ff342a605 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -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() diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py index b22cd9f31..f9a564549 100644 --- a/ietf/doc/tests_status_change.py +++ b/ietf/doc/tests_status_change.py @@ -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')) diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index d9d8489ec..0711b11cb 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -55,6 +55,7 @@ urlpatterns = patterns('', url(r'^(?P[A-Za-z0-9._+-]+)/(?:(?P[0-9-]+)/)?$', views_doc.document_main, name="doc_view"), url(r'^(?P[A-Za-z0-9._+-]+)/history/$', views_doc.document_history, name="doc_history"), url(r'^(?P[A-Za-z0-9._+-]+)/writeup/$', views_doc.document_writeup, name="doc_writeup"), + url(r'^(?P[A-Za-z0-9._+-]+)/email/$', views_doc.document_email, name="doc_email"), url(r'^(?P[A-Za-z0-9._+-]+)/shepherdwriteup/$', views_doc.document_shepherd_writeup, name="doc_shepherd_writeup"), url(r'^(?P[A-Za-z0-9._+-]+)/references/$', views_doc.document_references, name="doc_references"), url(r'^(?P[A-Za-z0-9._+-]+)/referencedby/$', views_doc.document_referenced_by, name="doc_referenced_by"), @@ -65,7 +66,7 @@ urlpatterns = patterns('', (r'^(?P[A-Za-z0-9._+-]+)/doc.json$', views_doc.document_json), (r'^(?P[A-Za-z0-9._+-]+)/ballotpopup/(?P[0-9]+)/$', views_doc.ballot_popup), - url(r'^(?P[A-Za-z0-9._+-]+)/email-aliases/$', views_doc.email_aliases), + url(r'^(?P[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='doc_email', permanent=False),name='doc_specific_email_aliases'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/state/$', views_draft.change_state, name='doc_change_state'), # IESG state url(r'^(?P[A-Za-z0-9._+-]+)/edit/state/(?Piana-action|iana-review)/$', views_draft.change_iana_state, name='doc_change_iana_state'), diff --git a/ietf/doc/urls_charter.py b/ietf/doc/urls_charter.py index 9384e5cbc..5bc52770f 100644 --- a/ietf/doc/urls_charter.py +++ b/ietf/doc/urls_charter.py @@ -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'^(?Paction|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