From c0acadf222bd38485d4520d9e9aed0a7f2290a47 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 6 Aug 2015 21:39:49 +0000 Subject: [PATCH 01/46] Working checkpoint - Legacy-Id: 9985 --- ietf/doc/mails.py | 13 +- ietf/doc/tests_ballot.py | 3 +- ietf/doc/views_ballot.py | 10 +- ietf/doc/views_draft.py | 3 +- ietf/eventmail/__init__.py | 0 ietf/eventmail/migrations/0001_initial.py | 35 +++++ ietf/eventmail/migrations/__init__.py | 0 ietf/eventmail/models.py | 155 ++++++++++++++++++++ ietf/eventmail/resources.py | 33 +++++ ietf/eventmail/tests.py | 43 ++++++ ietf/eventmail/urls.py | 11 ++ ietf/eventmail/utils.py | 20 +++ ietf/eventmail/views.py | 24 +++ ietf/settings.py | 3 +- ietf/templates/doc/mail/approval_mail.txt | 2 +- ietf/templates/eventmail/ingredient.html | 37 +++++ ietf/templates/eventmail/show_patterns.html | 34 +++++ ietf/urls.py | 1 + ietf/utils/test_data.py | 9 ++ 19 files changed, 421 insertions(+), 15 deletions(-) create mode 100644 ietf/eventmail/__init__.py create mode 100644 ietf/eventmail/migrations/0001_initial.py create mode 100644 ietf/eventmail/migrations/__init__.py create mode 100644 ietf/eventmail/models.py create mode 100644 ietf/eventmail/resources.py create mode 100644 ietf/eventmail/tests.py create mode 100644 ietf/eventmail/urls.py create mode 100644 ietf/eventmail/utils.py create mode 100644 ietf/eventmail/views.py create mode 100644 ietf/templates/eventmail/ingredient.html create mode 100644 ietf/templates/eventmail/show_patterns.html diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 88bc553c6..b2e4bfd6a 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -14,6 +14,7 @@ from ietf.doc.utils import needed_ballot_positions from ietf.person.models import Person from ietf.group.models import Group, Role from ietf.doc.models import Document +from ietf.eventmail.utils import gather_addresses def email_state_changed(request, doc, text): to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] @@ -170,16 +171,13 @@ def generate_approval_mail_approved(request, doc): else: action_type = "Document" - cc = [] - cc.extend(settings.DOC_APPROVAL_EMAIL_CC) + to = gather_addresses('ballot_approved_ietf_stream',doc=doc) + cc = gather_addresses('ballot_approved_ietf_stream_cc',doc=doc) # 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@tools.ietf.org>" % (doc.group.acronym, doc.group.acronym)) else: doc.group.name_with_wg = doc.group.name @@ -206,6 +204,7 @@ def generate_approval_mail_approved(request, doc): dict(doc=doc, docs=[doc], doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + to=",\n ".join(to), cc=",\n ".join(cc), doc_type=doc_type, made_by=made_by, @@ -309,7 +308,7 @@ def email_resurrection_completed(request, doc, requester): url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) def email_ballot_deferred(request, doc, by, telechat_date): - to = "iesg@ietf.org" + to = gather_addresses('ballot_deferred',doc=doc) frm = "DraftTracker Mail System " send_mail(request, to, frm, "IESG Deferred Ballot notification: %s" % doc.file_tag(), @@ -320,7 +319,7 @@ def email_ballot_deferred(request, doc, by, telechat_date): telechat_date=telechat_date)) def email_ballot_undeferred(request, doc, by, telechat_date): - to = "iesg@ietf.org" + to = gather_addresses('ballot_deferred',doc=doc) frm = "DraftTracker Mail System " send_mail(request, to, frm, "IESG Undeferred Ballot notification: %s" % doc.file_tag(), diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 5ba9fa867..b64206ff1 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -15,6 +15,7 @@ from ietf.utils.test_utils import TestCase from ietf.utils.mail import outbox from ietf.utils.test_data import make_test_data from ietf.utils.test_utils import login_testing_unauthorized +from ietf.eventmail.utils import gather_addresses class EditPositionTests(TestCase): @@ -171,7 +172,7 @@ class EditPositionTests(TestCase): self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 2) m = outbox[-1] - self.assertEqual(m['Cc'],None) + self.assertEqual(m['Cc'],','.join(gather_addresses('ballot_saved_cc',doc=draft))) class BallotWriteupsTests(TestCase): diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 79f73cca5..89b0a4b20 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -27,6 +27,7 @@ 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.eventmail.utils import gather_addresses BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -284,10 +285,13 @@ def send_ballot_comment(request, name, ballot_id): blocking_name=blocking_name, settings=settings)) frm = ad.role_email("ad").formatted_email() - to = "The IESG " + to = gather_addresses('ballot_saved',doc=doc) if request.method == 'POST': - cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] + cc = gather_addresses('ballot_saved_cc',doc=doc) + explicit_cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] + if explicit_cc: + cc.extend(explicit_cc) 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: @@ -712,7 +716,7 @@ def approve_ballot(request, name): if action == "to_announcement_list": 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": ",".join(gather_addresses('ballot_approved_ietf_stream_iana')), "CC": None, "Bcc": None, "Reply-To": None}) msg = infer_message(announcement) msg.by = login diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 2e358d855..7051a90f4 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -37,6 +37,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.eventmail.utils import gather_addresses class ChangeStateForm(forms.Form): state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg"), empty_label=None, required=True) @@ -1162,7 +1163,7 @@ def request_publication(request, name): send_mail_message(request, m) # IANA copy - m.to = settings.IANA_APPROVE_EMAIL + m.to = ",".join(gather_addresses('ballot_approved_ietf_stream_iana')) send_mail_message(request, m, extra=extra_automation_headers(doc)) e = DocEvent(doc=doc, type="requested_publication", by=request.user.person) diff --git a/ietf/eventmail/__init__.py b/ietf/eventmail/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/eventmail/migrations/0001_initial.py b/ietf/eventmail/migrations/0001_initial.py new file mode 100644 index 000000000..89f7b9a7a --- /dev/null +++ b/ietf/eventmail/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Ingredient', + fields=[ + ('slug', models.CharField(max_length=32, serialize=False, primary_key=True)), + ('desc', models.TextField(blank=True)), + ('template', models.CharField(max_length=512, null=True, blank=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Recipe', + fields=[ + ('slug', models.CharField(max_length=32, serialize=False, primary_key=True)), + ('desc', models.TextField(blank=True)), + ('ingredients', models.ManyToManyField(to='eventmail.Ingredient', null=True, blank=True)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/ietf/eventmail/migrations/__init__.py b/ietf/eventmail/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/eventmail/models.py b/ietf/eventmail/models.py new file mode 100644 index 000000000..83dcf26a9 --- /dev/null +++ b/ietf/eventmail/models.py @@ -0,0 +1,155 @@ +# Copyright The IETF Trust 2015, All Rights Reserved + +from django.db import models +from django.template import Template, Context + +class Recipe(models.Model): + slug = models.CharField(max_length=32, primary_key=True) + desc = models.TextField(blank=True) + ingredients = models.ManyToManyField('Ingredient', null=True, blank=True) + +class Ingredient(models.Model): + slug = models.CharField(max_length=32, primary_key=True) + desc = models.TextField(blank=True) + template = models.CharField(max_length=512, null=True, blank=True) + + 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(self.template).render(Context(kwargs)) + if rendering: + retval.extend(rendering.split(',')) + + return retval + + def gather_doc_group_chairs(self, **kwargs): + addrs = [] + if 'doc' in kwargs: + doc=kwargs['doc'] + if doc.group.type.slug in ['wg','rg']: + addrs.append('%s-chairs@ietf.org'%doc.group.acronym) + 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(Ingredient.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(Ingredient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc.document})) + return addrs + +def make_ingredients(): + + Ingredient.objects.all().delete() + Ingredient.objects.create(slug='iesg', + desc='The IESG', + template='The IESG ') + Ingredient.objects.create(slug='ietf_announce', + desc='The IETF Announce list', + template='IETF-Announce ') + Ingredient.objects.create(slug='rfc_editor', + desc='The RFC Editor', + template='') + Ingredient.objects.create(slug='iesg_secretary', + desc='The Secretariat', + template='') + Ingredient.objects.create(slug='doc_authors', + desc="The document's authors", + template='{{doc.name}}@ietf.org') + Ingredient.objects.create(slug='doc_notify', + desc="The addresses in the document's notify field", + template='{{doc.notify}}') + Ingredient.objects.create(slug='doc_group_chairs', + desc="The document's group chairs (if the document is assigned to a working or research group)", + template=None) + Ingredient.objects.create(slug='doc_affecteddoc_authors', + desc="The authors of the subject documents of a conflict-review or status-change", + template=None) + Ingredient.objects.create(slug='doc_affecteddoc_group_chairs', + desc="The chairs of groups of the subject documents of a conflict-review or status-change", + template=None) + Ingredient.objects.create(slug='doc_shepherd', + desc="The document's shepherd", + template='{% if doc.shepherd %}{{doc.shepherd.address}}{% endif %}' ) + Ingredient.objects.create(slug='doc_ad', + desc="The document's responsible Area Director", + template='{% if doc.ad %}{{doc.ad.email_address}}{% endif %}' ) + Ingredient.objects.create(slug='doc_group_mail_list', + desc="The list address of the document's group", + template=None ) + Ingredient.objects.create(slug='conflict_review_stream_owner', + desc="The stream owner of a document being reviewed for IETF stream conflicts", + template='{% ifequal doc.type_id "conflrev" %}{% ifequal doc.stream_id "ise" %}{% endifequal %}{% ifequal doc.stream_id "irtf" %}{% endifequal %}{% endifequal %}') + Ingredient.objects.create(slug='iana_approve', + desc="IANA's draft approval address", + template='IANA ') + +def make_recipes(): + + Recipe.objects.all().delete() + + r = Recipe.objects.create(slug='ballot_saved', + desc='Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved') + r.ingredients = Ingredient.objects.filter(slug__in=['iesg']) + + r = Recipe.objects.create(slug='ballot_saved_cc', + desc='Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved') + r.ingredients = Ingredient.objects.filter(slug__in=['doc_authors', + 'doc_group_chairs', + 'doc_shepherd', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'conflict_review_stream_owner', + ]) + + r = Recipe.objects.create(slug='ballot_deferred', + desc='Recipients when a ballot is deferred to or undeferred from a future telechat') + r.ingredients = Ingredient.objects.filter(slug__in=['iesg', + 'iesg_secretary', + 'doc_group_chairs', + 'doc_notify', + 'doc_authors', + 'doc_shepherd', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'conflict_review_stream_owner', + ]) + + r = Recipe.objects.create(slug='ballot_approved_ietf_stream', + desc='Recipients when an IETF stream document ballot is approved') + r.ingredients = Ingredient.objects.filter(slug__in=['ietf_announce']) + + r = Recipe.objects.create(slug='ballot_approved_ietf_stream_cc', + desc='Copied when an IETF stream document ballot is approved') + r.ingredients = Ingredient.objects.filter(slug__in=['iesg', + 'doc_notify', + 'doc_ad', + 'doc_authors', + 'doc_shepherd', + 'doc_group_mail_list', + 'doc_group_chairs', + 'rfc_editor', + ]) + + r = Recipe.objects.create(slug='ballot_approved_ietf_stream_iana', + desc='Recipients for IANA message when an IETF stream document ballot is approved') + r.ingredients = Ingredient.objects.filter(slug__in=['iana_approve']) + + diff --git a/ietf/eventmail/resources.py b/ietf/eventmail/resources.py new file mode 100644 index 000000000..828e66541 --- /dev/null +++ b/ietf/eventmail/resources.py @@ -0,0 +1,33 @@ +# 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.eventmail.models import * # pyflakes:ignore + + +class IngredientResource(ModelResource): + class Meta: + queryset = Ingredient.objects.all() + #resource_name = 'ingredient' + filtering = { + "slug": ALL, + "desc": ALL, + "template": ALL, + } +api.eventmail.register(IngredientResource()) + +class RecipeResource(ModelResource): + ingredients = ToManyField(IngredientResource, 'ingredients', null=True) + class Meta: + queryset = Recipe.objects.all() + #resource_name = 'recipe' + filtering = { + "slug": ALL, + "desc": ALL, + "ingredients": ALL_WITH_RELATIONS, + } +api.eventmail.register(RecipeResource()) + diff --git a/ietf/eventmail/tests.py b/ietf/eventmail/tests.py new file mode 100644 index 000000000..f1c84f51c --- /dev/null +++ b/ietf/eventmail/tests.py @@ -0,0 +1,43 @@ +from django.core.urlresolvers import reverse as urlreverse + +from ietf.utils.test_utils import TestCase +from ietf.utils.test_data import make_test_data +from ietf.eventmail.models import Ingredient + +class EventMailTests(TestCase): + + def setUp(self): + make_test_data() + + def test_show_patterns(self): + + url = urlreverse('ietf.eventmail.views.show_patterns') + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertTrue('ballot_saved_cc' in r.content) + + url = urlreverse('ietf.eventmail.views.show_patterns',kwargs=dict(eventmail_slug='ballot_saved_cc')) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertTrue('ballot_saved_cc' in r.content) + + def test_show_recipients(self): + + url = urlreverse('ietf.eventmail.views.show_ingredients') + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertTrue('bogus' in r.content) + + url = urlreverse('ietf.eventmail.views.show_ingredients',kwargs=dict(ingredient_slug='bogus')) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertTrue('bogus' in r.content) + +class IngredientTests(TestCase): + + def test_ingredient_functions(self): + draft = make_test_data() + ingredient = Ingredient.objects.first() + for funcname in [name for name in dir(ingredient) if name.startswith('gather_')]: + func=getattr(ingredient,funcname) + func(**{'doc':draft,'group':draft.group}) diff --git a/ietf/eventmail/urls.py b/ietf/eventmail/urls.py new file mode 100644 index 000000000..af64d9140 --- /dev/null +++ b/ietf/eventmail/urls.py @@ -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.eventmail.views', + url(r'^$', RedirectView.as_view(url=reverse_lazy('eventmail_show_patterns'), permanent=True)), + url(r'^event/$', 'show_patterns', name='eventmail_show_patterns' ), + url(r'^event/(?P[-\w]+)/$', 'show_patterns' ), + url(r'^recipient/$', 'show_ingredients' ), + url(r'^recipient/(?P[-\w]+)/$', 'show_ingredients' ), +) diff --git a/ietf/eventmail/utils.py b/ietf/eventmail/utils.py new file mode 100644 index 000000000..85f364e0a --- /dev/null +++ b/ietf/eventmail/utils.py @@ -0,0 +1,20 @@ + +from django.core.exceptions import ObjectDoesNotExist + +from ietf.eventmail.models import Recipe + +def gather_addresses(slug,**kwargs): + + addrs = [] + + try: + recipe = Recipe.objects.get(slug=slug) + except ObjectDoesNotExist: + # TODO remove the raise here, or find a better way to detect runtime misconfiguration + raise + return addrs + + for ingredient in recipe.ingredients.all(): + addrs.extend(ingredient.gather(**kwargs)) + + return addrs diff --git a/ietf/eventmail/views.py b/ietf/eventmail/views.py new file mode 100644 index 000000000..a3b2e6430 --- /dev/null +++ b/ietf/eventmail/views.py @@ -0,0 +1,24 @@ +# Copyright The IETF Trust 2015, All Rights Reserved + +from inspect import getsourcelines + +from django.shortcuts import render + +from ietf.eventmail.models import Recipe, Ingredient + +def show_patterns(request, eventmail_slug=None): + recipes = Recipe.objects.all() + if eventmail_slug: + recipes = recipes.filter(slug=eventmail_slug) # TODO better 404 behavior here and below + return render(request,'eventmail/show_patterns.html',{'eventmail_slug':eventmail_slug, + 'recipes':recipes}) +def show_ingredients(request, ingredient_slug=None): + ingredients = Ingredient.objects.all() + if ingredient_slug: + ingredients = ingredients.filter(slug=ingredient_slug) + for ingredient in ingredients: + fname = 'gather_%s'%ingredient.slug + if hasattr(ingredient,fname): + ingredient.code = ''.join(getsourcelines(getattr(ingredient,fname))[0]) + return render(request,'eventmail/ingredient.html',{'ingredient_slug':ingredient_slug, + 'ingredients':ingredients}) diff --git a/ietf/settings.py b/ietf/settings.py index b4866275d..1b414b780 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -212,6 +212,7 @@ INSTALLED_APPS = ( 'ietf.community', 'ietf.dbtemplate', 'ietf.doc', + 'ietf.eventmail', 'ietf.group', 'ietf.idindex', 'ietf.iesg', @@ -388,10 +389,8 @@ CACHES = { } IPR_EMAIL_TO = 'ietf-ipr@ietf.org' -DOC_APPROVAL_EMAIL_CC = ["RFC Editor ", ] 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" diff --git a/ietf/templates/doc/mail/approval_mail.txt b/ietf/templates/doc/mail/approval_mail.txt index 1be6fcd9b..dc1b4863b 100644 --- a/ietf/templates/doc/mail/approval_mail.txt +++ b/ietf/templates/doc/mail/approval_mail.txt @@ -1,5 +1,5 @@ {% load ietf_filters %}{%load mail_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce {% 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 }}) diff --git a/ietf/templates/eventmail/ingredient.html b/ietf/templates/eventmail/ingredient.html new file mode 100644 index 000000000..df0544e89 --- /dev/null +++ b/ietf/templates/eventmail/ingredient.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} + +{% block title %}Mail Recipients{% endblock %} + + +{% block content %} + {% origin %} +

Mail Recipients

+ + + + + + + + + + + + {% for ingredient in ingredients %} + + + + + + + {% endfor %} + +
RecipientEventTemplateCode
{{ingredient.slug}} + {% for recipe in ingredient.recipe_set.all %} + {{recipe.slug}}{% if not forloop.last %}, {%endif%} + {% endfor %} + {{ingredient.template}}{% if ingredient.code %}
{{ingredient.code}}
{% endif %}
+ +{% endblock %} diff --git a/ietf/templates/eventmail/show_patterns.html b/ietf/templates/eventmail/show_patterns.html new file mode 100644 index 000000000..e8ad6b941 --- /dev/null +++ b/ietf/templates/eventmail/show_patterns.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} + +{% block title %}Mail Events{% endblock %} + + +{% block content %} + {% origin %} +

Mail Events

+ + + + + + + + + + {% for recipe in recipes %} + + + + + {% endfor %} + +
EventRecipients
{{recipe.slug}} + {% for ingredient in recipe.ingredients.all %} + {% comment %}{{ingredient.slug}}{% endcomment %} + {{ingredient.slug}}{% if not forloop.last %}, {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/ietf/urls.py b/ietf/urls.py index a243c6d2f..8c80eb110 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -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'^eventmail/',include('ietf.eventmail.urls')), (r'^feed/', include('ietf.feed_urls')), (r'^group/', include('ietf.group.urls')), (r'^help/', include('ietf.help.urls')), diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 4a5790765..2b1c69c07 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -12,6 +12,7 @@ from ietf.ipr.models import HolderIprDisclosure, IprDocRel, IprDisclosureStateNa from ietf.meeting.models import Meeting from ietf.name.models import StreamName from ietf.person.models import Person, Email +from ietf.eventmail.models import Recipe, Ingredient def create_person(group, role_name, name=None, username=None, email_address=None, password=None): """Add person/user/email and role.""" @@ -330,4 +331,12 @@ def make_test_data(): other_doc_factory('minutes','minutes-42-mars') other_doc_factory('slides','slides-42-mars-1') + # EventMail tokens used by the views + # This won't allow testing the results of the production configuration - if we want to do that, we'll need to + # extract the production data either directly, or as a fixture + ingredient = Ingredient.objects.create(slug='bogus_ingredient',desc='Bogus Ingredient',template='bogus@example.com') + for slug in [u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc']: + r=Recipe.objects.create(slug=slug,desc=slug) + r.ingredients=[ingredient] + return draft From 7649397f5b6be11e6f65e0cb8ffbccc9d44a34f3 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sun, 9 Aug 2015 20:11:26 +0000 Subject: [PATCH 02/46] Better app and model names. Some model hygiene. Added admin. - Legacy-Id: 9993 --- ietf/doc/mails.py | 2 +- ietf/doc/tests_ballot.py | 2 +- ietf/doc/views_ballot.py | 2 +- ietf/doc/views_draft.py | 2 +- ietf/eventmail/urls.py | 11 --- ietf/eventmail/views.py | 24 ----- ietf/{eventmail => mailtoken}/__init__.py | 0 ietf/mailtoken/admin.py | 17 ++++ .../migrations/0001_initial.py | 29 +++--- .../migrations/__init__.py | 0 ietf/{eventmail => mailtoken}/models.py | 94 +++++++++++-------- ietf/{eventmail => mailtoken}/resources.py | 22 ++--- ietf/{eventmail => mailtoken}/tests.py | 22 ++--- ietf/mailtoken/urls.py | 11 +++ ietf/{eventmail => mailtoken}/utils.py | 8 +- ietf/mailtoken/views.py | 24 +++++ ietf/settings.py | 2 +- .../recipient.html} | 14 +-- .../token.html} | 10 +- ietf/urls.py | 2 +- ietf/utils/test_data.py | 8 +- 21 files changed, 171 insertions(+), 135 deletions(-) delete mode 100644 ietf/eventmail/urls.py delete mode 100644 ietf/eventmail/views.py rename ietf/{eventmail => mailtoken}/__init__.py (100%) create mode 100644 ietf/mailtoken/admin.py rename ietf/{eventmail => mailtoken}/migrations/0001_initial.py (70%) rename ietf/{eventmail => mailtoken}/migrations/__init__.py (100%) rename ietf/{eventmail => mailtoken}/models.py (66%) rename ietf/{eventmail => mailtoken}/resources.py (51%) rename ietf/{eventmail => mailtoken}/tests.py (56%) create mode 100644 ietf/mailtoken/urls.py rename ietf/{eventmail => mailtoken}/utils.py (60%) create mode 100644 ietf/mailtoken/views.py rename ietf/templates/{eventmail/ingredient.html => mailtoken/recipient.html} (52%) rename ietf/templates/{eventmail/show_patterns.html => mailtoken/token.html} (52%) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index b2e4bfd6a..8e849f0dd 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -14,7 +14,7 @@ from ietf.doc.utils import needed_ballot_positions from ietf.person.models import Person from ietf.group.models import Group, Role from ietf.doc.models import Document -from ietf.eventmail.utils import gather_addresses +from ietf.mailtoken.utils import gather_addresses def email_state_changed(request, doc, text): to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index b64206ff1..1ee4d1044 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -15,7 +15,7 @@ from ietf.utils.test_utils import TestCase from ietf.utils.mail import outbox from ietf.utils.test_data import make_test_data from ietf.utils.test_utils import login_testing_unauthorized -from ietf.eventmail.utils import gather_addresses +from ietf.mailtoken.utils import gather_addresses class EditPositionTests(TestCase): diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 89b0a4b20..f3e0b4cf4 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -27,7 +27,7 @@ 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.eventmail.utils import gather_addresses +from ietf.mailtoken.utils import gather_addresses BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 7051a90f4..c656c6cfe 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -37,7 +37,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.eventmail.utils import gather_addresses +from ietf.mailtoken.utils import gather_addresses class ChangeStateForm(forms.Form): state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg"), empty_label=None, required=True) diff --git a/ietf/eventmail/urls.py b/ietf/eventmail/urls.py deleted file mode 100644 index af64d9140..000000000 --- a/ietf/eventmail/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf.urls import patterns, url -from django.views.generic import RedirectView -from django.core.urlresolvers import reverse_lazy - -urlpatterns = patterns('ietf.eventmail.views', - url(r'^$', RedirectView.as_view(url=reverse_lazy('eventmail_show_patterns'), permanent=True)), - url(r'^event/$', 'show_patterns', name='eventmail_show_patterns' ), - url(r'^event/(?P[-\w]+)/$', 'show_patterns' ), - url(r'^recipient/$', 'show_ingredients' ), - url(r'^recipient/(?P[-\w]+)/$', 'show_ingredients' ), -) diff --git a/ietf/eventmail/views.py b/ietf/eventmail/views.py deleted file mode 100644 index a3b2e6430..000000000 --- a/ietf/eventmail/views.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright The IETF Trust 2015, All Rights Reserved - -from inspect import getsourcelines - -from django.shortcuts import render - -from ietf.eventmail.models import Recipe, Ingredient - -def show_patterns(request, eventmail_slug=None): - recipes = Recipe.objects.all() - if eventmail_slug: - recipes = recipes.filter(slug=eventmail_slug) # TODO better 404 behavior here and below - return render(request,'eventmail/show_patterns.html',{'eventmail_slug':eventmail_slug, - 'recipes':recipes}) -def show_ingredients(request, ingredient_slug=None): - ingredients = Ingredient.objects.all() - if ingredient_slug: - ingredients = ingredients.filter(slug=ingredient_slug) - for ingredient in ingredients: - fname = 'gather_%s'%ingredient.slug - if hasattr(ingredient,fname): - ingredient.code = ''.join(getsourcelines(getattr(ingredient,fname))[0]) - return render(request,'eventmail/ingredient.html',{'ingredient_slug':ingredient_slug, - 'ingredients':ingredients}) diff --git a/ietf/eventmail/__init__.py b/ietf/mailtoken/__init__.py similarity index 100% rename from ietf/eventmail/__init__.py rename to ietf/mailtoken/__init__.py diff --git a/ietf/mailtoken/admin.py b/ietf/mailtoken/admin.py new file mode 100644 index 000000000..1330dab64 --- /dev/null +++ b/ietf/mailtoken/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from ietf.mailtoken.models import MailToken, 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 MailTokenAdmin(admin.ModelAdmin): + list_display = [ 'slug', 'desc', ] + filter_horizontal = [ 'recipients' ] +admin.site.register(MailToken, MailTokenAdmin) + diff --git a/ietf/eventmail/migrations/0001_initial.py b/ietf/mailtoken/migrations/0001_initial.py similarity index 70% rename from ietf/eventmail/migrations/0001_initial.py rename to ietf/mailtoken/migrations/0001_initial.py index 89f7b9a7a..3c22d2b85 100644 --- a/ietf/eventmail/migrations/0001_initial.py +++ b/ietf/mailtoken/migrations/0001_initial.py @@ -11,25 +11,32 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Ingredient', + name='MailToken', + 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.CharField(max_length=512, null=True, blank=True)), ], options={ + 'ordering': ['slug'], }, bases=(models.Model,), ), - migrations.CreateModel( - name='Recipe', - fields=[ - ('slug', models.CharField(max_length=32, serialize=False, primary_key=True)), - ('desc', models.TextField(blank=True)), - ('ingredients', models.ManyToManyField(to='eventmail.Ingredient', null=True, blank=True)), - ], - options={ - }, - bases=(models.Model,), + migrations.AddField( + model_name='mailtoken', + name='recipients', + field=models.ManyToManyField(to='mailtoken.Recipient', null=True, blank=True), + preserve_default=True, ), ] diff --git a/ietf/eventmail/migrations/__init__.py b/ietf/mailtoken/migrations/__init__.py similarity index 100% rename from ietf/eventmail/migrations/__init__.py rename to ietf/mailtoken/migrations/__init__.py diff --git a/ietf/eventmail/models.py b/ietf/mailtoken/models.py similarity index 66% rename from ietf/eventmail/models.py rename to ietf/mailtoken/models.py index 83dcf26a9..f5bf8dfb4 100644 --- a/ietf/eventmail/models.py +++ b/ietf/mailtoken/models.py @@ -3,16 +3,28 @@ from django.db import models from django.template import Template, Context -class Recipe(models.Model): +class MailToken(models.Model): slug = models.CharField(max_length=32, primary_key=True) desc = models.TextField(blank=True) - ingredients = models.ManyToManyField('Ingredient', null=True, blank=True) + recipients = models.ManyToManyField('Recipient', null=True, blank=True) -class Ingredient(models.Model): + 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.CharField(max_length=512, 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): @@ -45,73 +57,73 @@ class Ingredient(models.Model): addrs = [] if 'doc' in kwargs: for reldoc in kwargs['doc'].related_that_doc(['conflrev','tohist','tois','tops']): - addrs.extend(Ingredient.objects.get(slug='doc_authors').gather(**{'doc':reldoc.document})) + 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(Ingredient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc.document})) + addrs.extend(Recipient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc.document})) return addrs -def make_ingredients(): +def make_recipients(): - Ingredient.objects.all().delete() - Ingredient.objects.create(slug='iesg', + Recipient.objects.all().delete() + Recipient.objects.create(slug='iesg', desc='The IESG', template='The IESG ') - Ingredient.objects.create(slug='ietf_announce', + Recipient.objects.create(slug='ietf_announce', desc='The IETF Announce list', template='IETF-Announce ') - Ingredient.objects.create(slug='rfc_editor', + Recipient.objects.create(slug='rfc_editor', desc='The RFC Editor', template='') - Ingredient.objects.create(slug='iesg_secretary', + Recipient.objects.create(slug='iesg_secretary', desc='The Secretariat', template='') - Ingredient.objects.create(slug='doc_authors', + Recipient.objects.create(slug='doc_authors', desc="The document's authors", template='{{doc.name}}@ietf.org') - Ingredient.objects.create(slug='doc_notify', + Recipient.objects.create(slug='doc_notify', desc="The addresses in the document's notify field", template='{{doc.notify}}') - Ingredient.objects.create(slug='doc_group_chairs', + Recipient.objects.create(slug='doc_group_chairs', desc="The document's group chairs (if the document is assigned to a working or research group)", template=None) - Ingredient.objects.create(slug='doc_affecteddoc_authors', + Recipient.objects.create(slug='doc_affecteddoc_authors', desc="The authors of the subject documents of a conflict-review or status-change", template=None) - Ingredient.objects.create(slug='doc_affecteddoc_group_chairs', + Recipient.objects.create(slug='doc_affecteddoc_group_chairs', desc="The chairs of groups of the subject documents of a conflict-review or status-change", template=None) - Ingredient.objects.create(slug='doc_shepherd', + Recipient.objects.create(slug='doc_shepherd', desc="The document's shepherd", template='{% if doc.shepherd %}{{doc.shepherd.address}}{% endif %}' ) - Ingredient.objects.create(slug='doc_ad', + Recipient.objects.create(slug='doc_ad', desc="The document's responsible Area Director", template='{% if doc.ad %}{{doc.ad.email_address}}{% endif %}' ) - Ingredient.objects.create(slug='doc_group_mail_list', + Recipient.objects.create(slug='doc_group_mail_list', desc="The list address of the document's group", template=None ) - Ingredient.objects.create(slug='conflict_review_stream_owner', + Recipient.objects.create(slug='conflict_review_stream_owner', desc="The stream owner of a document being reviewed for IETF stream conflicts", template='{% ifequal doc.type_id "conflrev" %}{% ifequal doc.stream_id "ise" %}{% endifequal %}{% ifequal doc.stream_id "irtf" %}{% endifequal %}{% endifequal %}') - Ingredient.objects.create(slug='iana_approve', + Recipient.objects.create(slug='iana_approve', desc="IANA's draft approval address", template='IANA ') -def make_recipes(): +def make_mailtokens(): - Recipe.objects.all().delete() + MailToken.objects.all().delete() - r = Recipe.objects.create(slug='ballot_saved', - desc='Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved') - r.ingredients = Ingredient.objects.filter(slug__in=['iesg']) + m = MailToken.objects.create(slug='ballot_saved', + desc='Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved') + m.recipients = Recipient.objects.filter(slug__in=['iesg']) - r = Recipe.objects.create(slug='ballot_saved_cc', - desc='Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved') - r.ingredients = Ingredient.objects.filter(slug__in=['doc_authors', + m = MailToken.objects.create(slug='ballot_saved_cc', + desc='Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved') + m.recipients = Recipient.objects.filter(slug__in=['doc_authors', 'doc_group_chairs', 'doc_shepherd', 'doc_affecteddoc_authors', @@ -119,9 +131,9 @@ def make_recipes(): 'conflict_review_stream_owner', ]) - r = Recipe.objects.create(slug='ballot_deferred', - desc='Recipients when a ballot is deferred to or undeferred from a future telechat') - r.ingredients = Ingredient.objects.filter(slug__in=['iesg', + m = MailToken.objects.create(slug='ballot_deferred', + desc='Recipients when a ballot is deferred to or undeferred from a future telechat') + m.recipients = Recipient.objects.filter(slug__in=['iesg', 'iesg_secretary', 'doc_group_chairs', 'doc_notify', @@ -132,13 +144,13 @@ def make_recipes(): 'conflict_review_stream_owner', ]) - r = Recipe.objects.create(slug='ballot_approved_ietf_stream', - desc='Recipients when an IETF stream document ballot is approved') - r.ingredients = Ingredient.objects.filter(slug__in=['ietf_announce']) + m = MailToken.objects.create(slug='ballot_approved_ietf_stream', + desc='Recipients when an IETF stream document ballot is approved') + m.recipients = Recipient.objects.filter(slug__in=['ietf_announce']) - r = Recipe.objects.create(slug='ballot_approved_ietf_stream_cc', - desc='Copied when an IETF stream document ballot is approved') - r.ingredients = Ingredient.objects.filter(slug__in=['iesg', + m = MailToken.objects.create(slug='ballot_approved_ietf_stream_cc', + desc='Copied when an IETF stream document ballot is approved') + m.recipients = Recipient.objects.filter(slug__in=['iesg', 'doc_notify', 'doc_ad', 'doc_authors', @@ -148,8 +160,8 @@ def make_recipes(): 'rfc_editor', ]) - r = Recipe.objects.create(slug='ballot_approved_ietf_stream_iana', - desc='Recipients for IANA message when an IETF stream document ballot is approved') - r.ingredients = Ingredient.objects.filter(slug__in=['iana_approve']) + m = MailToken.objects.create(slug='ballot_approved_ietf_stream_iana', + desc='Recipients for IANA message when an IETF stream document ballot is approved') + m.recipients = Recipient.objects.filter(slug__in=['iana_approve']) diff --git a/ietf/eventmail/resources.py b/ietf/mailtoken/resources.py similarity index 51% rename from ietf/eventmail/resources.py rename to ietf/mailtoken/resources.py index 828e66541..98f9ed7d3 100644 --- a/ietf/eventmail/resources.py +++ b/ietf/mailtoken/resources.py @@ -5,29 +5,29 @@ from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore from ietf import api -from ietf.eventmail.models import * # pyflakes:ignore +from ietf.mailtoken.models import * # pyflakes:ignore -class IngredientResource(ModelResource): +class RecipientResource(ModelResource): class Meta: - queryset = Ingredient.objects.all() - #resource_name = 'ingredient' + queryset = Recipient.objects.all() + #resource_name = 'recipient' filtering = { "slug": ALL, "desc": ALL, "template": ALL, } -api.eventmail.register(IngredientResource()) +api.mailtoken.register(RecipientResource()) -class RecipeResource(ModelResource): - ingredients = ToManyField(IngredientResource, 'ingredients', null=True) +class MailTokenResource(ModelResource): + recipients = ToManyField(RecipientResource, 'recipients', null=True) class Meta: - queryset = Recipe.objects.all() - #resource_name = 'recipe' + queryset = MailToken.objects.all() + #resource_name = 'mailtoken' filtering = { "slug": ALL, "desc": ALL, - "ingredients": ALL_WITH_RELATIONS, + "recipients": ALL_WITH_RELATIONS, } -api.eventmail.register(RecipeResource()) +api.mailtoken.register(MailTokenResource()) diff --git a/ietf/eventmail/tests.py b/ietf/mailtoken/tests.py similarity index 56% rename from ietf/eventmail/tests.py rename to ietf/mailtoken/tests.py index f1c84f51c..6bd0f4e9d 100644 --- a/ietf/eventmail/tests.py +++ b/ietf/mailtoken/tests.py @@ -2,42 +2,42 @@ from django.core.urlresolvers import reverse as urlreverse from ietf.utils.test_utils import TestCase from ietf.utils.test_data import make_test_data -from ietf.eventmail.models import Ingredient +from ietf.mailtoken.models import Recipient class EventMailTests(TestCase): def setUp(self): make_test_data() - def test_show_patterns(self): + def test_show_tokens(self): - url = urlreverse('ietf.eventmail.views.show_patterns') + url = urlreverse('ietf.mailtoken.views.show_tokens') r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertTrue('ballot_saved_cc' in r.content) - url = urlreverse('ietf.eventmail.views.show_patterns',kwargs=dict(eventmail_slug='ballot_saved_cc')) + url = urlreverse('ietf.mailtoken.views.show_tokens',kwargs=dict(mailtoken_slug='ballot_saved_cc')) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertTrue('ballot_saved_cc' in r.content) def test_show_recipients(self): - url = urlreverse('ietf.eventmail.views.show_ingredients') + url = urlreverse('ietf.mailtoken.views.show_recipients') r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertTrue('bogus' in r.content) - url = urlreverse('ietf.eventmail.views.show_ingredients',kwargs=dict(ingredient_slug='bogus')) + url = urlreverse('ietf.mailtoken.views.show_recipients',kwargs=dict(recipient_slug='bogus')) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertTrue('bogus' in r.content) -class IngredientTests(TestCase): +class RecipientTests(TestCase): - def test_ingredient_functions(self): + def test_recipient_functions(self): draft = make_test_data() - ingredient = Ingredient.objects.first() - for funcname in [name for name in dir(ingredient) if name.startswith('gather_')]: - func=getattr(ingredient,funcname) + recipient = Recipient.objects.first() + for funcname in [name for name in dir(recipient) if name.startswith('gather_')]: + func=getattr(recipient,funcname) func(**{'doc':draft,'group':draft.group}) diff --git a/ietf/mailtoken/urls.py b/ietf/mailtoken/urls.py new file mode 100644 index 000000000..1faf2b719 --- /dev/null +++ b/ietf/mailtoken/urls.py @@ -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.mailtoken.views', + url(r'^$', RedirectView.as_view(url=reverse_lazy('mailtoken_show_tokens'), permanent=True)), + url(r'^token/$', 'show_tokens', name='mailtoken_show_tokens' ), + url(r'^token/(?P[-\w]+)/$', 'show_tokens' ), + url(r'^recipient/$', 'show_recipients' ), + url(r'^recipient/(?P[-\w]+)/$', 'show_recipients' ), +) diff --git a/ietf/eventmail/utils.py b/ietf/mailtoken/utils.py similarity index 60% rename from ietf/eventmail/utils.py rename to ietf/mailtoken/utils.py index 85f364e0a..77635f213 100644 --- a/ietf/eventmail/utils.py +++ b/ietf/mailtoken/utils.py @@ -1,20 +1,20 @@ from django.core.exceptions import ObjectDoesNotExist -from ietf.eventmail.models import Recipe +from ietf.mailtoken.models import MailToken def gather_addresses(slug,**kwargs): addrs = [] try: - recipe = Recipe.objects.get(slug=slug) + mailtoken = MailToken.objects.get(slug=slug) except ObjectDoesNotExist: # TODO remove the raise here, or find a better way to detect runtime misconfiguration raise return addrs - for ingredient in recipe.ingredients.all(): - addrs.extend(ingredient.gather(**kwargs)) + for recipient in mailtoken.recipients.all(): + addrs.extend(recipient.gather(**kwargs)) return addrs diff --git a/ietf/mailtoken/views.py b/ietf/mailtoken/views.py new file mode 100644 index 000000000..1341f76f0 --- /dev/null +++ b/ietf/mailtoken/views.py @@ -0,0 +1,24 @@ +# Copyright The IETF Trust 2015, All Rights Reserved + +from inspect import getsourcelines + +from django.shortcuts import render + +from ietf.mailtoken.models import MailToken, Recipient + +def show_tokens(request, mailtoken_slug=None): + mailtokens = MailToken.objects.all() + if mailtoken_slug: + mailtokens = mailtokens.filter(slug=mailtoken_slug) # TODO better 404 behavior here and below + return render(request,'mailtoken/token.html',{'mailtoken_slug':mailtoken_slug, + 'mailtokens':mailtokens}) +def show_recipients(request, recipient_slug=None): + recipients = Recipient.objects.all() + if 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,'mailtoken/recipient.html',{'recipient_slug':recipient_slug, + 'recipients':recipients}) diff --git a/ietf/settings.py b/ietf/settings.py index 1b414b780..183c6b8a6 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -212,7 +212,6 @@ INSTALLED_APPS = ( 'ietf.community', 'ietf.dbtemplate', 'ietf.doc', - 'ietf.eventmail', 'ietf.group', 'ietf.idindex', 'ietf.iesg', @@ -220,6 +219,7 @@ INSTALLED_APPS = ( 'ietf.ipr', 'ietf.liaisons', 'ietf.mailinglists', + 'ietf.mailtoken', 'ietf.meeting', 'ietf.message', 'ietf.name', diff --git a/ietf/templates/eventmail/ingredient.html b/ietf/templates/mailtoken/recipient.html similarity index 52% rename from ietf/templates/eventmail/ingredient.html rename to ietf/templates/mailtoken/recipient.html index df0544e89..6122eaa10 100644 --- a/ietf/templates/eventmail/ingredient.html +++ b/ietf/templates/mailtoken/recipient.html @@ -13,22 +13,22 @@ Recipient - Event + Events Template Code - {% for ingredient in ingredients %} + {% for recipient in recipients %} - {{ingredient.slug}} + {{recipient.slug}} - {% for recipe in ingredient.recipe_set.all %} - {{recipe.slug}}{% if not forloop.last %}, {%endif%} + {% for mailtoken in recipient.mailtoken_set.all %} + {{mailtoken.slug}}{% if not forloop.last %}, {%endif%} {% endfor %} - {{ingredient.template}} - {% if ingredient.code %}
{{ingredient.code}}
{% endif %} + {{recipient.template}} + {% if recipient.code %}
{{recipient.code}}
{% endif %} {% endfor %} diff --git a/ietf/templates/eventmail/show_patterns.html b/ietf/templates/mailtoken/token.html similarity index 52% rename from ietf/templates/eventmail/show_patterns.html rename to ietf/templates/mailtoken/token.html index e8ad6b941..8525d95ee 100644 --- a/ietf/templates/eventmail/show_patterns.html +++ b/ietf/templates/mailtoken/token.html @@ -17,13 +17,13 @@ - {% for recipe in recipes %} + {% for mailtoken in mailtokens %} - {{recipe.slug}} + {{mailtoken.slug}} - {% for ingredient in recipe.ingredients.all %} - {% comment %}{{ingredient.slug}}{% endcomment %} -
{{ingredient.slug}}{% if not forloop.last %}, {% endif %} + {% for recipient in mailtoken.recipients.all %} + {% comment %}{{recipient.slug}}{% endcomment %} + {{recipient.slug}}{% if not forloop.last %}, {% endif %} {% endfor %} diff --git a/ietf/urls.py b/ietf/urls.py index 8c80eb110..f6956863a 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -35,7 +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'^eventmail/',include('ietf.eventmail.urls')), + (r'^mailtoken/',include('ietf.mailtoken.urls')), (r'^feed/', include('ietf.feed_urls')), (r'^group/', include('ietf.group.urls')), (r'^help/', include('ietf.help.urls')), diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 2b1c69c07..4d69123a1 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -12,7 +12,7 @@ from ietf.ipr.models import HolderIprDisclosure, IprDocRel, IprDisclosureStateNa from ietf.meeting.models import Meeting from ietf.name.models import StreamName from ietf.person.models import Person, Email -from ietf.eventmail.models import Recipe, Ingredient +from ietf.mailtoken.models import MailToken, Recipient def create_person(group, role_name, name=None, username=None, email_address=None, password=None): """Add person/user/email and role.""" @@ -334,9 +334,9 @@ def make_test_data(): # EventMail tokens used by the views # This won't allow testing the results of the production configuration - if we want to do that, we'll need to # extract the production data either directly, or as a fixture - ingredient = Ingredient.objects.create(slug='bogus_ingredient',desc='Bogus Ingredient',template='bogus@example.com') + recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') for slug in [u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc']: - r=Recipe.objects.create(slug=slug,desc=slug) - r.ingredients=[ingredient] + m = MailToken.objects.create(slug=slug,desc=slug) + m.recipients=[recipient] return draft From aebd885f2b2464bae079f1e890db694c80f27dd1 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sun, 9 Aug 2015 20:49:08 +0000 Subject: [PATCH 03/46] Moved the tokens created so far into a migration - Legacy-Id: 9994 --- .../migrations/0002_auto_20150809_1314.py | 147 ++++++++++++++++++ ietf/mailtoken/models.py | 98 ------------ ietf/templates/mailtoken/recipient.html | 2 +- ietf/templates/mailtoken/token.html | 6 +- 4 files changed, 151 insertions(+), 102 deletions(-) create mode 100644 ietf/mailtoken/migrations/0002_auto_20150809_1314.py diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py new file mode 100644 index 000000000..90d1ebcdb --- /dev/null +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def make_recipients(apps): + + Recipient=apps.get_model('mailtoken','Recipient') + + rc = Recipient.objects.create + + rc(slug='iesg', + desc='The IESG', + template='The IESG ') + + rc(slug='ietf_announce', + desc='The IETF Announce list', + template='IETF-Announce ') + + rc(slug='rfc_editor', + desc='The RFC Editor', + template='') + + rc(slug='iesg_secretary', + desc='The Secretariat', + template='') + + rc(slug='doc_authors', + desc="The document's authors", + template='{{doc.name}}@ietf.org') + + 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_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_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='conflict_review_stream_owner', + desc="The stream owner of a document being reviewed for IETF stream conflicts", + template='{% ifequal doc.type_id "conflrev" %}{% ifequal doc.stream_id "ise" %}{% endifequal %}{% ifequal doc.stream_id "irtf" %}{% endifequal %}{% endifequal %}') + + rc(slug='iana_approve', + desc="IANA's draft approval address", + template='IANA ') + +def make_mailtokens(apps): + + Recipient=apps.get_model('mailtoken','Recipient') + MailToken=apps.get_model('mailtoken','MailToken') + + def mt_factory(slug,desc,recipient_slugs): + m = MailToken.objects.create(slug=slug, desc=desc) + m.recipients = Recipient.objects.filter(slug__in=recipient_slugs) + + mt_factory(slug='ballot_saved', + desc='Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved', + recipient_slugs=['iesg']) + + mt_factory(slug='ballot_saved_cc', + desc='Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved', + recipient_slugs=['doc_authors', + 'doc_group_chairs', + 'doc_shepherd', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'conflict_review_stream_owner', + ]) + + mt_factory(slug='ballot_deferred', + desc='Recipients when a ballot is deferred to or undeferred from a future telechat', + recipient_slugs=['iesg', + 'iesg_secretary', + 'doc_group_chairs', + 'doc_notify', + 'doc_authors', + 'doc_shepherd', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'conflict_review_stream_owner', + ]) + + mt_factory(slug='ballot_approved_ietf_stream', + desc='Recipients when an IETF stream document ballot is approved', + recipient_slugs=['ietf_announce']) + + mt_factory(slug='ballot_approved_ietf_stream_cc', + desc='Copied when an IETF stream document ballot is approved', + recipient_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', + recipient_slugs=['iana_approve']) + + +def forward(apps, schema_editor): + + make_recipients(apps) + make_mailtokens(apps) + +def reverse(apps, schema_editor): + + Recipient=apps.get_model('mailtoken','Recipient') + MailToken=apps.get_model('mailtoken','MailToken') + + Recipient.objects.all().delete() + MailToken.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailtoken', '0001_initial'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index f5bf8dfb4..bafeec50b 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -67,101 +67,3 @@ class Recipient(models.Model): addrs.extend(Recipient.objects.get(slug='doc_group_chairs').gather(**{'doc':reldoc.document})) return addrs -def make_recipients(): - - Recipient.objects.all().delete() - Recipient.objects.create(slug='iesg', - desc='The IESG', - template='The IESG ') - Recipient.objects.create(slug='ietf_announce', - desc='The IETF Announce list', - template='IETF-Announce ') - Recipient.objects.create(slug='rfc_editor', - desc='The RFC Editor', - template='') - Recipient.objects.create(slug='iesg_secretary', - desc='The Secretariat', - template='') - Recipient.objects.create(slug='doc_authors', - desc="The document's authors", - template='{{doc.name}}@ietf.org') - Recipient.objects.create(slug='doc_notify', - desc="The addresses in the document's notify field", - template='{{doc.notify}}') - Recipient.objects.create(slug='doc_group_chairs', - desc="The document's group chairs (if the document is assigned to a working or research group)", - template=None) - Recipient.objects.create(slug='doc_affecteddoc_authors', - desc="The authors of the subject documents of a conflict-review or status-change", - template=None) - Recipient.objects.create(slug='doc_affecteddoc_group_chairs', - desc="The chairs of groups of the subject documents of a conflict-review or status-change", - template=None) - Recipient.objects.create(slug='doc_shepherd', - desc="The document's shepherd", - template='{% if doc.shepherd %}{{doc.shepherd.address}}{% endif %}' ) - Recipient.objects.create(slug='doc_ad', - desc="The document's responsible Area Director", - template='{% if doc.ad %}{{doc.ad.email_address}}{% endif %}' ) - Recipient.objects.create(slug='doc_group_mail_list', - desc="The list address of the document's group", - template=None ) - Recipient.objects.create(slug='conflict_review_stream_owner', - desc="The stream owner of a document being reviewed for IETF stream conflicts", - template='{% ifequal doc.type_id "conflrev" %}{% ifequal doc.stream_id "ise" %}{% endifequal %}{% ifequal doc.stream_id "irtf" %}{% endifequal %}{% endifequal %}') - Recipient.objects.create(slug='iana_approve', - desc="IANA's draft approval address", - template='IANA ') - -def make_mailtokens(): - - MailToken.objects.all().delete() - - m = MailToken.objects.create(slug='ballot_saved', - desc='Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved') - m.recipients = Recipient.objects.filter(slug__in=['iesg']) - - m = MailToken.objects.create(slug='ballot_saved_cc', - desc='Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved') - m.recipients = Recipient.objects.filter(slug__in=['doc_authors', - 'doc_group_chairs', - 'doc_shepherd', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'conflict_review_stream_owner', - ]) - - m = MailToken.objects.create(slug='ballot_deferred', - desc='Recipients when a ballot is deferred to or undeferred from a future telechat') - m.recipients = Recipient.objects.filter(slug__in=['iesg', - 'iesg_secretary', - 'doc_group_chairs', - 'doc_notify', - 'doc_authors', - 'doc_shepherd', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'conflict_review_stream_owner', - ]) - - m = MailToken.objects.create(slug='ballot_approved_ietf_stream', - desc='Recipients when an IETF stream document ballot is approved') - m.recipients = Recipient.objects.filter(slug__in=['ietf_announce']) - - m = MailToken.objects.create(slug='ballot_approved_ietf_stream_cc', - desc='Copied when an IETF stream document ballot is approved') - m.recipients = Recipient.objects.filter(slug__in=['iesg', - 'doc_notify', - 'doc_ad', - 'doc_authors', - 'doc_shepherd', - 'doc_group_mail_list', - 'doc_group_chairs', - 'rfc_editor', - ]) - - m = MailToken.objects.create(slug='ballot_approved_ietf_stream_iana', - desc='Recipients for IANA message when an IETF stream document ballot is approved') - m.recipients = Recipient.objects.filter(slug__in=['iana_approve']) - - diff --git a/ietf/templates/mailtoken/recipient.html b/ietf/templates/mailtoken/recipient.html index 6122eaa10..547854ead 100644 --- a/ietf/templates/mailtoken/recipient.html +++ b/ietf/templates/mailtoken/recipient.html @@ -13,7 +13,7 @@ Recipient - Events + Tokens Template Code diff --git a/ietf/templates/mailtoken/token.html b/ietf/templates/mailtoken/token.html index 8525d95ee..d4a888a86 100644 --- a/ietf/templates/mailtoken/token.html +++ b/ietf/templates/mailtoken/token.html @@ -2,17 +2,17 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} -{% block title %}Mail Events{% endblock %} +{% block title %}Mail Tokens{% endblock %} {% block content %} {% origin %} -

Mail Events

+

Mail Tokens

- + From ad444bd0bc04254e2b88fa655ed4c71d3422c8f8 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 10 Aug 2015 20:15:31 +0000 Subject: [PATCH 04/46] checkpoint - Legacy-Id: 10007 --- ietf/doc/mails.py | 21 ++---- ietf/doc/utils_charter.py | 3 + ietf/doc/views_status_change.py | 3 + .../migrations/0002_auto_20150809_1314.py | 75 ++++++++++++++++++- ietf/mailtoken/models.py | 30 ++++++++ ietf/templates/doc/charter/action_text.txt | 4 +- .../doc/mail/approval_mail_rfc_editor.txt | 2 +- .../doc/status_change/approval_text.txt | 4 +- ietf/utils/test_data.py | 2 +- 9 files changed, 121 insertions(+), 23 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 8e849f0dd..0d5b740cf 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -12,7 +12,7 @@ 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.group.models import Role from ietf.doc.models import Document from ietf.mailtoken.utils import gather_addresses @@ -217,27 +217,16 @@ def generate_approval_mail_rfc_editor(request, doc): disapproved = doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft" - to = [] - if doc.group.type_id != "individ": - for r in doc.group.role_set.filter(name="chair").select_related(): - to.append(r.formatted_email()) - - if doc.stream_id in ("ise", "irtf"): - # include ISE/IRTF chairs - g = Group.objects.get(acronym=doc.stream_id) - for r in g.role_set.filter(name="chair").select_related(): - to.append(r.formatted_email()) - - if doc.stream_id == "irtf": - # include IRSG - to.append('"Internet Research Steering Group" ') + to = gather_addresses('ballot_approved_conflrev', doc=doc) + cc = gather_addresses('ballot_approved_conflrev_cc', doc=doc) 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=",\n ".join(to), + cc=",\n ".join(cc), ) ) diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index b3812b11c..62d1b116d 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -8,6 +8,7 @@ from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent, BallotPosition from ietf.person.models import Person from ietf.utils.history import find_history_active_at from ietf.utils.mail import send_mail_text +from ietf.mailtoken.utils import gather_addresses def charter_name_for_group(group): if group.type_id == "rg": @@ -126,6 +127,8 @@ 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=", ".join(gather_addresses('ballot_approved_charter',doc=charter,group=group)), + cc=", ".join(gather_addresses('ballot_approved_charter_cc',doc=charter,group=group)), )) e.save() diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 39bab4214..3b2e67ee9 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -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.mailtoken.utils import gather_addresses 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) @@ -298,6 +299,8 @@ def default_approval_text(status_change,relateddoc): approved_text = current_text, action=action, newstatus=newstatus(relateddoc), + to=", ".join(gather_addresses('ballot_approved_status_change',doc=status_change)), + cc=", ".join(gather_addresses('ballot_approved_status_change_cc',doc=status_change)), ) ) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 90d1ebcdb..7e9867450 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -45,6 +45,10 @@ def make_recipients(apps): 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 %}' ) @@ -57,14 +61,38 @@ def make_recipients(apps): desc="The list address of the document's group", template=None ) + rc(slug='doc_stream_owner', + desc="The owner of the document's stream", + template='{% if doc.stream_id == "ise" %}{% endif %}{% if doc.stream_id == "irtf" %}{% endif %}{% if doc.stream_id == "ietf" %}{% endif %}') + rc(slug='conflict_review_stream_owner', desc="The stream owner of a document being reviewed for IETF stream conflicts", - template='{% ifequal doc.type_id "conflrev" %}{% ifequal doc.stream_id "ise" %}{% endifequal %}{% ifequal doc.stream_id "irtf" %}{% endifequal %}{% endifequal %}') + 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 ') + rc(slug='iana', + desc="IANA", + template='') + + rc(slug='group_mail_list', + desc="The group's mailing list", + template='{{ group.list_email }}') + + 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="{{group.acronym}}-chairs@ietf.org") + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -85,6 +113,7 @@ def make_mailtokens(apps): 'doc_shepherd', 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', 'conflict_review_stream_owner', ]) @@ -98,6 +127,7 @@ def make_mailtokens(apps): 'doc_shepherd', 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', 'conflict_review_stream_owner', ]) @@ -121,6 +151,49 @@ def make_mailtokens(apps): desc='Recipients for IANA message when an IETF stream document ballot is approved', recipient_slugs=['iana_approve']) + mt_factory(slug='ballot_approved_conflrev', + desc='Recipients when a conflict review ballot is approved', + recipient_slugs=['conflict_review_stream_owner', + 'conflict_review_steering_group', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', + 'doc_notify', + ]) + + mt_factory(slug='ballot_approved_conflrev_cc', + desc='Copied when a conflict review ballot is approved', + recipient_slugs=['iesg', + 'ietf_announce', + 'iana', + ]) + + mt_factory(slug='ballot_approved_charter', + desc='Recipients when a charter is approved', + recipient_slugs=['ietf_announce',]) + + mt_factory(slug='ballot_approved_charter_cc', + desc='Copied when a charter is approved', + recipient_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', + recipient_slugs=['ietf_announce',]) + + mt_factory(slug='ballot_approved_status_change_cc', + desc='Copied when a status change is approved', + recipient_slugs=['iesg', + 'rfc_editor', + 'doc_notify', + 'doc_affectddoc_authors', + 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', + ]) + def forward(apps, schema_editor): diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index bafeec50b..01bb07ace 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -67,3 +67,33 @@ class Recipient(models.Model): 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_owner(self, **kwargs): + addrs = [] + if 'doc' in kwargs: + for reldoc in kwargs['doc'].related_that_doc(['conflrev']): + addrs.extend(Recipient.objects.get(slug='doc_stream_owner').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" ') + return addrs + + def gather_group_steering_group(self,**kwargs): + addrs = [] + sg_map = dict( wg='"The IESG" ', rg='"Internet Research Steering Group" ' ) + if 'group' in kwargs and kwargs['group'].type_id in sg_map: + addrs.append(sg_map[kwargs['group'].type_id]) + return addrs + + diff --git a/ietf/templates/doc/charter/action_text.txt b/ietf/templates/doc/charter/action_text.txt index af508f788..c92807f64 100644 --- a/ietf/templates/doc/charter/action_text.txt +++ b/ietf/templates/doc/charter/action_text.txt @@ -1,6 +1,6 @@ {% load ietf_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce {% 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}}. diff --git a/ietf/templates/doc/mail/approval_mail_rfc_editor.txt b/ietf/templates/doc/mail/approval_mail_rfc_editor.txt index 38cc1705a..99420d58b 100644 --- a/ietf/templates/doc/mail/approval_mail_rfc_editor.txt +++ b/ietf/templates/doc/mail/approval_mail_rfc_editor.txt @@ -1,6 +1,6 @@ {% load mail_filters %}{% autoescape off %}From: The IESG To: {{to}} -Cc: The IESG , , +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. diff --git a/ietf/templates/doc/status_change/approval_text.txt b/ietf/templates/doc/status_change/approval_text.txt index 25310852e..04cf0f351 100644 --- a/ietf/templates/doc/status_change/approval_text.txt +++ b/ietf/templates/doc/status_change/approval_text.txt @@ -1,6 +1,6 @@ {% load mail_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce -Cc: RFC Editor , {{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: diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 4d69123a1..d90ffdb03 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -335,7 +335,7 @@ def make_test_data(): # This won't allow testing the results of the production configuration - if we want to do that, we'll need to # extract the production data either directly, or as a fixture recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') - for slug in [u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc']: + for slug in [u'ballot_approved_charter', u'ballot_approved_charter_cc', u'ballot_approved_conflrev', u'ballot_approved_conflrev_cc', u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_approved_status_change', u'ballot_approved_status_change_cc', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc']: m = MailToken.objects.create(slug=slug,desc=slug) m.recipients=[recipient] From ecad71eb8225a0f4e291576d61922075ed215992 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 10 Aug 2015 21:31:19 +0000 Subject: [PATCH 05/46] Improvements based on sanity checking - Legacy-Id: 10008 --- ietf/mailtoken/migrations/0002_auto_20150809_1314.py | 2 +- ietf/mailtoken/models.py | 5 +++-- ietf/mailtoken/utils.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 7e9867450..8c8a0dc6b 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -27,7 +27,7 @@ def make_recipients(apps): rc(slug='doc_authors', desc="The document's authors", - template='{{doc.name}}@ietf.org') + template='{% if doc.type_id == "draft" %}{{doc.name}}@ietf.org{% endif %}') rc(slug='doc_notify', desc="The addresses in the document's notify field", diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index 01bb07ace..97e885dc6 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -30,10 +30,11 @@ class Recipient(models.Model): if hasattr(self,'gather_%s'%self.slug): retval.extend(eval('self.gather_%s(**kwargs)'%self.slug)) if self.template: - rendering = Template(self.template).render(Context(kwargs)) + rendering = Template('{%% autoescape off %%}%s{%% endautoescape %%}'%self.template).render(Context(kwargs)) if rendering: - retval.extend(rendering.split(',')) + retval.extend([x.strip() for x in rendering.split(',')]) + retval = list(set(retval)) return retval def gather_doc_group_chairs(self, **kwargs): diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 77635f213..0746fe0a2 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -17,4 +17,4 @@ def gather_addresses(slug,**kwargs): for recipient in mailtoken.recipients.all(): addrs.extend(recipient.gather(**kwargs)) - return addrs + return list(set(addrs)) From 6c85bc558cb8c5d9519dfdf407052409913b912a Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Aug 2015 02:27:59 +0000 Subject: [PATCH 06/46] checkpoint. Sets initial notify to empty - Legacy-Id: 10009 --- ietf/doc/mails.py | 13 ++++--- ietf/doc/tests_charter.py | 3 +- ietf/doc/tests_conflict_review.py | 4 +- ietf/doc/tests_draft.py | 8 ++-- ietf/doc/tests_status_change.py | 6 +-- ietf/doc/utils.py | 38 +------------------ ietf/doc/utils_charter.py | 4 +- ietf/doc/views_status_change.py | 4 +- .../migrations/0002_auto_20150809_1314.py | 25 ++++++++++++ .../doc/mail/last_call_announcement.txt | 2 +- ietf/utils/test_data.py | 2 +- 11 files changed, 46 insertions(+), 63 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 0d5b740cf..536774e9c 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -105,14 +105,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) @@ -127,7 +124,8 @@ def generate_last_call_announcement(request, doc): 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=",\n ".join(gather_addresses('last_call_issued',doc=doc)), + cc=",\n ".join(gather_addresses('last_call_issued_cc',doc=doc)), group=group, docs=[ doc ], urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ], @@ -256,14 +254,17 @@ def generate_publication_request(request, doc): ) def send_last_call_request(request, doc): - to = "iesg-secretary@ietf.org" + to = gather_addresses('last_call_requested',doc=doc) + cc = gather_addresses('last_call_requested_cc',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 " diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index ac99b7552..c756cd05a 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -196,8 +196,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() diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py index 6de5ac13f..ec90c60ec 100644 --- a/ietf/doc/tests_conflict_review.py +++ b/ietf/doc/tests_conflict_review.py @@ -189,9 +189,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') diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 44a9b0b61..618ca12b5 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -330,7 +330,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() @@ -734,7 +734,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)) @@ -1092,9 +1092,7 @@ 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])) diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py index b22cd9f31..90e3587e6 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))) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index c3bf73fa3..e26c57512 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -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,10 +12,9 @@ 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 @@ -543,41 +541,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 mailtoken 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(', ') diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index 62d1b116d..fa2f83b2b 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -127,8 +127,8 @@ 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=", ".join(gather_addresses('ballot_approved_charter',doc=charter,group=group)), - cc=", ".join(gather_addresses('ballot_approved_charter_cc',doc=charter,group=group)), + to=",\n ".join(gather_addresses('ballot_approved_charter',doc=charter,group=group)), + cc=",\n ".join(gather_addresses('ballot_approved_charter_cc',doc=charter,group=group)), )) e.save() diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 3b2e67ee9..2ed24b7dd 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -299,8 +299,8 @@ def default_approval_text(status_change,relateddoc): approved_text = current_text, action=action, newstatus=newstatus(relateddoc), - to=", ".join(gather_addresses('ballot_approved_status_change',doc=status_change)), - cc=", ".join(gather_addresses('ballot_approved_status_change_cc',doc=status_change)), + to=",\n ".join(gather_addresses('ballot_approved_status_change',doc=status_change)), + cc=",\n ".join(gather_addresses('ballot_approved_status_change_cc',doc=status_change)), ) ) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 8c8a0dc6b..1c00c5ffb 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -194,6 +194,31 @@ def make_mailtokens(apps): 'doc_affecteddoc_notify', ]) + mt_factory(slug='last_call_requested', + desc='Recipients when AD requests a last call', + recipient_slugs=['iesg_secretary',]) + + mt_factory(slug='last_call_requested_cc', + desc='Copied when AD requests a last call', + recipient_slugs=['doc_ad', + 'doc_shepherd', + 'doc_notify']) + + mt_factory(slug='last_call_issued', + desc='Recipients when a last call is issued', + recipient_slugs=['ietf_announce',]) + + mt_factory(slug='last_call_issued_cc', + desc='Copied when a last call is issued', + recipient_slugs=['doc_ad', + 'doc_shepherd', + 'doc_authors', + 'doc_notify', + 'doc_group_list_email', + 'doc_group_chairs', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify']) def forward(apps, schema_editor): diff --git a/ietf/templates/doc/mail/last_call_announcement.txt b/ietf/templates/doc/mail/last_call_announcement.txt index 170132eb9..5d7421858 100644 --- a/ietf/templates/doc/mail/last_call_announcement.txt +++ b/ietf/templates/doc/mail/last_call_announcement.txt @@ -1,5 +1,5 @@ {% load ietf_filters %}{% load mail_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce {% if cc %} +To: {{ to }}{% if cc %} CC: {{ cc }}{% endif %} Reply-To: ietf@ietf.org Sender: diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index d90ffdb03..ff43d0dea 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -335,7 +335,7 @@ def make_test_data(): # This won't allow testing the results of the production configuration - if we want to do that, we'll need to # extract the production data either directly, or as a fixture recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') - for slug in [u'ballot_approved_charter', u'ballot_approved_charter_cc', u'ballot_approved_conflrev', u'ballot_approved_conflrev_cc', u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_approved_status_change', u'ballot_approved_status_change_cc', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc']: + for slug in [u'ballot_approved_charter', u'ballot_approved_charter_cc', u'ballot_approved_conflrev', u'ballot_approved_conflrev_cc', u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_approved_status_change', u'ballot_approved_status_change_cc', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc', u'last_call_issued', u'last_call_issued_cc', u'last_call_requested', u'last_call_requested_cc']: m = MailToken.objects.create(slug=slug,desc=slug) m.recipients=[recipient] From 15538ded7ebaebb0e2e5f0ebbdd5c04681ef3bcd Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Aug 2015 17:26:53 +0000 Subject: [PATCH 07/46] checkpoint - Legacy-Id: 10010 --- ietf/doc/mails.py | 8 +-- ietf/doc/views_ballot.py | 3 +- ietf/doc/views_draft.py | 8 +-- .../migrations/0002_auto_20150809_1314.py | 55 ++++++++++++++++--- ietf/utils/test_data.py | 2 +- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 536774e9c..46cc6ca5b 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -412,18 +412,18 @@ 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 - to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] - to.insert(0, "iesg@ietf.org") + to = gather_addresses('last_call_expired',doc=doc) + cc = gather_addresses('last_call_expired_cc',doc=doc) send_mail(None, - to, + ",\n ".join(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=",\n ".join(cc)) def stream_state_email_recipients(doc, extra_recipients=[]): persons = set() diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index f3e0b4cf4..d858c77b1 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -758,7 +758,8 @@ def make_last_call(request, name): send_mail_preformatted(request, announcement) if doc.type.slug == 'draft': send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), - override={ "To": "IANA ", "CC": None, "Bcc": None, "Reply-To": None}) + override={ "To": ",\n ".join(gather_addresses('last_call_issued_iana',doc=doc)), + "CC": None, "Bcc": None, "Reply-To": None}) msg = infer_message(announcement) msg.by = login diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index c656c6cfe..8e069f8a0 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -583,9 +583,9 @@ def to_iesg(request,name): doc.save() extra = {} - extra['Cc'] = "%s-chairs@tools.ietf.org, iesg-secretary@ietf.org, %s" % (doc.group.acronym,doc.notify) + extra['Cc'] = ",\n ".join(gather_addresses('pubreq_iesg_cc',doc=doc)) send_mail(request=request, - to = doc.ad.email_address(), + to = ",\n ".join(gather_addresses('pubreq_iesg',doc=doc)), frm = login.formatted_email(), subject = "Publication has been requested for %s-%s" % (doc.name,doc.rev), template = "doc/submit_to_iesg_email.txt", @@ -1133,7 +1133,7 @@ def request_publication(request, name): m = Message() m.frm = request.user.person.formatted_email() - m.to = "RFC Editor " + m.to = ",\n ".join(gather_addresses('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") @@ -1163,7 +1163,7 @@ def request_publication(request, name): send_mail_message(request, m) # IANA copy - m.to = ",".join(gather_addresses('ballot_approved_ietf_stream_iana')) + m.to = ", ".join(gather_addresses('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) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 1c00c5ffb..6ad3ba54f 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -61,12 +61,12 @@ def make_recipients(apps): desc="The list address of the document's group", template=None ) - rc(slug='doc_stream_owner', - desc="The owner of the document's stream", + rc(slug='doc_stream_manager', + desc="The manager of the document's stream", template='{% if doc.stream_id == "ise" %}{% endif %}{% if doc.stream_id == "irtf" %}{% endif %}{% if doc.stream_id == "ietf" %}{% endif %}') - rc(slug='conflict_review_stream_owner', - desc="The stream owner of a document being reviewed for IETF stream conflicts", + 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', @@ -77,6 +77,10 @@ def make_recipients(apps): desc="IANA's draft approval address", template='IANA ') + rc(slug='iana_last_call', + desc="IANA's draft last call address", + template='IANA ') + rc(slug='iana', desc="IANA", template='') @@ -114,7 +118,7 @@ def make_mailtokens(apps): 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', 'doc_affecteddoc_notify', - 'conflict_review_stream_owner', + 'conflict_review_stream_manager', ]) mt_factory(slug='ballot_deferred', @@ -128,7 +132,7 @@ def make_mailtokens(apps): 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', 'doc_affecteddoc_notify', - 'conflict_review_stream_owner', + 'conflict_review_stream_manager', ]) mt_factory(slug='ballot_approved_ietf_stream', @@ -153,7 +157,7 @@ def make_mailtokens(apps): mt_factory(slug='ballot_approved_conflrev', desc='Recipients when a conflict review ballot is approved', - recipient_slugs=['conflict_review_stream_owner', + recipient_slugs=['conflict_review_stream_manager', 'conflict_review_steering_group', 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', @@ -220,6 +224,43 @@ def make_mailtokens(apps): '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', + recipient_slugs=['iana_last_call']) + + mt_factory(slug='last_call_expired', + desc='Recipients when a last call has expired', + recipient_slugs=['iesg', + 'doc_notify', + 'doc_authors', + 'doc_shepherd', + ]) + + mt_factory(slug='last_call_expired_cc', + desc='Copied when a last call has expired', + recipient_slugs=['iesg_secretary',]) + + mt_factory(slug='pubreq_iesg', + desc='Recipients when a draft is submitted to the IESG', + recipient_slugs=['doc_ad',]) + + mt_factory(slug='pubreq_iesg_cc', + desc='Copied when a draft is submitted to the IESG', + recipient_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', + recipient_slugs=['rfc_editor', + ]) + + mt_factory(slug='pubreq_rfced_iana', + desc='Recipients for IANA message when a non-IETF stream manager requests publication', + recipient_slugs=['iana_approve',]) + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index ff43d0dea..f4cd7097e 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -335,7 +335,7 @@ def make_test_data(): # This won't allow testing the results of the production configuration - if we want to do that, we'll need to # extract the production data either directly, or as a fixture recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') - for slug in [u'ballot_approved_charter', u'ballot_approved_charter_cc', u'ballot_approved_conflrev', u'ballot_approved_conflrev_cc', u'ballot_approved_ietf_stream', u'ballot_approved_ietf_stream_cc', u'ballot_approved_ietf_stream_iana', u'ballot_approved_status_change', u'ballot_approved_status_change_cc', u'ballot_deferred', u'ballot_saved', u'ballot_saved_cc', u'last_call_issued', u'last_call_issued_cc', u'last_call_requested', u'last_call_requested_cc']: + for slug in ['ballot_approved_charter', 'ballot_approved_charter_cc', 'ballot_approved_conflrev', 'ballot_approved_conflrev_cc', 'ballot_approved_ietf_stream', 'ballot_approved_ietf_stream_cc', 'ballot_approved_ietf_stream_iana', 'ballot_approved_status_change', 'ballot_approved_status_change_cc', 'ballot_deferred', 'ballot_saved', 'ballot_saved_cc', 'last_call_expired', 'last_call_expired_cc', 'last_call_issued', 'last_call_issued_cc', 'last_call_issued_iana', 'last_call_requested', 'last_call_requested_cc', 'pubreq_iesg', 'pubreq_iesg_cc', 'pubreq_rfced', 'pubreq_rfced_iana']: m = MailToken.objects.create(slug=slug,desc=slug) m.recipients=[recipient] From 79d373fa712e8e39e2c50502072ff932679d499f Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Aug 2015 21:52:28 +0000 Subject: [PATCH 08/46] checkpoint. - Legacy-Id: 10011 --- ietf/doc/mails.py | 27 +++++------- ietf/doc/tests_ballot.py | 5 +-- ietf/doc/tests_conflict_review.py | 5 +-- ietf/doc/utils_charter.py | 6 ++- ietf/doc/views_ballot.py | 8 ++-- ietf/doc/views_conflict_review.py | 7 ++- ietf/doc/views_draft.py | 9 ++-- ietf/doc/views_status_change.py | 4 +- .../migrations/0002_auto_20150809_1314.py | 43 ++++++++++++++++++- ietf/mailtoken/models.py | 17 ++++++++ ietf/mailtoken/utils.py | 5 ++- ietf/templates/doc/charter/review_text.txt | 4 +- .../doc/conflict_review/review_started.txt | 5 ++- ietf/utils/test_data.py | 5 ++- 14 files changed, 106 insertions(+), 44 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 46cc6ca5b..3d0214bc7 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -30,13 +30,12 @@ def email_state_changed(request, doc, text): 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 = gather_addresses('doc_stream_changed',doc=doc,streams=streams) if not to: return @@ -124,8 +123,8 @@ def generate_last_call_announcement(request, doc): 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"), - to=",\n ".join(gather_addresses('last_call_issued',doc=doc)), - cc=",\n ".join(gather_addresses('last_call_issued_cc',doc=doc)), + to=gather_addresses('last_call_issued',doc=doc), + cc=gather_addresses('last_call_issued_cc',doc=doc), group=group, docs=[ doc ], urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ], @@ -169,9 +168,6 @@ def generate_approval_mail_approved(request, doc): else: action_type = "Document" - to = gather_addresses('ballot_approved_ietf_stream',doc=doc) - cc = gather_addresses('ballot_approved_ietf_stream_cc',doc=doc) - # 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"): @@ -202,8 +198,8 @@ def generate_approval_mail_approved(request, doc): dict(doc=doc, docs=[doc], doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), - to=",\n ".join(to), - cc=",\n ".join(cc), + to = gather_addresses('ballot_approved_ietf_stream',doc=doc), + cc = gather_addresses('ballot_approved_ietf_stream_cc',doc=doc), doc_type=doc_type, made_by=made_by, contacts=contacts, @@ -215,16 +211,13 @@ def generate_approval_mail_rfc_editor(request, doc): disapproved = doc.get_state_slug("draft-iesg") in DO_NOT_PUBLISH_IESG_STATES doc_type = "RFC" if doc.get_state_slug() == "rfc" else "Internet Draft" - to = gather_addresses('ballot_approved_conflrev', doc=doc) - cc = gather_addresses('ballot_approved_conflrev_cc', doc=doc) - 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=",\n ".join(to), - cc=",\n ".join(cc), + to = gather_addresses('ballot_approved_conflrev', doc=doc), + cc = gather_addresses('ballot_approved_conflrev_cc', doc=doc), ) ) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 1ee4d1044..ecb006ed9 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -15,7 +15,6 @@ from ietf.utils.test_utils import TestCase from ietf.utils.mail import outbox from ietf.utils.test_data import make_test_data from ietf.utils.test_utils import login_testing_unauthorized -from ietf.mailtoken.utils import gather_addresses class EditPositionTests(TestCase): @@ -171,8 +170,8 @@ class EditPositionTests(TestCase): 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'],','.join(gather_addresses('ballot_saved_cc',doc=draft))) + #TODO this would be a good place to test actual mailtoken expansions + #if we can find a way to get the real, or at least representative, data in place. class BallotWriteupsTests(TestCase): diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py index ec90c60ec..52c57c36d 100644 --- a/ietf/doc/tests_conflict_review.py +++ b/ietf/doc/tests_conflict_review.py @@ -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')) @@ -118,8 +117,8 @@ class ConflictReviewTests(TestCase): 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(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:])) def test_change_state(self): diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index fa2f83b2b..c5ff3457a 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -127,8 +127,8 @@ 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=",\n ".join(gather_addresses('ballot_approved_charter',doc=charter,group=group)), - cc=",\n ".join(gather_addresses('ballot_approved_charter_cc',doc=charter,group=group)), + to=gather_addresses('ballot_approved_charter',doc=charter,group=group), + cc=gather_addresses('ballot_approved_charter_cc',doc=charter,group=group), )) e.save() @@ -149,6 +149,8 @@ def default_review_text(group, charter, by): 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=gather_addresses('charter_external_review',group=group), + cc=gather_addresses('charter_external_review_cc',group=group) ) ) e.save() diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index d858c77b1..05faf5f42 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -27,7 +27,7 @@ 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.mailtoken.utils import gather_addresses +from ietf.mailtoken.utils import gather_addresses, gather_address_list BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -288,7 +288,7 @@ def send_ballot_comment(request, name, ballot_id): to = gather_addresses('ballot_saved',doc=doc) if request.method == 'POST': - cc = gather_addresses('ballot_saved_cc',doc=doc) + cc = gather_address_list('ballot_saved_cc',doc=doc) explicit_cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] if explicit_cc: cc.extend(explicit_cc) @@ -716,7 +716,7 @@ def approve_ballot(request, name): if action == "to_announcement_list": send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), - override={ "To": ",".join(gather_addresses('ballot_approved_ietf_stream_iana')), "CC": None, "Bcc": None, "Reply-To": None}) + override={ "To": gather_addresses('ballot_approved_ietf_stream_iana'), "CC": None, "Bcc": None, "Reply-To": None}) msg = infer_message(announcement) msg.by = login @@ -758,7 +758,7 @@ def make_last_call(request, name): send_mail_preformatted(request, announcement) if doc.type.slug == 'draft': send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), - override={ "To": ",\n ".join(gather_addresses('last_call_issued_iana',doc=doc)), + override={ "To": gather_addresses('last_call_issued_iana',doc=doc), "CC": None, "Bcc": None, "Reply-To": None}) msg = infer_message(announcement) diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index 23de7310f..0421c0b9f 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -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.mailtoken.utils import gather_addresses class ChangeStateForm(forms.Form): review_state = forms.ModelChoiceField(State.objects.filter(used=True, type="conflrev"), label="Conflict review state", empty_label=None, required=True) @@ -88,6 +89,8 @@ def change_state(request, name, option=None): def send_conflict_review_started_email(request, review): msg = render_to_string("doc/conflict_review/review_started.txt", dict(frm = settings.DEFAULT_FROM_EMAIL, + to = gather_addresses('conflrev_requested',doc=review), + cc = gather_addresses('conflrev_requested_cc',doc=review), by = request.user.person, review = review, reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target.document, @@ -98,8 +101,8 @@ def send_conflict_review_started_email(request, review): send_mail_preformatted(request,msg) email_iana(request, review.relateddocument_set.get(relationship__slug='conflrev').target.document, - settings.IANA_EVAL_EMAIL, - msg) + gather_addresses('conflrev_requested_iana',doc=review), + msg) def send_conflict_eval_email(request,review): msg = render_to_string("doc/eval_email.txt", diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 8e069f8a0..f06874601 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -583,9 +583,9 @@ def to_iesg(request,name): doc.save() extra = {} - extra['Cc'] = ",\n ".join(gather_addresses('pubreq_iesg_cc',doc=doc)) + extra['Cc'] = gather_addresses('pubreq_iesg_cc',doc=doc) send_mail(request=request, - to = ",\n ".join(gather_addresses('pubreq_iesg',doc=doc)), + to = gather_addresses('pubreq_iesg',doc=doc), frm = login.formatted_email(), subject = "Publication has been requested for %s-%s" % (doc.name,doc.rev), template = "doc/submit_to_iesg_email.txt", @@ -1133,7 +1133,7 @@ def request_publication(request, name): m = Message() m.frm = request.user.person.formatted_email() - m.to = ",\n ".join(gather_addresses('pubreq_rfced',doc=doc)) + m.to = gather_addresses('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") @@ -1163,7 +1163,7 @@ def request_publication(request, name): send_mail_message(request, m) # IANA copy - m.to = ", ".join(gather_addresses('pubreq_rfced_iana',doc=doc)) + m.to = gather_addresses('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) @@ -1299,6 +1299,7 @@ def adopt_draft(request, name): update_reminder(doc, "stream-s", e, due_date) + # TODO: Replace this with a message that's explicitly about the document adoption email_stream_state_changed(request, doc, prev_state, new_state, by, comment) # comment diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 2ed24b7dd..a40cd21ff 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -299,8 +299,8 @@ def default_approval_text(status_change,relateddoc): approved_text = current_text, action=action, newstatus=newstatus(relateddoc), - to=",\n ".join(gather_addresses('ballot_approved_status_change',doc=status_change)), - cc=",\n ".join(gather_addresses('ballot_approved_status_change_cc',doc=status_change)), + to=gather_addresses('ballot_approved_status_change',doc=status_change), + cc=gather_addresses('ballot_approved_status_change_cc',doc=status_change), ) ) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 6ad3ba54f..1d17c75d1 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -63,7 +63,11 @@ def make_recipients(apps): rc(slug='doc_stream_manager', desc="The manager of the document's stream", - template='{% if doc.stream_id == "ise" %}{% endif %}{% if doc.stream_id == "irtf" %}{% endif %}{% if doc.stream_id == "ietf" %}{% endif %}') + 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", @@ -81,6 +85,10 @@ def make_recipients(apps): desc="IANA's draft last call address", template='IANA ') + rc(slug='iana_eval', + desc="IANA's draft evaluation address", + template='IANA ') + rc(slug='iana', desc="IANA", template='') @@ -97,6 +105,7 @@ def make_recipients(apps): desc="The group's chairs", template="{{group.acronym}}-chairs@ietf.org") + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -261,6 +270,38 @@ def make_mailtokens(apps): desc='Recipients for IANA message when a non-IETF stream manager requests publication', recipient_slugs=['iana_approve',]) + mt_factory(slug='charter_external_review', + desc='Recipients for a charter external review', + recipient_slugs=['ietf_announce',]) + + mt_factory(slug='charter_external_review_cc', + desc='Copied on a charter external review', + recipient_slugs=['group_mail_list',]) + + mt_factory(slug='conflrev_requested', + desc="Recipients for a stream manager's request for an IETF conflict review", + recipient_slugs=['iesg_secretary']) + + mt_factory(slug='conflrev_requested_cc', + desc="Copied on a stream manager's request for an IETF conflict review", + recipient_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", + recipient_slugs=['iana_eval',]) + + mt_factory(slug='doc_stream_changed', + desc="Recipients for notification when a document's stream changes", + recipient_slugs=['stream_managers', + 'doc_notify', + ]) + + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index 97e885dc6..2f457a182 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -97,4 +97,21 @@ class Recipient(models.Model): addrs.append(sg_map[kwargs['group'].type_id]) return addrs + def gather_stream_managers(self, **kwargs): + addrs = [] + manager_map = dict(ise = '', + irtf = '', + ietf = '', + iab = '') + 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 diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 0746fe0a2..1b95a7f3d 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -3,7 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist from ietf.mailtoken.models import MailToken -def gather_addresses(slug,**kwargs): +def gather_address_list(slug,**kwargs): addrs = [] @@ -18,3 +18,6 @@ def gather_addresses(slug,**kwargs): addrs.extend(recipient.gather(**kwargs)) return list(set(addrs)) + +def gather_addresses(slug,**kwargs): + return ",\n ".join(gather_address_list(slug,**kwargs)) diff --git a/ietf/templates/doc/charter/review_text.txt b/ietf/templates/doc/charter/review_text.txt index c982eb5e1..ccf0c34c3 100644 --- a/ietf/templates/doc/charter/review_text.txt +++ b/ietf/templates/doc/charter/review_text.txt @@ -1,6 +1,6 @@ {% load ietf_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce {% 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 }}. diff --git a/ietf/templates/doc/conflict_review/review_started.txt b/ietf/templates/doc/conflict_review/review_started.txt index 5161e287f..6bab3d941 100644 --- a/ietf/templates/doc/conflict_review/review_started.txt +++ b/ietf/templates/doc/conflict_review/review_started.txt @@ -1,5 +1,6 @@ -{% load mail_filters %}{% autoescape off %}To: IESG Secretary -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: diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index f4cd7097e..8416bf4be 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -335,8 +335,11 @@ def make_test_data(): # This won't allow testing the results of the production configuration - if we want to do that, we'll need to # extract the production data either directly, or as a fixture recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') - for slug in ['ballot_approved_charter', 'ballot_approved_charter_cc', 'ballot_approved_conflrev', 'ballot_approved_conflrev_cc', 'ballot_approved_ietf_stream', 'ballot_approved_ietf_stream_cc', 'ballot_approved_ietf_stream_iana', 'ballot_approved_status_change', 'ballot_approved_status_change_cc', 'ballot_deferred', 'ballot_saved', 'ballot_saved_cc', 'last_call_expired', 'last_call_expired_cc', 'last_call_issued', 'last_call_issued_cc', 'last_call_issued_iana', 'last_call_requested', 'last_call_requested_cc', 'pubreq_iesg', 'pubreq_iesg_cc', 'pubreq_rfced', 'pubreq_rfced_iana']: + for slug in ['ballot_approved_charter', 'ballot_approved_charter_cc', 'ballot_approved_conflrev', 'ballot_approved_conflrev_cc', 'ballot_approved_ietf_stream', 'ballot_approved_ietf_stream_cc', 'ballot_approved_ietf_stream_iana', 'ballot_approved_status_change', 'ballot_approved_status_change_cc', 'ballot_deferred', 'ballot_saved', 'ballot_saved_cc', 'charter_external_review', 'charter_external_review_cc', 'conflrev_requested', 'conflrev_requested_cc', 'conflrev_requested_iana', 'doc_stream_changed', 'last_call_expired', 'last_call_expired_cc', 'last_call_issued', 'last_call_issued_cc', 'last_call_issued_iana', 'last_call_requested', 'last_call_requested_cc', 'pubreq_iesg', 'pubreq_iesg_cc', 'pubreq_rfced', 'pubreq_rfced_iana']: m = MailToken.objects.create(slug=slug,desc=slug) m.recipients=[recipient] + # Well, this isn't working out so well - Recipients that have code backing up their gather sometimes refer to other Recipients... + for slug in ['doc_authors','doc_group_chairs','doc_notify','doc_stream_owner','stream_managers']: + Recipient.objects.create(slug=slug,desc="Bogus Recipient",template='bogus@example.com') return draft From 1e84077e53c75a0fd4491c5397023d1c49c1e159 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Aug 2015 22:01:53 +0000 Subject: [PATCH 09/46] missed a cleanup - Legacy-Id: 10012 --- ietf/doc/mails.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 3d0214bc7..9b6ff4f13 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -405,18 +405,16 @@ 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 - to = gather_addresses('last_call_expired',doc=doc) - cc = gather_addresses('last_call_expired_cc',doc=doc) - send_mail(None, - ",\n ".join(to), + gather_addresses('last_call_expired',doc=doc) "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=",\n ".join(cc)) + cc = gather_addresses('last_call_expired_cc',doc=doc) + ) def stream_state_email_recipients(doc, extra_recipients=[]): persons = set() From 839d0f89ad7a1c55228331b7f9c7deaf752fe814 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 13 Aug 2015 19:41:02 +0000 Subject: [PATCH 10/46] checkpoint - Legacy-Id: 10016 --- ietf/doc/mails.py | 36 +- ietf/doc/tests_ballot.py | 12 +- ietf/doc/tests_draft.py | 13 +- ietf/doc/utils.py | 8 +- ietf/doc/views_ballot.py | 5 +- ietf/doc/views_doc.py | 3 +- ietf/doc/views_draft.py | 3 +- ietf/group/mails.py | 27 +- .../migrations/0002_auto_20150809_1314.py | 51 ++ ietf/mailtoken/models.py | 20 +- ietf/mailtoken/tests.py | 6 +- ietf/mailtoken/utils.py | 2 +- ietf/name/fixtures/names.json | 644 +++++++++++++++++- ietf/name/generate_fixtures.py | 4 + ietf/secr/telechat/views.py | 3 +- ietf/sync/iana.py | 3 +- ietf/sync/tests.py | 3 +- ietf/utils/test_data.py | 16 +- 18 files changed, 761 insertions(+), 98 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 9b6ff4f13..51f00454e 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -9,12 +9,12 @@ 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.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent from ietf.doc.utils import needed_ballot_positions from ietf.person.models import Person from ietf.group.models import Role from ietf.doc.models import Document -from ietf.mailtoken.utils import gather_addresses +from ietf.mailtoken.utils import gather_addresses, gather_address_list def email_state_changed(request, doc, text): to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] @@ -35,7 +35,7 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""): streams.append(old_stream.slug) if new_stream: streams.append(new_stream.slug) - to = gather_addresses('doc_stream_changed',doc=doc,streams=streams) + to = gather_address_list('doc_stream_changed',doc=doc,streams=streams) if not to: return @@ -74,6 +74,7 @@ def email_authors(request, doc, subject, text): def html_to_text(html): return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("
", "\n")) +#TODO Expunge this def email_ad(request, doc, ad, changed_by, text, subject=None): if not ad or not changed_by or ad == changed_by: return @@ -406,7 +407,7 @@ 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 send_mail(None, - gather_addresses('last_call_expired',doc=doc) + gather_addresses('last_call_expired',doc=doc), "DraftTracker Mail System ", "Last Call Expired: %s" % doc.file_tag(), "doc/mail/change_notice.txt", @@ -416,27 +417,8 @@ def email_last_call_expired(doc): cc = gather_addresses('last_call_expired_cc',doc=doc) ) -def stream_state_email_recipients(doc, extra_recipients=[]): - persons = set() - res = [] - for r in Role.objects.filter(group=doc.group, name__in=("chair", "delegate")).select_related("person", "email"): - res.append(r.formatted_email()) - persons.add(r.person) - - for email in doc.authors.all(): - if email.person not in persons: - res.append(email.formatted_email()) - persons.add(email.person) - - for e in extra_recipients: - if e.person not in persons: - res.append(e.formatted_email()) - persons.add(e.person) - - return res - def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""): - recipients = stream_state_email_recipients(doc) + recipients = gather_address_list('doc_stream_state_edited',doc=doc) state_type = (prev_state or new_state).type @@ -452,12 +434,8 @@ def email_stream_state_changed(request, doc, prev_state, new_state, by, comment= comment=comment)) def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""): - extra_recipients = [] - if DocTagName.objects.get(slug="sheph-u") in added_tags and doc.shepherd: - extra_recipients.append(doc.shepherd) - - recipients = stream_state_email_recipients(doc, extra_recipients) + recipients = gather_address_list('doc_stream_state_edited',doc=doc) send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL, u"Tags changed for %s" % doc.name, diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index ecb006ed9..ff42532a3 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -232,7 +232,7 @@ 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 + 2) self.assertTrue("Last Call" in outbox[-1]['Subject']) self.assertTrue(draft.name in outbox[-1]['Subject']) @@ -387,7 +387,7 @@ 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 + 3) self.assertTrue("Protocol Action" in outbox[-2]['Subject']) # the IANA copy self.assertTrue("Protocol Action" in outbox[-1]['Subject']) @@ -409,7 +409,7 @@ 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 + 2) self.assertTrue("NOT be published" in str(outbox[-1])) @@ -441,11 +441,11 @@ 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.assertEqual(len(outbox), mailbox_before + 3) - self.assertTrue("Last Call" in outbox[-4]['Subject']) - # the IANA copy self.assertTrue("Last Call" in outbox[-3]['Subject']) + # the IANA copy + self.assertTrue("Last Call" in outbox[-2]['Subject']) self.assertTrue("Last Call" in draft.message_set.order_by("-time")[0].subject) class DeferUndeferTestCase(TestCase): diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 618ca12b5..938c1a8f1 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -72,9 +72,8 @@ 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']) # check that we got a previous state now @@ -101,7 +100,7 @@ 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])) @@ -1095,7 +1094,7 @@ class AdoptDraftTests(TestCase): 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("mars-chairs@ietf.org" in unicode(outbox[-1])) self.assertTrue("marsdelegate@ietf.org" in unicode(outbox[-1])) self.assertFalse(mars.list_email in draft.notify) @@ -1137,7 +1136,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])) @@ -1181,7 +1180,7 @@ class ChangeStreamStateTests(TestCase): 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("mars-chairs@ietf.org" in unicode(outbox[-1])) self.assertTrue("marsdelegate@ietf.org" in unicode(outbox[-1])) class ChangeReplacesTests(TestCase): diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index e26c57512..6572d48e4 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -18,18 +18,18 @@ from ietf.group.models import Role from ietf.ietfauth.utils import has_role from ietf.utils import draft, markup_txt from ietf.utils.mail import send_mail +from ietf.mailtoken.utils import gather_address_list -#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 = gather_address_list('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, diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 05faf5f42..5127bb373 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -17,7 +17,7 @@ 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, +from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred, email_state_changed, 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 @@ -464,7 +464,6 @@ def lastcalltext(request, name): if e: email_state_changed(request, doc, e.desc) - email_ad(request, doc, doc.ad, login, e.desc) request_last_call(request, doc) @@ -708,7 +707,6 @@ def approve_ballot(request, name): doc.save() email_state_changed(request, doc, change_description) - email_ad(request, doc, doc.ad, login, change_description) # send announcement @@ -790,7 +788,6 @@ def make_last_call(request, name): 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" diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index d18221f11..e44355847 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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,7 @@ 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_ad def render_document_top(request, doc, tab, name): tabs = [] @@ -878,6 +878,7 @@ def add_comment(request, name): e.save() if doc.type_id == "draft": + # TODO - build an explicit message for when a comment is added email_ad(request, doc, doc.ad, login, "A new comment added by %s" % login.name) return redirect("doc_history", name=doc.name) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index f06874601..30e28c8aa 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -111,7 +111,6 @@ def change_state(request, name): doc.save() email_state_changed(request, doc, msg) - email_ad(request, doc, doc.ad, login, msg) if prev_state and prev_state.slug in ("ann", "rfcqueue") and new_state.slug not in ("rfcqueue", "pub"): @@ -450,6 +449,7 @@ def change_intention(request, name): doc.time = e.time doc.save() + # TODO: Build explicit changed_intended_publication_status email_ad(request, doc, doc.ad, login, email_desc) return HttpResponseRedirect(doc.get_absolute_url()) @@ -726,6 +726,7 @@ def edit_info(request, name): doc.time = datetime.datetime.now() if changes and not new_document: + #TODO - use the 'this thing changed' messages instead email_ad(request, doc, orig_ad, login, "\n".join(changes)) doc.save() diff --git a/ietf/group/mails.py b/ietf/group/mails.py index 290a6e2a3..c4ca308e6 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -12,6 +12,7 @@ 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.mailtoken.utils import gather_address_list def email_iesg_secretary_re_charter(request, group, subject, text): to = ["iesg-secretary@ietf.org"] @@ -77,26 +78,18 @@ def email_milestones_changed(request, group, changes): send_mail_text(request, to, None, subject, text) - # 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 ") - - for r in group.role_set.filter(name="chair"): - to.append(r.formatted_email()) - + # first send to those who should see any edits (such as management and chairs) + to = gather_address_list('group_milestones_edited',group=group) 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) + # 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") + to = gather_address_list('group_approved_milestones_edited',group=group) + msg = u"\n\n".join(c + "." for c in changes if not review_re.match(c)) + if to and msg: + wrap_up_email(to, msg) def email_milestone_review_reminder(group, grace_period=7): diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 1d17c75d1..58cc019c3 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -37,6 +37,10 @@ def make_recipients(apps): 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) @@ -105,6 +109,9 @@ def make_recipients(apps): desc="The group's chairs", template="{{group.acronym}}-chairs@ietf.org") + rc(slug='group_responsible_directors', + desc="The group's responsible AD(s) or IRTF chair", + template=None) def make_mailtokens(apps): @@ -300,7 +307,51 @@ def make_mailtokens(apps): recipient_slugs=['stream_managers', 'doc_notify', ]) + + mt_factory(slug='doc_stream_state_edited', + desc="Recipients when the stream state of a document is manually edited", + recipient_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", + recipient_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", + recipient_slugs=['group_mail_list', + ]) + + mt_factory(slug='doc_state_edited', + desc="Recipients when a document's state is manutally edited", + recipient_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", + recipient_slugs=['iesg', + 'iesg-secretary', + 'doc_notify', + 'doc_authors', + 'doc_shepherd', + 'doc_group_chairs', + 'doc_affecteddoc_authors', + 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', + ]) + def forward(apps, schema_editor): diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index 2f457a182..ef1e6c73d 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -3,6 +3,8 @@ from django.db import models from django.template import Template, Context +from ietf.group.models import Role + class MailToken(models.Model): slug = models.CharField(max_length=32, primary_key=True) desc = models.TextField(blank=True) @@ -41,10 +43,18 @@ class Recipient(models.Model): addrs = [] if 'doc' in kwargs: doc=kwargs['doc'] - if doc.group.type.slug in ['wg','rg']: + 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(Role.objects.filter(group=doc.group,name='delegate').values_list('email__address',flat=True)) + return addrs + def gather_doc_group_mail_list(self, **kwargs): addrs = [] if 'doc' in kwargs: @@ -115,3 +125,11 @@ class Recipient(models.Model): addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':[kwargs['doc'].stream_id]})) return addrs + def gather_group_responsible_directors(self, **kwargs): + addrs = [] + if 'group' in kwargs: + group = kwargs['group'] + addrs.extend(Role.objects.filter(group=group,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 diff --git a/ietf/mailtoken/tests.py b/ietf/mailtoken/tests.py index 6bd0f4e9d..82f741317 100644 --- a/ietf/mailtoken/tests.py +++ b/ietf/mailtoken/tests.py @@ -26,12 +26,12 @@ class EventMailTests(TestCase): url = urlreverse('ietf.mailtoken.views.show_recipients') r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertTrue('bogus' in r.content) + self.assertTrue('doc_group_mail_list' in r.content) - url = urlreverse('ietf.mailtoken.views.show_recipients',kwargs=dict(recipient_slug='bogus')) + url = urlreverse('ietf.mailtoken.views.show_recipients',kwargs=dict(recipient_slug='doc_group_mail_list')) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertTrue('bogus' in r.content) + self.assertTrue('doc_group_mail_list' in r.content) class RecipientTests(TestCase): diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 1b95a7f3d..2cf14a0ff 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -17,7 +17,7 @@ def gather_address_list(slug,**kwargs): for recipient in mailtoken.recipients.all(): addrs.extend(recipient.gather(**kwargs)) - return list(set(addrs)) + return list(set([addr for addr in addrs if addr])) def gather_addresses(slug,**kwargs): return ",\n ".join(gather_address_list(slug,**kwargs)) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 33c2a612d..bec077271 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -3017,7 +3017,7 @@ "slug": "rfcqueue", "type": "draft-iesg", "order": 31, - "desc": "The document is in the RFC editor Queue (as confirmed by https://www.rfc-editor.org/queue.html)." + "desc": "The document is in the RFC editor Queue (as confirmed by http://www.rfc-editor.org/queue.html)." }, "model": "doc.state", "pk": 17 @@ -3489,7 +3489,7 @@ "slug": "c-adopt", "type": "draft-stream-ietf", "order": 1, - "desc": "4.2.1. Call for Adoption by WG Issued\n\n The \"Call for Adoption by WG Issued\" state should be used to indicate when an I-D is being considered for adoption by an IETF WG. An I-D that is in this state is actively being considered for adoption and has not yet achieved consensus, preference, or selection in the WG.\n\n This state may be used to describe an I-D that someone has asked a WG to consider for adoption, if the WG Chair has agreed with the request. This state may also be used to identify an I-D that a WG Chair asked an author to write specifically for consideration as a candidate WG item [WGDTSPEC], and/or an I-D that is listed as a 'candidate draft' in the WG's charter.\n\n Under normal conditions, it should not be possible for an I-D to be in the \"Call for Adoption by WG Issued\" state in more than one working group at the same time. This said, it is not uncommon for authors to \"shop\" their I-Ds to more than one WG at a time, with the hope of getting their documents adopted somewhere.\n\n After this state is implemented in the Datatracker, an I-D that is in the \"Call for Adoption by WG Issued\" state will not be able to be \"shopped\" to any other WG without the consent of the WG Chairs and the responsible ADs impacted by the shopping.\n\n Note that Figure 1 includes an arc leading from this state to outside of the WG state machine. This illustrates that some I-Ds that are considered do not get adopted as WG drafts. An I-D that is not adopted as a WG draft will transition out of the WG state machine and revert back to having no stream-specific state; however, the status change history log of the I-D will record that the I-D was previously in the \"Call for Adoption by WG Issued\" state." + "desc": "4.2.1. Call for Adoption by WG Issued\n\n The \"Call for Adoption by WG Issued\" state should be used to indicate when an I-D is being considered for adoption by an IETF WG. An I-D that is in this state is actively being considered for adoption and has not yet achieved consensus, preference, or selection in the WG.\n\n This state may be used to describe an I-D that someone has asked a WG to consider for adoption, if the WG Chair has agreed with the request. This state may also be used to identify an I-D that a WG Chair asked an author to write specifically for consideration as a candidate WG item [WGDTSPEC], and/or an I-D that is listed as a 'candidate draft' in the WG's charter.\n\n Under normal conditions, it should not be possible for an I-D to be in the \"Call for Adoption by WG Issued\" state in more than one working group at the same time. This said, it is not uncommon for authors to \"shop\" their I-Ds to more than one WG at a time, with the hope of getting their documents adopted somewhere.\n\n After this state is implemented in the Datatracker, an I-D that is in the \"Call for Adoption by WG Issued\" state will not be able to be \"shopped\" to any other WG without the consent of the WG Chairs and the responsible ADs impacted by the shopping.\n\n Note that Figure 1 includes an arc leading from this state to outside of the WG state machine. This illustrates that some I-Ds that are considered do not get adopted as WG drafts. An I-D that is not adopted as a WG draft will transition out of the WG state machine and revert back to having no stream-specific state; however, the status change history log of the I-D will record that the I-D was previously in the \"Call for Adoption by WG Issued\" state." }, "model": "doc.state", "pk": 35 @@ -3504,7 +3504,7 @@ "slug": "adopt-wg", "type": "draft-stream-ietf", "order": 2, - "desc": "4.2.2. Adopted by a WG\n\n The \"Adopted by a WG\" state describes an individual submission I-D that an IETF WG has agreed to adopt as one of its WG drafts.\n\n WG Chairs who use this state will be able to clearly indicate when their WGs adopt individual submission I-Ds. This will facilitate the Datatracker's ability to correctly capture \"Replaces\" information for WG drafts and correct \"Replaced by\" information for individual submission I-Ds that have been replaced by WG drafts.\n\n This state is needed because the Datatracker uses the filename of an I-D as a key to search its database for status information about the I-D, and because the filename of a WG I-D is supposed to be different from the filename of an individual submission I-D. The filename of an individual submission I-D will typically be formatted as 'draft-author-wgname-topic-nn'.\n\n The filename of a WG document is supposed to be formatted as 'draft- ietf-wgname-topic-nn'.\n\n An individual I-D that is adopted by a WG may take weeks or months to be resubmitted by the author as a new (version-00) WG draft. If the \"Adopted by a WG\" state is not used, the Datatracker has no way to determine that an I-D has been adopted until a new version of the I-D is submitted to the WG by the author and until the I-D is approved for posting by a WG Chair." + "desc": "4.2.2. Adopted by a WG\n\n The \"Adopted by a WG\" state describes an individual submission I-D that an IETF WG has agreed to adopt as one of its WG drafts.\n\n WG Chairs who use this state will be able to clearly indicate when their WGs adopt individual submission I-Ds. This will facilitate the Datatracker's ability to correctly capture \"Replaces\" information for WG drafts and correct \"Replaced by\" information for individual submission I-Ds that have been replaced by WG drafts.\n\n This state is needed because the Datatracker uses the filename of an I-D as a key to search its database for status information about the I-D, and because the filename of a WG I-D is supposed to be different from the filename of an individual submission I-D. The filename of an individual submission I-D will typically be formatted as 'draft-author-wgname-topic-nn'.\n\n The filename of a WG document is supposed to be formatted as 'draft- ietf-wgname-topic-nn'.\n\n An individual I-D that is adopted by a WG may take weeks or months to be resubmitted by the author as a new (version-00) WG draft. If the \"Adopted by a WG\" state is not used, the Datatracker has no way to determine that an I-D has been adopted until a new version of the I-D is submitted to the WG by the author and until the I-D is approved for posting by a WG Chair." }, "model": "doc.state", "pk": 36 @@ -3517,7 +3517,7 @@ "slug": "info", "type": "draft-stream-ietf", "order": 3, - "desc": "4.2.3. Adopted for WG Info Only\n\n The \"Adopted for WG Info Only\" state describes a document that contains useful information for the WG that adopted it, but the document is not intended to be published as an RFC. The WG will not actively develop the contents of the I-D or progress it for publication as an RFC. The only purpose of the I-D is to provide information for internal use by the WG." + "desc": "4.2.3. Adopted for WG Info Only\n\n The \"Adopted for WG Info Only\" state describes a document that contains useful information for the WG that adopted it, but the document is not intended to be published as an RFC. The WG will not actively develop the contents of the I-D or progress it for publication as an RFC. The only purpose of the I-D is to provide information for internal use by the WG." }, "model": "doc.state", "pk": 37 @@ -3535,7 +3535,7 @@ "slug": "wg-doc", "type": "draft-stream-ietf", "order": 4, - "desc": "4.2.4. WG Document\n\n The \"WG Document\" state describes an I-D that has been adopted by an IETF WG and is being actively developed.\n\n A WG Chair may transition an I-D into the \"WG Document\" state at any time as long as the I-D is not being considered or developed in any other WG.\n\n Alternatively, WG Chairs may rely upon new functionality to be added to the Datatracker to automatically move version-00 drafts into the \"WG Document\" state as described in Section 4.1.\n\n Under normal conditions, it should not be possible for an I-D to be in the \"WG Document\" state in more than one WG at a time. This said, I-Ds may be transferred from one WG to another with the consent of the WG Chairs and the responsible ADs." + "desc": "4.2.4. WG Document\n\n The \"WG Document\" state describes an I-D that has been adopted by an IETF WG and is being actively developed.\n\n A WG Chair may transition an I-D into the \"WG Document\" state at any time as long as the I-D is not being considered or developed in any other WG.\n\n Alternatively, WG Chairs may rely upon new functionality to be added to the Datatracker to automatically move version-00 drafts into the \"WG Document\" state as described in Section 4.1.\n\n Under normal conditions, it should not be possible for an I-D to be in the \"WG Document\" state in more than one WG at a time. This said, I-Ds may be transferred from one WG to another with the consent of the WG Chairs and the responsible ADs." }, "model": "doc.state", "pk": 38 @@ -3550,7 +3550,7 @@ "slug": "parked", "type": "draft-stream-ietf", "order": 5, - "desc": "4.2.5. Parked WG Document\n\n A \"Parked WG Document\" is an I-D that has lost its author or editor, is waiting for another document to be written or for a review to be completed, or cannot be progressed by the working group for some other reason.\n\n Some of the annotation tags described in Section 4.3 may be used in conjunction with this state to indicate why an I-D has been parked, and/or what may need to happen for the I-D to be un-parked.\n\n Parking a WG draft will not prevent it from expiring; however, this state can be used to indicate why the I-D has stopped progressing in the WG.\n\n A \"Parked WG Document\" that is not expired may be transferred from one WG to another with the consent of the WG Chairs and the responsible ADs." + "desc": "4.2.5. Parked WG Document\n\n A \"Parked WG Document\" is an I-D that has lost its author or editor, is waiting for another document to be written or for a review to be completed, or cannot be progressed by the working group for some other reason.\n\n Some of the annotation tags described in Section 4.3 may be used in conjunction with this state to indicate why an I-D has been parked, and/or what may need to happen for the I-D to be un-parked.\n\n Parking a WG draft will not prevent it from expiring; however, this state can be used to indicate why the I-D has stopped progressing in the WG.\n\n A \"Parked WG Document\" that is not expired may be transferred from one WG to another with the consent of the WG Chairs and the responsible ADs." }, "model": "doc.state", "pk": 39 @@ -3565,7 +3565,7 @@ "slug": "dead", "type": "draft-stream-ietf", "order": 6, - "desc": "4.2.6. Dead WG Document\n\n A \"Dead WG Document\" is an I-D that has been abandoned. Note that 'Dead' is not always a final state for a WG I-D. If consensus is subsequently achieved, a \"Dead WG Document\" may be resurrected. A \"Dead WG Document\" that is not resurrected will eventually expire.\n\n Note that an I-D that is declared to be \"Dead\" in one WG and that is not expired may be transferred to a non-dead state in another WG with the consent of the WG Chairs and the responsible ADs." + "desc": "4.2.6. Dead WG Document\n\n A \"Dead WG Document\" is an I-D that has been abandoned. Note that 'Dead' is not always a final state for a WG I-D. If consensus is subsequently achieved, a \"Dead WG Document\" may be resurrected. A \"Dead WG Document\" that is not resurrected will eventually expire.\n\n Note that an I-D that is declared to be \"Dead\" in one WG and that is not expired may be transferred to a non-dead state in another WG with the consent of the WG Chairs and the responsible ADs." }, "model": "doc.state", "pk": 40 @@ -3582,7 +3582,7 @@ "slug": "wg-lc", "type": "draft-stream-ietf", "order": 7, - "desc": "4.2.7. In WG Last Call\n\n A document \"In WG Last Call\" is an I-D for which a WG Last Call (WGLC) has been issued and is in progress.\n\n Note that conducting a WGLC is an optional part of the IETF WG process, per Section 7.4 of RFC 2418 [RFC2418].\n\n If a WG Chair decides to conduct a WGLC on an I-D, the \"In WG Last Call\" state can be used to track the progress of the WGLC. The Chair may configure the Datatracker to send a WGLC message to one or more mailing lists when the Chair moves the I-D into this state. The WG Chair may also be able to select a different set of mailing lists for a different document undergoing a WGLC; some documents may deserve coordination with other WGs.\n\n A WG I-D in this state should remain \"In WG Last Call\" until the WG Chair moves it to another state. The WG Chair may configure the Datatracker to send an e-mail after a specified period of time to remind or 'nudge' the Chair to conclude the WGLC and to determine the next state for the document.\n\n It is possible for one WGLC to lead into another WGLC for the same document. For example, an I-D that completed a WGLC as an \"Informational\" document may need another WGLC if a decision is taken to convert the I-D into a Standards Track document." + "desc": "4.2.7. In WG Last Call\n\n A document \"In WG Last Call\" is an I-D for which a WG Last Call (WGLC) has been issued and is in progress.\n\n Note that conducting a WGLC is an optional part of the IETF WG process, per Section 7.4 of RFC 2418 [RFC2418].\n\n If a WG Chair decides to conduct a WGLC on an I-D, the \"In WG Last Call\" state can be used to track the progress of the WGLC. The Chair may configure the Datatracker to send a WGLC message to one or more mailing lists when the Chair moves the I-D into this state. The WG Chair may also be able to select a different set of mailing lists for a different document undergoing a WGLC; some documents may deserve coordination with other WGs.\n\n A WG I-D in this state should remain \"In WG Last Call\" until the WG Chair moves it to another state. The WG Chair may configure the Datatracker to send an e-mail after a specified period of time to remind or 'nudge' the Chair to conclude the WGLC and to determine the next state for the document.\n\n It is possible for one WGLC to lead into another WGLC for the same document. For example, an I-D that completed a WGLC as an \"Informational\" document may need another WGLC if a decision is taken to convert the I-D into a Standards Track document." }, "model": "doc.state", "pk": 41 @@ -3598,7 +3598,7 @@ "slug": "chair-w", "type": "draft-stream-ietf", "order": 8, - "desc": "4.2.8. Waiting for WG Chair Go-Ahead\n\n A WG Chair may wish to place an I-D that receives a lot of comments during a WGLC into the \"Waiting for WG Chair Go-Ahead\" state. This state describes an I-D that has undergone a WGLC; however, the Chair is not yet ready to call consensus on the document.\n\n If comments from the WGLC need to be responded to, or a revision to the I-D is needed, the Chair may place an I-D into this state until all of the WGLC comments are adequately addressed and the (possibly revised) document is in the I-D repository." + "desc": "4.2.8. Waiting for WG Chair Go-Ahead\n\n A WG Chair may wish to place an I-D that receives a lot of comments during a WGLC into the \"Waiting for WG Chair Go-Ahead\" state. This state describes an I-D that has undergone a WGLC; however, the Chair is not yet ready to call consensus on the document.\n\n If comments from the WGLC need to be responded to, or a revision to the I-D is needed, the Chair may place an I-D into this state until all of the WGLC comments are adequately addressed and the (possibly revised) document is in the I-D repository." }, "model": "doc.state", "pk": 42 @@ -3613,7 +3613,7 @@ "slug": "writeupw", "type": "draft-stream-ietf", "order": 9, - "desc": "4.2.9. WG Consensus: Waiting for Writeup\n\n A document in the \"WG Consensus: Waiting for Writeup\" state has essentially completed its development within the working group, and is nearly ready to be sent to the IESG for publication. The last thing to be done is the preparation of a protocol writeup by a Document Shepherd. The IESG requires that a document shepherd writeup be completed before publication of the I-D is requested. The IETF document shepherding process and the role of a WG Document Shepherd is described in RFC 4858 [RFC4858]\n\n A WG Chair may call consensus on an I-D without a formal WGLC and transition an I-D that was in the \"WG Document\" state directly into this state.\n\n The name of this state includes the words \"Waiting for Writeup\" because a good document shepherd writeup takes time to prepare." + "desc": "4.2.9. WG Consensus: Waiting for Writeup\n\n A document in the \"WG Consensus: Waiting for Writeup\" state has essentially completed its development within the working group, and is nearly ready to be sent to the IESG for publication. The last thing to be done is the preparation of a protocol writeup by a Document Shepherd. The IESG requires that a document shepherd writeup be completed before publication of the I-D is requested. The IETF document shepherding process and the role of a WG Document Shepherd is described in RFC 4858 [RFC4858]\n\n A WG Chair may call consensus on an I-D without a formal WGLC and transition an I-D that was in the \"WG Document\" state directly into this state.\n\n The name of this state includes the words \"Waiting for Writeup\" because a good document shepherd writeup takes time to prepare." }, "model": "doc.state", "pk": 43 @@ -3628,7 +3628,7 @@ "slug": "sub-pub", "type": "draft-stream-ietf", "order": 10, - "desc": "4.2.10. Submitted to IESG for Publication\n\n This state describes a WG document that has been submitted to the IESG for publication and that has not been sent back to the working group for revision.\n\n An I-D in this state may be under review by the IESG, it may have been approved and be in the RFC Editor's queue, or it may have been published as an RFC. Other possibilities exist too. The document may be \"Dead\" (in the IESG state machine) or in a \"Do Not Publish\" state." + "desc": "4.2.10. Submitted to IESG for Publication\n\n This state describes a WG document that has been submitted to the IESG for publication and that has not been sent back to the working group for revision.\n\n An I-D in this state may be under review by the IESG, it may have been approved and be in the RFC Editor's queue, or it may have been published as an RFC. Other possibilities exist too. The document may be \"Dead\" (in the IESG state machine) or in a \"Do Not Publish\" state." }, "model": "doc.state", "pk": 44 @@ -4326,5 +4326,627 @@ }, "model": "doc.ballottype", "pk": 3 +}, +{ + "fields": { + "template": null, + "desc": "The steering group (e.g. IRSG) of a document being reviewed for IETF stream conflicts" + }, + "model": "mailtoken.recipient", + "pk": "conflict_review_steering_group" +}, +{ + "fields": { + "template": null, + "desc": "The stream manager of a document being reviewed for IETF stream conflicts" + }, + "model": "mailtoken.recipient", + "pk": "conflict_review_stream_manager" +}, +{ + "fields": { + "template": "{% if doc.ad %}{{doc.ad.email_address}}{% endif %}", + "desc": "The document's responsible Area Director" + }, + "model": "mailtoken.recipient", + "pk": "doc_ad" +}, +{ + "fields": { + "template": null, + "desc": "The authors of the subject documents of a conflict-review or status-change" + }, + "model": "mailtoken.recipient", + "pk": "doc_affecteddoc_authors" +}, +{ + "fields": { + "template": null, + "desc": "The chairs of groups of the subject documents of a conflict-review or status-change" + }, + "model": "mailtoken.recipient", + "pk": "doc_affecteddoc_group_chairs" +}, +{ + "fields": { + "template": null, + "desc": "The notify field of the subject documents of a conflict-review or status-change" + }, + "model": "mailtoken.recipient", + "pk": "doc_affecteddoc_notify" +}, +{ + "fields": { + "template": "{% if doc.type_id == \"draft\" %}{{doc.name}}@ietf.org{% endif %}", + "desc": "The document's authors" + }, + "model": "mailtoken.recipient", + "pk": "doc_authors" +}, +{ + "fields": { + "template": null, + "desc": "The document's group chairs (if the document is assigned to a working or research group)" + }, + "model": "mailtoken.recipient", + "pk": "doc_group_chairs" +}, +{ + "fields": { + "template": null, + "desc": "The document's group delegates (if the document is assigned to a working or research group)" + }, + "model": "mailtoken.recipient", + "pk": "doc_group_delegates" +}, +{ + "fields": { + "template": null, + "desc": "The list address of the document's group" + }, + "model": "mailtoken.recipient", + "pk": "doc_group_mail_list" +}, +{ + "fields": { + "template": "{{doc.notify}}", + "desc": "The addresses in the document's notify field" + }, + "model": "mailtoken.recipient", + "pk": "doc_notify" +}, +{ + "fields": { + "template": "{% if doc.shepherd %}{{doc.shepherd.address}}{% endif %}", + "desc": "The document's shepherd" + }, + "model": "mailtoken.recipient", + "pk": "doc_shepherd" +}, +{ + "fields": { + "template": null, + "desc": "The manager of the document's stream" + }, + "model": "mailtoken.recipient", + "pk": "doc_stream_manager" +}, +{ + "fields": { + "template": "{{group.acronym}}-chairs@ietf.org", + "desc": "The group's chairs" + }, + "model": "mailtoken.recipient", + "pk": "group_chairs" +}, +{ + "fields": { + "template": "{{ group.list_email }}", + "desc": "The group's mailing list" + }, + "model": "mailtoken.recipient", + "pk": "group_mail_list" +}, +{ + "fields": { + "template": null, + "desc": "The group's responsible AD(s) or IRTF chair" + }, + "model": "mailtoken.recipient", + "pk": "group_responsible_directors" +}, +{ + "fields": { + "template": null, + "desc": "The group's steering group (IESG or IRSG)" + }, + "model": "mailtoken.recipient", + "pk": "group_steering_group" +}, +{ + "fields": { + "template": "", + "desc": "IANA" + }, + "model": "mailtoken.recipient", + "pk": "iana" +}, +{ + "fields": { + "template": "IANA ", + "desc": "IANA's draft approval address" + }, + "model": "mailtoken.recipient", + "pk": "iana_approve" +}, +{ + "fields": { + "template": "IANA ", + "desc": "IANA's draft evaluation address" + }, + "model": "mailtoken.recipient", + "pk": "iana_eval" +}, +{ + "fields": { + "template": "IANA ", + "desc": "IANA's draft last call address" + }, + "model": "mailtoken.recipient", + "pk": "iana_last_call" +}, +{ + "fields": { + "template": "The IESG ", + "desc": "The IESG" + }, + "model": "mailtoken.recipient", + "pk": "iesg" +}, +{ + "fields": { + "template": "", + "desc": "The Secretariat" + }, + "model": "mailtoken.recipient", + "pk": "iesg_secretary" +}, +{ + "fields": { + "template": "IETF-Announce ", + "desc": "The IETF Announce list" + }, + "model": "mailtoken.recipient", + "pk": "ietf_announce" +}, +{ + "fields": { + "template": "", + "desc": "The RFC Editor" + }, + "model": "mailtoken.recipient", + "pk": "rfc_editor" +}, +{ + "fields": { + "template": null, + "desc": "The managers of any related streams" + }, + "model": "mailtoken.recipient", + "pk": "stream_managers" +}, +{ + "fields": { + "recipients": [ + "ietf_announce" + ], + "desc": "Recipients when a charter is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_charter" +}, +{ + "fields": { + "recipients": [ + "doc_notify", + "group_chairs", + "group_mail_list", + "group_steering_group" + ], + "desc": "Copied when a charter is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_charter_cc" +}, +{ + "fields": { + "recipients": [ + "conflict_review_steering_group", + "conflict_review_stream_manager", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_notify" + ], + "desc": "Recipients when a conflict review ballot is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_conflrev" +}, +{ + "fields": { + "recipients": [ + "iana", + "iesg", + "ietf_announce" + ], + "desc": "Copied when a conflict review ballot is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_conflrev_cc" +}, +{ + "fields": { + "recipients": [ + "ietf_announce" + ], + "desc": "Recipients when an IETF stream document ballot is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_ietf_stream" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_authors", + "doc_group_chairs", + "doc_group_mail_list", + "doc_notify", + "doc_shepherd", + "iesg", + "rfc_editor" + ], + "desc": "Copied when an IETF stream document ballot is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_ietf_stream_cc" +}, +{ + "fields": { + "recipients": [ + "iana_approve" + ], + "desc": "Recipients for IANA message when an IETF stream document ballot is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_ietf_stream_iana" +}, +{ + "fields": { + "recipients": [ + "ietf_announce" + ], + "desc": "Recipients when a status change is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_status_change" +}, +{ + "fields": { + "recipients": [ + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_notify", + "iesg", + "rfc_editor" + ], + "desc": "Copied when a status change is approved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_approved_status_change_cc" +}, +{ + "fields": { + "recipients": [ + "conflict_review_stream_manager", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd", + "iesg", + "iesg_secretary" + ], + "desc": "Recipients when a ballot is deferred to or undeferred from a future telechat" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_deferred" +}, +{ + "fields": { + "recipients": [ + "iesg" + ], + "desc": "Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_saved" +}, +{ + "fields": { + "recipients": [ + "conflict_review_stream_manager", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_shepherd" + ], + "desc": "Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved" + }, + "model": "mailtoken.mailtoken", + "pk": "ballot_saved_cc" +}, +{ + "fields": { + "recipients": [ + "ietf_announce" + ], + "desc": "Recipients for a charter external review" + }, + "model": "mailtoken.mailtoken", + "pk": "charter_external_review" +}, +{ + "fields": { + "recipients": [ + "group_mail_list" + ], + "desc": "Copied on a charter external review" + }, + "model": "mailtoken.mailtoken", + "pk": "charter_external_review_cc" +}, +{ + "fields": { + "recipients": [ + "iesg_secretary" + ], + "desc": "Recipients for a stream manager's request for an IETF conflict review" + }, + "model": "mailtoken.mailtoken", + "pk": "conflrev_requested" +}, +{ + "fields": { + "recipients": [ + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_notify", + "iesg" + ], + "desc": "Copied on a stream manager's request for an IETF conflict review" + }, + "model": "mailtoken.mailtoken", + "pk": "conflrev_requested_cc" +}, +{ + "fields": { + "recipients": [ + "iana_eval" + ], + "desc": "Recipients for IANA message when a stream manager requests an IETF conflict review" + }, + "model": "mailtoken.mailtoken", + "pk": "conflrev_requested_iana" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd" + ], + "desc": "Recipients when a document's state is manutally edited" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_state_edited" +}, +{ + "fields": { + "recipients": [ + "doc_notify", + "stream_managers" + ], + "desc": "Recipients for notification when a document's stream changes" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_stream_changed" +}, +{ + "fields": { + "recipients": [ + "doc_authors", + "doc_group_chairs", + "doc_group_delegates", + "doc_shepherd" + ], + "desc": "Recipients when the stream state of a document is manually edited" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_stream_state_edited" +}, +{ + "fields": { + "recipients": [ + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd", + "iesg" + ], + "desc": "Recipients when a document's telechat date or other telechat specific details are changed" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_telechat_details_changed" +}, +{ + "fields": { + "recipients": [ + "group_mail_list" + ], + "desc": "Recipients when the set of approved milestones for a group are edited" + }, + "model": "mailtoken.mailtoken", + "pk": "group_approved_milestones_edited" +}, +{ + "fields": { + "recipients": [ + "group_chairs", + "group_responsible_directors" + ], + "desc": "Recipients when any of a group's milestones are edited" + }, + "model": "mailtoken.mailtoken", + "pk": "group_milestones_edited" +}, +{ + "fields": { + "recipients": [ + "doc_authors", + "doc_notify", + "doc_shepherd", + "iesg" + ], + "desc": "Recipients when a last call has expired" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_expired" +}, +{ + "fields": { + "recipients": [ + "iesg_secretary" + ], + "desc": "Copied when a last call has expired" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_expired_cc" +}, +{ + "fields": { + "recipients": [ + "ietf_announce" + ], + "desc": "Recipients when a last call is issued" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_issued" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd" + ], + "desc": "Copied when a last call is issued" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_issued_cc" +}, +{ + "fields": { + "recipients": [ + "iana_last_call" + ], + "desc": "Recipients for IANA message when a last call is issued" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_issued_iana" +}, +{ + "fields": { + "recipients": [ + "iesg_secretary" + ], + "desc": "Recipients when AD requests a last call" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_requested" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_notify", + "doc_shepherd" + ], + "desc": "Copied when AD requests a last call" + }, + "model": "mailtoken.mailtoken", + "pk": "last_call_requested_cc" +}, +{ + "fields": { + "recipients": [ + "doc_ad" + ], + "desc": "Recipients when a draft is submitted to the IESG" + }, + "model": "mailtoken.mailtoken", + "pk": "pubreq_iesg" +}, +{ + "fields": { + "recipients": [ + "doc_group_chairs", + "doc_notify", + "doc_shepherd", + "iesg_secretary" + ], + "desc": "Copied when a draft is submitted to the IESG" + }, + "model": "mailtoken.mailtoken", + "pk": "pubreq_iesg_cc" +}, +{ + "fields": { + "recipients": [ + "rfc_editor" + ], + "desc": "Recipients when a non-IETF stream manager requests publication" + }, + "model": "mailtoken.mailtoken", + "pk": "pubreq_rfced" +}, +{ + "fields": { + "recipients": [ + "iana_approve" + ], + "desc": "Recipients for IANA message when a non-IETF stream manager requests publication" + }, + "model": "mailtoken.mailtoken", + "pk": "pubreq_rfced_iana" } ] diff --git a/ietf/name/generate_fixtures.py b/ietf/name/generate_fixtures.py index bcfa375c8..6169e7b89 100644 --- a/ietf/name/generate_fixtures.py +++ b/ietf/name/generate_fixtures.py @@ -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.mailtoken.models +objects += ietf.mailtoken.models.Recipient.objects.all() +objects += ietf.mailtoken.models.MailToken.objects.all() + output("names", objects) diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index c3f2be7df..eef55a74d 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -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 @@ -262,7 +262,6 @@ def doc_detail(request, date, name): doc.save() email_state_changed(request, doc, e.desc) - email_ad(request, doc, doc.ad, login, e.desc) if new_state.slug == "lc-req": request_last_call(request, doc) diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index 3772ecfca..9fad942b9 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -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 @@ -206,7 +206,6 @@ def update_history_with_changes(changes, send_email=True): 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)) if doc.time < timestamp: doc.time = timestamp diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index c84a04109..50dcb8118 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -81,7 +81,8 @@ 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 Waiting on RFC Editor 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), mailbox_before + 3 ) + # TODO look sensibly at the message here # make sure it doesn't create duplicates added_events, warnings = iana.update_history_with_changes(changes) diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 8416bf4be..1f2fd9af1 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -12,7 +12,6 @@ from ietf.ipr.models import HolderIprDisclosure, IprDocRel, IprDisclosureStateNa from ietf.meeting.models import Meeting from ietf.name.models import StreamName from ietf.person.models import Person, Email -from ietf.mailtoken.models import MailToken, Recipient def create_person(group, role_name, name=None, username=None, email_address=None, password=None): """Add person/user/email and role.""" @@ -331,15 +330,16 @@ def make_test_data(): other_doc_factory('minutes','minutes-42-mars') other_doc_factory('slides','slides-42-mars-1') +# Trying the fixture route instead # EventMail tokens used by the views # This won't allow testing the results of the production configuration - if we want to do that, we'll need to # extract the production data either directly, or as a fixture - recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') - for slug in ['ballot_approved_charter', 'ballot_approved_charter_cc', 'ballot_approved_conflrev', 'ballot_approved_conflrev_cc', 'ballot_approved_ietf_stream', 'ballot_approved_ietf_stream_cc', 'ballot_approved_ietf_stream_iana', 'ballot_approved_status_change', 'ballot_approved_status_change_cc', 'ballot_deferred', 'ballot_saved', 'ballot_saved_cc', 'charter_external_review', 'charter_external_review_cc', 'conflrev_requested', 'conflrev_requested_cc', 'conflrev_requested_iana', 'doc_stream_changed', 'last_call_expired', 'last_call_expired_cc', 'last_call_issued', 'last_call_issued_cc', 'last_call_issued_iana', 'last_call_requested', 'last_call_requested_cc', 'pubreq_iesg', 'pubreq_iesg_cc', 'pubreq_rfced', 'pubreq_rfced_iana']: - m = MailToken.objects.create(slug=slug,desc=slug) - m.recipients=[recipient] - # Well, this isn't working out so well - Recipients that have code backing up their gather sometimes refer to other Recipients... - for slug in ['doc_authors','doc_group_chairs','doc_notify','doc_stream_owner','stream_managers']: - Recipient.objects.create(slug=slug,desc="Bogus Recipient",template='bogus@example.com') +# recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') +# for slug in ['ballot_approved_charter', 'ballot_approved_charter_cc', 'ballot_approved_conflrev', 'ballot_approved_conflrev_cc', 'ballot_approved_ietf_stream', 'ballot_approved_ietf_stream_cc', 'ballot_approved_ietf_stream_iana', 'ballot_approved_status_change', 'ballot_approved_status_change_cc', 'ballot_deferred', 'ballot_saved', 'ballot_saved_cc', 'charter_external_review', 'charter_external_review_cc', 'conflrev_requested', 'conflrev_requested_cc', 'conflrev_requested_iana', 'doc_stream_changed', 'last_call_expired', 'last_call_expired_cc', 'last_call_issued', 'last_call_issued_cc', 'last_call_issued_iana', 'last_call_requested', 'last_call_requested_cc', 'pubreq_iesg', 'pubreq_iesg_cc', 'pubreq_rfced', 'pubreq_rfced_iana']: +# m = MailToken.objects.create(slug=slug,desc=slug) +# m.recipients=[recipient] +# # Well, this isn't working out so well - Recipients that have code backing up their gather sometimes refer to other Recipients... +# for slug in ['doc_authors','doc_group_chairs','doc_notify','doc_stream_owner','stream_managers']: +# Recipient.objects.create(slug=slug,desc="Bogus Recipient",template='bogus@example.com') return draft From f30f5c39f15ec20397582e51c88ec4cfd0fd8ff2 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 13 Aug 2015 21:16:21 +0000 Subject: [PATCH 11/46] checkpoint - Legacy-Id: 10017 --- ietf/doc/mails.py | 4 ++-- ietf/doc/tests_ballot.py | 20 +++++++++---------- ietf/doc/tests_charter.py | 4 ++-- ietf/doc/utils_charter.py | 15 -------------- ietf/doc/views_ballot.py | 18 +---------------- ietf/doc/views_charter.py | 8 +++++--- ietf/doc/views_draft.py | 2 +- .../migrations/0002_auto_20150809_1314.py | 14 ++++++++++++- ietf/mailtoken/models.py | 4 ++-- ietf/name/fixtures/names.json | 19 +++++++++++++++++- ietf/secr/telechat/views.py | 2 +- ietf/sync/iana.py | 2 +- 12 files changed, 55 insertions(+), 57 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 51f00454e..d7e5b31fe 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -16,8 +16,8 @@ from ietf.group.models import Role from ietf.doc.models import Document from ietf.mailtoken.utils import gather_addresses, gather_address_list -def email_state_changed(request, doc, text): - to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] +def email_state_changed(request, doc, text, mailtoken_id=None): + to = gather_address_list(mailtoken_id or 'doc_state_edited',doc=doc) if not to: return diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index ff42532a3..5f9737044 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -232,7 +232,7 @@ 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 + 2) + self.assertEqual(len(outbox), mailbox_before + 1) self.assertTrue("Last Call" in outbox[-1]['Subject']) self.assertTrue(draft.name in outbox[-1]['Subject']) @@ -387,7 +387,7 @@ 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 + 3) + self.assertEqual(len(outbox), mailbox_before + 2) self.assertTrue("Protocol Action" in outbox[-2]['Subject']) # the IANA copy self.assertTrue("Protocol Action" in outbox[-1]['Subject']) @@ -409,7 +409,7 @@ 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 + 2) + self.assertEqual(len(outbox), mailbox_before + 1) self.assertTrue("NOT be published" in str(outbox[-1])) @@ -441,11 +441,10 @@ 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 + 3) + self.assertEqual(len(outbox), mailbox_before + 2) - self.assertTrue("Last Call" in outbox[-3]['Subject']) - # the IANA copy self.assertTrue("Last Call" in outbox[-2]['Subject']) + self.assertTrue("Last Call" in outbox[-1]['Subject']) self.assertTrue("Last Call" in draft.message_set.order_by("-time")[0].subject) class DeferUndeferTestCase(TestCase): @@ -491,8 +490,8 @@ 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.assertEqual(len(outbox), mailbox_before + 2) + #self.assertTrue("State Update" in outbox[-3]['Subject']) self.assertTrue("Telechat update" in outbox[-2]['Subject']) self.assertTrue("Deferred" in outbox[-1]['Subject']) self.assertTrue(doc.file_tag() in outbox[-1]['Subject']) @@ -546,9 +545,8 @@ 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("Undeferred" in outbox[-1]['Subject']) self.assertTrue(doc.file_tag() in outbox[-1]['Subject']) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index c756cd05a..2876b7dcd 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -96,8 +96,8 @@ 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), mailbox_before + 2) + self.assertTrue("state changed" in outbox[-2]['Subject'].lower()) def test_edit_telechat_date(self): make_test_data() diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index c5ff3457a..b92cde3e4 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -1,13 +1,11 @@ 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.utils.history import find_history_active_at -from ietf.utils.mail import send_mail_text from ietf.mailtoken.utils import gather_addresses def charter_name_for_group(group): @@ -84,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" diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 5127bb373..55a10d7e4 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -18,7 +18,7 @@ from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent, BallotP 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_ballot_deferred, email_ballot_undeferred, - email_state_changed, extra_automation_headers, generate_last_call_announcement, + 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 @@ -69,8 +69,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): @@ -367,9 +365,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) @@ -462,9 +457,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) - request_last_call(request, doc) return render_to_response('doc/draft/last_call_requested.html', @@ -699,15 +691,11 @@ 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) - # send announcement send_mail_preformatted(request, announcement) @@ -785,10 +773,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) - e = LastCallDocEvent(doc=doc, by=login) e.type = "sent_last_call" e.desc = "The following Last Call announcement was sent out:

" diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index fe6380481..8fc8fa106 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -18,8 +18,9 @@ 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 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 @@ -142,7 +143,8 @@ def change_state(request, name, option=None): 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) - email_state_changed(request, charter, "State changed to %s." % charter_state) + # 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"): @@ -266,7 +268,7 @@ def change_title(request, name, option=None): 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_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) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 30e28c8aa..36155714b 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -110,7 +110,7 @@ def change_state(request, name): doc.time = e.time doc.save() - email_state_changed(request, doc, 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"): diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 58cc019c3..0c656cb67 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -328,7 +328,19 @@ def make_mailtokens(apps): ]) mt_factory(slug='doc_state_edited', - desc="Recipients when a document's state is manutally edited", + desc="Recipients when a document's state is manually edited", + recipient_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_iana_state_changed', + desc="Recipients when IANA state information for a document changes ", recipient_slugs=['doc_notify', 'doc_ad', 'doc_authors', diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index ef1e6c73d..659a63430 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -85,11 +85,11 @@ class Recipient(models.Model): addrs.extend(Recipient.objects.get(slug='doc_notify').gather(**{'doc':reldoc.document})) return addrs - def gather_conflict_review_stream_owner(self, **kwargs): + 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_owner').gather(**{'doc':reldoc.document})) + addrs.extend(Recipient.objects.get(slug='doc_stream_manager').gather(**{'doc':reldoc.document})) return addrs def gather_conflict_review_steering_group(self,**kwargs): diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index bec077271..4b72f2fc7 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4757,7 +4757,24 @@ "doc_notify", "doc_shepherd" ], - "desc": "Recipients when a document's state is manutally edited" + "desc": "Recipients when IANA state information for a document changes " + }, + "model": "mailtoken.mailtoken", + "pk": "doc_iana_state_changed" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd" + ], + "desc": "Recipients when a document's state is manually edited" }, "model": "mailtoken.mailtoken", "pk": "doc_state_edited" diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index eef55a74d..e32199a8e 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -261,7 +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_state_changed(request, doc, e.desc, 'doc_state_edited') if new_state.slug == "lc-req": request_last_call(request, doc) diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index 9fad942b9..b611aedb4 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -205,7 +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_state_changed(None, doc, "IANA %s state changed to %s" % (kind, state.name),'doc_iana_state_changed') if doc.time < timestamp: doc.time = timestamp From c39ebe4f569e5996153a534dabf815394dbe219f Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 13 Aug 2015 22:42:39 +0000 Subject: [PATCH 12/46] checkpoint - Legacy-Id: 10018 --- ietf/doc/mails.py | 4 +- ietf/doc/utils.py | 31 ++---------- ietf/doc/views_draft.py | 2 +- .../migrations/0002_auto_20150809_1314.py | 28 +++++++++++ ietf/mailtoken/models.py | 8 ++++ ietf/name/fixtures/names.json | 47 +++++++++++++++++++ 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index d7e5b31fe..9d0d27e73 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -52,8 +52,8 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""): 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, + extra['Cc'] = gather_addresses('doc_pulled_from_rfc_queue_cc',doc=doc) + send_mail(request, gather_address_list('doc_pulled_from_rfc_queue',doc=doc), 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, diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 6572d48e4..37cee0b11 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -456,35 +456,15 @@ 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@tools.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) + to = gather_address_list('doc_replacement_changed',doc=doc) 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) + to.extend(gather_address_list('doc_replacement_changed',doc=d.document)) 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' @@ -492,7 +472,7 @@ 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) + to.extend(gather_address_list('doc_replacement_changed',doc=d.document)) RelatedDocument.objects.create(source=doc, target=d, relationship=relationship) d.document.set_state(State.objects.get(type='draft', slug='repl')) @@ -510,10 +490,7 @@ 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) - ] + to = list(set([addr for addr in to if addr])) from ietf.doc.mails import html_to_text diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 36155714b..f654dd0be 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -450,7 +450,7 @@ def change_intention(request, name): doc.save() # TODO: Build explicit changed_intended_publication_status - email_ad(request, doc, doc.ad, login, email_desc) + email_state_changed(request, doc, email_desc,'doc_state_edited') return HttpResponseRedirect(doc.get_absolute_url()) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 0c656cb67..abab3a997 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -113,6 +113,10 @@ def make_recipients(apps): 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) + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -364,6 +368,30 @@ def make_mailtokens(apps): '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", + recipient_slugs=['iana', + 'rfc_editor', + ]) + + mt_factory(slug='doc_pulled_from_rfc_queue_cc', + desc="Recipients when a document is taken out of the RFC's editor queue before publication", + recipient_slugs=['iesg-secretary', + 'doc_ad', + '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", + recipient_slugs=['doc_authors', + 'doc_notify', + 'doc_shepherd', + 'doc_group_chairs', + 'doc_group_responsible_directors', + ]) def forward(apps, schema_editor): diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index 659a63430..632e55b26 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -133,3 +133,11 @@ class Recipient(models.Model): 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: + addrs.extend(Recipient.objects.get(slug='group_responsible_directors').gather(**{'group':group})) + return addrs diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 4b72f2fc7..b31ea91f1 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4407,6 +4407,14 @@ "model": "mailtoken.recipient", "pk": "doc_group_mail_list" }, +{ + "fields": { + "template": null, + "desc": "The document's group's responsible AD(s) or IRTF chair" + }, + "model": "mailtoken.recipient", + "pk": "doc_group_responsible_directors" +}, { "fields": { "template": "{{doc.notify}}", @@ -4762,6 +4770,45 @@ "model": "mailtoken.mailtoken", "pk": "doc_iana_state_changed" }, +{ + "fields": { + "recipients": [ + "iana", + "rfc_editor" + ], + "desc": "Recipients when a document is taken out of the RFC's editor queue before publication" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_pulled_from_rfc_queue" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd" + ], + "desc": "Recipients when a document is taken out of the RFC's editor queue before publication" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_pulled_from_rfc_queue_cc" +}, +{ + "fields": { + "recipients": [ + "doc_authors", + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_notify", + "doc_shepherd" + ], + "desc": "Recipients when what a document replaces or is replaced by changes" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_replacement_changed" +}, { "fields": { "recipients": [ From 89f038a1e3cde192b3e9d158b7a7eab65cca89fb Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 17 Aug 2015 20:24:16 +0000 Subject: [PATCH 13/46] checkpoint - Legacy-Id: 10019 --- ietf/doc/expire.py | 17 +- ietf/doc/mails.py | 9 +- ietf/doc/tests_draft.py | 2 +- ietf/group/mails.py | 2 +- .../migrations/0002_auto_20150809_1314.py | 124 +++++++++ ietf/mailtoken/models.py | 73 +++++- ietf/name/fixtures/names.json | 245 ++++++++++++++++++ ietf/settings.py | 1 - ietf/submit/mail.py | 61 +---- ietf/submit/models.py | 3 + ietf/submit/views.py | 7 +- 11 files changed, 468 insertions(+), 76 deletions(-) diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index b2a62ff91..1551abab5 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -7,9 +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.mailtoken.utils import gather_address_list @@ -70,10 +71,8 @@ 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 = gather_address_list('doc_expires_soon',doc=doc) + cc = gather_address_list('doc_expires_soon_cc',doc=doc) s = doc.get_state("draft-iesg") state = s.name if s else "I-D Exists" @@ -91,21 +90,23 @@ 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 = gather_address_list('doc_expired',doc=doc) + cc = gather_address_list('doc_expired_cc',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 9d0d27e73..2d3356871 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -261,7 +261,7 @@ def send_last_call_request(request, doc): cc=cc) def email_resurrect_requested(request, doc, by): - to = "I-D Administrator " + to = gather_address_list('resurrection_requested',doc=doc) if by.role_set.filter(name="secr", group__acronym="secretariat"): e = by.role_email("secr", group="secretariat") @@ -277,12 +277,7 @@ def email_resurrect_requested(request, doc, by): url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) def email_resurrection_completed(request, doc, requester): - if requester.role_set.filter(name="secr", group__acronym="secretariat"): - e = requester.role_email("secr", group="secretariat") - else: - e = requester.role_email("ad") - - to = e.formatted_email() + to = gather_address_list('resurrection_completed',doc=doc) frm = "I-D Administrator " send_mail(request, to, frm, "I-D Resurrection Completed - %s" % doc.file_tag(), diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 938c1a8f1..b68dfe53f 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -524,7 +524,7 @@ class ExpireIDsTests(TestCase): 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("mars-chairs@ietf.org" in str(outbox[-1])) def test_expire_drafts(self): from ietf.doc.expire import get_expired_drafts, send_expire_notice_for_draft, expire_draft diff --git a/ietf/group/mails.py b/ietf/group/mails.py index c4ca308e6..0f4c1654e 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -15,7 +15,7 @@ from ietf.group.utils import milestone_reviewer_for_group_type from ietf.mailtoken.utils import gather_address_list def email_iesg_secretary_re_charter(request, group, subject, text): - to = ["iesg-secretary@ietf.org"] + to = gather_address_list('charter_state_message_provided',group=group) full_subject = u"Regarding %s %s: %s" % (group.type.name, group.acronym, subject) text = strip_tags(text) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index abab3a997..dd5490ba6 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -117,6 +117,42 @@ def make_recipients(apps): desc="The document's group's responsible AD(s) or IRTF chair", template=None) + rc(slug='internet_drafts', + 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) + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -393,6 +429,94 @@ def make_mailtokens(apps): 'doc_group_responsible_directors', ]) + mt_factory(slug='charter_state_message_provided', + desc="Recipients for extra message when provided on the charter state editing form", + recipient_slugs=['iesg_secretary']) + + mt_factory(slug='doc_expires_soon', + desc="Recipients for notification of impending expiration of a document", + recipient_slugs=['doc_authors']) + + mt_factory(slug='doc_expires_soon_cc', + desc="Copied on notification of impending expiration of a document", + recipient_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", + recipient_slugs=['doc_authors']) + + mt_factory(slug='doc_expired_cc', + desc="Copied on notification of a document's expiration", + recipient_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'", + recipient_slugs=['internet_drafts',]) + + mt_factory(slug='resurrection_completed', + desc="Recipients when a draft resurrection request has been completed", + recipient_slugs=['iesg_secretary', + 'doc_ad', + ]) + + mt_factory(slug='sub_manual_post_requested', + desc="Recipients for a manual post request for a draft submission", + recipient_slugs=['internet_drafts', + ]) + + mt_factory(slug='sub_manual_post_requested_cc', + desc="Copied on a manual post request for a draft submission", + recipient_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", + recipient_slugs=['submission_group_chairs',]) + + mt_factory(slug='sub_confirmation_requested', + desc="Recipients for a message requesting confirmation of a draft submission", + recipient_slugs=['submission_confirmers',]) + + mt_factory(slug='sub_management_url_requested', + desc="Recipients for a message with the full URL for managing a draft submission", + recipient_slugs=['submission_confirmers',]) + + mt_factory(slug='sub_announced', + desc="Recipients for the announcement of a successfully submitted draft", + recipient_slugs=['ietf_announce', + ]) + + mt_factory(slug='sub_announced_cc', + desc="Copied on the announcement of a successfully submitted draft", + recipient_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", + recipient_slugs=['submission_authors', + 'submission_confirmers', + ]) + + mt_factory(slug='sub_new_version', + desc="Recipient for notification of a new version of an existing document", + recipient_slugs=['doc_notify', + 'doc_ad', + 'non_ietf_stream_manager', + 'rfc_editor_if_doc_in_queue', + 'doc_discussing_ads', + ]) + + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index 632e55b26..1579f6de0 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -3,8 +3,6 @@ from django.db import models from django.template import Template, Context -from ietf.group.models import Role - class MailToken(models.Model): slug = models.CharField(max_length=32, primary_key=True) desc = models.TextField(blank=True) @@ -52,7 +50,7 @@ class Recipient(models.Model): if 'doc' in kwargs: doc=kwargs['doc'] if doc.group and doc.group.type.slug in ['wg','rg']: - addrs.extend(Role.objects.filter(group=doc.group,name='delegate').values_list('email__address',flat=True)) + 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): @@ -125,11 +123,19 @@ class Recipient(models.Model): 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'] - addrs.extend(Role.objects.filter(group=group,name='ad').values_list('email__address',flat=True)) + 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 @@ -141,3 +147,62 @@ class Recipient(models.Model): if group: 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 diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index b31ea91f1..ec116799d 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4383,6 +4383,14 @@ "model": "mailtoken.recipient", "pk": "doc_authors" }, +{ + "fields": { + "template": null, + "desc": "Any ADs holding an active DISCUSS position on a given document" + }, + "model": "mailtoken.recipient", + "pk": "doc_discussing_ads" +}, { "fields": { "template": null, @@ -4415,6 +4423,14 @@ "model": "mailtoken.recipient", "pk": "doc_group_responsible_directors" }, +{ + "fields": { + "template": null, + "desc": "The document's stream manager if the document is not in the IETF stream" + }, + "model": "mailtoken.recipient", + "pk": "doc_non_ietf_stream_manager" +}, { "fields": { "template": "{{doc.notify}}", @@ -4527,6 +4543,14 @@ "model": "mailtoken.recipient", "pk": "ietf_announce" }, +{ + "fields": { + "template": "internet-drafts@ietf.org", + "desc": "The internet drafts ticketing system" + }, + "model": "mailtoken.recipient", + "pk": "internet_drafts" +}, { "fields": { "template": "", @@ -4535,6 +4559,14 @@ "model": "mailtoken.recipient", "pk": "rfc_editor" }, +{ + "fields": { + "template": null, + "desc": "The RFC Editor if a document is in the RFC Editor queue" + }, + "model": "mailtoken.recipient", + "pk": "rfc_editor_if_doc_in_queue" +}, { "fields": { "template": null, @@ -4543,6 +4575,46 @@ "model": "mailtoken.recipient", "pk": "stream_managers" }, +{ + "fields": { + "template": null, + "desc": "The authors of a submitted draft" + }, + "model": "mailtoken.recipient", + "pk": "submission_authors" +}, +{ + "fields": { + "template": null, + "desc": "The people who can confirm a draft submission" + }, + "model": "mailtoken.recipient", + "pk": "submission_confirmers" +}, +{ + "fields": { + "template": null, + "desc": "The chairs of a submitted draft belonging to a group" + }, + "model": "mailtoken.recipient", + "pk": "submission_group_chairs" +}, +{ + "fields": { + "template": null, + "desc": "The people who can confirm a draft submission" + }, + "model": "mailtoken.recipient", + "pk": "submission_group_mail_list" +}, +{ + "fields": { + "template": "{{submission.submitter}}", + "desc": "The person that submitted a draft" + }, + "model": "mailtoken.recipient", + "pk": "submission_submitter" +}, { "fields": { "recipients": [ @@ -4719,6 +4791,16 @@ "model": "mailtoken.mailtoken", "pk": "charter_external_review_cc" }, +{ + "fields": { + "recipients": [ + "iesg_secretary" + ], + "desc": "Recipients for extra message when provided on the charter state editing form" + }, + "model": "mailtoken.mailtoken", + "pk": "charter_state_message_provided" +}, { "fields": { "recipients": [ @@ -4753,6 +4835,52 @@ "model": "mailtoken.mailtoken", "pk": "conflrev_requested_iana" }, +{ + "fields": { + "recipients": [ + "doc_authors" + ], + "desc": "Recipients for notification of a document's expiration" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_expired" +}, +{ + "fields": { + "recipients": [ + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_notify", + "doc_shepherd" + ], + "desc": "Copied on notification of a document's expiration" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_expired_cc" +}, +{ + "fields": { + "recipients": [ + "doc_authors" + ], + "desc": "Recipients for notification of impending expiration of a document" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_expires_soon" +}, +{ + "fields": { + "recipients": [ + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_notify", + "doc_shepherd" + ], + "desc": "Copied on notification of impending expiration of a document" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_expires_soon_cc" +}, { "fields": { "recipients": [ @@ -5012,5 +5140,122 @@ }, "model": "mailtoken.mailtoken", "pk": "pubreq_rfced_iana" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "iesg_secretary" + ], + "desc": "Recipients when a draft resurrection request has been completed" + }, + "model": "mailtoken.mailtoken", + "pk": "resurrection_completed" +}, +{ + "fields": { + "recipients": [ + "internet_drafts" + ], + "desc": "Recipients of a request to change the state of a draft away from 'Dead'" + }, + "model": "mailtoken.mailtoken", + "pk": "resurrection_requested" +}, +{ + "fields": { + "recipients": [ + "ietf_announce" + ], + "desc": "Recipients for the announcement of a successfully submitted draft" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_announced" +}, +{ + "fields": { + "recipients": [ + "submission_group_mail_list" + ], + "desc": "Copied on the announcement of a successfully submitted draft" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_announced_cc" +}, +{ + "fields": { + "recipients": [ + "submission_authors", + "submission_confirmers" + ], + "desc": "Recipients for the announcement to the authors of a successfully submitted draft" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_announced_to_authors" +}, +{ + "fields": { + "recipients": [ + "submission_group_chairs" + ], + "desc": "Recipients for a message requesting group chair approval of a draft submission" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_chair_approval_requested" +}, +{ + "fields": { + "recipients": [ + "submission_confirmers" + ], + "desc": "Recipients for a message requesting confirmation of a draft submission" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_confirmation_requested" +}, +{ + "fields": { + "recipients": [ + "submission_confirmers" + ], + "desc": "Recipients for a message with the full URL for managing a draft submission" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_management_url_requested" +}, +{ + "fields": { + "recipients": [ + "internet_drafts" + ], + "desc": "Recipients for a manual post request for a draft submission" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_manual_post_requested" +}, +{ + "fields": { + "recipients": [ + "submission_authors", + "submission_group_chairs", + "submission_submitter" + ], + "desc": "Copied on a manual post request for a draft submission" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_manual_post_requested_cc" +}, +{ + "fields": { + "recipients": [ + "doc_ad", + "doc_discussing_ads", + "doc_notify", + "rfc_editor_if_doc_in_queue" + ], + "desc": "Recipient for notification of a new version of an existing document" + }, + "model": "mailtoken.mailtoken", + "pk": "sub_new_version" } ] diff --git a/ietf/settings.py b/ietf/settings.py index 183c6b8a6..b45d96396 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -421,7 +421,6 @@ NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina'] # ID Submission Tool settings IDSUBMIT_FROM_EMAIL = 'IETF I-D Submission Tool ' -IDSUBMIT_TO_EMAIL = 'internet-drafts@ietf.org' IDSUBMIT_ANNOUNCE_FROM_EMAIL = 'internet-drafts@ietf.org' IDSUBMIT_ANNOUNCE_LIST_EMAIL = 'i-d-announce@ietf.org' diff --git a/ietf/submit/mail.py b/ietf/submit/mail.py index 72bf6ce8e..d6148df1b 100644 --- a/ietf/submit/mail.py +++ b/ietf/submit/mail.py @@ -6,25 +6,14 @@ 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.mailtoken.utils import gather_address_list 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 = gather_address_list('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())) @@ -40,7 +29,7 @@ def send_submission_confirmation(request, submission): 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 = gather_address_list('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', { @@ -53,7 +42,7 @@ def send_full_url(request, submission): 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 = gather_address_list('sub_chair_approval_requested',submission=submission) if not to_email: return to_email @@ -67,15 +56,8 @@ def send_approval_request_to_group(request, submission): 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 = gather_address_list('sub_manual_post_requested',submission=submission) + cc = gather_address_list('sub_manual_post_requested_cc',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 +75,8 @@ 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 = gather_address_list('sub_announced',submission=submission) + m.cc = gather_address_list('sub_announced_cc',submission=submission) m.body = render_to_string('submit/announce_to_lists.txt', dict(submission=submission, settings=settings)) @@ -106,28 +87,7 @@ 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 ") - elif draft.stream_id == "ise": - to_email.append("Independent Submission Editor ") - elif draft.stream_id == "irtf": - to_email.append("IRSG ") - - # 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 ") - - 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 = gather_address_list('sub_new_version',doc=draft,submission=submission) if to_email: subject = 'New Version Notification - %s-%s.txt' % (submission.name, submission.rev) @@ -137,8 +97,7 @@ def announce_new_version(request, submission, draft, state_change_msg): 'msg': state_change_msg}) 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 = gather_address_list('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: diff --git a/ietf/submit/models.py b/ietf/submit/models.py index 97761cb10..8e7900990 100644 --- a/ietf/submit/models.py +++ b/ietf/submit/models.py @@ -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) diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 8cd73e9a7..caf0a55e3 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -16,13 +16,14 @@ 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 from ietf.submit.utils import post_submission, cancel_submission, rename_submission_files from ietf.utils.accesstoken import generate_random_key, generate_access_token from ietf.utils.draft import Draft +from ietf.mailtoken.utils import gather_address_list def upload_submission(request): @@ -180,7 +181,7 @@ 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) + confirmation_list = gather_address_list('sub_confirmation_requested',submission=submission) 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()) @@ -205,7 +206,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) From 56ddf4ca2ebfa3cc9724cab62e5605ccbb5a3109 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 17 Aug 2015 22:11:54 +0000 Subject: [PATCH 14/46] checkpoint. includes moderate change to how personnel changes are sent out - Legacy-Id: 10020 --- ietf/doc/views_charter.py | 8 +- ietf/group/edit.py | 10 +-- ietf/group/mails.py | 53 ++----------- .../migrations/0002_auto_20150809_1314.py | 41 +++++++++- ietf/name/fixtures/names.json | 77 ++++++++++++++++++- 5 files changed, 129 insertions(+), 60 deletions(-) diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index 8fc8fa106..2cfb56171 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -29,7 +29,7 @@ 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.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) @@ -141,7 +141,7 @@ def change_state(request, name, option=None): 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) + 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') @@ -267,7 +267,7 @@ 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_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: @@ -645,7 +645,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"))) diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 390a34495..10bf84be7 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -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 @@ -255,6 +254,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] @@ -276,10 +276,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'] @@ -357,7 +357,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" diff --git a/ietf/group/mails.py b/ietf/group/mails.py index 0f4c1654e..28650e38d 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -14,8 +14,8 @@ from ietf.group.models import Group from ietf.group.utils import milestone_reviewer_for_group_type from ietf.mailtoken.utils import gather_address_list -def email_iesg_secretary_re_charter(request, group, subject, text): - to = gather_address_list('charter_state_message_provided',group=group) +def email_admin_re_charter(request, group, subject, text, mailtoken): + to = gather_address_list(mailtoken,group=group) full_subject = u"Regarding %s %s: %s" % (group.type.name, group.acronym, subject) text = strip_tags(text) @@ -28,42 +28,11 @@ def email_iesg_secretary_re_charter(request, group, subject, text): ) ) -def email_iesg_secretary_personnel_change(request, group, text): - to = ["iesg-secretary@ietf.org"] +def email_personnel_change(request, group, text, changed_personnel): + to = gather_address_list('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 ") - - 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) - def email_milestones_changed(request, group, changes): def wrap_up_email(to, text): @@ -94,18 +63,12 @@ def email_milestones_changed(request, group, changes): 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 ") + to = gather_address_list('milestone_review_reminder',group=group) + cc = gather_address_list('milestone_review_reminder_cc',group=group) 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 @@ -139,7 +102,7 @@ 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")] + to = gather_address_list('milestones_due_soon',group=group) today = datetime.date.today() early_warning = today + datetime.timedelta(days=early_warning_days) @@ -166,7 +129,7 @@ def groups_needing_milestones_due_reminder(early_warning_days): 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")] + to = gather_address_list('milestones_overdue',group=group) today = datetime.date.today() diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index dd5490ba6..945ee6043 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -153,6 +153,10 @@ def make_recipients(apps): 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:", " }}') + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -429,8 +433,12 @@ def make_mailtokens(apps): 'doc_group_responsible_directors', ]) - mt_factory(slug='charter_state_message_provided', - desc="Recipients for extra message when provided on the charter state editing form", + mt_factory(slug='charter_state_edit_admin_needed', + desc="Recipients for message to adminstrators when a charter state edit needs followon administrative action", + recipient_slugs=['iesg_secretary']) + + mt_factory(slug='group_closure_requested', + desc="Recipients for message requesting closure of a group", recipient_slugs=['iesg_secretary']) mt_factory(slug='doc_expires_soon', @@ -508,7 +516,7 @@ def make_mailtokens(apps): ]) mt_factory(slug='sub_new_version', - desc="Recipient for notification of a new version of an existing document", + desc="Recipients for notification of a new version of an existing document", recipient_slugs=['doc_notify', 'doc_ad', 'non_ietf_stream_manager', @@ -516,6 +524,33 @@ def make_mailtokens(apps): 'doc_discussing_ads', ]) + mt_factory(slug='milestone_review_reminder', + desc="Recipients for reminder message that unapproved milestone changes need review", + recipient_slugs=['group_responsible_directors', + ]) + + mt_factory(slug='milestone_review_reminder_cc', + desc="Copied on reminder message that unapproved milestone changes need review", + recipient_slugs=['group_chairs', + ]) + + mt_factory(slug='milestones_due_soon', + desc='Recipients for reminder message for milestones about to become overdue', + recipient_slugs=['group_chairs', + ]) + + mt_factory(slug='milestones_overdue', + desc='Recipients for message about milestones that are overdue', + recipient_slugs=['group_chairs', + ]) + + mt_factory(slug='group_personnel_change', + desc="Recipients for a message noting changes in a group's personnel", + recipient_slugs=['iesg_secretary', + 'group_responsible_directors', + 'group_chairs', + 'group_changed_personnel', + ]) def forward(apps, schema_editor): diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index ec116799d..88f2dc314 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4463,6 +4463,14 @@ "model": "mailtoken.recipient", "pk": "group_chairs" }, +{ + "fields": { + "template": "{{ changed_personnel | join:\", \" }}", + "desc": "Any personnel who were added or deleted when a group's personnel changes" + }, + "model": "mailtoken.recipient", + "pk": "group_changed_personnel" +}, { "fields": { "template": "{{ group.list_email }}", @@ -4796,10 +4804,10 @@ "recipients": [ "iesg_secretary" ], - "desc": "Recipients for extra message when provided on the charter state editing form" + "desc": "Recipients for message to adminstrators when a charter state edit needs followon administrative action" }, "model": "mailtoken.mailtoken", - "pk": "charter_state_message_provided" + "pk": "charter_state_edit_admin_needed" }, { "fields": { @@ -5005,6 +5013,16 @@ "model": "mailtoken.mailtoken", "pk": "group_approved_milestones_edited" }, +{ + "fields": { + "recipients": [ + "iesg_secretary" + ], + "desc": "Recipients for message requesting closure of a group" + }, + "model": "mailtoken.mailtoken", + "pk": "group_closure_requested" +}, { "fields": { "recipients": [ @@ -5016,6 +5034,19 @@ "model": "mailtoken.mailtoken", "pk": "group_milestones_edited" }, +{ + "fields": { + "recipients": [ + "group_chairs", + "group_changed_personnel", + "group_responsible_directors", + "iesg_secretary" + ], + "desc": "Recipients for a message noting changes in a group's personnel" + }, + "model": "mailtoken.mailtoken", + "pk": "group_personnel_change" +}, { "fields": { "recipients": [ @@ -5098,6 +5129,46 @@ "model": "mailtoken.mailtoken", "pk": "last_call_requested_cc" }, +{ + "fields": { + "recipients": [ + "group_chairs" + ], + "desc": "Recipients for reminder message for milestones about to become overdue" + }, + "model": "mailtoken.mailtoken", + "pk": "milestones_due_soon" +}, +{ + "fields": { + "recipients": [ + "group_chairs" + ], + "desc": "Recipients for message about milestones that are overdue" + }, + "model": "mailtoken.mailtoken", + "pk": "milestones_overdue" +}, +{ + "fields": { + "recipients": [ + "group_responsible_directors" + ], + "desc": "Recipients for reminder message that unapproved milestone changes need review" + }, + "model": "mailtoken.mailtoken", + "pk": "milestone_review_reminder" +}, +{ + "fields": { + "recipients": [ + "group_chairs" + ], + "desc": "Copied on reminder message that unapproved milestone changes need review" + }, + "model": "mailtoken.mailtoken", + "pk": "milestone_review_reminder_cc" +}, { "fields": { "recipients": [ @@ -5253,7 +5324,7 @@ "doc_notify", "rfc_editor_if_doc_in_queue" ], - "desc": "Recipient for notification of a new version of an existing document" + "desc": "Recipients for notification of a new version of an existing document" }, "model": "mailtoken.mailtoken", "pk": "sub_new_version" From 23a2ada5f91db54eaee0c6aa38e2c3a1b2eaea5f Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 18 Aug 2015 18:52:22 +0000 Subject: [PATCH 15/46] checkpoint - Legacy-Id: 10027 --- ietf/ipr/mail.py | 8 +- ietf/ipr/tests.py | 4 +- ietf/ipr/views.py | 6 +- .../migrations/0002_auto_20150809_1314.py | 86 +++++++++- ietf/mailtoken/utils.py | 8 +- ietf/name/fixtures/names.json | 152 +++++++++++++++++- ietf/secr/meetings/views.py | 7 +- ietf/secr/sreq/views.py | 22 ++- ietf/settings.py | 2 +- 9 files changed, 262 insertions(+), 33 deletions(-) diff --git a/ietf/ipr/mail.py b/ietf/ipr/mail.py index 1532125dd..98aa16b3e 100644 --- a/ietf/ipr/mail.py +++ b/ietf/ipr/mail.py @@ -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.mailtoken.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) @@ -167,7 +167,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 +193,4 @@ def process_response_email(msg): ) log(u"Received IPR email from %s" % ietf_message.frm) - return ietf_message \ No newline at end of file + return ietf_message diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index b1496a2ec..907b920d1 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -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 @@ -16,6 +15,7 @@ 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.mailtoken.utils import gather_addresses class IprTests(TestCase): @@ -519,7 +519,7 @@ I would like to revoke this declaration. From: joe@test.com Date: {} Subject: test -""".format(settings.IPR_EMAIL_TO,datetime.datetime.now().ctime()) +""".format(gather_addresses('ipr_disclosure_submitted'),datetime.datetime.now().ctime()) result = process_response_email(message_string) self.assertIsNone(result) diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 0058ca6c5..b0589ebc7 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -35,6 +35,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.mailtoken.utils import gather_address_list # ---------------------------------------------------------------- # Globals @@ -379,7 +380,7 @@ def email(request, id): reply_to = get_reply_to() initial = { 'to': ipr.submitter_email, - 'frm': settings.IPR_EMAIL_TO, + 'frm': settings.IPR_EMAIL_FROM, 'subject': 'Regarding {}'.format(ipr.title), 'reply_to': reply_to, } @@ -474,7 +475,8 @@ 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 = gather_address_list('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,}) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 945ee6043..fbf0a564e 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -117,7 +117,7 @@ def make_recipients(apps): desc="The document's group's responsible AD(s) or IRTF chair", template=None) - rc(slug='internet_drafts', + rc(slug='internet_draft_requests', desc="The internet drafts ticketing system", template='internet-drafts@ietf.org') @@ -157,6 +157,18 @@ def make_recipients(apps): 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='ipr_requests', + desc="The ipr disclosure handling system", + template='ietf-ipr@ietf.org') + + 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 %}') + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -467,7 +479,7 @@ def make_mailtokens(apps): mt_factory(slug='resurrection_requested', desc="Recipients of a request to change the state of a draft away from 'Dead'", - recipient_slugs=['internet_drafts',]) + recipient_slugs=['internet_draft_requests',]) mt_factory(slug='resurrection_completed', desc="Recipients when a draft resurrection request has been completed", @@ -477,7 +489,7 @@ def make_mailtokens(apps): mt_factory(slug='sub_manual_post_requested', desc="Recipients for a manual post request for a draft submission", - recipient_slugs=['internet_drafts', + recipient_slugs=['internet_draft_requests', ]) mt_factory(slug='sub_manual_post_requested_cc', @@ -552,6 +564,74 @@ def make_mailtokens(apps): 'group_changed_personnel', ]) + mt_factory(slug='session_requested', + desc="Recipients for a normal meeting session request", + recipient_slugs=['session_requests', + ]) + + mt_factory(slug='session_requested_cc', + desc="Copied on a normal meeting session request", + recipient_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", + recipient_slugs=['group_responsible_directors', + ]) + + mt_factory(slug='session_requested_long_cc', + desc="Copied on a meeting session request for more than 2 sessions", + recipient_slugs=['session_requests', + 'group_chairs', + 'logged_in_person', + ]) + + mt_factory(slug='session_request_cancelled', + desc="Recipients for a message cancelling a session request", + recipient_slugs=['session_requests', + ]) + + mt_factory(slug='session_request_cancelled_cc', + desc="Copied on a message cancelling a session request", + recipient_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", + recipient_slugs=['session_requests', + ]) + + mt_factory(slug='session_request_not_meeting_cc', + desc="Copied on a message noting a group plans to not meet", + recipient_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", + recipient_slugs=['session_requester', + 'group_chairs', + ]) + + mt_factory(slug='session_scheduled_cc', + desc="Recipients for details when a session has been scheduled", + recipient_slugs=['group_mail_list', + 'group_responsible_directors', + ]) + + mt_factory(slug='ipr_disclosure_submitted', + desc="Recipients when an IPR notification is submitted", + recipient_slugs=['ipr_requests', + ]) + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 2cf14a0ff..56b330082 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -1,7 +1,6 @@ - from django.core.exceptions import ObjectDoesNotExist -from ietf.mailtoken.models import MailToken +from ietf.mailtoken.models import MailToken, Recipient def gather_address_list(slug,**kwargs): @@ -21,3 +20,8 @@ def gather_address_list(slug,**kwargs): def gather_addresses(slug,**kwargs): return ",\n ".join(gather_address_list(slug,**kwargs)) + +def get_base_ipr_request_address(): + return Recipient.objects.get(slug='ipr_requests').gather()[0] + + diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 88f2dc314..8e854a5bb 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4557,7 +4557,23 @@ "desc": "The internet drafts ticketing system" }, "model": "mailtoken.recipient", - "pk": "internet_drafts" + "pk": "internet_draft_requests" +}, +{ + "fields": { + "template": "ietf-ipr@ietf.org", + "desc": "The ipr disclosure handling system" + }, + "model": "mailtoken.recipient", + "pk": "ipr_requests" +}, +{ + "fields": { + "template": "{% if person and person.email_address %}{{ person.email_address }}{% endif %}", + "desc": "The person currently logged into the datatracker who initiated a given action" + }, + "model": "mailtoken.recipient", + "pk": "logged_in_person" }, { "fields": { @@ -4575,6 +4591,14 @@ "model": "mailtoken.recipient", "pk": "rfc_editor_if_doc_in_queue" }, +{ + "fields": { + "template": "session-request@ietf.org", + "desc": "The session request ticketing system" + }, + "model": "mailtoken.recipient", + "pk": "session_requests" +}, { "fields": { "template": null, @@ -5047,6 +5071,16 @@ "model": "mailtoken.mailtoken", "pk": "group_personnel_change" }, +{ + "fields": { + "recipients": [ + "ipr_requests" + ], + "desc": "Recipients when an IPR notification is submitted" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_disclosure_submitted" +}, { "fields": { "recipients": [ @@ -5226,13 +5260,125 @@ { "fields": { "recipients": [ - "internet_drafts" + "internet_draft_requests" ], "desc": "Recipients of a request to change the state of a draft away from 'Dead'" }, "model": "mailtoken.mailtoken", "pk": "resurrection_requested" }, +{ + "fields": { + "recipients": [ + "session_requests" + ], + "desc": "Recipients for a normal meeting session request" + }, + "model": "mailtoken.mailtoken", + "pk": "session_requested" +}, +{ + "fields": { + "recipients": [ + "group_chairs", + "group_mail_list", + "group_responsible_directors", + "logged_in_person" + ], + "desc": "Copied on a normal meeting session request" + }, + "model": "mailtoken.mailtoken", + "pk": "session_requested_cc" +}, +{ + "fields": { + "recipients": [ + "group_responsible_directors" + ], + "desc": "Recipients for a meeting session request for more than 2 sessions" + }, + "model": "mailtoken.mailtoken", + "pk": "session_requested_long" +}, +{ + "fields": { + "recipients": [ + "group_chairs", + "logged_in_person", + "session_requests" + ], + "desc": "Copied on a meeting session request for more than 2 sessions" + }, + "model": "mailtoken.mailtoken", + "pk": "session_requested_long_cc" +}, +{ + "fields": { + "recipients": [ + "session_requests" + ], + "desc": "Recipients for a message cancelling a session request" + }, + "model": "mailtoken.mailtoken", + "pk": "session_request_cancelled" +}, +{ + "fields": { + "recipients": [ + "group_chairs", + "group_mail_list", + "group_responsible_directors", + "logged_in_person" + ], + "desc": "Copied on a message cancelling a session request" + }, + "model": "mailtoken.mailtoken", + "pk": "session_request_cancelled_cc" +}, +{ + "fields": { + "recipients": [ + "session_requests" + ], + "desc": "Recipients for a message noting a group plans to not meet" + }, + "model": "mailtoken.mailtoken", + "pk": "session_request_not_meeting" +}, +{ + "fields": { + "recipients": [ + "group_chairs", + "group_mail_list", + "group_responsible_directors", + "logged_in_person" + ], + "desc": "Copied on a message noting a group plans to not meet" + }, + "model": "mailtoken.mailtoken", + "pk": "session_request_not_meeting_cc" +}, +{ + "fields": { + "recipients": [ + "group_chairs" + ], + "desc": "Recipients for details when a session has been scheduled" + }, + "model": "mailtoken.mailtoken", + "pk": "session_scheduled" +}, +{ + "fields": { + "recipients": [ + "group_mail_list", + "group_responsible_directors" + ], + "desc": "Recipients for details when a session has been scheduled" + }, + "model": "mailtoken.mailtoken", + "pk": "session_scheduled_cc" +}, { "fields": { "recipients": [ @@ -5297,7 +5443,7 @@ { "fields": { "recipients": [ - "internet_drafts" + "internet_draft_requests" ], "desc": "Recipients for a manual post request for a draft submission" }, diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 7a66eb622..827298278 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -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.mailtoken.utils import gather_address_list # prep for agenda changes @@ -188,9 +188,8 @@ 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) + to_email = gather_address_list('session_scheduled',group=group,person=sessions[0].requested_by) + cc_list = gather_address_list('session_scheduled_cc',group=group,person=sessions[0].requested_by) 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) diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index a02347611..2f3ad1e1a 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -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.mailtoken.utils import gather_address_list # ------------------------------------------------- # 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,8 @@ 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 = gather_address_list('session_requested',group=group,person=login) + cc_list = gather_address_list('session_requested_cc',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 +137,8 @@ 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 = gather_address_list('session_requested_long',group=group,person=login) + cc_list = gather_address_list('session_requested_long_cc',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 +209,8 @@ 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 = gather_address_list('session_request_cancelled',group=group,person=login) + cc_list = gather_address_list('session_request_cancelled_cc',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 +626,8 @@ def no_session(request, acronym): session_save(session) # send notification - to_email = SESSION_REQUEST_EMAIL - cc_list = get_cc_list(group, login) + to_email = gather_address_list('session_request_not_meeting',group=group,person=login) + cc_list = gather_address_list('session_request_not_meeting_cc',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', diff --git a/ietf/settings.py b/ietf/settings.py index b45d96396..1ccf829cf 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -388,7 +388,7 @@ CACHES = { } } -IPR_EMAIL_TO = 'ietf-ipr@ietf.org' +IPR_EMAIL_FROM = 'ietf-ipr@ietf.org' IANA_EVAL_EMAIL = "drafts-eval@icann.org" From 093ff52eac50eae251c3e42d129230d9ff7f273b Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 18 Aug 2015 20:47:35 +0000 Subject: [PATCH 16/46] Small things before second run at reworking IPR addressing - Legacy-Id: 10028 --- ietf/ipr/views.py | 5 ++-- .../migrations/0002_auto_20150809_1314.py | 15 +++++++++- ietf/name/fixtures/names.json | 28 ++++++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index b0589ebc7..a7b11c2ef 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -35,7 +35,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.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_list, gather_addresses # ---------------------------------------------------------------- # Globals @@ -379,7 +379,8 @@ def email(request, id): else: reply_to = get_reply_to() initial = { - 'to': ipr.submitter_email, + 'to': gather_addresses('ipr_disclosure_followup',ipr=ipr), + 'cc': gather_addresses('ipr_disclosure_followup_cc',ipr=ipr), 'frm': settings.IPR_EMAIL_FROM, 'subject': 'Regarding {}'.format(ipr.title), 'reply_to': reply_to, diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index fbf0a564e..76ce9d54b 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -165,6 +165,10 @@ def make_recipients(apps): 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='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 %}') @@ -628,10 +632,19 @@ def make_mailtokens(apps): ]) mt_factory(slug='ipr_disclosure_submitted', - desc="Recipients when an IPR notification is submitted", + desc="Recipients when an IPR disclosure is submitted", recipient_slugs=['ipr_requests', ]) + mt_factory(slug='ipr_disclosure_followup', + desc="Recipients when the secretary follows up on an IPR disclosure submission", + recipient_slugs=['ipr_submitter', + ]) + + mt_factory(slug='ipr_disclosure_followup_cc', + desc="Copied when the secretary follows up on an IPR disclosure submission", + recipient_slugs=[]) + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 8e854a5bb..c43c08eb3 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4567,6 +4567,14 @@ "model": "mailtoken.recipient", "pk": "ipr_requests" }, +{ + "fields": { + "template": "{% if ipr.submitter_email %}{{ ipr.submitter_email }}{% endif %}", + "desc": "The submitter of an IPR disclosure" + }, + "model": "mailtoken.recipient", + "pk": "ipr_submitter" +}, { "fields": { "template": "{% if person and person.email_address %}{{ person.email_address }}{% endif %}", @@ -5071,12 +5079,30 @@ "model": "mailtoken.mailtoken", "pk": "group_personnel_change" }, +{ + "fields": { + "recipients": [ + "ipr_submitter" + ], + "desc": "Recipients when the secretary follows up on an IPR disclosure submission" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_disclosure_followup" +}, +{ + "fields": { + "recipients": [], + "desc": "Copied when the secretary follows up on an IPR disclosure submission" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_disclosure_followup_cc" +}, { "fields": { "recipients": [ "ipr_requests" ], - "desc": "Recipients when an IPR notification is submitted" + "desc": "Recipients when an IPR disclosure is submitted" }, "model": "mailtoken.mailtoken", "pk": "ipr_disclosure_submitted" From 5db9e0d6a80ab76309e72b03764977170c76fcf5 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 19 Aug 2015 19:53:09 +0000 Subject: [PATCH 17/46] checkpoint - Legacy-Id: 10029 --- ietf/ipr/mail.py | 2 + ietf/ipr/models.py | 12 ++ ietf/ipr/tests.py | 2 +- ietf/ipr/views.py | 27 ++- ietf/liaisons/mails.py | 22 +-- .../migrations/0002_auto_20150809_1314.py | 104 +++++++++- ietf/mailtoken/models.py | 45 +++++ ietf/name/fixtures/names.json | 186 ++++++++++++++++++ ietf/secr/utils/mail.py | 34 ---- 9 files changed, 367 insertions(+), 67 deletions(-) delete mode 100644 ietf/secr/utils/mail.py diff --git a/ietf/ipr/mail.py b/ietf/ipr/mail.py index 98aa16b3e..abb1338a1 100644 --- a/ietf/ipr/mail.py +++ b/ietf/ipr/mail.py @@ -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 mailtoken. 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, diff --git a/ietf/ipr/models.py b/ietf/ipr/models.py index 79d91c58a..355bb6dec 100644 --- a/ietf/ipr/models.py +++ b/ietf/ipr/models.py @@ -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) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 907b920d1..fa3f3d94b 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -489,7 +489,7 @@ I would like to revoke this declaration. 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): diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index a7b11c2ef..22f406e97 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -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, @@ -80,13 +79,13 @@ 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 = gather_addresses('ipr_posted_on_doc',doc=doc) + cc_list = gather_addresses('ipr_posted_on_doc_cc',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) @@ -99,16 +98,14 @@ 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) + + context = dict( + to_email=gather_addresses('ipr_posting_confirmation',ipr=ipr), + to_name=ipr.submitter_name, + cc_email=gather_addresses('ipr_posting_confirmation_cc',ipr=ipr), + 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(): diff --git a/ietf/liaisons/mails.py b/ietf/liaisons/mails.py index f2d9b9a89..49923fb6c 100644 --- a/ietf/liaisons/mails.py +++ b/ietf/liaisons/mails.py @@ -7,16 +7,13 @@ 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.mailtoken.utils import gather_address_list 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 = gather_address_list('liaison_statement_posted',liaison=liaison) + cc = gather_address_list('liaison_statement_posted_cc',liaison=liaison) bcc = ['statements@ietf.org'] body = render_to_string('liaisons/liaison_mail.txt', dict( liaison=liaison, @@ -42,13 +39,14 @@ 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 = gather_address_list('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) def send_sdo_reminder(sdo): roles = Role.objects.filter(name="liaiman", group=sdo) @@ -58,7 +56,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 = gather_address_list('liaison_manager_update_request',group=sdo) name = manager_role.person.plain_name() authorized_list = role_persons_with_fixed_email(sdo, "auth") @@ -95,12 +93,8 @@ 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 = gather_address_list('liaison_deadline_soon',liaison=liaison) + cc = gather_address_list('liaison_deadline_soon_cc',liaison=liaison) bcc = 'statements@ietf.org' body = render_to_string('liaisons/liaison_deadline_mail.txt', dict(liaison=liaison, diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 76ce9d54b..8db9424bc 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -161,6 +161,10 @@ def make_recipients(apps): desc="The session request ticketing system", template='session-request@ietf.org') + 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') @@ -169,9 +173,47 @@ def make_recipients(apps): desc="The submitter of an IPR disclosure", template='{% if ipr.submitter_email %}{{ ipr.submitter_email }}{% endif %}') - 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_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) def make_mailtokens(apps): @@ -645,6 +687,62 @@ def make_mailtokens(apps): desc="Copied when the secretary follows up on an IPR disclosure submission", recipient_slugs=[]) + mt_factory(slug='ipr_posting_confirmation', + desc="Recipients for a message confirming that a disclosure has been posted", + recipient_slugs=['ipr_submitter', + ]) + + mt_factory(slug='ipr_posting_confirmation_cc', + desc="Copied on a message confirming that a disclosure has been posted", + recipient_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", + recipient_slugs=['doc_authors', + ]) + + mt_factory(slug='ipr_posted_on_doc_cc', + desc="Copied when an IPR disclosure calls out a given document", + recipient_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", + recipient_slugs=['liaison_to_contact', + ]) + + mt_factory(slug='liaison_statement_posted_cc', + desc="Copied on a message when a new liaison statement is posted", + recipient_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", + recipient_slugs=['liaison_statements_list', + ]) + + mt_factory(slug='liaison_deadline_soon', + desc="Recipients for a message about a liaison statement deadline that is approaching.", + recipient_slugs=['liaison_to_contact', + ]) + + mt_factory(slug='liaison_deadline_soon_cc', + desc="Copied on a message about a liaison statement deadline that is approaching.", + recipient_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", + recipient_slugs=['liaison_manager', + ]) + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index 1579f6de0..e89c8ad7c 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -3,6 +3,8 @@ from django.db import models from django.template import Template, Context +from ietf.group.models import Role + class MailToken(models.Model): slug = models.CharField(max_length=32, primary_key=True) desc = models.TextField(blank=True) @@ -206,3 +208,46 @@ class Recipient(models.Model): 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 diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index c43c08eb3..e65455762 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4423,6 +4423,14 @@ "model": "mailtoken.recipient", "pk": "doc_group_responsible_directors" }, +{ + "fields": { + "template": null, + "desc": "Leadership for a document that has a new IPR disclosure" + }, + "model": "mailtoken.recipient", + "pk": "doc_ipr_group_or_ad" +}, { "fields": { "template": null, @@ -4559,6 +4567,14 @@ "model": "mailtoken.recipient", "pk": "internet_draft_requests" }, +{ + "fields": { + "template": "ipr-announce@ietf.org", + "desc": "The IETF IPR announce list" + }, + "model": "mailtoken.recipient", + "pk": "ipr_announce" +}, { "fields": { "template": "ietf-ipr@ietf.org", @@ -4575,6 +4591,70 @@ "model": "mailtoken.recipient", "pk": "ipr_submitter" }, +{ + "fields": { + "template": null, + "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." + }, + "model": "mailtoken.recipient", + "pk": "ipr_updatedipr_contacts" +}, +{ + "fields": { + "template": null, + "desc": "The holders of all IPR disclosures updated by disclosure and disclosures updated by those and so on." + }, + "model": "mailtoken.recipient", + "pk": "ipr_updatedipr_holders" +}, +{ + "fields": { + "template": "{{liaison.cc}}", + "desc": "The addresses captured in the Cc field of the liaison statement form" + }, + "model": "mailtoken.recipient", + "pk": "liaison_cc" +}, +{ + "fields": { + "template": null, + "desc": "The assigned liaison manager for an external group " + }, + "model": "mailtoken.recipient", + "pk": "liaison_manager" +}, +{ + "fields": { + "template": "{{liaison.response_contact}}", + "desc": "The addresses captured in the response contact field of the liaison statement form" + }, + "model": "mailtoken.recipient", + "pk": "liaison_response_contact" +}, +{ + "fields": { + "template": "statements@ietf.org", + "desc": "The IETF liaison statement ticketing system" + }, + "model": "mailtoken.recipient", + "pk": "liaison_statements_list" +}, +{ + "fields": { + "template": "{{liaison.technical_contact}}", + "desc": "The addresses captured in the technical contact field of the liaison statement form" + }, + "model": "mailtoken.recipient", + "pk": "liaison_technical_contact" +}, +{ + "fields": { + "template": "{{liaison.to_contact}}", + "desc": "The addresses captured in the To field of the liaison statement form" + }, + "model": "mailtoken.recipient", + "pk": "liaison_to_contact" +}, { "fields": { "template": "{% if person and person.email_address %}{{ person.email_address }}{% endif %}", @@ -5107,6 +5187,48 @@ "model": "mailtoken.mailtoken", "pk": "ipr_disclosure_submitted" }, +{ + "fields": { + "recipients": [ + "doc_authors" + ], + "desc": "Recipients when an IPR disclosure calls out a given document" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_posted_on_doc" +}, +{ + "fields": { + "recipients": [ + "doc_ipr_group_or_ad", + "ipr_announce" + ], + "desc": "Copied when an IPR disclosure calls out a given document" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_posted_on_doc_cc" +}, +{ + "fields": { + "recipients": [ + "ipr_submitter" + ], + "desc": "Recipients for a message confirming that a disclosure has been posted" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_posting_confirmation" +}, +{ + "fields": { + "recipients": [ + "ipr_updatedipr_contacts", + "ipr_updatedipr_holders" + ], + "desc": "Copied on a message confirming that a disclosure has been posted" + }, + "model": "mailtoken.mailtoken", + "pk": "ipr_posting_confirmation_cc" +}, { "fields": { "recipients": [ @@ -5189,6 +5311,70 @@ "model": "mailtoken.mailtoken", "pk": "last_call_requested_cc" }, +{ + "fields": { + "recipients": [ + "liaison_statements_list" + ], + "desc": "Recipients for a message that a pending liaison statement needs approval" + }, + "model": "mailtoken.mailtoken", + "pk": "liaison_approval_requested" +}, +{ + "fields": { + "recipients": [ + "liaison_to_contact" + ], + "desc": "Recipients for a message about a liaison statement deadline that is approaching." + }, + "model": "mailtoken.mailtoken", + "pk": "liaison_deadline_soon" +}, +{ + "fields": { + "recipients": [ + "liaison_cc", + "liaison_response_contact", + "liaison_technical_contact" + ], + "desc": "Copied on a message about a liaison statement deadline that is approaching." + }, + "model": "mailtoken.mailtoken", + "pk": "liaison_deadline_soon_cc" +}, +{ + "fields": { + "recipients": [ + "liaison_manager" + ], + "desc": "Recipients for a message requesting an updated list of authorized individuals" + }, + "model": "mailtoken.mailtoken", + "pk": "liaison_manager_update_request" +}, +{ + "fields": { + "recipients": [ + "liaison_to_contact" + ], + "desc": "Recipient for a message when a new liaison statement is posted" + }, + "model": "mailtoken.mailtoken", + "pk": "liaison_statement_posted" +}, +{ + "fields": { + "recipients": [ + "liaison_cc", + "liaison_response_contact", + "liaison_technical_contact" + ], + "desc": "Copied on a message when a new liaison statement is posted" + }, + "model": "mailtoken.mailtoken", + "pk": "liaison_statement_posted_cc" +}, { "fields": { "recipients": [ diff --git a/ietf/secr/utils/mail.py b/ietf/secr/utils/mail.py deleted file mode 100644 index 01133dd27..000000000 --- a/ietf/secr/utils/mail.py +++ /dev/null @@ -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@tools.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') ] - From 65be4190a7faa61aba5bddc40f11f7ff99f66d79 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 19 Aug 2015 21:53:02 +0000 Subject: [PATCH 18/46] Done with the first pass instrumenting all mail points. There is one spot in the ipr code that's complicated and will require some refactor before instrumenting. There are a few places to add explicit messages (templates and all) to replace the simple use of email_ad. The next pass will be to make sure every spot that sends mail is covered by a test, and that the tests look at the headers sensibly. That will be followed by a moderately heavy refactor of MailToken and the gather_address utilities. Then pages showing the expansions of a token for a given document/group. Long way to go, but this is working well, and I think it will make things much easier later. - Legacy-Id: 10030 --- .../migrations/0002_auto_20150809_1314.py | 80 ++++++++-- ietf/name/fixtures/names.json | 139 ++++++++++++++++-- ietf/nomcom/forms.py | 5 +- ietf/nomcom/utils.py | 17 +-- ietf/settings.py | 1 - 5 files changed, 211 insertions(+), 31 deletions(-) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 8db9424bc..435605e7a 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -25,9 +25,13 @@ def make_recipients(apps): desc='The Secretariat', template='') + rc(slug='ietf_secretariat', + desc='The Secretariat', + template='') + rc(slug='doc_authors', desc="The document's authors", - template='{% if doc.type_id == "draft" %}{{doc.name}}@ietf.org{% endif %}') + template='{% if doc.type_id == "draft" %}<{{doc.name}}@ietf.org>{% endif %}') rc(slug='doc_notify', desc="The addresses in the document's notify field", @@ -55,11 +59,11 @@ def make_recipients(apps): rc(slug='doc_shepherd', desc="The document's shepherd", - template='{% if doc.shepherd %}{{doc.shepherd.address}}{% endif %}' ) + 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 %}' ) + template='{% if doc.ad %}<{{doc.ad.email_address}}>{% endif %}' ) rc(slug='doc_group_mail_list', desc="The list address of the document's group", @@ -99,7 +103,7 @@ def make_recipients(apps): rc(slug='group_mail_list', desc="The group's mailing list", - template='{{ group.list_email }}') + template='<{{ group.list_email }}>') rc(slug='group_steering_group', desc="The group's steering group (IESG or IRSG)", @@ -119,7 +123,7 @@ def make_recipients(apps): rc(slug='internet_draft_requests', desc="The internet drafts ticketing system", - template='internet-drafts@ietf.org') + template='') rc(slug='submission_submitter', desc="The person that submitted a draft", @@ -159,15 +163,15 @@ def make_recipients(apps): rc(slug='session_requests', desc="The session request ticketing system", - template='session-request@ietf.org') + template='') 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 %}') + 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') + template='') rc(slug='ipr_submitter', desc="The submitter of an IPR disclosure", @@ -209,12 +213,28 @@ def make_recipients(apps): rc(slug='liaison_statements_list', desc="The IETF liaison statement ticketing system", - template='statements@ietf.org') + template='') 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}}') + def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') @@ -743,6 +763,48 @@ def make_mailtokens(apps): recipient_slugs=['liaison_manager', ]) + mt_factory(slug='nomination_received', + desc="Recipients for a message noting a new nomination has been received", + recipient_slugs=['nomcom_chair', + ]) + + mt_factory(slug='nomination_receipt_requested', + desc="Recipients for a message confirming a nomination was made", + recipient_slugs=['nominator', + ]) + + mt_factory(slug='nomcom_comment_receipt_requested', + desc="Recipients for a message confirming a comment was made", + recipient_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", + recipient_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", + recipient_slugs=['nominee', + ]) + + mt_factory(slug='nomination_accept_reminder', + desc="Recipeints of message reminding a nominee to accept or decline a nomination", + recipient_slugs=['nominee', + ]) + + mt_factory(slug='nomcom_questionnaire', + desc="Recipients for the questionairre that nominees should complete", + recipient_slugs=['nominee', + ]) + + mt_factory(slug='nomcom_questionnaire_reminder', + desc="Recipients for a message reminding a nominee to return a completed questionairre response", + recipient_slugs=['nominee', + ]) + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index e65455762..a89be6245 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4327,6 +4327,14 @@ "model": "doc.ballottype", "pk": 3 }, +{ + "fields": { + "template": "{{commenter}}", + "desc": "The person providing a comment to nomcom" + }, + "model": "mailtoken.recipient", + "pk": "commenter" +}, { "fields": { "template": null, @@ -4345,7 +4353,7 @@ }, { "fields": { - "template": "{% if doc.ad %}{{doc.ad.email_address}}{% endif %}", + "template": "{% if doc.ad %}<{{doc.ad.email_address}}>{% endif %}", "desc": "The document's responsible Area Director" }, "model": "mailtoken.recipient", @@ -4377,7 +4385,7 @@ }, { "fields": { - "template": "{% if doc.type_id == \"draft\" %}{{doc.name}}@ietf.org{% endif %}", + "template": "{% if doc.type_id == \"draft\" %}<{{doc.name}}@ietf.org>{% endif %}", "desc": "The document's authors" }, "model": "mailtoken.recipient", @@ -4449,7 +4457,7 @@ }, { "fields": { - "template": "{% if doc.shepherd %}{{doc.shepherd.address}}{% endif %}", + "template": "{% if doc.shepherd %}<{{doc.shepherd.address}}>{% endif %}", "desc": "The document's shepherd" }, "model": "mailtoken.recipient", @@ -4481,7 +4489,7 @@ }, { "fields": { - "template": "{{ group.list_email }}", + "template": "<{{ group.list_email }}>", "desc": "The group's mailing list" }, "model": "mailtoken.recipient", @@ -4561,7 +4569,15 @@ }, { "fields": { - "template": "internet-drafts@ietf.org", + "template": "", + "desc": "The Secretariat" + }, + "model": "mailtoken.recipient", + "pk": "ietf_secretariat" +}, +{ + "fields": { + "template": "", "desc": "The internet drafts ticketing system" }, "model": "mailtoken.recipient", @@ -4577,7 +4593,7 @@ }, { "fields": { - "template": "ietf-ipr@ietf.org", + "template": "", "desc": "The ipr disclosure handling system" }, "model": "mailtoken.recipient", @@ -4633,7 +4649,7 @@ }, { "fields": { - "template": "statements@ietf.org", + "template": "", "desc": "The IETF liaison statement ticketing system" }, "model": "mailtoken.recipient", @@ -4657,12 +4673,36 @@ }, { "fields": { - "template": "{% if person and person.email_address %}{{ person.email_address }}{% endif %}", + "template": "{% if person and person.email_address %}<{{ person.email_address }}>{% endif %}", "desc": "The person currently logged into the datatracker who initiated a given action" }, "model": "mailtoken.recipient", "pk": "logged_in_person" }, +{ + "fields": { + "template": "{{nomcom.group.get_chair.email.address}}", + "desc": "The chair of a given nomcom" + }, + "model": "mailtoken.recipient", + "pk": "nomcom_chair" +}, +{ + "fields": { + "template": "{{nominator}}", + "desc": "The person that submitted a nomination to nomcom" + }, + "model": "mailtoken.recipient", + "pk": "nominator" +}, +{ + "fields": { + "template": "{{nominee}}", + "desc": "The person nominated for a position" + }, + "model": "mailtoken.recipient", + "pk": "nominee" +}, { "fields": { "template": "", @@ -4681,7 +4721,7 @@ }, { "fields": { - "template": "session-request@ietf.org", + "template": "", "desc": "The session request ticketing system" }, "model": "mailtoken.recipient", @@ -5415,6 +5455,87 @@ "model": "mailtoken.mailtoken", "pk": "milestone_review_reminder_cc" }, +{ + "fields": { + "recipients": [ + "commenter" + ], + "desc": "Recipients for a message confirming a comment was made" + }, + "model": "mailtoken.mailtoken", + "pk": "nomcom_comment_receipt_requested" +}, +{ + "fields": { + "recipients": [ + "nominee" + ], + "desc": "Recipients for the questionairre that nominees should complete" + }, + "model": "mailtoken.mailtoken", + "pk": "nomcom_questionnaire" +}, +{ + "fields": { + "recipients": [ + "nominee" + ], + "desc": "Recipients for a message reminding a nominee to return a completed questionairre response" + }, + "model": "mailtoken.mailtoken", + "pk": "nomcom_questionnaire_reminder" +}, +{ + "fields": { + "recipients": [ + "nominee" + ], + "desc": "Recipeints of message reminding a nominee to accept or decline a nomination" + }, + "model": "mailtoken.mailtoken", + "pk": "nomination_accept_reminder" +}, +{ + "fields": { + "recipients": [ + "ietf_secretariat", + "nomcom_chair" + ], + "desc": "Recipients for a message noting that a nomination caused a new Person record to be created in the datatracker" + }, + "model": "mailtoken.mailtoken", + "pk": "nomination_created_person" +}, +{ + "fields": { + "recipients": [ + "nominee" + ], + "desc": "Recipients the first time a person is nominated for a position, asking them to accept or decline the nomination" + }, + "model": "mailtoken.mailtoken", + "pk": "nomination_new_nominee" +}, +{ + "fields": { + "recipients": [ + "nominator" + ], + "desc": "Recipients for a message confirming a nomination was made" + }, + "model": "mailtoken.mailtoken", + "pk": "nomination_receipt_requested" +}, +{ + "fields": { + "recipients": [ + "nomcom_chair" + ], + "desc": "Recipients for a message noting a new nomination has been received" + }, + "model": "mailtoken.mailtoken", + "pk": "nomination_received" +}, { "fields": { "recipients": [ diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 6a34e8938..7be1e6322 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -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.mailtoken.utils import gather_address_list ROLODEX_URL = getattr(settings, 'ROLODEX_URL', None) @@ -407,7 +408,7 @@ class NominateForm(BaseNomcomForm, forms.ModelForm): if author: subject = 'Nomination receipt' from_email = settings.NOMCOM_FROM_EMAIL - to_email = author.address + to_email = gather_address_list('nomination_receipt_requested',nominator=author.address) context = {'nominee': nominee.email.person.name, 'comments': comments, 'position': position.name} @@ -525,7 +526,7 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm): if author: subject = "NomCom comment confirmation" from_email = settings.NOMCOM_FROM_EMAIL - to_email = author.address + to_email = gather_address_list('nomcom_comment_receipt_requested',commenter=author.address) context = {'nominee': self.nominee.email.person.name, 'comments': comments, 'position': self.position.name} diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 0c2a697c9..b5bf08d0c 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -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.mailtoken.utils import gather_address_list 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 = gather_address_list('nomination_accept_reminder',nominee=nominee.email.address) hash = get_hash_nominee_position(today, nominee_position.id) accept_url = reverse('nomcom_process_nomination_status', @@ -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 = gather_address_list('nomcom_questionnaire_reminder',nominee=nominee.email.address) context = {'nominee': nominee, 'position': position, @@ -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 = gather_address_list('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) if nominee_position_created: # send email to nominee subject = 'IETF Nomination Information' from_email = settings.NOMCOM_FROM_EMAIL - to_email = email.address + to_email = gather_address_list('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) @@ -341,7 +338,7 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut if nomcom.send_questionnaire: subject = '%s Questionnaire' % position from_email = settings.NOMCOM_FROM_EMAIL - to_email = email.address + to_email = gather_address_list('nomcom_questionnaire',nominee=email.address) context = {'nominee': email.person.name, 'position': position.name} path = '%s%d/%s' % (nomcom_template_path, @@ -355,7 +352,7 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut # send emails to nomcom chair subject = 'Nomination Information' from_email = settings.NOMCOM_FROM_EMAIL - to_email = nomcom_chair_mail + to_email = gather_address_list('nomination_received',nomcom=nomcom) context = {'nominee': email.person.name, 'nominee_email': email.address, 'position': position.name} diff --git a/ietf/settings.py b/ietf/settings.py index 1ccf829cf..a4205a226 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -413,7 +413,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' From c01934bd10c84d071a0049e0faa41c10ebc2a47b Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 20 Aug 2015 02:31:21 +0000 Subject: [PATCH 19/46] Remove dead code - Legacy-Id: 10032 --- ietf/doc/mails.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 2d3356871..29ee01058 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -63,14 +63,6 @@ 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 html_to_text(html): return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("
", "\n")) From e545f5e131108dad672e812cf64394a5858f8ee4 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 20 Aug 2015 02:32:01 +0000 Subject: [PATCH 20/46] rename test member functions so that the tests actually run them. Consider cherrypicking this commit. - Legacy-Id: 10033 --- ietf/doc/tests_draft.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index b68dfe53f..a38b02550 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -963,7 +963,7 @@ class IndividualInfoFormsTests(TestCase): class SubmitToIesgTests(TestCase): - def verify_permissions(self): + def test_verify_permissions(self): def verify_fail(username): if username: @@ -986,7 +986,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") @@ -996,7 +996,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") From 83753dd24ef45d800133d82fd042cefb7b2fd1ac Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 20 Aug 2015 04:23:40 +0000 Subject: [PATCH 21/46] Tests nomcom confirmation receipts. Fixes bug in submission blackout test where test was using local time, but view was using utc - Legacy-Id: 10034 --- ietf/nomcom/tests.py | 57 ++++++++++++++++++++++++++++++++++++++------ ietf/submit/tests.py | 2 +- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index f54401280..707f0c4de 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -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,8 +466,35 @@ 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) @@ -479,6 +506,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) if public: nominate_url = self.public_nominate_url @@ -511,7 +539,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 +628,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.assertTrue(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 +653,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 +687,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 diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index d06c40ff0..da71e4f59 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -710,7 +710,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 From 8a74604a1bc13cb4532a41690c5706d81ff230b9 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 21 Aug 2015 16:39:00 +0000 Subject: [PATCH 22/46] Done with the second pass focusing on test coverage. Lots of test tweaks, some new tests, a couple of bugs fixed. The session request code views need to be redone to allow testing. - Legacy-Id: 10035 --- ietf/doc/mails.py | 2 + ietf/doc/tests_ballot.py | 38 +++++-- ietf/doc/tests_charter.py | 26 +++-- ietf/doc/tests_conflict_review.py | 23 ++-- ietf/doc/tests_draft.py | 105 ++++++++++++++---- ietf/doc/tests_status_change.py | 4 +- ietf/doc/views_conflict_review.py | 4 +- ietf/group/mails.py | 4 +- ietf/group/tests_info.py | 13 ++- ietf/ipr/tests.py | 26 ++++- ietf/liaisons/tests.py | 4 + .../migrations/0002_auto_20150809_1314.py | 4 +- ietf/mailtoken/tests.py | 9 -- ietf/name/fixtures/names.json | 6 +- ietf/nomcom/tests.py | 18 ++- ietf/secr/sreq/tests.py | 7 ++ ietf/submit/tests.py | 4 + ietf/sync/tests.py | 9 +- .../doc/conflict_review/approval_text.txt | 4 +- ietf/utils/test_data.py | 12 -- 20 files changed, 239 insertions(+), 83 deletions(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 29ee01058..97393ef9f 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -201,6 +201,8 @@ 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" diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 5f9737044..999dd1f5a 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -152,7 +152,7 @@ class EditPositionTests(TestCase): # 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(cc="test298347@example.com", cc_state_change="1",cc_group_list="1")) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 1) @@ -162,16 +162,18 @@ 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']) self.assertTrue("somebody@example.com" in m['Cc']) - self.assertTrue("test@example.com" in m['Cc']) + self.assertTrue("test298347@example.com" in m['Cc']) self.assertTrue(draft.group.list_email) self.assertTrue(draft.group.list_email in m['Cc']) r = self.client.post(url, dict(cc="")) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 2) - #TODO this would be a good place to test actual mailtoken expansions - #if we can find a way to get the real, or at least representative, data in place. + m = outbox[-1] + self.assertTrue("iesg@" in m['To']) + self.assertFalse(draft.group.list_email in m['Cc']) class BallotWriteupsTests(TestCase): @@ -235,6 +237,8 @@ class BallotWriteupsTests(TestCase): 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() @@ -389,12 +393,18 @@ class ApproveBallotTests(TestCase): self.assertEqual(draft.get_state_slug("draft-iesg"), "ann") 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")) @@ -412,7 +422,6 @@ class ApproveBallotTests(TestCase): 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,10 +450,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 + 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): @@ -490,11 +506,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 + 2) - #self.assertTrue("State Update" in outbox[-3]['Subject']) - self.assertTrue("Telechat update" in outbox[-2]['Subject']) + + 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) @@ -547,8 +568,11 @@ class DeferUndeferTestCase(TestCase): self.assertFalse(doc.active_defer_event()) 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 2876b7dcd..0e72680ab 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -14,7 +14,7 @@ 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 @@ -97,7 +97,12 @@ class EditCharterTests(TestCase): self.assertTrue(find_event("created_ballot")) self.assertEqual(len(outbox), mailbox_before + 2) + 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() @@ -285,6 +290,9 @@ class EditCharterTests(TestCase): announcement_text=default_review_text(draft.group, charter, by).text, send_text="1")) self.assertEqual(len(outbox), mailbox_before + 1) + self.assertTrue('WG Review' in outbox[-1]['Subject']) + self.assertTrue('ietf-announce@' in outbox[-1]['To']) + self.assertTrue('mars-wg@' in outbox[-1]['Cc']) # save r = self.client.post(url, dict( @@ -333,11 +341,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() @@ -393,7 +402,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) @@ -405,9 +414,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 52c57c36d..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 @@ -115,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): @@ -278,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) @@ -286,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 a38b02550..3672b80ee 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 @@ -74,7 +74,9 @@ class ChangeStateTests(TestCase): self.assertTrue("IESG state changed" in draft.docevent_set.all()[1].desc) 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) @@ -100,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) + 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() @@ -144,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) @@ -170,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) @@ -251,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) @@ -430,6 +448,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() @@ -462,6 +481,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): @@ -523,8 +545,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("mars-chairs@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 @@ -555,6 +578,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) @@ -679,7 +705,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('iesg@' in outbox[-1]['To']) + self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To']) class IndividualInfoFormsTests(TestCase): def test_doc_change_stream(self): @@ -693,22 +721,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)) @@ -772,12 +803,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="")) @@ -1013,6 +1049,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() @@ -1051,12 +1089,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): @@ -1160,7 +1202,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, @@ -1178,10 +1220,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("mars-chairs@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): @@ -1252,6 +1294,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') @@ -1260,7 +1303,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') @@ -1268,18 +1316,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 90e3587e6..f9a564549 100644 --- a/ietf/doc/tests_status_change.py +++ b/ietf/doc/tests_status_change.py @@ -322,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/views_conflict_review.py b/ietf/doc/views_conflict_review.py index 0421c0b9f..dbe5e533a 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -260,7 +260,9 @@ def default_approval_text(review): conflictdoc = conflictdoc, conflictdoc_url = settings.IDTRACKER_BASE_URL+conflictdoc.get_absolute_url(), receiver=receiver, - approved_review = current_text + approved_review = current_text, + to = gather_addresses('ballot_approved_conflrev',doc=review), + cc = gather_addresses('ballot_approved_conflrev_cc',doc=review), ) ) diff --git a/ietf/group/mails.py b/ietf/group/mails.py index 28650e38d..312e6f2b8 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -92,8 +92,8 @@ def email_milestone_review_reminder(group, grace_period=7): 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, - ) + ), + cc=cc, ) return True diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index c75286ef1..c259c6ba8 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -17,7 +17,7 @@ from ietf.group.utils import save_group_in_history from ietf.name.models import DocTagName, GroupStateName 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, @@ -444,6 +444,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", @@ -474,6 +475,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_conclude(self): make_test_data() @@ -500,6 +505,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") @@ -602,6 +608,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() diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index fa3f3d94b..f448ccd5d 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -14,7 +14,7 @@ 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.mailtoken.utils import gather_addresses @@ -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,7 +500,10 @@ 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) @@ -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,6 +533,8 @@ 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 message_string = """To: {} diff --git a/ietf/liaisons/tests.py b/ietf/liaisons/tests.py index 8e06aad63..f0f010ef3 100644 --- a/ietf/liaisons/tests.py +++ b/ietf/liaisons/tests.py @@ -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() diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 435605e7a..51b13175a 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -476,7 +476,7 @@ def make_mailtokens(apps): mt_factory(slug='doc_telechat_details_changed', desc="Recipients when a document's telechat date or other telechat specific details are changed", recipient_slugs=['iesg', - 'iesg-secretary', + 'iesg_secretary', 'doc_notify', 'doc_authors', 'doc_shepherd', @@ -494,7 +494,7 @@ def make_mailtokens(apps): mt_factory(slug='doc_pulled_from_rfc_queue_cc', desc="Recipients when a document is taken out of the RFC's editor queue before publication", - recipient_slugs=['iesg-secretary', + recipient_slugs=['iesg_secretary', 'doc_ad', 'doc_notify', 'doc_authors', diff --git a/ietf/mailtoken/tests.py b/ietf/mailtoken/tests.py index 82f741317..2a9eeb944 100644 --- a/ietf/mailtoken/tests.py +++ b/ietf/mailtoken/tests.py @@ -2,7 +2,6 @@ from django.core.urlresolvers import reverse as urlreverse from ietf.utils.test_utils import TestCase from ietf.utils.test_data import make_test_data -from ietf.mailtoken.models import Recipient class EventMailTests(TestCase): @@ -33,11 +32,3 @@ class EventMailTests(TestCase): self.assertEqual(r.status_code, 200) self.assertTrue('doc_group_mail_list' in r.content) -class RecipientTests(TestCase): - - def test_recipient_functions(self): - draft = make_test_data() - recipient = Recipient.objects.first() - for funcname in [name for name in dir(recipient) if name.startswith('gather_')]: - func=getattr(recipient,funcname) - func(**{'doc':draft,'group':draft.group}) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index a89be6245..49bf95d49 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -5076,7 +5076,8 @@ "doc_authors", "doc_group_chairs", "doc_notify", - "doc_shepherd" + "doc_shepherd", + "iesg_secretary" ], "desc": "Recipients when a document is taken out of the RFC's editor queue before publication" }, @@ -5148,7 +5149,8 @@ "doc_group_chairs", "doc_notify", "doc_shepherd", - "iesg" + "iesg", + "iesg_secretary" ], "desc": "Recipients when a document's telechat date or other telechat specific details are changed" }, diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 707f0c4de..69772bf70 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -501,6 +501,19 @@ class NomcomViewsTest(TestCase): 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') @@ -640,7 +653,7 @@ class NomcomViewsTest(TestCase): empty_outbox() self.feedback_view(public=True) - self.assertTrue(len(outbox),1) + self.assertEqual(len(outbox),1) self.assertFalse('confirmation' in outbox[0]['Subject']) def test_private_feedback(self): @@ -897,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'}) @@ -906,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']) diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index b679a12b5..c873f16c5 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -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 diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index da71e4f59..1e12a62dc 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -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() diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index 50dcb8118..c90e5b0a0 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -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,8 +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 Waiting on RFC Editor from In Progress") # self.assertEqual(e.time, datetime.datetime(2011, 10, 9, 5, 0)) # check timezone handling - self.assertEqual(len(outbox), mailbox_before + 3 ) - # TODO look sensibly at the message here + 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) diff --git a/ietf/templates/doc/conflict_review/approval_text.txt b/ietf/templates/doc/conflict_review/approval_text.txt index 3a2b21191..d793675b1 100644 --- a/ietf/templates/doc/conflict_review/approval_text.txt +++ b/ietf/templates/doc/conflict_review/approval_text.txt @@ -1,6 +1,6 @@ {% load mail_filters %}{% autoescape off %}From: The IESG -To: {{ review.notify }} -Cc: The IESG , , +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. diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 1f2fd9af1..4a5790765 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -330,16 +330,4 @@ def make_test_data(): other_doc_factory('minutes','minutes-42-mars') other_doc_factory('slides','slides-42-mars-1') -# Trying the fixture route instead - # EventMail tokens used by the views - # This won't allow testing the results of the production configuration - if we want to do that, we'll need to - # extract the production data either directly, or as a fixture -# recipient = Recipient.objects.create(slug='bogus_recipient',desc='Bogus Recipient',template='bogus@example.com') -# for slug in ['ballot_approved_charter', 'ballot_approved_charter_cc', 'ballot_approved_conflrev', 'ballot_approved_conflrev_cc', 'ballot_approved_ietf_stream', 'ballot_approved_ietf_stream_cc', 'ballot_approved_ietf_stream_iana', 'ballot_approved_status_change', 'ballot_approved_status_change_cc', 'ballot_deferred', 'ballot_saved', 'ballot_saved_cc', 'charter_external_review', 'charter_external_review_cc', 'conflrev_requested', 'conflrev_requested_cc', 'conflrev_requested_iana', 'doc_stream_changed', 'last_call_expired', 'last_call_expired_cc', 'last_call_issued', 'last_call_issued_cc', 'last_call_issued_iana', 'last_call_requested', 'last_call_requested_cc', 'pubreq_iesg', 'pubreq_iesg_cc', 'pubreq_rfced', 'pubreq_rfced_iana']: -# m = MailToken.objects.create(slug=slug,desc=slug) -# m.recipients=[recipient] -# # Well, this isn't working out so well - Recipients that have code backing up their gather sometimes refer to other Recipients... -# for slug in ['doc_authors','doc_group_chairs','doc_notify','doc_stream_owner','stream_managers']: -# Recipient.objects.create(slug=slug,desc="Bogus Recipient",template='bogus@example.com') - return draft From d16efcc6cf953c3e8f15fe5ea9fa0e35ce0471a7 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 21 Aug 2015 21:07:18 +0000 Subject: [PATCH 23/46] Refactor of MailToken to keep the lists of To and CC tokens with one object - Legacy-Id: 10036 --- ietf/mailtoken/admin.py | 2 +- ietf/mailtoken/migrations/0001_initial.py | 10 +- .../migrations/0002_auto_20150809_1314.py | 729 ++++++++---------- ietf/mailtoken/models.py | 3 +- ietf/mailtoken/resources.py | 6 +- ietf/mailtoken/tests.py | 6 +- ietf/mailtoken/utils.py | 19 +- ietf/mailtoken/views.py | 6 +- ietf/name/fixtures/names.json | 588 ++++++-------- ietf/templates/mailtoken/token.html | 11 +- 10 files changed, 591 insertions(+), 789 deletions(-) diff --git a/ietf/mailtoken/admin.py b/ietf/mailtoken/admin.py index 1330dab64..c58f4c551 100644 --- a/ietf/mailtoken/admin.py +++ b/ietf/mailtoken/admin.py @@ -12,6 +12,6 @@ admin.site.register(Recipient, RecipientAdmin) class MailTokenAdmin(admin.ModelAdmin): list_display = [ 'slug', 'desc', ] - filter_horizontal = [ 'recipients' ] + filter_horizontal = [ 'to', 'cc', ] admin.site.register(MailToken, MailTokenAdmin) diff --git a/ietf/mailtoken/migrations/0001_initial.py b/ietf/mailtoken/migrations/0001_initial.py index 3c22d2b85..bcfa776f2 100644 --- a/ietf/mailtoken/migrations/0001_initial.py +++ b/ietf/mailtoken/migrations/0001_initial.py @@ -35,8 +35,14 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='mailtoken', - name='recipients', - field=models.ManyToManyField(to='mailtoken.Recipient', null=True, blank=True), + name='cc', + field=models.ManyToManyField(related_name='used_in_cc', null=True, to='mailtoken.Recipient', blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='mailtoken', + name='to', + field=models.ManyToManyField(related_name='used_in_to', null=True, to='mailtoken.Recipient', blank=True), preserve_default=True, ), ] diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 51b13175a..984c8b1a9 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -240,570 +240,509 @@ def make_mailtokens(apps): Recipient=apps.get_model('mailtoken','Recipient') MailToken=apps.get_model('mailtoken','MailToken') - def mt_factory(slug,desc,recipient_slugs): + def mt_factory(slug,desc,to_slugs,cc_slugs=[]): m = MailToken.objects.create(slug=slug, desc=desc) - m.recipients = Recipient.objects.filter(slug__in=recipient_slugs) + 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', - recipient_slugs=['iesg']) - - mt_factory(slug='ballot_saved_cc', - desc='Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved', - recipient_slugs=['doc_authors', - 'doc_group_chairs', - 'doc_shepherd', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - 'conflict_review_stream_manager', - ]) + desc="Recipients when a new ballot position " + "(with discusses, other blocking positions, " + "or comments) is saved", + to_slugs=['iesg'], + cc_slugs=['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', - recipient_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', - ]) + 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', - recipient_slugs=['ietf_announce']) - - mt_factory(slug='ballot_approved_ietf_stream_cc', - desc='Copied when an IETF stream document ballot is approved', - recipient_slugs=['iesg', - 'doc_notify', - 'doc_ad', - 'doc_authors', - 'doc_shepherd', - 'doc_group_mail_list', - 'doc_group_chairs', - 'rfc_editor', - ]) + 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', - recipient_slugs=['iana_approve']) + 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', - recipient_slugs=['conflict_review_stream_manager', - 'conflict_review_steering_group', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - 'doc_notify', - ]) - - mt_factory(slug='ballot_approved_conflrev_cc', - desc='Copied when a conflict review ballot is approved', - recipient_slugs=['iesg', - 'ietf_announce', - 'iana', - ]) + 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', - recipient_slugs=['ietf_announce',]) - - mt_factory(slug='ballot_approved_charter_cc', - desc='Copied when a charter is approved', - recipient_slugs=['group_mail_list', - 'group_steering_group', - 'group_chairs', - 'doc_notify', - ]) + 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', - recipient_slugs=['ietf_announce',]) - - mt_factory(slug='ballot_approved_status_change_cc', - desc='Copied when a status change is approved', - recipient_slugs=['iesg', - 'rfc_editor', - 'doc_notify', - 'doc_affectddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - ]) + desc="Recipients when a status change is approved", + to_slugs=['ietf_announce',], + cc_slugs=['iesg', + 'rfc_editor', + 'doc_notify', + 'doc_affectddoc_authors', + 'doc_affecteddoc_group_chairs', + 'doc_affecteddoc_notify', + ], + ) mt_factory(slug='last_call_requested', - desc='Recipients when AD requests a last call', - recipient_slugs=['iesg_secretary',]) - - mt_factory(slug='last_call_requested_cc', - desc='Copied when AD requests a last call', - recipient_slugs=['doc_ad', - 'doc_shepherd', - 'doc_notify']) + 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', - recipient_slugs=['ietf_announce',]) - - mt_factory(slug='last_call_issued_cc', - desc='Copied when a last call is issued', - recipient_slugs=['doc_ad', - 'doc_shepherd', - 'doc_authors', - 'doc_notify', - 'doc_group_list_email', - 'doc_group_chairs', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify']) + desc="Recipients when a last call is issued", + to_slugs=['ietf_announce',], + cc_slugs=['doc_ad', + 'doc_shepherd', + 'doc_authors', + 'doc_notify', + 'doc_group_list_email', + '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', - recipient_slugs=['iana_last_call']) + 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', - recipient_slugs=['iesg', - 'doc_notify', - 'doc_authors', - 'doc_shepherd', - ]) - - mt_factory(slug='last_call_expired_cc', - desc='Copied when a last call has expired', - recipient_slugs=['iesg_secretary',]) + desc="Recipients when a last call has expired", + to_slugs=['iesg', + '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', - recipient_slugs=['doc_ad',]) - - mt_factory(slug='pubreq_iesg_cc', - desc='Copied when a draft is submitted to the IESG', - recipient_slugs=['iesg_secretary', - 'doc_notify', - 'doc_shepherd', - 'doc_group_chairs', - ]) + 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', - recipient_slugs=['rfc_editor', - ]) + 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', - recipient_slugs=['iana_approve',]) + desc="Recipients for IANA message when a non-IETF stream manager " + "requests publication", + to_slugs=['iana_approve',]) mt_factory(slug='charter_external_review', - desc='Recipients for a charter external review', - recipient_slugs=['ietf_announce',]) - - mt_factory(slug='charter_external_review_cc', - desc='Copied on a charter external review', - recipient_slugs=['group_mail_list',]) + desc="Recipients for a charter external review", + to_slugs=['ietf_announce',], + cc_slugs=['group_mail_list',], + ) mt_factory(slug='conflrev_requested', desc="Recipients for a stream manager's request for an IETF conflict review", - recipient_slugs=['iesg_secretary']) - - mt_factory(slug='conflrev_requested_cc', - desc="Copied on a stream manager's request for an IETF conflict review", - recipient_slugs=['iesg', - 'doc_notify', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - ]) + 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", - recipient_slugs=['iana_eval',]) + 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", - recipient_slugs=['stream_managers', - 'doc_notify', - ]) + to_slugs=['stream_managers', + 'doc_notify', + ]) mt_factory(slug='doc_stream_state_edited', desc="Recipients when the stream state of a document is manually edited", - recipient_slugs=['doc_group_chairs', - 'doc_group_delegates', - 'doc_shepherd', - 'doc_authors', - ]) + 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", - recipient_slugs=['group_responsible_directors', - 'group_chairs', - ]) + 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", - recipient_slugs=['group_mail_list', - ]) + to_slugs=['group_mail_list', + ]) mt_factory(slug='doc_state_edited', desc="Recipients when a document's state is manually edited", - recipient_slugs=['doc_notify', - 'doc_ad', - 'doc_authors', - 'doc_shepherd', - 'doc_group_chairs', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - ]) + 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_iana_state_changed', desc="Recipients when IANA state information for a document changes ", - recipient_slugs=['doc_notify', - 'doc_ad', - 'doc_authors', - 'doc_shepherd', - 'doc_group_chairs', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - ]) + 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", - recipient_slugs=['iesg', - 'iesg_secretary', - 'doc_notify', - 'doc_authors', - 'doc_shepherd', - 'doc_group_chairs', - 'doc_affecteddoc_authors', - 'doc_affecteddoc_group_chairs', - 'doc_affecteddoc_notify', - ]) + 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", - recipient_slugs=['iana', - 'rfc_editor', - ]) - - mt_factory(slug='doc_pulled_from_rfc_queue_cc', - desc="Recipients when a document is taken out of the RFC's editor queue before publication", - recipient_slugs=['iesg_secretary', - 'doc_ad', - 'doc_notify', - 'doc_authors', - 'doc_shepherd', - 'doc_group_chairs', - ]) + 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', + 'doc_ad', + '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", - recipient_slugs=['doc_authors', - 'doc_notify', - 'doc_shepherd', - 'doc_group_chairs', - 'doc_group_responsible_directors', - ]) + 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", - recipient_slugs=['iesg_secretary']) + 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", - recipient_slugs=['iesg_secretary']) + to_slugs=['iesg_secretary']) mt_factory(slug='doc_expires_soon', desc="Recipients for notification of impending expiration of a document", - recipient_slugs=['doc_authors']) - - mt_factory(slug='doc_expires_soon_cc', - desc="Copied on notification of impending expiration of a document", - recipient_slugs=['doc_notify', - 'doc_shepherd', - 'doc_group_chairs', - 'doc_group_responsible_directors', - ]) + 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", - recipient_slugs=['doc_authors']) - - mt_factory(slug='doc_expired_cc', - desc="Copied on notification of a document's expiration", - recipient_slugs=['doc_notify', - 'doc_shepherd', - 'doc_group_chairs', - 'doc_group_responsible_directors', - ]) + 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'", - recipient_slugs=['internet_draft_requests',]) + to_slugs=['internet_draft_requests',]) mt_factory(slug='resurrection_completed', desc="Recipients when a draft resurrection request has been completed", - recipient_slugs=['iesg_secretary', - 'doc_ad', - ]) + to_slugs=['iesg_secretary', + 'doc_ad', + ]) mt_factory(slug='sub_manual_post_requested', desc="Recipients for a manual post request for a draft submission", - recipient_slugs=['internet_draft_requests', - ]) - - mt_factory(slug='sub_manual_post_requested_cc', - desc="Copied on a manual post request for a draft submission", - recipient_slugs=['submission_submitter', - 'submission_authors', - 'submission_group_chairs', - ]) + 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", - recipient_slugs=['submission_group_chairs',]) + 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", - recipient_slugs=['submission_confirmers',]) + 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", - recipient_slugs=['submission_confirmers',]) + to_slugs=['submission_confirmers',]) mt_factory(slug='sub_announced', desc="Recipients for the announcement of a successfully submitted draft", - recipient_slugs=['ietf_announce', - ]) - - mt_factory(slug='sub_announced_cc', - desc="Copied on the announcement of a successfully submitted draft", - recipient_slugs=['submission_group_mail_list', - ]) + 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", - recipient_slugs=['submission_authors', - 'submission_confirmers', - ]) + 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", - recipient_slugs=['doc_notify', - 'doc_ad', - 'non_ietf_stream_manager', - 'rfc_editor_if_doc_in_queue', - 'doc_discussing_ads', - ]) + to_slugs=['doc_notify', + 'doc_ad', + 'non_ietf_stream_manager', + 'rfc_editor_if_doc_in_queue', + 'doc_discussing_ads', + ]) mt_factory(slug='milestone_review_reminder', desc="Recipients for reminder message that unapproved milestone changes need review", - recipient_slugs=['group_responsible_directors', - ]) - - mt_factory(slug='milestone_review_reminder_cc', - desc="Copied on reminder message that unapproved milestone changes need review", - recipient_slugs=['group_chairs', - ]) + to_slugs=['group_responsible_directors',], + cc_slugs=['group_chairs', ], + ) mt_factory(slug='milestones_due_soon', - desc='Recipients for reminder message for milestones about to become overdue', - recipient_slugs=['group_chairs', - ]) + desc="Recipients for reminder message for milestones about to become overdue", + to_slugs=['group_chairs', ]) mt_factory(slug='milestones_overdue', - desc='Recipients for message about milestones that are overdue', - recipient_slugs=['group_chairs', - ]) + desc="Recipients for message about milestones that are overdue", + to_slugs=['group_chairs', ]) mt_factory(slug='group_personnel_change', desc="Recipients for a message noting changes in a group's personnel", - recipient_slugs=['iesg_secretary', - 'group_responsible_directors', - 'group_chairs', - 'group_changed_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", - recipient_slugs=['session_requests', - ]) - - mt_factory(slug='session_requested_cc', - desc="Copied on a normal meeting session request", - recipient_slugs=['group_mail_list', - 'group_chairs', - 'group_responsible_directors', - 'logged_in_person', - ]) + 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", - recipient_slugs=['group_responsible_directors', - ]) - - mt_factory(slug='session_requested_long_cc', - desc="Copied on a meeting session request for more than 2 sessions", - recipient_slugs=['session_requests', - 'group_chairs', - 'logged_in_person', - ]) + 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", - recipient_slugs=['session_requests', - ]) - - mt_factory(slug='session_request_cancelled_cc', - desc="Copied on a message cancelling a session request", - recipient_slugs=['group_mail_list', - 'group_chairs', - 'group_responsible_directors', - 'logged_in_person', - ]) + 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", - recipient_slugs=['session_requests', - ]) - - mt_factory(slug='session_request_not_meeting_cc', - desc="Copied on a message noting a group plans to not meet", - recipient_slugs=['group_mail_list', - 'group_chairs', - 'group_responsible_directors', - 'logged_in_person', - ]) + 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", - recipient_slugs=['session_requester', - 'group_chairs', - ]) - - mt_factory(slug='session_scheduled_cc', - desc="Recipients for details when a session has been scheduled", - recipient_slugs=['group_mail_list', - 'group_responsible_directors', - ]) + 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", - recipient_slugs=['ipr_requests', - ]) + to_slugs=['ipr_requests', ]) mt_factory(slug='ipr_disclosure_followup', desc="Recipients when the secretary follows up on an IPR disclosure submission", - recipient_slugs=['ipr_submitter', - ]) - - mt_factory(slug='ipr_disclosure_followup_cc', - desc="Copied when the secretary follows up on an IPR disclosure submission", - recipient_slugs=[]) + to_slugs=['ipr_submitter', ],) mt_factory(slug='ipr_posting_confirmation', desc="Recipients for a message confirming that a disclosure has been posted", - recipient_slugs=['ipr_submitter', - ]) - - mt_factory(slug='ipr_posting_confirmation_cc', - desc="Copied on a message confirming that a disclosure has been posted", - recipient_slugs=['ipr_updatedipr_contacts', - 'ipr_updatedipr_holders', - ]) + 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", - recipient_slugs=['doc_authors', - ]) - - mt_factory(slug='ipr_posted_on_doc_cc', - desc="Copied when an IPR disclosure calls out a given document", - recipient_slugs=['doc_ipr_group_or_ad', - 'ipr_announce', - ]) + 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", - recipient_slugs=['liaison_to_contact', - ]) - - mt_factory(slug='liaison_statement_posted_cc', - desc="Copied on a message when a new liaison statement is posted", - recipient_slugs=['liaison_cc', - 'liaison_technical_contact', - 'liaison_response_contact', - ]) + 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", - recipient_slugs=['liaison_statements_list', - ]) + to_slugs=['liaison_statements_list', + ]) mt_factory(slug='liaison_deadline_soon', - desc="Recipients for a message about a liaison statement deadline that is approaching.", - recipient_slugs=['liaison_to_contact', - ]) - - mt_factory(slug='liaison_deadline_soon_cc', - desc="Copied on a message about a liaison statement deadline that is approaching.", - recipient_slugs=['liaison_cc', - 'liaison_technical_contact', - 'liaison_response_contact', - ]) + 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", - recipient_slugs=['liaison_manager', - ]) + to_slugs=['liaison_manager', ]) mt_factory(slug='nomination_received', desc="Recipients for a message noting a new nomination has been received", - recipient_slugs=['nomcom_chair', - ]) + to_slugs=['nomcom_chair', ]) mt_factory(slug='nomination_receipt_requested', desc="Recipients for a message confirming a nomination was made", - recipient_slugs=['nominator', - ]) + to_slugs=['nominator', ]) mt_factory(slug='nomcom_comment_receipt_requested', desc="Recipients for a message confirming a comment was made", - recipient_slugs=['commenter', - ]) + 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", - recipient_slugs=['ietf_secretariat', - 'nomcom_chair', - ]) + 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", - recipient_slugs=['nominee', - ]) + to_slugs=['nominee', ]) mt_factory(slug='nomination_accept_reminder', desc="Recipeints of message reminding a nominee to accept or decline a nomination", - recipient_slugs=['nominee', - ]) + to_slugs=['nominee', ]) mt_factory(slug='nomcom_questionnaire', desc="Recipients for the questionairre that nominees should complete", - recipient_slugs=['nominee', - ]) + to_slugs=['nominee', ]) mt_factory(slug='nomcom_questionnaire_reminder', - desc="Recipients for a message reminding a nominee to return a completed questionairre response", - recipient_slugs=['nominee', - ]) + desc="Recipients for a message reminding a nominee to return a " + "completed questionairre response", + to_slugs=['nominee', ]) def forward(apps, schema_editor): diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index e89c8ad7c..ee393a830 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -8,7 +8,8 @@ from ietf.group.models import Role class MailToken(models.Model): slug = models.CharField(max_length=32, primary_key=True) desc = models.TextField(blank=True) - recipients = models.ManyToManyField('Recipient', null=True, 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"] diff --git a/ietf/mailtoken/resources.py b/ietf/mailtoken/resources.py index 98f9ed7d3..8a46af244 100644 --- a/ietf/mailtoken/resources.py +++ b/ietf/mailtoken/resources.py @@ -20,14 +20,16 @@ class RecipientResource(ModelResource): api.mailtoken.register(RecipientResource()) class MailTokenResource(ModelResource): - recipients = ToManyField(RecipientResource, 'recipients', null=True) + to = ToManyField(RecipientResource, 'to', null=True) + cc = ToManyField(RecipientResource, 'cc', null=True) class Meta: queryset = MailToken.objects.all() #resource_name = 'mailtoken' filtering = { "slug": ALL, "desc": ALL, - "recipients": ALL_WITH_RELATIONS, + "to": ALL_WITH_RELATIONS, + "cc": ALL_WITH_RELATIONS, } api.mailtoken.register(MailTokenResource()) diff --git a/ietf/mailtoken/tests.py b/ietf/mailtoken/tests.py index 2a9eeb944..e16a46fa6 100644 --- a/ietf/mailtoken/tests.py +++ b/ietf/mailtoken/tests.py @@ -13,12 +13,12 @@ class EventMailTests(TestCase): url = urlreverse('ietf.mailtoken.views.show_tokens') r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertTrue('ballot_saved_cc' in r.content) + self.assertTrue('ballot_saved' in r.content) - url = urlreverse('ietf.mailtoken.views.show_tokens',kwargs=dict(mailtoken_slug='ballot_saved_cc')) + url = urlreverse('ietf.mailtoken.views.show_tokens',kwargs=dict(mailtoken_slug='ballot_saved')) r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertTrue('ballot_saved_cc' in r.content) + self.assertTrue('ballot_saved' in r.content) def test_show_recipients(self): diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 56b330082..d469e730f 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -1,20 +1,17 @@ -from django.core.exceptions import ObjectDoesNotExist - from ietf.mailtoken.models import MailToken, Recipient def gather_address_list(slug,**kwargs): addrs = [] - try: - mailtoken = MailToken.objects.get(slug=slug) - except ObjectDoesNotExist: - # TODO remove the raise here, or find a better way to detect runtime misconfiguration - raise - return addrs - - for recipient in mailtoken.recipients.all(): - addrs.extend(recipient.gather(**kwargs)) + if slug.endswith('_cc'): + mailtoken = MailToken.objects.get(slug=slug[:-3]) + for recipient in mailtoken.cc.all(): + addrs.extend(recipient.gather(**kwargs)) + else: + mailtoken = MailToken.objects.get(slug=slug) + for recipient in mailtoken.to.all(): + addrs.extend(recipient.gather(**kwargs)) return list(set([addr for addr in addrs if addr])) diff --git a/ietf/mailtoken/views.py b/ietf/mailtoken/views.py index 1341f76f0..66d45a298 100644 --- a/ietf/mailtoken/views.py +++ b/ietf/mailtoken/views.py @@ -2,19 +2,21 @@ from inspect import getsourcelines -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from ietf.mailtoken.models import MailToken, Recipient def show_tokens(request, mailtoken_slug=None): mailtokens = MailToken.objects.all() if mailtoken_slug: - mailtokens = mailtokens.filter(slug=mailtoken_slug) # TODO better 404 behavior here and below + get_object_or_404(MailToken,slug=mailtoken_slug) + mailtokens = mailtokens.filter(slug=mailtoken_slug) return render(request,'mailtoken/token.html',{'mailtoken_slug':mailtoken_slug, 'mailtokens':mailtokens}) 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 diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 49bf95d49..1a7bb6446 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4777,7 +4777,13 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_notify", + "group_chairs", + "group_mail_list", + "group_steering_group" + ], + "to": [ "ietf_announce" ], "desc": "Recipients when a charter is approved" @@ -4787,20 +4793,12 @@ }, { "fields": { - "recipients": [ - "doc_notify", - "group_chairs", - "group_mail_list", - "group_steering_group" + "cc": [ + "iana", + "iesg", + "ietf_announce" ], - "desc": "Copied when a charter is approved" - }, - "model": "mailtoken.mailtoken", - "pk": "ballot_approved_charter_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "conflict_review_steering_group", "conflict_review_stream_manager", "doc_affecteddoc_authors", @@ -4815,29 +4813,7 @@ }, { "fields": { - "recipients": [ - "iana", - "iesg", - "ietf_announce" - ], - "desc": "Copied when a conflict review ballot is approved" - }, - "model": "mailtoken.mailtoken", - "pk": "ballot_approved_conflrev_cc" -}, -{ - "fields": { - "recipients": [ - "ietf_announce" - ], - "desc": "Recipients when an IETF stream document ballot is approved" - }, - "model": "mailtoken.mailtoken", - "pk": "ballot_approved_ietf_stream" -}, -{ - "fields": { - "recipients": [ + "cc": [ "doc_ad", "doc_authors", "doc_group_chairs", @@ -4847,14 +4823,18 @@ "iesg", "rfc_editor" ], - "desc": "Copied when an IETF stream document ballot is approved" + "to": [ + "ietf_announce" + ], + "desc": "Recipients when an IETF stream document ballot is approved" }, "model": "mailtoken.mailtoken", - "pk": "ballot_approved_ietf_stream_cc" + "pk": "ballot_approved_ietf_stream" }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "iana_approve" ], "desc": "Recipients for IANA message when an IETF stream document ballot is approved" @@ -4864,7 +4844,14 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_notify", + "iesg", + "rfc_editor" + ], + "to": [ "ietf_announce" ], "desc": "Recipients when a status change is approved" @@ -4874,21 +4861,8 @@ }, { "fields": { - "recipients": [ - "doc_affecteddoc_group_chairs", - "doc_affecteddoc_notify", - "doc_notify", - "iesg", - "rfc_editor" - ], - "desc": "Copied when a status change is approved" - }, - "model": "mailtoken.mailtoken", - "pk": "ballot_approved_status_change_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "conflict_review_stream_manager", "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", @@ -4907,7 +4881,16 @@ }, { "fields": { - "recipients": [ + "cc": [ + "conflict_review_stream_manager", + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_authors", + "doc_group_chairs", + "doc_shepherd" + ], + "to": [ "iesg" ], "desc": "Recipients when a new ballot position (with discusses, other blocking positions, or comments) is saved" @@ -4917,23 +4900,10 @@ }, { "fields": { - "recipients": [ - "conflict_review_stream_manager", - "doc_affecteddoc_authors", - "doc_affecteddoc_group_chairs", - "doc_affecteddoc_notify", - "doc_authors", - "doc_group_chairs", - "doc_shepherd" + "cc": [ + "group_mail_list" ], - "desc": "Copied when a new ballot position (with discusses, other blocking positions, or comments) is saved" - }, - "model": "mailtoken.mailtoken", - "pk": "ballot_saved_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "ietf_announce" ], "desc": "Recipients for a charter external review" @@ -4943,17 +4913,8 @@ }, { "fields": { - "recipients": [ - "group_mail_list" - ], - "desc": "Copied on a charter external review" - }, - "model": "mailtoken.mailtoken", - "pk": "charter_external_review_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "iesg_secretary" ], "desc": "Recipients for message to adminstrators when a charter state edit needs followon administrative action" @@ -4963,7 +4924,14 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_affecteddoc_authors", + "doc_affecteddoc_group_chairs", + "doc_affecteddoc_notify", + "doc_notify", + "iesg" + ], + "to": [ "iesg_secretary" ], "desc": "Recipients for a stream manager's request for an IETF conflict review" @@ -4973,21 +4941,8 @@ }, { "fields": { - "recipients": [ - "doc_affecteddoc_authors", - "doc_affecteddoc_group_chairs", - "doc_affecteddoc_notify", - "doc_notify", - "iesg" - ], - "desc": "Copied on a stream manager's request for an IETF conflict review" - }, - "model": "mailtoken.mailtoken", - "pk": "conflrev_requested_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "iana_eval" ], "desc": "Recipients for IANA message when a stream manager requests an IETF conflict review" @@ -4997,7 +4952,13 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_notify", + "doc_shepherd" + ], + "to": [ "doc_authors" ], "desc": "Recipients for notification of a document's expiration" @@ -5007,20 +4968,13 @@ }, { "fields": { - "recipients": [ + "cc": [ "doc_group_chairs", "doc_group_responsible_directors", "doc_notify", "doc_shepherd" ], - "desc": "Copied on notification of a document's expiration" - }, - "model": "mailtoken.mailtoken", - "pk": "doc_expired_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "doc_authors" ], "desc": "Recipients for notification of impending expiration of a document" @@ -5030,20 +4984,8 @@ }, { "fields": { - "recipients": [ - "doc_group_chairs", - "doc_group_responsible_directors", - "doc_notify", - "doc_shepherd" - ], - "desc": "Copied on notification of impending expiration of a document" - }, - "model": "mailtoken.mailtoken", - "pk": "doc_expires_soon_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_ad", "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", @@ -5060,7 +5002,15 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_ad", + "doc_authors", + "doc_group_chairs", + "doc_notify", + "doc_shepherd", + "iesg_secretary" + ], + "to": [ "iana", "rfc_editor" ], @@ -5071,22 +5021,8 @@ }, { "fields": { - "recipients": [ - "doc_ad", - "doc_authors", - "doc_group_chairs", - "doc_notify", - "doc_shepherd", - "iesg_secretary" - ], - "desc": "Recipients when a document is taken out of the RFC's editor queue before publication" - }, - "model": "mailtoken.mailtoken", - "pk": "doc_pulled_from_rfc_queue_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_authors", "doc_group_chairs", "doc_group_responsible_directors", @@ -5100,7 +5036,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_ad", "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", @@ -5117,7 +5054,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_notify", "stream_managers" ], @@ -5128,7 +5066,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_authors", "doc_group_chairs", "doc_group_delegates", @@ -5141,7 +5080,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", "doc_affecteddoc_notify", @@ -5159,7 +5099,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "group_mail_list" ], "desc": "Recipients when the set of approved milestones for a group are edited" @@ -5169,7 +5110,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "iesg_secretary" ], "desc": "Recipients for message requesting closure of a group" @@ -5179,7 +5121,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "group_chairs", "group_responsible_directors" ], @@ -5190,7 +5133,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "group_chairs", "group_changed_personnel", "group_responsible_directors", @@ -5203,7 +5147,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "ipr_submitter" ], "desc": "Recipients when the secretary follows up on an IPR disclosure submission" @@ -5213,15 +5158,8 @@ }, { "fields": { - "recipients": [], - "desc": "Copied when the secretary follows up on an IPR disclosure submission" - }, - "model": "mailtoken.mailtoken", - "pk": "ipr_disclosure_followup_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "ipr_requests" ], "desc": "Recipients when an IPR disclosure is submitted" @@ -5231,7 +5169,11 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_ipr_group_or_ad", + "ipr_announce" + ], + "to": [ "doc_authors" ], "desc": "Recipients when an IPR disclosure calls out a given document" @@ -5241,18 +5183,11 @@ }, { "fields": { - "recipients": [ - "doc_ipr_group_or_ad", - "ipr_announce" + "cc": [ + "ipr_updatedipr_contacts", + "ipr_updatedipr_holders" ], - "desc": "Copied when an IPR disclosure calls out a given document" - }, - "model": "mailtoken.mailtoken", - "pk": "ipr_posted_on_doc_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "ipr_submitter" ], "desc": "Recipients for a message confirming that a disclosure has been posted" @@ -5262,18 +5197,10 @@ }, { "fields": { - "recipients": [ - "ipr_updatedipr_contacts", - "ipr_updatedipr_holders" + "cc": [ + "iesg_secretary" ], - "desc": "Copied on a message confirming that a disclosure has been posted" - }, - "model": "mailtoken.mailtoken", - "pk": "ipr_posting_confirmation_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "doc_authors", "doc_notify", "doc_shepherd", @@ -5286,27 +5213,7 @@ }, { "fields": { - "recipients": [ - "iesg_secretary" - ], - "desc": "Copied when a last call has expired" - }, - "model": "mailtoken.mailtoken", - "pk": "last_call_expired_cc" -}, -{ - "fields": { - "recipients": [ - "ietf_announce" - ], - "desc": "Recipients when a last call is issued" - }, - "model": "mailtoken.mailtoken", - "pk": "last_call_issued" -}, -{ - "fields": { - "recipients": [ + "cc": [ "doc_ad", "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", @@ -5316,14 +5223,18 @@ "doc_notify", "doc_shepherd" ], - "desc": "Copied when a last call is issued" + "to": [ + "ietf_announce" + ], + "desc": "Recipients when a last call is issued" }, "model": "mailtoken.mailtoken", - "pk": "last_call_issued_cc" + "pk": "last_call_issued" }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "iana_last_call" ], "desc": "Recipients for IANA message when a last call is issued" @@ -5333,7 +5244,12 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_ad", + "doc_notify", + "doc_shepherd" + ], + "to": [ "iesg_secretary" ], "desc": "Recipients when AD requests a last call" @@ -5343,19 +5259,8 @@ }, { "fields": { - "recipients": [ - "doc_ad", - "doc_notify", - "doc_shepherd" - ], - "desc": "Copied when AD requests a last call" - }, - "model": "mailtoken.mailtoken", - "pk": "last_call_requested_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "liaison_statements_list" ], "desc": "Recipients for a message that a pending liaison statement needs approval" @@ -5365,7 +5270,12 @@ }, { "fields": { - "recipients": [ + "cc": [ + "liaison_cc", + "liaison_response_contact", + "liaison_technical_contact" + ], + "to": [ "liaison_to_contact" ], "desc": "Recipients for a message about a liaison statement deadline that is approaching." @@ -5375,19 +5285,8 @@ }, { "fields": { - "recipients": [ - "liaison_cc", - "liaison_response_contact", - "liaison_technical_contact" - ], - "desc": "Copied on a message about a liaison statement deadline that is approaching." - }, - "model": "mailtoken.mailtoken", - "pk": "liaison_deadline_soon_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "liaison_manager" ], "desc": "Recipients for a message requesting an updated list of authorized individuals" @@ -5397,7 +5296,12 @@ }, { "fields": { - "recipients": [ + "cc": [ + "liaison_cc", + "liaison_response_contact", + "liaison_technical_contact" + ], + "to": [ "liaison_to_contact" ], "desc": "Recipient for a message when a new liaison statement is posted" @@ -5407,19 +5311,8 @@ }, { "fields": { - "recipients": [ - "liaison_cc", - "liaison_response_contact", - "liaison_technical_contact" - ], - "desc": "Copied on a message when a new liaison statement is posted" - }, - "model": "mailtoken.mailtoken", - "pk": "liaison_statement_posted_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "group_chairs" ], "desc": "Recipients for reminder message for milestones about to become overdue" @@ -5429,7 +5322,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "group_chairs" ], "desc": "Recipients for message about milestones that are overdue" @@ -5439,7 +5333,10 @@ }, { "fields": { - "recipients": [ + "cc": [ + "group_chairs" + ], + "to": [ "group_responsible_directors" ], "desc": "Recipients for reminder message that unapproved milestone changes need review" @@ -5449,17 +5346,8 @@ }, { "fields": { - "recipients": [ - "group_chairs" - ], - "desc": "Copied on reminder message that unapproved milestone changes need review" - }, - "model": "mailtoken.mailtoken", - "pk": "milestone_review_reminder_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "commenter" ], "desc": "Recipients for a message confirming a comment was made" @@ -5469,7 +5357,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "nominee" ], "desc": "Recipients for the questionairre that nominees should complete" @@ -5479,7 +5368,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "nominee" ], "desc": "Recipients for a message reminding a nominee to return a completed questionairre response" @@ -5489,7 +5379,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "nominee" ], "desc": "Recipeints of message reminding a nominee to accept or decline a nomination" @@ -5499,7 +5390,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "ietf_secretariat", "nomcom_chair" ], @@ -5510,7 +5402,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "nominee" ], "desc": "Recipients the first time a person is nominated for a position, asking them to accept or decline the nomination" @@ -5520,7 +5413,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "nominator" ], "desc": "Recipients for a message confirming a nomination was made" @@ -5530,7 +5424,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "nomcom_chair" ], "desc": "Recipients for a message noting a new nomination has been received" @@ -5540,7 +5435,13 @@ }, { "fields": { - "recipients": [ + "cc": [ + "doc_group_chairs", + "doc_notify", + "doc_shepherd", + "iesg_secretary" + ], + "to": [ "doc_ad" ], "desc": "Recipients when a draft is submitted to the IESG" @@ -5550,20 +5451,8 @@ }, { "fields": { - "recipients": [ - "doc_group_chairs", - "doc_notify", - "doc_shepherd", - "iesg_secretary" - ], - "desc": "Copied when a draft is submitted to the IESG" - }, - "model": "mailtoken.mailtoken", - "pk": "pubreq_iesg_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "rfc_editor" ], "desc": "Recipients when a non-IETF stream manager requests publication" @@ -5573,7 +5462,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "iana_approve" ], "desc": "Recipients for IANA message when a non-IETF stream manager requests publication" @@ -5583,7 +5473,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_ad", "iesg_secretary" ], @@ -5594,7 +5485,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "internet_draft_requests" ], "desc": "Recipients of a request to change the state of a draft away from 'Dead'" @@ -5604,7 +5496,13 @@ }, { "fields": { - "recipients": [ + "cc": [ + "group_chairs", + "group_mail_list", + "group_responsible_directors", + "logged_in_person" + ], + "to": [ "session_requests" ], "desc": "Recipients for a normal meeting session request" @@ -5614,20 +5512,12 @@ }, { "fields": { - "recipients": [ + "cc": [ "group_chairs", - "group_mail_list", - "group_responsible_directors", - "logged_in_person" + "logged_in_person", + "session_requests" ], - "desc": "Copied on a normal meeting session request" - }, - "model": "mailtoken.mailtoken", - "pk": "session_requested_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "group_responsible_directors" ], "desc": "Recipients for a meeting session request for more than 2 sessions" @@ -5637,19 +5527,13 @@ }, { "fields": { - "recipients": [ + "cc": [ "group_chairs", - "logged_in_person", - "session_requests" + "group_mail_list", + "group_responsible_directors", + "logged_in_person" ], - "desc": "Copied on a meeting session request for more than 2 sessions" - }, - "model": "mailtoken.mailtoken", - "pk": "session_requested_long_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "session_requests" ], "desc": "Recipients for a message cancelling a session request" @@ -5659,20 +5543,13 @@ }, { "fields": { - "recipients": [ + "cc": [ "group_chairs", "group_mail_list", "group_responsible_directors", "logged_in_person" ], - "desc": "Copied on a message cancelling a session request" - }, - "model": "mailtoken.mailtoken", - "pk": "session_request_cancelled_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "session_requests" ], "desc": "Recipients for a message noting a group plans to not meet" @@ -5682,20 +5559,11 @@ }, { "fields": { - "recipients": [ - "group_chairs", + "cc": [ "group_mail_list", - "group_responsible_directors", - "logged_in_person" + "group_responsible_directors" ], - "desc": "Copied on a message noting a group plans to not meet" - }, - "model": "mailtoken.mailtoken", - "pk": "session_request_not_meeting_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "group_chairs" ], "desc": "Recipients for details when a session has been scheduled" @@ -5705,18 +5573,10 @@ }, { "fields": { - "recipients": [ - "group_mail_list", - "group_responsible_directors" + "cc": [ + "submission_group_mail_list" ], - "desc": "Recipients for details when a session has been scheduled" - }, - "model": "mailtoken.mailtoken", - "pk": "session_scheduled_cc" -}, -{ - "fields": { - "recipients": [ + "to": [ "ietf_announce" ], "desc": "Recipients for the announcement of a successfully submitted draft" @@ -5726,17 +5586,8 @@ }, { "fields": { - "recipients": [ - "submission_group_mail_list" - ], - "desc": "Copied on the announcement of a successfully submitted draft" - }, - "model": "mailtoken.mailtoken", - "pk": "sub_announced_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "submission_authors", "submission_confirmers" ], @@ -5747,7 +5598,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "submission_group_chairs" ], "desc": "Recipients for a message requesting group chair approval of a draft submission" @@ -5757,7 +5609,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "submission_confirmers" ], "desc": "Recipients for a message requesting confirmation of a draft submission" @@ -5767,7 +5620,8 @@ }, { "fields": { - "recipients": [ + "cc": [], + "to": [ "submission_confirmers" ], "desc": "Recipients for a message with the full URL for managing a draft submission" @@ -5777,7 +5631,12 @@ }, { "fields": { - "recipients": [ + "cc": [ + "submission_authors", + "submission_group_chairs", + "submission_submitter" + ], + "to": [ "internet_draft_requests" ], "desc": "Recipients for a manual post request for a draft submission" @@ -5787,19 +5646,8 @@ }, { "fields": { - "recipients": [ - "submission_authors", - "submission_group_chairs", - "submission_submitter" - ], - "desc": "Copied on a manual post request for a draft submission" - }, - "model": "mailtoken.mailtoken", - "pk": "sub_manual_post_requested_cc" -}, -{ - "fields": { - "recipients": [ + "cc": [], + "to": [ "doc_ad", "doc_discussing_ads", "doc_notify", diff --git a/ietf/templates/mailtoken/token.html b/ietf/templates/mailtoken/token.html index d4a888a86..7cd5a351c 100644 --- a/ietf/templates/mailtoken/token.html +++ b/ietf/templates/mailtoken/token.html @@ -20,11 +20,18 @@ {% for mailtoken in mailtokens %}
- {% endfor %} From 90c3426e2feb8bbdea9e69f4fb82e7e935fb5c28 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sun, 23 Aug 2015 18:15:06 +0000 Subject: [PATCH 24/46] updated mailtoken utilities to match the model refactor. Morphed all the code using the utilities to match. Added paths to get Cc through as needed. Next up: Recipient selection form for the iesg save-and-send-email workflow - Legacy-Id: 10040 --- ietf/doc/expire.py | 9 +- ietf/doc/mails.py | 118 +++++++++--------- ietf/doc/tests_ballot.py | 2 +- ietf/doc/utils.py | 26 ++-- ietf/doc/utils_charter.py | 12 +- ietf/doc/views_ballot.py | 25 ++-- ietf/doc/views_conflict_review.py | 19 +-- ietf/doc/views_draft.py | 11 +- ietf/doc/views_status_change.py | 9 +- ietf/group/mails.py | 44 ++++--- ietf/ipr/tests.py | 6 +- ietf/ipr/views.py | 20 +-- ietf/liaisons/mails.py | 17 ++- .../migrations/0002_auto_20150809_1314.py | 33 ++++- ietf/mailtoken/models.py | 7 ++ ietf/mailtoken/utils.py | 38 +++--- ietf/name/fixtures/names.json | 28 ++++- ietf/nomcom/forms.py | 10 +- ietf/nomcom/utils.py | 26 ++-- ietf/secr/meetings/views.py | 9 +- ietf/secr/sreq/views.py | 14 +-- ietf/submit/mail.py | 69 +++++----- ietf/submit/tests.py | 4 +- ietf/submit/views.py | 6 +- 24 files changed, 331 insertions(+), 231 deletions(-) diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index 1551abab5..2288472e6 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -10,8 +10,7 @@ from ietf.doc.models import Document, DocEvent, State, save_document_in_history, from ietf.person.models import Person from ietf.meeting.models import Meeting from ietf.doc.utils import add_state_change_event -from ietf.mailtoken.utils import gather_address_list - +from ietf.mailtoken.utils import gather_address_lists def expirable_draft(draft): @@ -71,8 +70,7 @@ def send_expire_warning_for_draft(doc): expiration = doc.expires.date() - to = gather_address_list('doc_expires_soon',doc=doc) - cc = gather_address_list('doc_expires_soon_cc',doc=doc) + (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" @@ -97,8 +95,7 @@ def send_expire_notice_for_draft(doc): state = s.name if s else "I-D Exists" request = None - to = gather_address_list('doc_expired',doc=doc) - cc = gather_address_list('doc_expired_cc',doc=doc) + (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(), diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 97393ef9f..2f0fce9f8 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -14,10 +14,10 @@ from ietf.doc.utils import needed_ballot_positions from ietf.person.models import Person from ietf.group.models import Role from ietf.doc.models import Document -from ietf.mailtoken.utils import gather_addresses, gather_address_list +from ietf.mailtoken.utils import gather_address_lists def email_state_changed(request, doc, text, mailtoken_id=None): - to = gather_address_list(mailtoken_id or 'doc_state_edited',doc=doc) + (to,cc) = gather_address_lists(mailtoken_id or 'doc_state_edited',doc=doc) if not to: return @@ -26,7 +26,8 @@ def email_state_changed(request, doc, text, mailtoken_id=None): "ID Tracker State Update Notice: %s" % doc.file_tag(), "doc/mail/state_changed_email.txt", dict(text=text, - url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + 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""" @@ -35,7 +36,7 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""): streams.append(old_stream.slug) if new_stream: streams.append(new_stream.slug) - to = gather_address_list('doc_stream_changed',doc=doc,streams=streams) + (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'] = gather_addresses('doc_pulled_from_rfc_queue_cc',doc=doc) - send_mail(request, gather_address_list('doc_pulled_from_rfc_queue',doc=doc), 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, @@ -112,12 +115,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"), - to=gather_addresses('last_call_issued',doc=doc), - cc=gather_addresses('last_call_issued_cc',doc=doc), + to=addrs.to, + cc=addrs.cc, group=group, docs=[ doc ], urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ], @@ -187,12 +192,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(), - to = gather_addresses('ballot_approved_ietf_stream',doc=doc), - cc = gather_addresses('ballot_approved_ietf_stream_cc',doc=doc), + to = addrs.to, + cc = addrs.cc, doc_type=doc_type, made_by=made_by, contacts=contacts, @@ -205,14 +211,15 @@ def generate_approval_mail_rfc_editor(request, doc): # 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" + 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 = gather_addresses('ballot_approved_conflrev', doc=doc), - cc = gather_addresses('ballot_approved_conflrev_cc', doc=doc), + to = addrs.to, + cc = addrs.cc, ) ) @@ -242,8 +249,7 @@ def generate_publication_request(request, doc): ) def send_last_call_request(request, doc): - to = gather_addresses('last_call_requested',doc=doc) - cc = gather_addresses('last_call_requested_cc',doc=doc) + (to, cc) = gather_address_lists('last_call_requested',doc=doc) frm = '"DraftTracker Mail System" ' send_mail(request, to, frm, @@ -255,7 +261,7 @@ def send_last_call_request(request, doc): cc=cc) def email_resurrect_requested(request, doc, by): - to = gather_address_list('resurrection_requested',doc=doc) + (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") @@ -268,20 +274,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): - to = gather_address_list('resurrection_completed',doc=doc) + (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 = gather_addresses('ballot_deferred',doc=doc) + (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(), @@ -289,10 +297,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 = gather_addresses('ballot_deferred',doc=doc) + (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(), @@ -300,7 +309,8 @@ 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() @@ -370,7 +380,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")) @@ -383,7 +393,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 = {} @@ -394,24 +405,24 @@ 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) send_mail(None, - gather_addresses('last_call_expired',doc=doc), + 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 = gather_addresses('last_call_expired_cc',doc=doc) - ) + cc = addrs.cc) def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""): - recipients = gather_address_list('doc_stream_state_edited',doc=doc) + (to, cc)= gather_address_lists('doc_stream_state_edited',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"%s changed for %s" % (state_type.label, doc.name), 'doc/mail/stream_state_changed_email.txt', dict(doc=doc, @@ -420,13 +431,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=""): - recipients = gather_address_list('doc_stream_state_edited',doc=doc) + (to, cc) = gather_address_lists('doc_stream_state_edited',doc=doc) - 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, @@ -434,32 +446,24 @@ 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") - - 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 }), - }) + 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),) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 999dd1f5a..ecaa72c99 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -173,7 +173,7 @@ class EditPositionTests(TestCase): self.assertEqual(len(outbox), mailbox_before + 2) m = outbox[-1] self.assertTrue("iesg@" in m['To']) - self.assertFalse(draft.group.list_email in m['Cc']) + self.assertFalse(m['Cc'] and draft.group.list_email in m['Cc']) class BallotWriteupsTests(TestCase): diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 37cee0b11..91900f0b2 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -18,12 +18,12 @@ from ietf.group.models import Role from ietf.ietfauth.utils import has_role from ietf.utils import draft, markup_txt from ietf.utils.mail import send_mail -from ietf.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_lists #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 = gather_address_list('doc_telechat_details_changed',doc=doc) + (to, cc) = gather_address_lists('doc_telechat_details_changed',doc=doc) if not to: return @@ -33,7 +33,8 @@ def email_update_telechat(request, doc, text): "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 = [] @@ -457,14 +458,18 @@ def rebuild_reference_relations(doc,filename=None): return ret def set_replaces_for_document(request, doc, new_replaces, by, email_subject, email_comment=""): - to = gather_address_list('doc_replacement_changed',doc=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: - to.extend(gather_address_list('doc_replacement_changed',doc=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' @@ -472,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: - to.extend(gather_address_list('doc_replacement_changed',doc=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')) @@ -490,17 +497,16 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, ema if email_comment: email_desc += "\n" + email_comment - to = list(set([addr for addr in to if addr])) - from ietf.doc.mails import html_to_text - send_mail(request, to, + send_mail(request, list(to), "DraftTracker Mail System ", 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 diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index b92cde3e4..9c3c7d2f8 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -6,7 +6,7 @@ from django.conf import settings from ietf.doc.models import NewRevisionDocEvent, WriteupDocEvent, BallotPositionDocEvent from ietf.person.models import Person from ietf.utils.history import find_history_active_at -from ietf.mailtoken.utils import gather_addresses +from ietf.mailtoken.utils import gather_address_lists def charter_name_for_group(group): if group.type_id == "rg": @@ -99,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" @@ -112,14 +113,15 @@ 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=gather_addresses('ballot_approved_charter',doc=charter,group=group), - cc=gather_addresses('ballot_approved_charter_cc',doc=charter,group=group), + to=addrs.to, + cc=addrs.cc, )) e.save() return e def default_review_text(group, charter, by): + addrs=gather_address_lists('charter_external_review',group=group).as_strings(compact=False) e = WriteupDocEvent(doc=charter, by=by) e.by = by e.type = "changed_review_announcement" @@ -134,8 +136,8 @@ def default_review_text(group, charter, by): 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=gather_addresses('charter_external_review',group=group), - cc=gather_addresses('charter_external_review_cc',group=group) + to=addrs.to, + cc=addrs.cc, ) ) e.save() diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 55a10d7e4..3a2fa36d7 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -27,7 +27,7 @@ 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.mailtoken.utils import gather_addresses, gather_address_list +from ietf.mailtoken.utils import gather_address_lists BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -283,19 +283,20 @@ def send_ballot_comment(request, name, ballot_id): blocking_name=blocking_name, settings=settings)) frm = ad.role_email("ad").formatted_email() - to = gather_addresses('ballot_saved',doc=doc) + + addrs = gather_address_lists('ballot_saved',doc=doc) if request.method == 'POST': - cc = gather_address_list('ballot_saved_cc',doc=doc) - explicit_cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] - if explicit_cc: - cc.extend(explicit_cc) + # The send_ballot_comments form provides an unusual case where the form separates out + # the cc addresses to be edited before sending into a separate field + # TODO: We should consider undoing this, and going back at most to an "extra_cc" model + 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) - 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) @@ -304,7 +305,8 @@ def send_ballot_comment(request, name, ballot_id): subject=subject, body=body, frm=frm, - to=to, + to=addrs.as_strings().to, + cc=addrs.as_strings().cc, ad=ad, can_send=d or c, back_url=back_url, @@ -701,8 +703,9 @@ def approve_ballot(request, name): 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": gather_addresses('ballot_approved_ietf_stream_iana'), "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 @@ -743,9 +746,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": gather_addresses('last_call_issued_iana',doc=doc), - "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 diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index dbe5e533a..a4fcb0dd9 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -20,7 +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.mailtoken.utils import gather_addresses +from ietf.mailtoken.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) @@ -87,10 +87,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 = gather_addresses('conflrev_requested',doc=review), - cc = gather_addresses('conflrev_requested_cc',doc=review), + to = addrs.to, + cc = addrs.cc, by = request.user.person, review = review, reviewed_doc = review.relateddocument_set.get(relationship__slug='conflrev').target.document, @@ -99,10 +100,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, - gather_addresses('conflrev_requested_iana',doc=review), - msg) + addrs.to, + msg, + cc=addrs.cc) def send_conflict_eval_email(request,review): msg = render_to_string("doc/eval_email.txt", @@ -254,6 +258,7 @@ 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(), @@ -261,8 +266,8 @@ def default_approval_text(review): conflictdoc_url = settings.IDTRACKER_BASE_URL+conflictdoc.get_absolute_url(), receiver=receiver, approved_review = current_text, - to = gather_addresses('ballot_approved_conflrev',doc=review), - cc = gather_addresses('ballot_approved_conflrev_cc',doc=review), + to = addrs.to, + cc = addrs.cc, ) ) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index f654dd0be..629b0f48b 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -37,7 +37,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.mailtoken.utils import gather_addresses +from ietf.mailtoken.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) @@ -582,10 +582,11 @@ def to_iesg(request,name): doc.save() + addrs= gather_address_lists('pubreq_iesg',doc=doc) extra = {} - extra['Cc'] = gather_addresses('pubreq_iesg_cc',doc=doc) + extra['Cc'] = addrs.as_strings().cc send_mail(request=request, - to = gather_addresses('pubreq_iesg',doc=doc), + 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", @@ -1134,7 +1135,7 @@ def request_publication(request, name): m = Message() m.frm = request.user.person.formatted_email() - m.to = gather_addresses('pubreq_rfced',doc=doc) + (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 = gather_addresses('pubreq_rfced_iana',doc=doc) + (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) diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index a40cd21ff..e177d9bd8 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -22,7 +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.mailtoken.utils import gather_addresses +from ietf.mailtoken.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) @@ -290,7 +290,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(), @@ -299,8 +300,8 @@ def default_approval_text(status_change,relateddoc): approved_text = current_text, action=action, newstatus=newstatus(relateddoc), - to=gather_addresses('ballot_approved_status_change',doc=status_change), - cc=gather_addresses('ballot_approved_status_change_cc',doc=status_change), + to=addrs.to, + cc=addrs.cc, ) ) diff --git a/ietf/group/mails.py b/ietf/group/mails.py index 312e6f2b8..66324dad9 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -12,10 +12,10 @@ 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.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_lists def email_admin_re_charter(request, group, subject, text, mailtoken): - to = gather_address_list(mailtoken,group=group) + (to,cc) = gather_address_lists(mailtoken,group=group) full_subject = u"Regarding %s %s: %s" % (group.type.name, group.acronym, subject) text = strip_tags(text) @@ -25,17 +25,18 @@ def email_admin_re_charter(request, group, subject, text, mailtoken): 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_personnel_change(request, group, text, changed_personnel): - to = gather_address_list('group_personnel_change',group=group,changed_personnel=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) + 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): @@ -45,26 +46,25 @@ 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 those who should see any edits (such as management and chairs) - to = gather_address_list('group_milestones_edited',group=group) - if to: - wrap_up_email(to, u"\n\n".join(c + "." for c in changes)) + 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)) # 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") - to = gather_address_list('group_approved_milestones_edited',group=group) + 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 to and msg: - wrap_up_email(to, msg) + if (addrs.to or addrs.cc) and msg: + wrap_up_email(addrs, msg) def email_milestone_review_reminder(group, grace_period=7): """Email reminders about milestones needing review to management.""" - to = gather_address_list('milestone_review_reminder',group=group) - cc = gather_address_list('milestone_review_reminder_cc',group=group) + (to, cc) = gather_address_lists('milestone_review_reminder',group=group) if not to: return False @@ -102,7 +102,7 @@ def groups_with_milestones_needing_review(): return Group.objects.filter(groupmilestone__state="review").distinct() def email_milestones_due(group, early_warning_days): - to = gather_address_list('milestones_due_soon',group=group) + (to, cc) = gather_address_lists('milestones_due_soon',group=group) today = datetime.date.today() early_warning = today + datetime.timedelta(days=early_warning_days) @@ -120,7 +120,9 @@ def email_milestones_due(group, early_warning_days): today=today, early_warning_days=early_warning_days, url=settings.IDTRACKER_BASE_URL + group.about_url(), - )) + ), + cc=cc, + ) def groups_needing_milestones_due_reminder(early_warning_days): """Return groups having milestones that are either @@ -129,7 +131,7 @@ def groups_needing_milestones_due_reminder(early_warning_days): 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 = gather_address_list('milestones_overdue',group=group) + (to, cc) = gather_address_lists('milestones_overdue',group=group) today = datetime.date.today() @@ -145,7 +147,9 @@ def email_milestones_overdue(group): dict(group=group, milestones=milestones, url=settings.IDTRACKER_BASE_URL + group.about_url(), - )) + ), + cc=cc, + ) def groups_needing_milestones_overdue_reminder(grace_period=30): cut_off = datetime.date.today() - datetime.timedelta(days=grace_period) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index f448ccd5d..f7b139c73 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -15,7 +15,7 @@ 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, empty_outbox -from ietf.mailtoken.utils import gather_addresses +from ietf.mailtoken.utils import gather_address_lists class IprTests(TestCase): @@ -537,11 +537,13 @@ I would like to revoke this declaration. 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(gather_addresses('ipr_disclosure_submitted'),datetime.datetime.now().ctime()) +""".format(addrs.to, addrs.cc, datetime.datetime.now().ctime()) result = process_response_email(message_string) self.assertIsNone(result) diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 22f406e97..29b5a8a18 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -34,7 +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.mailtoken.utils import gather_address_list, gather_addresses +from ietf.mailtoken.utils import gather_address_lists # ---------------------------------------------------------------- # Globals @@ -79,8 +79,7 @@ def get_document_emails(ipr): else: cc_list = get_wg_email_list(doc.group) - to_list = gather_addresses('ipr_posted_on_doc',doc=doc) - cc_list = gather_addresses('ipr_posted_on_doc_cc',doc=doc) + (to_list,cc_list) = gather_address_lists('ipr_posted_on_doc',doc=doc) author_names = ', '.join([a.person.name for a in authors]) context = dict( @@ -99,10 +98,11 @@ def get_posted_emails(ipr): the notify view when a new disclosure is posted""" messages = [] + addrs = gather_address_lists('ipr_posting_confirmation',ipr=ipr).as_strings(compact=False) context = dict( - to_email=gather_addresses('ipr_posting_confirmation',ipr=ipr), + to_email=addrs.to, to_name=ipr.submitter_name, - cc_email=gather_addresses('ipr_posting_confirmation_cc',ipr=ipr), + cc_email=addrs.cc, ipr=ipr) text = render_to_string('ipr/posted_submitter_email.txt',context) messages.append(text) @@ -375,9 +375,10 @@ 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': gather_addresses('ipr_disclosure_followup',ipr=ipr), - 'cc': gather_addresses('ipr_disclosure_followup_cc',ipr=ipr), + 'to': addrs.to, + 'cc': addrs.cc, 'frm': settings.IPR_EMAIL_FROM, 'subject': 'Regarding {}'.format(ipr.title), 'reply_to': reply_to, @@ -473,11 +474,12 @@ def new(request, type, updates=None): desc="Disclosure Submitted") # send email notification - to = gather_address_list('ipr_disclosure_submitted') + (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") diff --git a/ietf/liaisons/mails.py b/ietf/liaisons/mails.py index 49923fb6c..ab330f773 100644 --- a/ietf/liaisons/mails.py +++ b/ietf/liaisons/mails.py @@ -7,13 +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.mailtoken.utils import gather_address_list +from ietf.mailtoken.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 = gather_address_list('liaison_statement_posted',liaison=liaison) - cc = gather_address_list('liaison_statement_posted_cc',liaison=liaison) + (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, @@ -39,14 +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 = gather_address_list('liaison_approval_requested',liaison=liaison) + (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, to, 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) @@ -56,7 +54,7 @@ def send_sdo_reminder(sdo): manager_role = roles[0] subject = 'Request for update of list of authorized individuals' - to_email = gather_address_list('liaison_manager_update_request',group=sdo) + (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") @@ -66,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 @@ -93,8 +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 = gather_address_list('liaison_deadline_soon',liaison=liaison) - cc = gather_address_list('liaison_deadline_soon_cc',liaison=liaison) + (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, diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 984c8b1a9..d1afb3a1c 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -79,7 +79,7 @@ def make_recipients(apps): rc(slug='conflict_review_stream_manager', desc="The stream manager of a document being reviewed for IETF stream conflicts", - template = None ) + template=None ) rc(slug='conflict_review_steering_group', desc="The steering group (e.g. IRSG) of a document being reviewed for IETF stream conflicts", @@ -165,6 +165,10 @@ def make_recipients(apps): desc="The session request ticketing system", template='') + 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 %}') @@ -241,6 +245,17 @@ def make_mailtokens(apps): MailToken=apps.get_model('mailtoken','MailToken') 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 = MailToken.objects.create(slug=slug, desc=desc) m.to = Recipient.objects.filter(slug__in=to_slugs) m.cc = Recipient.objects.filter(slug__in=cc_slugs) @@ -325,7 +340,7 @@ def make_mailtokens(apps): cc_slugs=['iesg', 'rfc_editor', 'doc_notify', - 'doc_affectddoc_authors', + 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', 'doc_affecteddoc_notify', ], @@ -347,7 +362,7 @@ def make_mailtokens(apps): 'doc_shepherd', 'doc_authors', 'doc_notify', - 'doc_group_list_email', + 'doc_group_mail_list', 'doc_group_chairs', 'doc_affecteddoc_authors', 'doc_affecteddoc_group_chairs', @@ -577,7 +592,7 @@ def make_mailtokens(apps): desc="Recipients for notification of a new version of an existing document", to_slugs=['doc_notify', 'doc_ad', - 'non_ietf_stream_manager', + 'doc_non_ietf_stream_manager', 'rfc_editor_if_doc_in_queue', 'doc_discussing_ads', ]) @@ -744,6 +759,16 @@ def make_mailtokens(apps): "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', + ]) + + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/mailtoken/models.py b/ietf/mailtoken/models.py index ee393a830..83bb732a7 100644 --- a/ietf/mailtoken/models.py +++ b/ietf/mailtoken/models.py @@ -252,3 +252,10 @@ class Recipient(models.Model): 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 diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index d469e730f..a3cdca85b 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -1,22 +1,32 @@ +from collections import namedtuple from ietf.mailtoken.models import MailToken, Recipient -def gather_address_list(slug,**kwargs): - - addrs = [] +class AddrLists(namedtuple('AddrLists',['to','cc'])): - if slug.endswith('_cc'): - mailtoken = MailToken.objects.get(slug=slug[:-3]) - for recipient in mailtoken.cc.all(): - addrs.extend(recipient.gather(**kwargs)) - else: - mailtoken = MailToken.objects.get(slug=slug) - for recipient in mailtoken.to.all(): - addrs.extend(recipient.gather(**kwargs)) + __slots__ = () - return list(set([addr for addr in addrs if addr])) + def as_strings(self,compact=True): -def gather_addresses(slug,**kwargs): - return ",\n ".join(gather_address_list(slug,**kwargs)) + 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): + mailtoken = MailToken.objects.get(slug=slug) + + to = set() + for recipient in mailtoken.to.all(): + to.update(recipient.gather(**kwargs)) + to.discard('') + + cc = set() + for recipient in mailtoken.cc.all(): + cc.update(recipient.gather(**kwargs)) + cc.discard('') + + return AddrLists(to=list(to),cc=list(cc)) def get_base_ipr_request_address(): return Recipient.objects.get(slug='ipr_requests').gather()[0] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 1a7bb6446..805841d6d 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4719,6 +4719,14 @@ "model": "mailtoken.recipient", "pk": "rfc_editor_if_doc_in_queue" }, +{ + "fields": { + "template": null, + "desc": "The person that requested a meeting slot for a given group" + }, + "model": "mailtoken.recipient", + "pk": "session_requester" +}, { "fields": { "template": "", @@ -4845,6 +4853,7 @@ { "fields": { "cc": [ + "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", "doc_affecteddoc_notify", "doc_notify", @@ -5034,6 +5043,20 @@ "model": "mailtoken.mailtoken", "pk": "doc_replacement_changed" }, +{ + "fields": { + "cc": [], + "to": [ + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_non_ietf_stream_manager", + "iesg_secretary" + ], + "desc": "Recipients for suggestions that this doc replaces or is replace by some other document" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_replacement_suggested" +}, { "fields": { "cc": [], @@ -5220,6 +5243,7 @@ "doc_affecteddoc_notify", "doc_authors", "doc_group_chairs", + "doc_group_mail_list", "doc_notify", "doc_shepherd" ], @@ -5564,7 +5588,8 @@ "group_responsible_directors" ], "to": [ - "group_chairs" + "group_chairs", + "session_requester" ], "desc": "Recipients for details when a session has been scheduled" }, @@ -5650,6 +5675,7 @@ "to": [ "doc_ad", "doc_discussing_ads", + "doc_non_ietf_stream_manager", "doc_notify", "rfc_editor_if_doc_in_queue" ], diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 7be1e6322..0c571733f 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -20,7 +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.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_lists ROLODEX_URL = getattr(settings, 'ROLODEX_URL', None) @@ -408,12 +408,12 @@ class NominateForm(BaseNomcomForm, forms.ModelForm): if author: subject = 'Nomination receipt' from_email = settings.NOMCOM_FROM_EMAIL - to_email = gather_address_list('nomination_receipt_requested',nominator=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 @@ -526,12 +526,12 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm): if author: subject = "NomCom comment confirmation" from_email = settings.NOMCOM_FROM_EMAIL - to_email = gather_address_list('nomcom_comment_receipt_requested',commenter=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 diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index b5bf08d0c..83ed384e7 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -18,7 +18,7 @@ from django.utils.encoding import smart_str from ietf.dbtemplate.models import DBTemplate from ietf.person.models import Email, Person -from ietf.mailtoken.utils import gather_address_list +from ietf.mailtoken.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 @@ -208,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 = gather_address_list('nomination_accept_reminder',nominee=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', @@ -234,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.' @@ -245,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 = gather_address_list('nomcom_questionnaire_reminder',nominee=nominee.email.address) + (to_email,cc) = gather_address_lists('nomcom_questionnaire_reminder',nominee=nominee.email.address) context = {'nominee': nominee, 'position': position, @@ -254,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 = [] @@ -295,18 +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 = gather_address_list('nomination_created_person',nomcom=nomcom) + (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 - 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 = gather_address_list('nomination_new_nominee',nominee=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) @@ -332,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 = gather_address_list('nomcom_questionnaire',nominee=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, @@ -347,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 = gather_address_list('nomination_received',nomcom=nomcom) + (to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom) context = {'nominee': email.person.name, 'nominee_email': email.address, 'position': position.name} @@ -365,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 diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 827298278..4a7ef84d3 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -26,7 +26,7 @@ from ietf.secr.proceedings.views import build_choices, handle_upload_file, make_ from ietf.secr.sreq.forms import GroupSelectForm from ietf.secr.sreq.views import get_initial_session from ietf.secr.utils.meeting import get_session, get_timeslot -from ietf.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_lists # prep for agenda changes @@ -188,8 +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 = gather_address_list('session_scheduled',group=group,person=sessions[0].requested_by) - cc_list = gather_address_list('session_scheduled_cc',group=group,person=sessions[0].requested_by) + 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) @@ -222,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', diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 2f3ad1e1a..f960c30f0 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -16,7 +16,7 @@ from ietf.secr.utils.decorators import check_permissions from ietf.secr.utils.group import groups_by_session from ietf.utils.mail import send_mail from ietf.person.models import Person -from ietf.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_lists # ------------------------------------------------- # Globals @@ -113,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 = gather_address_list('session_requested',group=group,person=login) - cc_list = gather_address_list('session_requested_cc',group=group,person=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' @@ -137,8 +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 = gather_address_list('session_requested_long',group=group,person=login) - cc_list = gather_address_list('session_requested_long_cc',group=group,person=login) + (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 @@ -209,8 +207,7 @@ def cancel(request, acronym): session.scheduledsession_set.all().delete() # send notifitcation - to_email = gather_address_list('session_request_cancelled',group=group,person=login) - cc_list = gather_address_list('session_request_cancelled_cc',group=group,person=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', @@ -626,8 +623,7 @@ def no_session(request, acronym): session_save(session) # send notification - to_email = gather_address_list('session_request_not_meeting',group=group,person=login) - cc_list = gather_address_list('session_request_not_meeting_cc',group=group,person=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', diff --git a/ietf/submit/mail.py b/ietf/submit/mail.py index d6148df1b..2939b33f8 100644 --- a/ietf/submit/mail.py +++ b/ietf/submit/mail.py @@ -8,56 +8,66 @@ from ietf.doc.models import Document from ietf.person.models import Person from ietf.message.models import Message from ietf.utils.accesstoken import generate_access_token -from ietf.mailtoken.utils import gather_address_list +from ietf.mailtoken.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 = gather_address_list('sub_confirmation_requested',submission=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 = gather_address_list('sub_management_url_requested',submission=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 = gather_address_list('sub_chair_approval_requested',submission=submission) + (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 = gather_address_list('sub_manual_post_requested',submission=submission) - cc = gather_address_list('sub_manual_post_requested_cc',submission=submission) + (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)), @@ -75,8 +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 = gather_address_list('sub_announced',submission=submission) - m.cc = gather_address_list('sub_announced_cc',submission=submission) + (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)) @@ -87,17 +96,18 @@ def announce_to_lists(request, submission): def announce_new_version(request, submission, draft, state_change_msg): - to_email = gather_address_list('sub_new_version',doc=draft,submission=submission) + (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): - to_email = gather_address_list('sub_announced_to_authors',submission=submission) + (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: @@ -108,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) diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 1e12a62dc..5cff51526 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -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"]) diff --git a/ietf/submit/views.py b/ietf/submit/views.py index caf0a55e3..6c5325bdb 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -23,7 +23,7 @@ from ietf.submit.utils import check_idnits, found_idnits, validate_submission, c from ietf.submit.utils import post_submission, cancel_submission, rename_submission_files from ietf.utils.accesstoken import generate_random_key, generate_access_token from ietf.utils.draft import Draft -from ietf.mailtoken.utils import gather_address_list +from ietf.mailtoken.utils import gather_address_lists def upload_submission(request): @@ -181,7 +181,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 = gather_address_list('sub_confirmation_requested',submission=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()) From 4e61776c8905a3c67b59d8ef1fd3d31f473d22f8 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 24 Aug 2015 18:25:39 +0000 Subject: [PATCH 25/46] Smarter send_ballot_comment form - Legacy-Id: 10054 --- ietf/doc/tests_ballot.py | 12 ++++++--- ietf/doc/views_ballot.py | 22 +++++++++------- .../migrations/0002_auto_20150809_1314.py | 6 +++-- ietf/name/fixtures/names.json | 4 ++- .../doc/ballot/send_ballot_comment.html | 26 +++++-------------- 5 files changed, 34 insertions(+), 36 deletions(-) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index ecaa72c99..32fcbae0d 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -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="test298347@example.com", cc_state_change="1",cc_group_list="1")) + r = self.client.post(url, dict(extra_cc="test298347@example.com", cc_tokens=['doc_notify','doc_group_chairs'])) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 1) @@ -163,10 +163,14 @@ class EditPositionTests(TestCase): self.assertTrue("clearer title" in str(m)) self.assertTrue("Test!" in str(m)) self.assertTrue("iesg@" in m['To']) + # cc_token doc_group_chairs + self.assertTrue("mars-chairs@" in m['Cc']) + # cc_token doc_notify self.assertTrue("somebody@example.com" in m['Cc']) + # cc_token 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']) - self.assertTrue(draft.group.list_email) - self.assertTrue(draft.group.list_email in m['Cc']) r = self.client.post(url, dict(cc="")) self.assertEqual(r.status_code, 302) diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 3a2fa36d7..557022275 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -28,6 +28,7 @@ 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.mailtoken.utils import gather_address_lists +from ietf.mailtoken.forms import CcSelectForm BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -287,18 +288,21 @@ def send_ballot_comment(request, name, ballot_id): addrs = gather_address_lists('ballot_saved',doc=doc) if request.method == 'POST': - # The send_ballot_comments form provides an unusual case where the form separates out - # the cc addresses to be edited before sending into a separate field - # TODO: We should consider undoing this, and going back at most to an "extra_cc" model - 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,mailtoken_slug='ballot_saved',mailtoken_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, addrs.to, frm, subject, body, cc=u", ".join(cc)) return HttpResponseRedirect(return_to_url) + + else: + + cc_select_form = CcSelectForm(mailtoken_slug='ballot_saved',mailtoken_context={'doc':doc}) return render_to_response('doc/ballot/send_ballot_comment.html', dict(doc=doc, @@ -306,10 +310,10 @@ def send_ballot_comment(request, name, ballot_id): body=body, frm=frm, to=addrs.as_strings().to, - cc=addrs.as_strings().cc, ad=ad, can_send=d or c, back_url=back_url, + cc_select_form = cc_select_form, ), context_instance=RequestContext(request)) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index d1afb3a1c..1337441ee 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -111,7 +111,7 @@ def make_recipients(apps): rc(slug='group_chairs', desc="The group's chairs", - template="{{group.acronym}}-chairs@ietf.org") + template="<{{group.acronym}}-chairs@ietf.org>") rc(slug='group_responsible_directors', desc="The group's responsible AD(s) or IRTF chair", @@ -265,7 +265,9 @@ def make_mailtokens(apps): "(with discusses, other blocking positions, " "or comments) is saved", to_slugs=['iesg'], - cc_slugs=['doc_authors', + cc_slugs=['doc_notify', + 'doc_group_mail_list', + 'doc_authors', 'doc_group_chairs', 'doc_shepherd', 'doc_affecteddoc_authors', diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 805841d6d..8987a7408 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4473,7 +4473,7 @@ }, { "fields": { - "template": "{{group.acronym}}-chairs@ietf.org", + "template": "<{{group.acronym}}-chairs@ietf.org>", "desc": "The group's chairs" }, "model": "mailtoken.recipient", @@ -4897,6 +4897,8 @@ "doc_affecteddoc_notify", "doc_authors", "doc_group_chairs", + "doc_group_mail_list", + "doc_notify", "doc_shepherd" ], "to": [ diff --git a/ietf/templates/doc/ballot/send_ballot_comment.html b/ietf/templates/doc/ballot/send_ballot_comment.html index 6ee3710a9..fd9944f9a 100644 --- a/ietf/templates/doc/ballot/send_ballot_comment.html +++ b/ietf/templates/doc/ballot/send_ballot_comment.html @@ -25,30 +25,16 @@ +
+ {% bootstrap_form cc_select_form %} +
+
- - + +
Separate email addresses with commas.
- {% if doc.notify %} -
- -
- {% endif %} - - {% if doc.group.list_email %} -
- -
- {% endif %} -
From 275023f231974b67e29636bdf370fd0a1ec620dc Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 24 Aug 2015 19:37:22 +0000 Subject: [PATCH 26/46] Missed adding the new form - Legacy-Id: 10055 --- ietf/mailtoken/forms.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ietf/mailtoken/forms.py diff --git a/ietf/mailtoken/forms.py b/ietf/mailtoken/forms.py new file mode 100644 index 000000000..97e55f99f --- /dev/null +++ b/ietf/mailtoken/forms.py @@ -0,0 +1,31 @@ +from django import forms + +from ietf.mailtoken.models import MailToken + +class CcSelectForm(forms.Form): + expansions = dict() + cc_tokens = forms.MultipleChoiceField( + label='Cc', + choices=[], + widget=forms.CheckboxSelectMultiple(attrs={'frob':'knob'}), + ) + + def __init__(self, mailtoken_slug, mailtoken_context, *args, **kwargs): + super(CcSelectForm,self).__init__(*args,**kwargs) + mailtoken = MailToken.objects.get(slug=mailtoken_slug) + + for r in mailtoken.cc.all(): + self.expansions[r.slug] = r.gather(**mailtoken_context) + + non_empty_expansions = [x for x in self.expansions if self.expansions[x]] + self.fields['cc_tokens'].initial = non_empty_expansions + self.fields['cc_tokens'].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_tokens']: + addrs.extend(self.expansions[t]) + return addrs + else: + raise forms.ValidationError('Cannot get selected addresses from an invalid form.') From 9abd67058787b86e34655bc5e373eeeddc505496 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 24 Aug 2015 22:00:07 +0000 Subject: [PATCH 27/46] Rudimentary stab at an email-expansion tag for docs - Legacy-Id: 10056 --- ietf/doc/urls.py | 1 + ietf/doc/views_doc.py | 14 +++++ .../migrations/0002_auto_20150809_1314.py | 7 ++- ietf/mailtoken/utils.py | 44 ++++++++++++++ ietf/name/fixtures/names.json | 5 +- ietf/templates/doc/document_email.html | 57 +++++++++++++++++++ ietf/templates/mailtoken/recipient.html | 7 ++- 7 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 ietf/templates/doc/document_email.html diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index d9d8489ec..6c8108f44 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"), diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index e44355847..654b6f50c 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -59,6 +59,7 @@ 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_ad +from ietf.mailtoken.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"): @@ -572,6 +574,18 @@ def document_main(request, name, rev=None): +def document_email(request,name): + doc = get_object_or_404(Document, docalias__name=name) + top = render_document_top(request, doc, "email", name) + + expansions = gather_relevant_expansions(doc=doc) + + return render(request, "doc/document_email.html", + dict(doc=doc, + top=top, + expansions=expansions, + ) + ) def document_history(request, name): diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 1337441ee..ac75d4919 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -103,7 +103,7 @@ def make_recipients(apps): rc(slug='group_mail_list', desc="The group's mailing list", - template='<{{ group.list_email }}>') + template='{% if group.list_email %}<{{ group.list_email }}>{% endif %}') rc(slug='group_steering_group', desc="The group's steering group (IESG or IRSG)", @@ -111,7 +111,7 @@ def make_recipients(apps): rc(slug='group_chairs', desc="The group's chairs", - template="<{{group.acronym}}-chairs@ietf.org>") + 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", @@ -429,7 +429,8 @@ def make_mailtokens(apps): mt_factory(slug='doc_stream_changed', desc="Recipients for notification when a document's stream changes", - to_slugs=['stream_managers', + to_slugs=['doc_authors', + 'stream_managers', 'doc_notify', ]) diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index a3cdca85b..5f4bedb58 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -28,6 +28,50 @@ def gather_address_lists(slug, **kwargs): return AddrLists(to=list(to),cc=list(cc)) +def gather_relevant_expansions(**kwargs): + relevant = set() + + if 'doc' in kwargs: + + doc = kwargs['doc'] + + relevant.update(MailToken.objects.filter(slug__startswith='doc_').values_list('slug',flat=True)) + + if doc.stream_id == 'ietf': + relevant.update(['ballot_approved_ietf_stream']) + else: + relevant.update(['pubreq_rfced']) + + if doc.type_id in ['draft','statchg']: + relevant.update(MailToken.objects.filter(slug__startswith='last_call_').values_list('slug',flat=True)) + if doc.type_id == 'draft': + relevant.update(['ipr_posted_on_doc',]) + 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']) + + rule_list = [] + for mailtoken in MailToken.objects.filter(slug__in=relevant): + addrs = gather_address_lists(mailtoken.slug,**kwargs) + rule_list.append((mailtoken.slug,mailtoken.desc,addrs.to,addrs.cc)) + return sorted(rule_list) + +#def gather_relevant_expansions_recipient(**kwargs): +# relevant_tokens = [] +# +# if 'doc' in kwargs: +# relevant_tokens.extend(Recipient.objects.filter(slug__startswith='doc').values_list('slug',flat=True)) +# +# rule_dict = {} +# +# for recipient in Recipient.objects.filter(slug__in=relevant_tokens): +# #for recipient in Recipient.objects.all(): +# addrs = recipient.gather(**kwargs) +# if addrs: +# rule_dict[recipient.slug] = recipient.gather(**kwargs) +# return sorted(rule_dict.iteritems()) + def get_base_ipr_request_address(): return Recipient.objects.get(slug='ipr_requests').gather()[0] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 8987a7408..1bfb7923a 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4473,7 +4473,7 @@ }, { "fields": { - "template": "<{{group.acronym}}-chairs@ietf.org>", + "template": "{% if group and group.acronym %}<{{group.acronym}}-chairs@ietf.org>{% endif %}", "desc": "The group's chairs" }, "model": "mailtoken.recipient", @@ -4489,7 +4489,7 @@ }, { "fields": { - "template": "<{{ group.list_email }}>", + "template": "{% if group.list_email %}<{{ group.list_email }}>{% endif %}", "desc": "The group's mailing list" }, "model": "mailtoken.recipient", @@ -5081,6 +5081,7 @@ "fields": { "cc": [], "to": [ + "doc_authors", "doc_notify", "stream_managers" ], diff --git a/ietf/templates/doc/document_email.html b/ietf/templates/doc/document_email.html new file mode 100644 index 000000000..8eae383c3 --- /dev/null +++ b/ietf/templates/doc/document_email.html @@ -0,0 +1,57 @@ +{% 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 }} + +

Recipient Expansions

+ +
EventToken Recipients
{{mailtoken.slug}} - {% for recipient in mailtoken.recipients.all %} + To: + {% for recipient in mailtoken.to.all %} {% comment %}{{recipient.slug}}{% endcomment %} {{recipient.slug}}{% if not forloop.last %}, {% endif %} {% endfor %} + {% if mailtoken.cc.exists %} +
Cc: + {% for recipient in mailtoken.cc.all %} + {% comment %}{{recipient.slug}}{% endcomment %} + {{recipient.slug}}{% if not forloop.last %}, {% endif %} + {% endfor %} + {% endif %}
+ + + + + + + + + {% for token,desc,to,cc in expansions %} + + + + + + {% endfor %} + +
MailTokenToCc
{{token}} {{to|join:', '}} {{cc|join:', '}}
+{% comment %} + + + + + + + + + + + + {% for e in events %} + + + + + + + {% endfor %} + +
DateRev.ByAction
{{ e.time|date:"Y-m-d" }}{{ e.rev }}{{ e.by|escape }}{{ e.desc|format_history_text|safe }}
+{% endcomment %} +{% endblock content %} diff --git a/ietf/templates/mailtoken/recipient.html b/ietf/templates/mailtoken/recipient.html index 547854ead..9f2c24a01 100644 --- a/ietf/templates/mailtoken/recipient.html +++ b/ietf/templates/mailtoken/recipient.html @@ -23,8 +23,11 @@ {{recipient.slug}} - {% for mailtoken in recipient.mailtoken_set.all %} - {{mailtoken.slug}}{% if not forloop.last %}, {%endif%} + {% for mailtoken in recipient.used_in_to.all %} + {{mailtoken.slug}}{% if not forloop.last %}, {%endif%} + {% endfor %}{% if recipient.used_in_to.exists and recipient.used_in_cc.exists %},{% endif %} + {% for mailtoken in recipient.used_in_cc.all %} + {{mailtoken.slug}}{% if not forloop.last %}, {%endif%} {% endfor %} {{recipient.template}} From 46c5b3ff64797000a7dfcc354be81c2987dd61f2 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 25 Aug 2015 15:19:55 +0000 Subject: [PATCH 28/46] Moved doc-specific email aliases onto the email tab for the document - Legacy-Id: 10057 --- ietf/doc/tests.py | 12 +++++++--- ietf/doc/urls.py | 2 +- ietf/doc/views_doc.py | 31 ++++++++++++++++---------- ietf/mailtoken/utils.py | 29 +++++++----------------- ietf/templates/doc/document_email.html | 15 +++++++++++++ 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 53b023bf9..82e16238d 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -788,14 +788,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/urls.py b/ietf/doc/urls.py index 6c8108f44..0711b11cb 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -66,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/views_doc.py b/ietf/doc/views_doc.py index 654b6f50c..5b097876c 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -571,19 +571,34 @@ 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, ) ) @@ -999,20 +1014,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}) diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 5f4bedb58..5b8fd6e0f 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -35,17 +35,19 @@ def gather_relevant_expansions(**kwargs): doc = kwargs['doc'] - relevant.update(MailToken.objects.filter(slug__startswith='doc_').values_list('slug',flat=True)) - - if doc.stream_id == 'ietf': - relevant.update(['ballot_approved_ietf_stream']) - else: - relevant.update(['pubreq_rfced']) + relevant.update(['doc_state_edited','doc_telechat_details_changed','ballot_deferred','ballot_saved']) if doc.type_id in ['draft','statchg']: relevant.update(MailToken.objects.filter(slug__startswith='last_call_').values_list('slug',flat=True)) + if doc.type_id == 'draft': + relevant.update(MailToken.objects.filter(slug__startswith='doc_').values_list('slug',flat=True)) relevant.update(['ipr_posted_on_doc',]) + if doc.stream_id == 'ietf': + relevant.update(['ballot_approved_ietf_stream']) + else: + relevant.update(['pubreq_rfced']) + if doc.type_id == 'conflrev': relevant.update(['conflrev_requested','ballot_approved_conflrev']) if doc.type_id == 'charter': @@ -57,21 +59,6 @@ def gather_relevant_expansions(**kwargs): rule_list.append((mailtoken.slug,mailtoken.desc,addrs.to,addrs.cc)) return sorted(rule_list) -#def gather_relevant_expansions_recipient(**kwargs): -# relevant_tokens = [] -# -# if 'doc' in kwargs: -# relevant_tokens.extend(Recipient.objects.filter(slug__startswith='doc').values_list('slug',flat=True)) -# -# rule_dict = {} -# -# for recipient in Recipient.objects.filter(slug__in=relevant_tokens): -# #for recipient in Recipient.objects.all(): -# addrs = recipient.gather(**kwargs) -# if addrs: -# rule_dict[recipient.slug] = recipient.gather(**kwargs) -# return sorted(rule_dict.iteritems()) - def get_base_ipr_request_address(): return Recipient.objects.get(slug='ipr_requests').gather()[0] diff --git a/ietf/templates/doc/document_email.html b/ietf/templates/doc/document_email.html index 8eae383c3..c8542181e 100644 --- a/ietf/templates/doc/document_email.html +++ b/ietf/templates/doc/document_email.html @@ -10,6 +10,21 @@ {% origin %} {{ top|safe }} + {% if aliases %} +

Email Aliases

+ + + + {% for alias in aliases %} + + + + + {% endfor %} + +
{{ doc.name }}{{ alias.alias_type|default:''}}@{{ietf_domain}}{{ alias.expansion }}
+ {% endif %} +

Recipient Expansions

From e5a6ab438580a98089f403c9e317d272ef0d0fdf Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 25 Aug 2015 18:10:51 +0000 Subject: [PATCH 29/46] email expansions for groups - Legacy-Id: 10058 --- ietf/group/info.py | 45 +++++++++++++++++------- ietf/group/tests_info.py | 19 ++++++++--- ietf/group/urls.py | 8 +++-- ietf/group/urls_info.py | 5 +-- ietf/mailtoken/utils.py | 29 +++++++++++++--- ietf/templates/doc/document_email.html | 23 ------------- ietf/templates/group/email.html | 47 ++++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 ietf/templates/group/email.html diff --git a/ietf/group/info.py b/ietf/group/info.py index 4d36e1546..33ab37792 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -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.mailtoken.utils import gather_relevant_expansions def roles(group, role_name): return Role.objects.filter(group=group, name=role_name).select_related("email", "person") @@ -296,6 +297,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 »"), urlreverse("ietf.group.info.dependencies_pdf", kwargs=kwargs))) @@ -444,6 +446,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) @@ -638,22 +668,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}) diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index c259c6ba8..eb07f8ed1 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -1009,25 +1009,36 @@ expand-ames-chairs@virtual.ietf.org mars_chair@ietf def tearDown(self): os.unlink(self.group_alias_file.name) - def testNothing(self): - url = urlreverse('ietf.group.info.email_aliases', kwargs=dict(acronym="mars")) + def testAliases(self): + url = urlreverse('old_group_email_aliases', kwargs=dict(acronym="mars")) r = self.client.get(url) - 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@']])) + self.assertEqual(r.status_code, 302) 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): diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 08853bbbe..295e242c4 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -1,6 +1,8 @@ # Copyright The IETF Trust 2007, All Rights Reserved -from django.conf.urls import patterns +from django.conf.urls import patterns, url + +from django.views.generic import RedirectView urlpatterns = patterns('', (r'^groupmenu.json', 'ietf.group.ajax.group_menu_data', None, "group_menu_data"), @@ -15,6 +17,7 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-._]+)/$', 'ietf.group.info.group_home', None, "group_home"), (r'^(?P[a-zA-Z0-9-._]+)/documents/$', 'ietf.group.info.group_documents', None, "group_docs"), (r'^(?P[a-zA-Z0-9-._]+)/charter/$', 'ietf.group.info.group_about', None, 'group_charter'), + (r'^(?P[a-zA-Z0-9-._]+)/email/$', 'ietf.group.info.email'), (r'^(?P[a-zA-Z0-9-._]+)/history/$', 'ietf.group.info.history'), (r'^(?P[a-zA-Z0-9-._]+)/deps/dot/$', 'ietf.group.info.dependencies_dot'), (r'^(?P[a-zA-Z0-9-._]+)/deps/pdf/$', 'ietf.group.info.dependencies_pdf'), @@ -30,7 +33,6 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-._]+)/materials/$', 'ietf.group.info.materials', None, "group_materials"), (r'^(?P[a-zA-Z0-9-._]+)/materials/new/$', 'ietf.doc.views_material.choose_material_type'), (r'^(?P[a-zA-Z0-9-._]+)/materials/new/(?P[\w-]+)/$', 'ietf.doc.views_material.edit_material', { 'action': "new" }, "group_new_material"), - (r'^(?P[A-Za-z0-9._+-]+)/email-aliases/$', 'ietf.group.info.email_aliases'), + url(r'^(?P[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='ietf.group.info.email',permanent=False),name='old_group_email_aliases'), ) - diff --git a/ietf/group/urls_info.py b/ietf/group/urls_info.py index dea946c16..8ce2e9a99 100644 --- a/ietf/group/urls_info.py +++ b/ietf/group/urls_info.py @@ -1,6 +1,6 @@ # Copyright The IETF Trust 2008, All Rights Reserved -from django.conf.urls import patterns +from django.conf.urls import patterns, url from django.views.generic import RedirectView from ietf.group import info, edit, milestones @@ -23,6 +23,7 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-._]+)/$', info.group_home, None, "group_home"), (r'^(?P[a-zA-Z0-9-._]+)/documents/$', info.group_documents, None, "group_docs"), (r'^(?P[a-zA-Z0-9-._]+)/charter/$', info.group_about, None, 'group_charter'), + (r'^(?P[a-zA-Z0-9-._]+)/email/$', info.email), (r'^(?P[a-zA-Z0-9-._]+)/history/$', info.history), (r'^(?P[a-zA-Z0-9-._]+)/deps/dot/$', info.dependencies_dot), (r'^(?P[a-zA-Z0-9-._]+)/deps/pdf/$', info.dependencies_pdf), @@ -33,5 +34,5 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-._]+)/milestones/charter/$', milestones.edit_milestones, {'milestone_set': "charter"}, "group_edit_charter_milestones"), (r'^(?P[a-zA-Z0-9-._]+)/milestones/charter/reset/$', milestones.reset_charter_milestones, None, "group_reset_charter_milestones"), (r'^(?P[a-zA-Z0-9-._]+)/workflow/$', edit.customize_workflow), - (r'^(?P[A-Za-z0-9._+-]+)/email-aliases/$', 'ietf.group.info.email_aliases'), + url(r'^(?P[A-Za-z0-9._+-]+)/email-aliases/$', RedirectView.as_view(pattern_name='ietf.group.info.email',permanent=False),name='old_group_email_aliases'), ) diff --git a/ietf/mailtoken/utils.py b/ietf/mailtoken/utils.py index 5b8fd6e0f..199fa43c9 100644 --- a/ietf/mailtoken/utils.py +++ b/ietf/mailtoken/utils.py @@ -1,5 +1,6 @@ from collections import namedtuple from ietf.mailtoken.models import MailToken, Recipient +from ietf.submit.models import Submission class AddrLists(namedtuple('AddrLists',['to','cc'])): @@ -29,6 +30,10 @@ def gather_address_lists(slug, **kwargs): return AddrLists(to=list(to),cc=list(cc)) def gather_relevant_expansions(**kwargs): + + def starts_with(prefix): + return MailToken.objects.filter(slug__startswith=prefix).values_list('slug',flat=True) + relevant = set() if 'doc' in kwargs: @@ -38,25 +43,41 @@ def gather_relevant_expansions(**kwargs): relevant.update(['doc_state_edited','doc_telechat_details_changed','ballot_deferred','ballot_saved']) if doc.type_id in ['draft','statchg']: - relevant.update(MailToken.objects.filter(slug__startswith='last_call_').values_list('slug',flat=True)) + relevant.update(starts_with('last_call_')) if doc.type_id == 'draft': - relevant.update(MailToken.objects.filter(slug__startswith='doc_').values_list('slug',flat=True)) + 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']) + 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 mailtoken in MailToken.objects.filter(slug__in=relevant): addrs = gather_address_lists(mailtoken.slug,**kwargs) - rule_list.append((mailtoken.slug,mailtoken.desc,addrs.to,addrs.cc)) + if addrs.to or addrs.cc: + rule_list.append((mailtoken.slug,mailtoken.desc,addrs.to,addrs.cc)) return sorted(rule_list) def get_base_ipr_request_address(): diff --git a/ietf/templates/doc/document_email.html b/ietf/templates/doc/document_email.html index c8542181e..8832966ac 100644 --- a/ietf/templates/doc/document_email.html +++ b/ietf/templates/doc/document_email.html @@ -46,27 +46,4 @@ {% endfor %}
-{% comment %} - - - - - - - - - - - - {% for e in events %} - - - - - - - {% endfor %} - -
DateRev.ByAction
{{ e.time|date:"Y-m-d" }}{{ e.rev }}{{ e.by|escape }}{{ e.desc|format_history_text|safe }}
-{% endcomment %} {% endblock content %} diff --git a/ietf/templates/group/email.html b/ietf/templates/group/email.html new file mode 100644 index 000000000..0476a4266 --- /dev/null +++ b/ietf/templates/group/email.html @@ -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 %} +

Email Aliases

+ + + + {% for alias in aliases %} + + + + + {% endfor %} + +
{{ group.acronym }}{{ alias.alias_type|default:''}}@{{ietf_domain}}{{ alias.expansion }}
+ {% endif %} + +

Recipient Expansions

+ + + + + + + + + + + {% for token,desc,to,cc in expansions %} + + + + + + {% endfor %} + +
MailTokenToCc
{{token}} {{to|join:', '}} {{cc|join:', '}}
+ +{% endblock %} From 9d239843e65c6456fe3bde2b41352b8afd7c53c4 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 25 Aug 2015 18:41:04 +0000 Subject: [PATCH 30/46] New explicit "Document has been adopted by group" message - Legacy-Id: 10059 --- ietf/doc/mails.py | 18 ++++++++++++++++++ ietf/doc/tests_draft.py | 7 ++++--- ietf/doc/views_draft.py | 5 ++--- .../migrations/0002_auto_20150809_1314.py | 12 ++++++++++++ ietf/name/fixtures/names.json | 17 +++++++++++++++++ ietf/templates/doc/mail/doc_adopted_email.txt | 10 ++++++++++ 6 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 ietf/templates/doc/mail/doc_adopted_email.txt diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 2f0fce9f8..e0d2d40cf 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -417,6 +417,24 @@ def email_last_call_expired(doc): url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc = addrs.cc) +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, 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) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 3672b80ee..f4a0a4cda 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1135,9 +1135,10 @@ class AdoptDraftTests(TestCase): self.assertEqual(draft.docevent_set.count() - events_before, 5) 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("mars-chairs@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) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 629b0f48b..ab9ee420f 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -19,7 +19,7 @@ from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State, from ietf.doc.mails import ( email_ad, 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 ) 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, @@ -1301,8 +1301,7 @@ def adopt_draft(request, name): update_reminder(doc, "stream-s", e, due_date) - # TODO: Replace this with a message that's explicitly about the document adoption - 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: diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index ac75d4919..b3a6cf96c 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -771,6 +771,18 @@ def make_mailtokens(apps): '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', + ], + ) + def forward(apps, schema_editor): diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 1bfb7923a..ddc958807 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4961,6 +4961,23 @@ "model": "mailtoken.mailtoken", "pk": "conflrev_requested_iana" }, +{ + "fields": { + "cc": [ + "doc_ad", + "doc_notify", + "doc_shepherd" + ], + "to": [ + "doc_authors", + "doc_group_chairs", + "doc_group_mail_list" + ], + "desc": "Recipients for notification that a document has been adopted by a group" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_adopted_by_group" +}, { "fields": { "cc": [ diff --git a/ietf/templates/doc/mail/doc_adopted_email.txt b/ietf/templates/doc/mail/doc_adopted_email.txt new file mode 100644 index 000000000..e8dd9bbeb --- /dev/null +++ b/ietf/templates/doc/mail/doc_adopted_email.txt @@ -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 %} From 5c13bddd77ee22424e24cbffb0234d4b0cf0473a Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 25 Aug 2015 20:05:11 +0000 Subject: [PATCH 31/46] New explicit "Comment has been added to document history" message - Legacy-Id: 10060 --- ietf/doc/mails.py | 14 ++++++++++++++ ietf/doc/tests.py | 3 ++- ietf/doc/views_doc.py | 8 +++----- .../migrations/0002_auto_20150809_1314.py | 9 +++++++++ ietf/name/fixtures/names.json | 15 +++++++++++++++ ietf/templates/doc/mail/comment_added_email.txt | 12 ++++++++++++ 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 ietf/templates/doc/mail/comment_added_email.txt diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index e0d2d40cf..4cf2c8200 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -417,6 +417,20 @@ def email_last_call_expired(doc): url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc = addrs.cc) +def email_comment(request, doc, comment): + (to, cc) = gather_address_lists('doc_added_comment',doc=doc) + + 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_adopted(request, doc, prev_state, new_state, by, comment=""): (to, cc) = gather_address_lists('doc_adopted_by_group',doc=doc) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 82e16238d..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") diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 5b097876c..e60c1e0ff 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -58,7 +58,7 @@ 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_ad +from ietf.doc.mails import email_comment from ietf.mailtoken.utils import gather_relevant_expansions def render_document_top(request, doc, tab, name): @@ -906,10 +906,8 @@ def add_comment(request, name): e.desc = c e.save() - if doc.type_id == "draft": - # TODO - build an explicit message for when a comment is added - 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() diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index b3a6cf96c..06f4738cf 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -783,6 +783,15 @@ def make_mailtokens(apps): ], ) + 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', + ]) + def forward(apps, schema_editor): diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index ddc958807..351ea48ea 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4961,6 +4961,21 @@ "model": "mailtoken.mailtoken", "pk": "conflrev_requested_iana" }, +{ + "fields": { + "cc": [], + "to": [ + "doc_authors", + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_non_ietf_stream_manager", + "doc_shepherd" + ], + "desc": "Recipients for a message when a new comment is manually entered into the document's history" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_added_comment" +}, { "fields": { "cc": [ diff --git a/ietf/templates/doc/mail/comment_added_email.txt b/ietf/templates/doc/mail/comment_added_email.txt new file mode 100644 index 000000000..721c0e8e8 --- /dev/null +++ b/ietf/templates/doc/mail/comment_added_email.txt @@ -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%} From ef2260ea6364095c8ade04c04e17bde1a0498149 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 25 Aug 2015 20:28:01 +0000 Subject: [PATCH 32/46] New exlicit "Intended status changed" message - Legacy-Id: 10061 --- ietf/doc/mails.py | 14 ++++++++++++++ ietf/doc/tests_draft.py | 3 +++ ietf/doc/views_draft.py | 5 ++--- .../migrations/0002_auto_20150809_1314.py | 10 +++++++++- ietf/name/fixtures/names.json | 15 +++++++++++++++ .../doc/mail/intended_status_changed_email.txt | 5 +++++ 6 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 ietf/templates/doc/mail/intended_status_changed_email.txt diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 4cf2c8200..f2b7b45c7 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -417,6 +417,20 @@ def email_last_call_expired(doc): url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc = addrs.cc) +def email_intended_status_changed(request, doc, text): + (to,cc) = gather_address_lists('doc_intended_status_changed',doc=doc) + + 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) + def email_comment(request, doc, comment): (to, cc) = gather_address_lists('doc_added_comment',doc=doc) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index f4a0a4cda..29eaa9e12 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -789,7 +789,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): diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index ab9ee420f..805ac0d17 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -19,7 +19,7 @@ from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State, from ietf.doc.mails import ( email_ad, 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, email_adopted ) + generate_publication_request, email_adopted, email_intended_status_changed ) 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, @@ -449,8 +449,7 @@ def change_intention(request, name): doc.time = e.time doc.save() - # TODO: Build explicit changed_intended_publication_status - email_state_changed(request, doc, email_desc,'doc_state_edited') + email_intended_status_changed(request, doc, email_desc) return HttpResponseRedirect(doc.get_absolute_url()) diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 06f4738cf..80e069a27 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -792,7 +792,15 @@ def make_mailtokens(apps): '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', + ]) + def forward(apps, schema_editor): make_recipients(apps) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 351ea48ea..a2c596529 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -5043,6 +5043,21 @@ "model": "mailtoken.mailtoken", "pk": "doc_iana_state_changed" }, +{ + "fields": { + "cc": [], + "to": [ + "doc_authors", + "doc_group_chairs", + "doc_group_responsible_directors", + "doc_non_ietf_stream_manager", + "doc_shepherd" + ], + "desc": "Recipients for a message when a document's intended publication status changes" + }, + "model": "mailtoken.mailtoken", + "pk": "doc_intended_status_changed" +}, { "fields": { "cc": [ diff --git a/ietf/templates/doc/mail/intended_status_changed_email.txt b/ietf/templates/doc/mail/intended_status_changed_email.txt new file mode 100644 index 000000000..c4efcd490 --- /dev/null +++ b/ietf/templates/doc/mail/intended_status_changed_email.txt @@ -0,0 +1,5 @@ +{% autoescape off %}{{ text }} + +The document can be found at +ID Tracker URL: {{ url }} +{% endautoescape %} From 12a03d299bcc508544b89060ac4b7996980fa268 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 26 Aug 2015 21:31:11 +0000 Subject: [PATCH 33/46] Automatically send the Internal Review message that the secretary currently has to send manually - Legacy-Id: 10063 --- ietf/doc/mails.py | 25 ++++++++++++++++- ietf/doc/tests_charter.py | 9 ++++-- ietf/doc/views_charter.py | 11 +++++--- .../migrations/0002_auto_20150809_1314.py | 15 +++++++++- ietf/name/fixtures/names.json | 21 ++++++++++++++ .../doc/mail/charter_internal_review.txt | 28 +++++++++++++++++++ 6 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 ietf/templates/doc/mail/charter_internal_review.txt diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index f2b7b45c7..005f80478 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 @@ -10,7 +11,7 @@ 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 -from ietf.doc.utils import needed_ballot_positions +from ietf.doc.utils import needed_ballot_positions, get_document_content from ietf.person.models import Person from ietf.group.models import Role from ietf.doc.models import Document @@ -513,3 +514,25 @@ def send_review_possibly_replaces_request(request, 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),) + +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_charter.py b/ietf/doc/tests_charter.py index 0e72680ab..cfc1dc28c 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -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,7 +97,11 @@ class EditCharterTests(TestCase): if slug in ("intrev", "iesgrev"): self.assertTrue(find_event("created_ballot")) - self.assertEqual(len(outbox), mailbox_before + 2) + 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']) diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index 2cfb56171..dc83619da 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -20,7 +20,7 @@ from ietf.doc.utils import ( add_state_change_event, close_open_ballots, from ietf.doc.utils_charter import ( historic_milestones_for_charter, 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 +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 @@ -140,6 +140,9 @@ def change_state(request, name, option=None): charter.time = datetime.datetime.now() charter.save() + if charter_state.slug == 'intrev': + email_charter_internal_review(request,charter) + 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') @@ -204,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": diff --git a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py index 80e069a27..99d2bf26a 100644 --- a/ietf/mailtoken/migrations/0002_auto_20150809_1314.py +++ b/ietf/mailtoken/migrations/0002_auto_20150809_1314.py @@ -13,6 +13,10 @@ def make_recipients(apps): desc='The IESG', template='The IESG ') + rc(slug='iab', + desc='The IAB', + template='The IAB ') + rc(slug='ietf_announce', desc='The IETF Announce list', template='IETF-Announce ') @@ -461,6 +465,7 @@ def make_mailtokens(apps): 'doc_shepherd', 'doc_group_chairs', 'doc_affecteddoc_authors', + 'doc_group_responsible_directors', 'doc_affecteddoc_group_chairs', 'doc_affecteddoc_notify', ]) @@ -793,13 +798,21 @@ def make_mailtokens(apps): ]) mt_factory(slug='doc_intended_status_changed', - desc="Recipients for a message when a document's intended publication status changes", + 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='charter_internal_review', + desc="Recipients for message noting that internal review has " + "started on a charter", + to_slugs=['iesg', + 'iab', + ]) def forward(apps, schema_editor): diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index a2c596529..0337d1a40 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4511,6 +4511,14 @@ "model": "mailtoken.recipient", "pk": "group_steering_group" }, +{ + "fields": { + "template": "The IAB ", + "desc": "The IAB" + }, + "model": "mailtoken.recipient", + "pk": "iab" +}, { "fields": { "template": "", @@ -4922,6 +4930,18 @@ "model": "mailtoken.mailtoken", "pk": "charter_external_review" }, +{ + "fields": { + "cc": [], + "to": [ + "iab", + "iesg" + ], + "desc": "Recipients for message noting that internal review has started on a charter" + }, + "model": "mailtoken.mailtoken", + "pk": "charter_internal_review" +}, { "fields": { "cc": [], @@ -5116,6 +5136,7 @@ "doc_affecteddoc_notify", "doc_authors", "doc_group_chairs", + "doc_group_responsible_directors", "doc_notify", "doc_shepherd" ], diff --git a/ietf/templates/doc/mail/charter_internal_review.txt b/ietf/templates/doc/mail/charter_internal_review.txt new file mode 100644 index 000000000..87885466a --- /dev/null +++ b/ietf/templates/doc/mail/charter_internal_review.txt @@ -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%} From 150106619b9c0c1d1e5f4d6093e1febfd14e0df2 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 27 Aug 2015 21:27:10 +0000 Subject: [PATCH 34/46] Handle the new-work message - Legacy-Id: 10066 --- ietf/doc/tests_charter.py | 125 +++++++++++++----- ietf/doc/urls_charter.py | 3 +- ietf/doc/utils_charter.py | 58 +++++--- ietf/doc/views_charter.py | 121 ++++++++++++++--- ietf/doc/views_doc.py | 4 +- .../migrations/0002_auto_20150809_1314.py | 22 ++- ietf/name/fixtures/names.json | 19 +++ ...ext.html => action_announcement_text.html} | 8 +- ietf/templates/doc/charter/approve.html | 2 +- .../doc/charter/review_announcement_text.html | 34 +++++ 10 files changed, 305 insertions(+), 91 deletions(-) rename ietf/templates/doc/charter/{announcement_text.html => action_announcement_text.html} (65%) create mode 100644 ietf/templates/doc/charter/review_announcement_text.html diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index cfc1dc28c..2ea147f36 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -9,7 +9,7 @@ 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 @@ -273,46 +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) - self.assertTrue('WG Review' in outbox[-1]['Subject']) - self.assertTrue('ietf-announce@' in outbox[-1]['To']) - self.assertTrue('mars-wg@' in outbox[-1]['Cc']) + # 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() 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