Merge lp:~cjwatson/launchpad/team-mail into lp:launchpad

Proposed by Colin Watson on 2015-08-27
Status: Merged
Merged at revision: 17719
Proposed branch: lp:~cjwatson/launchpad/team-mail
Merge into: lp:launchpad
Diff against target: 2820 lines (+1263/-549)
31 files modified
database/schema/security.cfg (+3/-0)
lib/lp/code/mail/branchmergeproposal.py (+2/-2)
lib/lp/code/mail/tests/test_codereviewcomment.py (+6/-2)
lib/lp/registry/configure.zcml (+40/-0)
lib/lp/registry/doc/teammembership-email-notification.txt (+327/-91)
lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt (+2/-3)
lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt (+2/-3)
lib/lp/registry/emailtemplates/membership-expired-bulk.txt (+2/-2)
lib/lp/registry/emailtemplates/membership-expired-personal.txt (+2/-2)
lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt (+2/-2)
lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt (+2/-2)
lib/lp/registry/emailtemplates/membership-invitation.txt (+1/-1)
lib/lp/registry/emailtemplates/membership-member-renewed.txt (+2/-2)
lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt (+2/-2)
lib/lp/registry/emailtemplates/membership-statuschange-personal.txt (+2/-2)
lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt (+5/-5)
lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt (+1/-1)
lib/lp/registry/emailtemplates/new-member-notification.txt (+1/-1)
lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt (+5/-5)
lib/lp/registry/emailtemplates/pending-membership-approval.txt (+5/-5)
lib/lp/registry/enums.py (+25/-0)
lib/lp/registry/interfaces/persontransferjob.py (+93/-1)
lib/lp/registry/mail/notification.py (+8/-182)
lib/lp/registry/mail/teammembership.py (+435/-0)
lib/lp/registry/model/persontransferjob.py (+187/-119)
lib/lp/registry/model/teammembership.py (+7/-96)
lib/lp/registry/stories/person/xx-approve-members.txt (+1/-1)
lib/lp/registry/tests/test_teammembership.py (+26/-6)
lib/lp/services/config/schema-lazr.conf (+24/-0)
lib/lp/services/mail/basemailer.py (+12/-3)
lib/lp/testing/mail_helpers.py (+31/-8)
To merge this branch: bzr merge lp:~cjwatson/launchpad/team-mail
Reviewer Review Type Date Requested Status
William Grant code 2015-08-27 Approve on 2015-09-09
Review via email: mp+269382@code.launchpad.net

Commit message

Convert team membership notifications to BaseMailer.

Description of the change

Convert team membership notifications to BaseMailer.

The main subtlety here is that there may be multiple teams involved (for example, you may be receiving a notification about a change in one team due to your membership of another team), so I had to think quite carefully to make sure that the semantics of X-Launchpad-Message-Rationale construction were followed properly. Also, I've tried to ensure that X-Launchpad-Message-Rationale describes your relation to the object that causes you to receive the message, while X-Launchpad-Notification-Type describes what action was performed. The diff to lib/lp/registry/doc/teammembership-email-notification.txt serves as a fairly good summary.

To post a comment you must log in.
William Grant (wgrant) wrote :

I realise that most of the notification type names are adapted from the template filenames, but they weren't previously exposed and they seem deliberately designed to make filtering as difficult as possible. I'd call them "team-membership-new", "team-membership-pending" etc. so people can easily blackhole all notifications about a particularly boring team's membership.

I'd also call the new thing TeamMembershipMailer, and put it in teammembership.py, as there are other types of team emails.

review: Approve (code)
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2015-09-03 15:14:07 +0000
3+++ database/schema/security.cfg 2015-09-08 11:57:29 +0000
4@@ -2124,9 +2124,12 @@
5 public.account = SELECT
6 public.emailaddress = SELECT
7 public.job = SELECT, UPDATE
8+public.mailinglist = SELECT
9 public.person = SELECT
10+public.personsettings = SELECT
11 public.persontransferjob = SELECT
12 public.teammembership = SELECT
13+public.teamparticipation = SELECT
14 type=user
15
16 [person-merge-job]
17
18=== modified file 'lib/lp/code/mail/branchmergeproposal.py'
19--- lib/lp/code/mail/branchmergeproposal.py 2015-09-01 17:10:46 +0000
20+++ lib/lp/code/mail/branchmergeproposal.py 2015-09-08 11:57:29 +0000
21@@ -1,4 +1,4 @@
22-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
23+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
24 # GNU Affero General Public License version 3 (see the file LICENSE).
25
26 """Email notifications related to branch merge proposals."""
27@@ -96,7 +96,7 @@
28 merge_proposal, from_address, message_id=get_msgid(),
29 preview_diff=merge_proposal.preview_diff, direct_email=True)
30
31- def _getReplyToAddress(self):
32+ def _getReplyToAddress(self, email, recipient):
33 """Return the address to use for the reply-to header."""
34 return self.merge_proposal.address
35
36
37=== modified file 'lib/lp/code/mail/tests/test_codereviewcomment.py'
38--- lib/lp/code/mail/tests/test_codereviewcomment.py 2015-09-02 16:54:24 +0000
39+++ lib/lp/code/mail/tests/test_codereviewcomment.py 2015-09-08 11:57:29 +0000
40@@ -140,7 +140,10 @@
41 mailer, subscriber = self.makeMailer()
42 merge_proposal = mailer.code_review_comment.branch_merge_proposal
43 expected = 'mp+%d@code.launchpad.dev' % merge_proposal.id
44- self.assertEqual(expected, mailer._getReplyToAddress())
45+ self.assertEqual(
46+ expected,
47+ mailer._getReplyToAddress(
48+ subscriber.preferredemail.email, subscriber))
49
50 def test_generateEmail(self):
51 """Ensure mailer's generateEmail method produces expected values."""
52@@ -164,7 +167,8 @@
53 'X-Launchpad-Notification-Type': 'code-review',
54 'X-Launchpad-Project': source_branch.product.name,
55 'Message-Id': message.rfc822msgid,
56- 'Reply-To': mailer._getReplyToAddress(),
57+ 'Reply-To': mailer._getReplyToAddress(
58+ subscriber.preferredemail.email, subscriber),
59 'In-Reply-To': message.parent.rfc822msgid}
60 for header, value in expected.items():
61 self.assertEqual(value, ctrl.headers[header], header)
62
63=== modified file 'lib/lp/registry/configure.zcml'
64--- lib/lp/registry/configure.zcml 2015-06-26 14:00:41 +0000
65+++ lib/lp/registry/configure.zcml 2015-09-08 11:57:29 +0000
66@@ -104,6 +104,30 @@
67 <allow interface=".interfaces.persontransferjob.IPersonDeactivateJobSource"/>
68 </securedutility>
69
70+ <securedutility
71+ component=".model.persontransferjob.TeamInvitationNotificationJob"
72+ provides=".interfaces.persontransferjob.ITeamInvitationNotificationJobSource">
73+ <allow interface=".interfaces.persontransferjob.ITeamInvitationNotificationJobSource"/>
74+ </securedutility>
75+
76+ <securedutility
77+ component=".model.persontransferjob.TeamJoinNotificationJob"
78+ provides=".interfaces.persontransferjob.ITeamJoinNotificationJobSource">
79+ <allow interface=".interfaces.persontransferjob.ITeamJoinNotificationJobSource"/>
80+ </securedutility>
81+
82+ <securedutility
83+ component=".model.persontransferjob.ExpiringMembershipNotificationJob"
84+ provides=".interfaces.persontransferjob.IExpiringMembershipNotificationJobSource">
85+ <allow interface=".interfaces.persontransferjob.IExpiringMembershipNotificationJobSource"/>
86+ </securedutility>
87+
88+ <securedutility
89+ component=".model.persontransferjob.SelfRenewalNotificationJob"
90+ provides=".interfaces.persontransferjob.ISelfRenewalNotificationJobSource">
91+ <allow interface=".interfaces.persontransferjob.ISelfRenewalNotificationJobSource"/>
92+ </securedutility>
93+
94 <class class=".model.persontransferjob.PersonTransferJob">
95 <allow interface=".interfaces.persontransferjob.IPersonTransferJob"/>
96 </class>
97@@ -120,6 +144,22 @@
98 <allow interface=".interfaces.persontransferjob.IPersonDeactivateJob"/>
99 </class>
100
101+ <class class=".model.persontransferjob.TeamInvitationNotificationJob">
102+ <allow interface=".interfaces.persontransferjob.ITeamInvitationNotificationJob"/>
103+ </class>
104+
105+ <class class=".model.persontransferjob.TeamJoinNotificationJob">
106+ <allow interface=".interfaces.persontransferjob.ITeamJoinNotificationJob"/>
107+ </class>
108+
109+ <class class=".model.persontransferjob.ExpiringMembershipNotificationJob">
110+ <allow interface=".interfaces.persontransferjob.IExpiringMembershipNotificationJob"/>
111+ </class>
112+
113+ <class class=".model.persontransferjob.SelfRenewalNotificationJob">
114+ <allow interface=".interfaces.persontransferjob.ISelfRenewalNotificationJob"/>
115+ </class>
116+
117 <!-- IProductNotificationJob -->
118 <securedutility
119 component=".model.productjob.CommercialExpiredJob"
120
121=== modified file 'lib/lp/registry/doc/teammembership-email-notification.txt'
122--- lib/lp/registry/doc/teammembership-email-notification.txt 2015-02-26 03:00:35 +0000
123+++ lib/lp/registry/doc/teammembership-email-notification.txt 2015-09-08 11:57:29 +0000
124@@ -84,10 +84,13 @@
125
126 >>> print_distinct_emails(include_reply_to=True)
127 From: Ubuntu Team <noreply@launchpad.net>
128- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
129- jeff.waugh@ubuntulinux.com, limi@plone.org
130+ To: Alexander Limi <limi@plone.org>,
131+ Colin Watson <colin.watson@ubuntulinux.com>,
132+ Foo Bar <foo.bar@canonical.com>,
133+ Jeff Waugh <jeff.waugh@ubuntulinux.com>
134 Reply-To: robertc@robertcollins.net
135 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
136+ X-Launchpad-Notification-Type: team-membership-pending
137 Subject: lifeless wants to join
138 <BLANKLINE>
139 Robert Collins (lifeless) wants to be a member of Ubuntu Team (ubuntu-
140@@ -100,14 +103,17 @@
141 -- =
142 <BLANKLINE>
143 You received this email because you are an admin of the Ubuntu Team team.
144+ <BLANKLINE>
145 ----------------------------------------
146 From: Ubuntu Team <noreply@launchpad.net>
147- To: mark@example.com
148+ To: Mark Shuttleworth <mark@example.com>
149 Reply-To: robertc@robertcollins.net
150 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
151+ X-Launchpad-Notification-Type: team-membership-pending
152 Subject: lifeless wants to join
153 ...
154 You received this email because you are the owner of the Ubuntu Team team.
155+ <BLANKLINE>
156 ----------------------------------------
157
158 Declining a proposed member should generate notifications for both the
159@@ -128,22 +134,39 @@
160
161 >>> print_distinct_emails()
162 From: Ubuntu Team <noreply@launchpad.net>
163- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
164- jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com
165+ To: Alexander Limi <limi@plone.org>,
166+ Colin Watson <colin.watson@ubuntulinux.com>,
167+ Foo Bar <foo.bar@canonical.com>,
168+ Jeff Waugh <jeff.waugh@ubuntulinux.com>,
169+ Mark Shuttleworth <mark@example.com>
170+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
171+ X-Launchpad-Notification-Type: team-membership-change
172 Subject: lifeless declined by mark
173 <BLANKLINE>
174 The membership status of Robert Collins (lifeless) in the team Ubuntu
175 Team (ubuntu-team) was changed by Mark Shuttleworth (mark) from
176 Proposed to Declined.
177 <http://launchpad.dev/~ubuntu-team>
178+ <BLANKLINE>
179+ -- =
180+ <BLANKLINE>
181+ You received this email because you are an admin of the Ubuntu Team team.
182+ <BLANKLINE>
183 ----------------------------------------
184 From: Ubuntu Team <noreply@launchpad.net>
185- To: robertc@robertcollins.net
186+ To: Robert Collins <robertc@robertcollins.net>
187+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
188+ X-Launchpad-Notification-Type: team-membership-change
189 Subject: lifeless declined by mark
190 <BLANKLINE>
191 The status of your membership in the team Ubuntu Team (ubuntu-team) was
192 changed by Mark Shuttleworth (mark) from Proposed to Declined.
193 <http://launchpad.dev/~ubuntu-team>
194+ <BLANKLINE>
195+ -- =
196+ <BLANKLINE>
197+ You received this email because you are the affected member.
198+ <BLANKLINE>
199 ----------------------------------------
200
201 The same goes for approving a proposed member.
202@@ -157,7 +180,7 @@
203 # Remove notification of daf's membership pending approval from
204 # stub.test_emails
205
206- >>> transaction.commit()
207+ >>> run_mail_jobs()
208 >>> dummy = pop_notifications()
209
210 >>> setStatus(daf_membership, TeamMembershipStatus.APPROVED,
211@@ -169,8 +192,13 @@
212
213 >>> print_distinct_emails()
214 From: Ubuntu Team <noreply@launchpad.net>
215- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
216- jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com
217+ To: Alexander Limi <limi@plone.org>,
218+ Colin Watson <colin.watson@ubuntulinux.com>,
219+ Foo Bar <foo.bar@canonical.com>,
220+ Jeff Waugh <jeff.waugh@ubuntulinux.com>,
221+ Mark Shuttleworth <mark@example.com>
222+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
223+ X-Launchpad-Notification-Type: team-membership-change
224 Subject: daf approved by mark
225 <BLANKLINE>
226 The membership status of Dafydd Harries (daf) in the team Ubuntu Team
227@@ -180,9 +208,15 @@
228 <BLANKLINE>
229 Mark Shuttleworth said:
230 This is a nice guy; I like him
231+ -- =
232+ <BLANKLINE>
233+ You received this email because you are an admin of the Ubuntu Team team.
234+ <BLANKLINE>
235 ----------------------------------------
236 From: Ubuntu Team <noreply@launchpad.net>
237- To: daf@canonical.com
238+ To: Dafydd Harries <daf@canonical.com>
239+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
240+ X-Launchpad-Notification-Type: team-membership-change
241 Subject: daf approved by mark
242 <BLANKLINE>
243 The status of your membership in the team Ubuntu Team (ubuntu-team) was
244@@ -191,6 +225,10 @@
245 <BLANKLINE>
246 Mark Shuttleworth said:
247 This is a nice guy; I like him
248+ -- =
249+ <BLANKLINE>
250+ You received this email because you are the affected member.
251+ <BLANKLINE>
252 ----------------------------------------
253
254 The same for deactivating a membership.
255@@ -204,22 +242,37 @@
256
257 >>> print_distinct_emails()
258 From: Ubuntu Team <noreply@launchpad.net>
259- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
260- jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com
261+ To: Alexander Limi <limi@plone.org>,
262+ Colin Watson <colin.watson@ubuntulinux.com>,
263+ Foo Bar <foo.bar@canonical.com>,
264+ Jeff Waugh <jeff.waugh@ubuntulinux.com>,
265+ Mark Shuttleworth <mark@example.com>
266+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
267+ X-Launchpad-Notification-Type: team-membership-change
268 Subject: daf deactivated by mark
269 <BLANKLINE>
270 The membership status of Dafydd Harries (daf) in the team Ubuntu Team
271 (ubuntu-team) was changed by Mark Shuttleworth (mark) from Approved to
272 Deactivated.
273 <http://launchpad.dev/~ubuntu-team>
274+ -- =
275+ <BLANKLINE>
276+ You received this email because you are an admin of the Ubuntu Team team.
277+ <BLANKLINE>
278 ----------------------------------------
279 From: Ubuntu Team <noreply@launchpad.net>
280- To: daf@canonical.com
281+ To: Dafydd Harries <daf@canonical.com>
282+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
283+ X-Launchpad-Notification-Type: team-membership-change
284 Subject: daf deactivated by mark
285 <BLANKLINE>
286 The status of your membership in the team Ubuntu Team (ubuntu-team) was
287 changed by Mark Shuttleworth (mark) from Approved to Deactivated.
288 <http://launchpad.dev/~ubuntu-team>
289+ -- =
290+ <BLANKLINE>
291+ You received this email because you are the affected member.
292+ <BLANKLINE>
293 ----------------------------------------
294
295 Team admins can propose their teams using the join() method as well, but
296@@ -235,10 +288,13 @@
297
298 >>> print_distinct_emails(include_reply_to=True)
299 From: Ubuntu Team <noreply@launchpad.net>
300- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
301- jeff.waugh@ubuntulinux.com, limi@plone.org
302+ To: Alexander Limi <limi@plone.org>,
303+ Colin Watson <colin.watson@ubuntulinux.com>,
304+ Foo Bar <foo.bar@canonical.com>,
305+ Jeff Waugh <jeff.waugh@ubuntulinux.com>
306 Reply-To: mark@example.com
307 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
308+ X-Launchpad-Notification-Type: team-membership-pending
309 Subject: admins wants to join
310 <BLANKLINE>
311 Mark Shuttleworth (mark) wants to make Launchpad Administrators
312@@ -251,14 +307,17 @@
313 -- =
314 <BLANKLINE>
315 You received this email because you are an admin of the Ubuntu Team team.
316+ <BLANKLINE>
317 ----------------------------------------
318 From: Ubuntu Team <noreply@launchpad.net>
319- To: mark@example.com
320+ To: Mark Shuttleworth <mark@example.com>
321 Reply-To: mark@example.com
322 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
323+ X-Launchpad-Notification-Type: team-membership-pending
324 Subject: admins wants to join
325 ...
326 You received this email because you are the owner of the Ubuntu Team team.
327+ <BLANKLINE>
328 ----------------------------------------
329
330
331@@ -281,22 +340,27 @@
332
333 >>> print_distinct_emails()
334 From: Ubuntu Team <noreply@launchpad.net>
335- To: marilize@hbd.com
336+ To: Marilize Coetzee <marilize@hbd.com>
337 X-Launchpad-Message-Rationale: Member (ubuntu-team)
338+ X-Launchpad-Notification-Type: team-membership-new
339 Subject: You have been added to ubuntu-team
340 <BLANKLINE>
341 Celso Providelo (cprov) added you as a member of Ubuntu Team (ubuntu-
342 team).
343- <http://launchpad.dev/~ubuntu-team>
344+ <http://launchpad.dev/~ubuntu-team>
345 <BLANKLINE>
346 -- =
347 <BLANKLINE>
348 You received this email because you are the new member.
349+ <BLANKLINE>
350 ----------------------------------------
351 From: Ubuntu Team <noreply@launchpad.net>
352- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
353- jeff.waugh@ubuntulinux.com, limi@plone.org
354+ To: Alexander Limi <limi@plone.org>,
355+ Colin Watson <colin.watson@ubuntulinux.com>,
356+ Foo Bar <foo.bar@canonical.com>,
357+ Jeff Waugh <jeff.waugh@ubuntulinux.com>
358 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
359+ X-Launchpad-Notification-Type: team-membership-new
360 Subject: marilize joined ubuntu-team
361 <BLANKLINE>
362 Marilize Coetzee (marilize) has been added as a member of Ubuntu Team
363@@ -308,13 +372,16 @@
364 -- =
365 <BLANKLINE>
366 You received this email because you are an admin of the Ubuntu Team team.
367+ <BLANKLINE>
368 ----------------------------------------
369 From: Ubuntu Team <noreply@launchpad.net>
370- To: mark@example.com
371+ To: Mark Shuttleworth <mark@example.com>
372 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
373+ X-Launchpad-Notification-Type: team-membership-new
374 Subject: marilize joined ubuntu-team
375 ...
376 You received this email because you are the owner of the Ubuntu Team team.
377+ <BLANKLINE>
378 ----------------------------------------
379
380 By default, if the newly added member is actually a team, we'll only
381@@ -332,7 +399,9 @@
382
383 >>> print_distinct_emails()
384 From: Ubuntu Team <noreply@launchpad.net>
385- To: mark@example.com
386+ To: Mark Shuttleworth <mark@example.com>
387+ X-Launchpad-Message-Rationale: Admin (ubuntu-mirror-admins)
388+ X-Launchpad-Notification-Type: team-membership-invitation
389 Subject: Invitation for ubuntu-mirror-admins to join
390 <BLANKLINE>
391 Celso Providelo (cprov) has invited Mirror Administrators (ubuntu-
392@@ -346,6 +415,13 @@
393 <BLANKLINE>
394 Regards,
395 The Launchpad team
396+ <BLANKLINE>
397+ -- =
398+ <BLANKLINE>
399+ You received this email because you are an admin of the Mirror
400+ Administrato=
401+ rs team.
402+ <BLANKLINE>
403 ----------------------------------------
404
405 If one of the admins accept the invitation, then a notification is sent
406@@ -362,18 +438,48 @@
407
408 >>> print_distinct_emails()
409 From: Ubuntu Team <noreply@launchpad.net>
410- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
411- jeff.waugh@ubuntulinux.com, karl@canonical.com, limi@plone.org,
412- mark@example.com
413- Subject: Invitation to ubuntu-mirror-admins accepted by mark
414- <BLANKLINE>
415- Mark Shuttleworth (mark) has accepted the invitation to make Mirror
416- Administrators (ubuntu-mirror-admins) a member of Ubuntu Team (ubuntu-
417- team).
418- <http://launchpad.dev/~ubuntu-team>
419- <BLANKLINE>
420- Mark Shuttleworth said:
421- Of course I want to be part of ubuntu!
422+ To: Alexander Limi <limi@plone.org>,
423+ Colin Watson <colin.watson@ubuntulinux.com>,
424+ Foo Bar <foo.bar@canonical.com>,
425+ Jeff Waugh <jeff.waugh@ubuntulinux.com>
426+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
427+ X-Launchpad-Notification-Type: team-membership-invitation-accepted
428+ Subject: Invitation to ubuntu-mirror-admins accepted by mark
429+ <BLANKLINE>
430+ Mark Shuttleworth (mark) has accepted the invitation to make Mirror
431+ Administrators (ubuntu-mirror-admins) a member of Ubuntu Team (ubuntu-
432+ team).
433+ <http://launchpad.dev/~ubuntu-team>
434+ <BLANKLINE>
435+ Mark Shuttleworth said:
436+ Of course I want to be part of ubuntu!
437+ <BLANKLINE>
438+ -- =
439+ <BLANKLINE>
440+ You received this email because you are an admin of the Ubuntu Team team.
441+ <BLANKLINE>
442+ ----------------------------------------
443+ From: Ubuntu Team <noreply@launchpad.net>
444+ To: Karl Tilbury <karl@canonical.com>,
445+ Mark Shuttleworth <mark@example.com>
446+ X-Launchpad-Message-Rationale: Member (ubuntu-team) @ubuntu-mirror-admins
447+ X-Launchpad-Notification-Type: team-membership-invitation-accepted
448+ Subject: Invitation to ubuntu-mirror-admins accepted by mark
449+ <BLANKLINE>
450+ Mark Shuttleworth (mark) has accepted the invitation to make Mirror
451+ Administrators (ubuntu-mirror-admins) a member of Ubuntu Team (ubuntu-
452+ team).
453+ <http://launchpad.dev/~ubuntu-team>
454+ <BLANKLINE>
455+ Mark Shuttleworth said:
456+ Of course I want to be part of ubuntu!
457+ <BLANKLINE>
458+ -- =
459+ <BLANKLINE>
460+ You received this email because your team Mirror Administrators is the
461+ affe=
462+ cted member.
463+ <BLANKLINE>
464 ----------------------------------------
465
466 Similarly, a notification is sent if the invitation is declined.
467@@ -384,7 +490,7 @@
468 # Reset stub.test_emails as we don't care about the notification triggered
469 # by the addMember() call.
470
471- >>> transaction.commit()
472+ >>> run_mail_jobs()
473 >>> stub.test_emails = []
474
475 >>> comment = "Landscape has nothing to do with ubuntu, unfortunately."
476@@ -397,17 +503,47 @@
477
478 >>> print_distinct_emails()
479 From: Ubuntu Team <noreply@launchpad.net>
480- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
481- guilherme.salgado@canonical.com, jeff.waugh@ubuntulinux.com,
482- limi@plone.org, mark@example.com, test@canonical.com
483- Subject: Invitation to landscape-developers declined by mark
484- <BLANKLINE>
485- Mark Shuttleworth (mark) has declined the invitation to make Landscape
486- Developers (landscape-developers) a member of Ubuntu Team (ubuntu-team).
487- <http://launchpad.dev/~ubuntu-team>
488- <BLANKLINE>
489- Mark Shuttleworth said:
490- Landscape has nothing to do with ubuntu, unfortunately.
491+ To: Alexander Limi <limi@plone.org>,
492+ Colin Watson <colin.watson@ubuntulinux.com>,
493+ Foo Bar <foo.bar@canonical.com>,
494+ Jeff Waugh <jeff.waugh@ubuntulinux.com>,
495+ Mark Shuttleworth <mark@example.com>
496+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
497+ X-Launchpad-Notification-Type: team-membership-invitation-declined
498+ Subject: Invitation to landscape-developers declined by mark
499+ <BLANKLINE>
500+ Mark Shuttleworth (mark) has declined the invitation to make Landscape
501+ Developers (landscape-developers) a member of Ubuntu Team (ubuntu-team).
502+ <http://launchpad.dev/~ubuntu-team>
503+ <BLANKLINE>
504+ Mark Shuttleworth said:
505+ Landscape has nothing to do with ubuntu, unfortunately.
506+ <BLANKLINE>
507+ -- =
508+ <BLANKLINE>
509+ You received this email because you are an admin of the Ubuntu Team team.
510+ <BLANKLINE>
511+ ----------------------------------------
512+ From: Ubuntu Team <noreply@launchpad.net>
513+ To: Guilherme Salgado <guilherme.salgado@canonical.com>,
514+ Sample Person <test@canonical.com>
515+ X-Launchpad-Message-Rationale: Member (ubuntu-team) @landscape-developers
516+ X-Launchpad-Notification-Type: team-membership-invitation-declined
517+ Subject: Invitation to landscape-developers declined by mark
518+ <BLANKLINE>
519+ Mark Shuttleworth (mark) has declined the invitation to make Landscape
520+ Developers (landscape-developers) a member of Ubuntu Team (ubuntu-team).
521+ <http://launchpad.dev/~ubuntu-team>
522+ <BLANKLINE>
523+ Mark Shuttleworth said:
524+ Landscape has nothing to do with ubuntu, unfortunately.
525+ <BLANKLINE>
526+ -- =
527+ <BLANKLINE>
528+ You received this email because your team Landscape Developers is the
529+ affec=
530+ ted member.
531+ <BLANKLINE>
532 ----------------------------------------
533
534 It's also possible to forcibly add a team as a member of another one, by
535@@ -423,16 +559,22 @@
536
537 >>> print_distinct_emails()
538 From: Ubuntu Team <noreply@launchpad.net>
539- To: foo.bar@canonical.com
540- X-Launchpad-Message-Rationale: Indirect member (ubuntu-team)
541+ To: Foo Bar <foo.bar@canonical.com>
542+ X-Launchpad-Message-Rationale: Member (ubuntu-team) @launchpad
543+ X-Launchpad-Notification-Type: team-membership-new
544 Subject: launchpad joined ubuntu-team
545 ...
546- You received this email because launchpad is the new member.
547+ You received this email because your team Launchpad Developers is the
548+ new m=
549+ ember.
550+ <BLANKLINE>
551 ----------------------------------------
552 From: Ubuntu Team <noreply@launchpad.net>
553- To: colin.watson@ubuntulinux.com, jeff.waugh@ubuntulinux.com,
554- limi@plone.org
555+ To: Alexander Limi <limi@plone.org>,
556+ Colin Watson <colin.watson@ubuntulinux.com>,
557+ Jeff Waugh <jeff.waugh@ubuntulinux.com>
558 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
559+ X-Launchpad-Notification-Type: team-membership-new
560 Subject: launchpad joined ubuntu-team
561 <BLANKLINE>
562 Launchpad Developers (launchpad) has been added as a member of Ubuntu
563@@ -444,13 +586,16 @@
564 -- =
565 <BLANKLINE>
566 You received this email because you are an admin of the Ubuntu Team team.
567+ <BLANKLINE>
568 ----------------------------------------
569 From: Ubuntu Team <noreply@launchpad.net>
570- To: mark@example.com
571+ To: Mark Shuttleworth <mark@example.com>
572 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
573+ X-Launchpad-Notification-Type: team-membership-new
574 Subject: launchpad joined ubuntu-team
575 ...
576 You received this email because you are the owner of the Ubuntu Team team.
577+ <BLANKLINE>
578 ----------------------------------------
579
580
581@@ -480,16 +625,19 @@
582 ... utc_now + timedelta(days=9), mark)
583 >>> flush_database_updates()
584 >>> beta_testers_on_ubuntu_team.sendExpirationWarningEmail()
585- >>> transaction.commit()
586+ >>> run_mail_jobs()
587 >>> print_distinct_emails()
588 From: Ubuntu Team <noreply@launchpad.net>
589- To: beta-admin@launchpad.net
590+ To: Launchpad Beta Testers Owner <beta-admin@launchpad.net>
591+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
592+ @launchpad-beta-testers
593+ X-Launchpad-Notification-Type: team-membership-expiration-warning
594 Subject: launchpad-beta-testers will expire soon from ubuntu-team
595 <BLANKLINE>
596 On ..., 9 days from now, the membership
597- of Launchpad Beta Testers (launchpad-beta-testers) (which you are
598- the owner of) in the Ubuntu Team (ubuntu-team) Launchpad team
599- is due to expire.
600+ of Launchpad Beta Testers (launchpad-beta-testers) (which you are the
601+ owner=
602+ of) in the Ubuntu Team (ubuntu-team) Launchpad team is due to expire.
603 <http://launchpad.dev/~ubuntu-team>
604 <BLANKLINE>
605 To prevent this membership from expiring, you should get in touch
606@@ -505,6 +653,12 @@
607 <BLANKLINE>
608 Thanks for using Launchpad!
609 <BLANKLINE>
610+ -- =
611+ <BLANKLINE>
612+ You received this email because your team Launchpad Beta Testers is the
613+ aff=
614+ ected member.
615+ <BLANKLINE>
616 ----------------------------------------
617
618 If the team's renewal policy is ONDEMAND, though, the member is invited
619@@ -518,10 +672,12 @@
620 ... utc_now + timedelta(days=9), mark)
621 >>> flush_database_updates()
622 >>> kamion_on_ubuntu_team.sendExpirationWarningEmail()
623- >>> transaction.commit()
624+ >>> run_mail_jobs()
625 >>> print_distinct_emails()
626 From: Ubuntu Team <noreply@launchpad.net>
627- To: colin.watson@ubuntulinux.com
628+ To: Colin Watson <colin.watson@ubuntulinux.com>
629+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
630+ X-Launchpad-Notification-Type: team-membership-expiration-warning
631 Subject: Your membership in ubuntu-team is about to expire
632 <BLANKLINE>
633 On ..., 9 days from now, your membership
634@@ -537,19 +693,26 @@
635 <BLANKLINE>
636 Thanks for using Launchpad!
637 <BLANKLINE>
638+ -- =
639+ <BLANKLINE>
640+ You received this email because you are the affected member.
641+ <BLANKLINE>
642 ----------------------------------------
643
644 >>> beta_testers_on_ubuntu_team.sendExpirationWarningEmail()
645- >>> transaction.commit()
646+ >>> run_mail_jobs()
647 >>> print_distinct_emails()
648 From: Ubuntu Team <noreply@launchpad.net>
649- To: beta-admin@launchpad.net
650+ To: Launchpad Beta Testers Owner <beta-admin@launchpad.net>
651+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
652+ @launchpad-beta-testers
653+ X-Launchpad-Notification-Type: team-membership-expiration-warning
654 Subject: launchpad-beta-testers will expire soon from ubuntu-team
655 <BLANKLINE>
656 On ..., 9 days from now, the membership
657- of Launchpad Beta Testers (launchpad-beta-testers) (which you are
658- the owner of) in the Ubuntu Team (ubuntu-team) Launchpad team
659- is due to expire.
660+ of Launchpad Beta Testers (launchpad-beta-testers) (which you are the
661+ owner=
662+ of) in the Ubuntu Team (ubuntu-team) Launchpad team is due to expire.
663 <http://launchpad.dev/~ubuntu-team>
664 <BLANKLINE>
665 If you want, you can renew this membership at
666@@ -560,6 +723,12 @@
667 <BLANKLINE>
668 Thanks for using Launchpad!
669 <BLANKLINE>
670+ -- =
671+ <BLANKLINE>
672+ You received this email because your team Launchpad Beta Testers is the
673+ aff=
674+ ected member.
675+ <BLANKLINE>
676 ----------------------------------------
677
678 If the team's renewal policy is NONE but the member has the necessary
679@@ -577,15 +746,18 @@
680 ... utc_now + timedelta(days=9), sampleperson)
681 >>> flush_database_updates()
682 >>> sampleperson_on_landscape.sendExpirationWarningEmail()
683- >>> transaction.commit()
684+ >>> run_mail_jobs()
685 >>> print_distinct_emails()
686 From: Landscape Developers <noreply@launchpad.net>
687- To: test@canonical.com
688+ To: Sample Person <test@canonical.com>
689+ X-Launchpad-Message-Rationale: Member (landscape-developers)
690+ X-Launchpad-Notification-Type: team-membership-expiration-warning
691 Subject: Your membership in landscape-developers is about to expire
692 <BLANKLINE>
693 On ..., 9 days from now, your membership
694- in the Landscape Developers (landscape-developers) Launchpad team
695- is due to expire.
696+ in the Landscape Developers (landscape-developers) Launchpad team is due
697+ to=
698+ expire.
699 <http://launchpad.dev/~landscape-developers>
700 <BLANKLINE>
701 To stay a member of this team you should extend your membership at
702@@ -596,6 +768,10 @@
703 <BLANKLINE>
704 Thanks for using Launchpad!
705 <BLANKLINE>
706+ -- =
707+ <BLANKLINE>
708+ You received this email because you are the affected member.
709+ <BLANKLINE>
710 ----------------------------------------
711
712
713@@ -634,7 +810,9 @@
714
715 >>> print_distinct_emails()
716 From: Mirror Administrators <noreply@launchpad.net>
717- To: mark@example.com
718+ To: Mark Shuttleworth <mark@example.com>
719+ X-Launchpad-Message-Rationale: Admin (ubuntu-mirror-admins)
720+ X-Launchpad-Notification-Type: team-membership-renewed
721 Subject: karl extended their membership
722 <BLANKLINE>
723 Karl Tilbury (karl) renewed their own membership in the Mirror
724@@ -643,6 +821,13 @@
725 <BLANKLINE>
726 Regards,
727 The Launchpad team
728+ <BLANKLINE>
729+ -- =
730+ <BLANKLINE>
731+ You received this email because you are an admin of the Mirror
732+ Administrato=
733+ rs team.
734+ <BLANKLINE>
735 ----------------------------------------
736
737
738@@ -678,22 +863,37 @@
739
740 >>> print_distinct_emails()
741 From: Ubuntu Team <noreply@launchpad.net>
742- To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,
743- jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com
744+ To: Alexander Limi <limi@plone.org>,
745+ Colin Watson <colin.watson@ubuntulinux.com>,
746+ Foo Bar <foo.bar@canonical.com>,
747+ Jeff Waugh <jeff.waugh@ubuntulinux.com>,
748+ Mark Shuttleworth <mark@example.com>
749+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
750+ X-Launchpad-Notification-Type: team-membership-change
751 Subject: cprov made admin by mark
752 <BLANKLINE>
753 The membership status of Celso Providelo (cprov) in the team Ubuntu Team
754 (ubuntu-team) was changed by Mark Shuttleworth (mark) from Approved to
755 Administrator.
756 <http://launchpad.dev/~ubuntu-team>
757+ <BLANKLINE>
758+ -- =
759+ You received this email because you are an admin of the Ubuntu Team team.
760+ <BLANKLINE>
761 ----------------------------------------
762 From: Ubuntu Team <noreply@launchpad.net>
763- To: celso.providelo@canonical.com
764+ To: Celso Providelo <celso.providelo@canonical.com>
765+ X-Launchpad-Message-Rationale: Member (ubuntu-team)
766+ X-Launchpad-Notification-Type: team-membership-change
767 Subject: cprov made admin by mark
768 <BLANKLINE>
769 The status of your membership in the team Ubuntu Team (ubuntu-team) was
770 changed by Mark Shuttleworth (mark) from Approved to Administrator.
771 <http://launchpad.dev/~ubuntu-team>
772+ <BLANKLINE>
773+ -- =
774+ You received this email because you are the affected member.
775+ <BLANKLINE>
776 ----------------------------------------
777
778 If a team admin changes his own membership, the notification sent will
779@@ -710,14 +910,23 @@
780
781 >>> print_distinct_emails()
782 From: Ubuntu Team <noreply@launchpad.net>
783- To: celso.providelo@canonical.com, colin.watson@ubuntulinux.com,
784- foo.bar@canonical.com, limi@plone.org, mark@example.com
785+ To: Alexander Limi <limi@plone.org>,
786+ Celso Providelo <celso.providelo@canonical.com>,
787+ Colin Watson <colin.watson@ubuntulinux.com>,
788+ Foo Bar <foo.bar@canonical.com>,
789+ Mark Shuttleworth <mark@example.com>
790+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
791+ X-Launchpad-Notification-Type: team-membership-change
792 Subject: Membership change: jdub in ubuntu-team
793 <BLANKLINE>
794 The membership status of Jeff Waugh (jdub) in the team Ubuntu Team
795 (ubuntu-team) was changed by the user from Administrator to
796 Approved.
797 <http://launchpad.dev/~ubuntu-team>
798+ <BLANKLINE>
799+ -- =
800+ You received this email because you are an admin of the Ubuntu Team team.
801+ <BLANKLINE>
802 ----------------------------------------
803
804 Deactivating the membership of a team also generates notifications for
805@@ -736,15 +945,38 @@
806
807 >>> print_distinct_emails()
808 From: Ubuntu Team <noreply@launchpad.net>
809- To: celso.providelo@canonical.com, colin.watson@ubuntulinux.com,
810- foo.bar@canonical.com, karl@canonical.com, limi@plone.org,
811- mark@example.com
812- Subject: ubuntu-mirror-admins deactivated by mark
813- <BLANKLINE>
814- The membership status of Mirror Administrators (ubuntu-mirror-admins) in
815- the team Ubuntu Team (ubuntu-team) was changed by Mark Shuttleworth
816- (mark) from Approved to Deactivated.
817- <http://launchpad.dev/~ubuntu-team>
818+ To: Alexander Limi <limi@plone.org>,
819+ Celso Providelo <celso.providelo@canonical.com>,
820+ Colin Watson <colin.watson@ubuntulinux.com>,
821+ Foo Bar <foo.bar@canonical.com>
822+ X-Launchpad-Message-Rationale: Admin (ubuntu-team)
823+ X-Launchpad-Notification-Type: team-membership-change
824+ Subject: ubuntu-mirror-admins deactivated by mark
825+ <BLANKLINE>
826+ The membership status of Mirror Administrators (ubuntu-mirror-admins) in
827+ the team Ubuntu Team (ubuntu-team) was changed by Mark Shuttleworth
828+ (mark) from Approved to Deactivated.
829+ <http://launchpad.dev/~ubuntu-team>
830+ <BLANKLINE>
831+ -- =
832+ You received this email because you are an admin of the Ubuntu Team team.
833+ ----------------------------------------
834+ From: Ubuntu Team <noreply@launchpad.net>
835+ To: Karl Tilbury <karl@canonical.com>,
836+ Mark Shuttleworth <mark@example.com>
837+ X-Launchpad-Message-Rationale: Member (ubuntu-team) @ubuntu-mirror-admins
838+ X-Launchpad-Notification-Type: team-membership-change
839+ Subject: ubuntu-mirror-admins deactivated by mark
840+ <BLANKLINE>
841+ The membership status of Mirror Administrators (ubuntu-mirror-admins) in
842+ the team Ubuntu Team (ubuntu-team) was changed by Mark Shuttleworth
843+ (mark) from Approved to Deactivated.
844+ <http://launchpad.dev/~ubuntu-team>
845+ <BLANKLINE>
846+ -- =
847+ You received this email because your team Mirror Administrators is the
848+ affe=
849+ cted member.
850 ----------------------------------------
851
852 Deactivating memberships can also be done silently (no email
853@@ -807,10 +1039,12 @@
854 >>> member = factory.makePerson(
855 ... name='team-member', email='team-member@example.com')
856 >>> ignored = team_one.addMember(member, owner)
857+ >>> run_mail_jobs()
858 >>> print_distinct_emails()
859 From: Team One ...
860- To: team-member...
861+ To: Team-member <team-member...>
862 X-Launchpad-Message-Rationale: Member (team-one)
863+ X-Launchpad-Notification-Type: team-membership-new
864 Subject: You have been added to team-one
865 <BLANKLINE>
866 Team-owner (team-owner) added you as a member of Team One (team-one).
867@@ -818,11 +1052,12 @@
868 <BLANKLINE>
869 If you would like to subscribe to the team list, use the link below
870 to update your Mailing List Subscription preferences.
871- <http://launchpad.dev/people/+me/+editmailinglists>
872+ <http://launchpad.dev/~/+editmailinglists>
873 <BLANKLINE>
874 -- =
875 <BLANKLINE>
876 You received this email because you are the new member.
877+ <BLANKLINE>
878 ----------------------------------------
879
880 When a team join a team with a mailing list, the new member notification
881@@ -831,10 +1066,12 @@
882 >>> team_two = factory.makeTeam(
883 ... name='team-two', email='team-two@example.com', owner=owner)
884 >>> ignored = team_one.addMember(team_two, owner, force_team_add=True)
885+ >>> run_mail_jobs()
886 >>> print_distinct_emails()
887 From: Team One ...
888- To: team-two...
889- X-Launchpad-Message-Rationale: Indirect member (team-one)
890+ To: Team Two <team-two...>
891+ X-Launchpad-Message-Rationale: Member (team-one) @team-two
892+ X-Launchpad-Notification-Type: team-membership-new
893 Subject: team-two joined team-one
894 <BLANKLINE>
895 Team-owner (team-owner) added Team Two (team-two) (which you are a
896@@ -843,11 +1080,10 @@
897 <BLANKLINE>
898 If you would like to subscribe to the team list, use the link below
899 to update your Mailing List Subscription preferences.
900- <http://launchpad.dev/people/+me/+editmailinglists>
901+ <http://launchpad.dev/~/+editmailinglists>
902 <BLANKLINE>
903 -- =
904 <BLANKLINE>
905- You received this email because team-two is the new member.
906+ You received this email because your team Team Two is the new member.
907+ <BLANKLINE>
908 ----------------------------------------
909-
910-
911
912=== modified file 'lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt'
913--- lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt 2011-03-09 18:18:02 +0000
914+++ lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt 2015-09-08 11:57:29 +0000
915@@ -1,8 +1,7 @@
916-Hello %(recipient_name)s,
917+Hello %(recipient)s,
918
919 On %(expiration_date)s, %(approximate_duration)s from now, the membership
920-of %(member_name)s (which you are
921-the owner of) in the %(team_name)s Launchpad team
922+of %(member)s (which you are the owner of) in the %(team)s Launchpad team
923 is due to expire.
924 <%(team_url)s>
925
926
927=== modified file 'lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt'
928--- lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt 2011-03-09 18:18:02 +0000
929+++ lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt 2015-09-08 11:57:29 +0000
930@@ -1,8 +1,7 @@
931-Hello %(recipient_name)s,
932+Hello %(recipient)s,
933
934 On %(expiration_date)s, %(approximate_duration)s from now, your membership
935-in the %(team_name)s Launchpad team
936-is due to expire.
937+in the %(team)s Launchpad team is due to expire.
938 <%(team_url)s>
939
940 %(how_to_renew)s
941
942=== modified file 'lib/lp/registry/emailtemplates/membership-expired-bulk.txt'
943--- lib/lp/registry/emailtemplates/membership-expired-bulk.txt 2011-03-09 18:18:02 +0000
944+++ lib/lp/registry/emailtemplates/membership-expired-bulk.txt 2015-09-08 11:57:29 +0000
945@@ -1,6 +1,6 @@
946-Hello %(recipient_name)s,
947+Hello %(recipient)s,
948
949-The membership of %(member_name)s in the %(team_name)s team has expired.
950+The membership of %(member)s in the %(team)s team has expired.
951 <%(team_url)s>
952
953 Regards,
954
955=== modified file 'lib/lp/registry/emailtemplates/membership-expired-personal.txt'
956--- lib/lp/registry/emailtemplates/membership-expired-personal.txt 2011-03-09 18:18:02 +0000
957+++ lib/lp/registry/emailtemplates/membership-expired-personal.txt 2015-09-08 11:57:29 +0000
958@@ -1,6 +1,6 @@
959-Hello %(recipient_name)s,
960+Hello %(recipient)s,
961
962-Your membership in the %(team_name)s team has expired.
963+Your membership in the %(team)s team has expired.
964 <%(team_url)s>
965
966 Regards,
967
968=== modified file 'lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt'
969--- lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt 2011-03-09 18:18:02 +0000
970+++ lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt 2015-09-08 11:57:29 +0000
971@@ -1,5 +1,5 @@
972-Hello %(recipient_name)s,
973+Hello %(recipient)s,
974
975-%(reviewer_name)s has accepted the invitation to make %(member_name)s a member of %(team_name)s.
976+%(reviewer)s has accepted the invitation to make %(member)s a member of %(team)s.
977 <%(team_url)s>
978 %(comment)s
979
980=== modified file 'lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt'
981--- lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt 2011-03-09 18:18:02 +0000
982+++ lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt 2015-09-08 11:57:29 +0000
983@@ -1,5 +1,5 @@
984-Hello %(recipient_name)s,
985+Hello %(recipient)s,
986
987-%(reviewer_name)s has declined the invitation to make %(member_name)s a member of %(team_name)s.
988+%(reviewer)s has declined the invitation to make %(member)s a member of %(team)s.
989 <%(team_url)s>
990 %(comment)s
991
992=== modified file 'lib/lp/registry/emailtemplates/membership-invitation.txt'
993--- lib/lp/registry/emailtemplates/membership-invitation.txt 2011-03-09 17:51:28 +0000
994+++ lib/lp/registry/emailtemplates/membership-invitation.txt 2015-09-08 11:57:29 +0000
995@@ -1,4 +1,4 @@
996-Hello %(recipient_name)s,
997+Hello %(recipient)s,
998
999 %(reviewer)s has invited %(member)s (which you are an administrator of) to join %(team)s.
1000 <%(team_url)s>
1001
1002=== modified file 'lib/lp/registry/emailtemplates/membership-member-renewed.txt'
1003--- lib/lp/registry/emailtemplates/membership-member-renewed.txt 2011-03-09 18:18:02 +0000
1004+++ lib/lp/registry/emailtemplates/membership-member-renewed.txt 2015-09-08 11:57:29 +0000
1005@@ -1,6 +1,6 @@
1006-Hello %(recipient_name)s,
1007+Hello %(recipient)s,
1008
1009-%(member_name)s renewed their own membership in the %(team_name)s team until %(dateexpires)s.
1010+%(member)s renewed their own membership in the %(team)s team until %(dateexpires)s.
1011 <%(team_url)s>
1012
1013 Regards,
1014
1015=== modified file 'lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt'
1016--- lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt 2011-03-09 18:18:02 +0000
1017+++ lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt 2015-09-08 11:57:29 +0000
1018@@ -1,5 +1,5 @@
1019-Hello %(recipient_name)s,
1020+Hello %(recipient)s,
1021
1022-The membership status of %(member_name)s in the team %(team_name)s was changed by %(reviewer_name)s from %(old_status)s to %(new_status)s.
1023+The membership status of %(member)s in the team %(team)s was changed by %(reviewer)s from %(old_status)s to %(new_status)s.
1024 <%(team_url)s>
1025 %(comment)s
1026
1027=== modified file 'lib/lp/registry/emailtemplates/membership-statuschange-personal.txt'
1028--- lib/lp/registry/emailtemplates/membership-statuschange-personal.txt 2011-03-09 18:18:02 +0000
1029+++ lib/lp/registry/emailtemplates/membership-statuschange-personal.txt 2015-09-08 11:57:29 +0000
1030@@ -1,5 +1,5 @@
1031-Hello %(recipient_name)s,
1032+Hello %(recipient)s,
1033
1034-The status of your membership in the team %(team_name)s was changed by %(reviewer_name)s from %(old_status)s to %(new_status)s.
1035+The status of your membership in the team %(team)s was changed by %(reviewer)s from %(old_status)s to %(new_status)s.
1036 <%(team_url)s>
1037 %(comment)s
1038
1039=== modified file 'lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt'
1040--- lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt 2011-03-09 17:51:28 +0000
1041+++ lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt 2015-09-08 11:57:29 +0000
1042@@ -1,5 +1,5 @@
1043-Hello %(recipient_name)s,
1044-
1045-%(person_name)s has been added as a member of %(team_name)s by %(reviewer_name)s. Follow the link below for more details.
1046-
1047- %(url)s
1048+Hello %(recipient)s,
1049+
1050+%(member)s has been added as a member of %(team)s by %(reviewer)s. Follow the link below for more details.
1051+
1052+ %(membership_url)s
1053
1054=== modified file 'lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt'
1055--- lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt 2011-03-09 17:51:28 +0000
1056+++ lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt 2015-09-08 11:57:29 +0000
1057@@ -1,4 +1,4 @@
1058-Hello %(recipient_name)s,
1059+Hello %(recipient)s,
1060
1061 %(reviewer)s added %(member)s (which you are a member of) as a member of %(team)s.
1062 <%(team_url)s>
1063
1064=== modified file 'lib/lp/registry/emailtemplates/new-member-notification.txt'
1065--- lib/lp/registry/emailtemplates/new-member-notification.txt 2011-03-09 17:51:28 +0000
1066+++ lib/lp/registry/emailtemplates/new-member-notification.txt 2015-09-08 11:57:29 +0000
1067@@ -1,4 +1,4 @@
1068-Hello %(recipient_name)s,
1069+Hello %(recipient)s,
1070
1071 %(reviewer)s added you as a member of %(team)s.
1072 <%(team_url)s>
1073
1074=== modified file 'lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt'
1075--- lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt 2011-03-09 17:51:28 +0000
1076+++ lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt 2015-09-08 11:57:29 +0000
1077@@ -1,5 +1,5 @@
1078-Hello %(recipient_name)s,
1079-
1080-%(reviewer_name)s wants to make %(person_name)s a member of %(team_name)s, but this is a moderated team, so that membership has to be approved. You can approve, decline or leave it as proposed by following the link below.
1081-
1082- %(url)s
1083+Hello %(recipient)s,
1084+
1085+%(reviewer)s wants to make %(member)s a member of %(team)s, but this is a moderated team, so that membership has to be approved. You can approve, decline or leave it as proposed by following the link below.
1086+
1087+ %(membership_url)s
1088
1089=== modified file 'lib/lp/registry/emailtemplates/pending-membership-approval.txt'
1090--- lib/lp/registry/emailtemplates/pending-membership-approval.txt 2011-03-09 17:51:28 +0000
1091+++ lib/lp/registry/emailtemplates/pending-membership-approval.txt 2015-09-08 11:57:29 +0000
1092@@ -1,5 +1,5 @@
1093-Hello %(recipient_name)s,
1094-
1095-%(person_name)s wants to be a member of %(team_name)s, but this is a moderated team, so that membership has to be approved. You can approve, decline or leave it as proposed by following the link below.
1096-
1097- %(url)s
1098+Hello %(recipient)s,
1099+
1100+%(member)s wants to be a member of %(team)s, but this is a moderated team, so that membership has to be approved. You can approve, decline or leave it as proposed by following the link below.
1101+
1102+ %(membership_url)s
1103
1104=== modified file 'lib/lp/registry/enums.py'
1105--- lib/lp/registry/enums.py 2015-05-14 02:03:31 +0000
1106+++ lib/lp/registry/enums.py 2015-09-08 11:57:29 +0000
1107@@ -354,6 +354,31 @@
1108 the user from teams.
1109 """)
1110
1111+ TEAM_INVITATION_NOTIFICATION = DBItem(3, """
1112+ Notification of invitation to join team
1113+
1114+ Notify team admins that the team has been invited to join another
1115+ team.
1116+ """)
1117+
1118+ TEAM_JOIN_NOTIFICATION = DBItem(4, """
1119+ Notification of new member joining team
1120+
1121+ Notify that a new member has been added to a team.
1122+ """)
1123+
1124+ EXPIRING_MEMBERSHIP_NOTIFICATION = DBItem(5, """
1125+ Notification of expiring membership
1126+
1127+ Notify a member that their membership of a team is about to expire.
1128+ """)
1129+
1130+ SELF_RENEWAL_NOTIFICATION = DBItem(6, """
1131+ Notification of self-renewal
1132+
1133+ Notify team admins that a member renewed their own membership.
1134+ """)
1135+
1136
1137 class ProductJobType(DBEnumeratedType):
1138 """Values that IProductJob.job_type can take."""
1139
1140=== modified file 'lib/lp/registry/interfaces/persontransferjob.py'
1141--- lib/lp/registry/interfaces/persontransferjob.py 2013-03-12 05:51:28 +0000
1142+++ lib/lp/registry/interfaces/persontransferjob.py 2015-09-08 11:57:29 +0000
1143@@ -1,10 +1,12 @@
1144-# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
1145+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
1146 # GNU Affero General Public License version 3 (see the file LICENSE).
1147
1148 """Interface for the Jobs system to change memberships or merge persons."""
1149
1150 __metaclass__ = type
1151 __all__ = [
1152+ 'IExpiringMembershipNotificationJob',
1153+ 'IExpiringMembershipNotificationJobSource',
1154 'IMembershipNotificationJob',
1155 'IMembershipNotificationJobSource',
1156 'IPersonDeactivateJob',
1157@@ -13,6 +15,12 @@
1158 'IPersonMergeJobSource',
1159 'IPersonTransferJob',
1160 'IPersonTransferJobSource',
1161+ 'ISelfRenewalNotificationJob',
1162+ 'ISelfRenewalNotificationJobSource',
1163+ 'ITeamInvitationNotificationJob',
1164+ 'ITeamInvitationNotificationJobSource',
1165+ 'ITeamJoinNotificationJob',
1166+ 'ITeamJoinNotificationJobSource',
1167 ]
1168
1169 from zope.interface import Attribute
1170@@ -161,3 +169,87 @@
1171 :param person: Match jobs on `person`, or `None` to ignore.
1172 :return: A `ResultSet` yielding `IPersonDeactivateJob`.
1173 """
1174+
1175+
1176+class ITeamInvitationNotificationJob(IPersonTransferJob):
1177+ """A Job to notify about team joining invitations."""
1178+
1179+ member = PublicPersonChoice(
1180+ title=_('Alias for minor_person attribute'),
1181+ vocabulary='ValidPersonOrTeam',
1182+ required=True)
1183+
1184+ team = PublicPersonChoice(
1185+ title=_('Alias for major_person attribute'),
1186+ vocabulary='ValidPersonOrTeam',
1187+ required=True)
1188+
1189+
1190+class ITeamInvitationNotificationJobSource(IJobSource):
1191+ """An interface for acquiring ITeamInvitationNotificationJobs."""
1192+
1193+ def create(member, team):
1194+ """Create a new ITeamInvitationNotificationJob."""
1195+
1196+
1197+class ITeamJoinNotificationJob(IPersonTransferJob):
1198+ """A Job to notify about a new member joining a team."""
1199+
1200+ member = PublicPersonChoice(
1201+ title=_('Alias for minor_person attribute'),
1202+ vocabulary='ValidPersonOrTeam',
1203+ required=True)
1204+
1205+ team = PublicPersonChoice(
1206+ title=_('Alias for major_person attribute'),
1207+ vocabulary='ValidPersonOrTeam',
1208+ required=True)
1209+
1210+
1211+class ITeamJoinNotificationJobSource(IJobSource):
1212+ """An interface for acquiring ITeamJoinNotificationJobs."""
1213+
1214+ def create(member, team):
1215+ """Create a new ITeamJoinNotificationJob."""
1216+
1217+
1218+class IExpiringMembershipNotificationJob(IPersonTransferJob):
1219+ """A Job to send a warning about expiring membership."""
1220+
1221+ member = PublicPersonChoice(
1222+ title=_('Alias for minor_person attribute'),
1223+ vocabulary='ValidPersonOrTeam',
1224+ required=True)
1225+
1226+ team = PublicPersonChoice(
1227+ title=_('Alias for major_person attribute'),
1228+ vocabulary='ValidPersonOrTeam',
1229+ required=True)
1230+
1231+
1232+class IExpiringMembershipNotificationJobSource(IJobSource):
1233+ """An interface for acquiring IExpiringMembershipNotificationJobs."""
1234+
1235+ def create(member, team, dateexpires):
1236+ """Create a new IExpiringMembershipNotificationJob."""
1237+
1238+
1239+class ISelfRenewalNotificationJob(IPersonTransferJob):
1240+ """A Job to notify about a self-renewal."""
1241+
1242+ member = PublicPersonChoice(
1243+ title=_('Alias for minor_person attribute'),
1244+ vocabulary='ValidPersonOrTeam',
1245+ required=True)
1246+
1247+ team = PublicPersonChoice(
1248+ title=_('Alias for major_person attribute'),
1249+ vocabulary='ValidPersonOrTeam',
1250+ required=True)
1251+
1252+
1253+class ISelfRenewalNotificationJobSource(IJobSource):
1254+ """An interface for acquiring ISelfRenewalNotificationJobs."""
1255+
1256+ def create(member, team, dateexpires):
1257+ """Create a new ISelfRenewalNotificationJob."""
1258
1259=== modified file 'lib/lp/registry/mail/notification.py'
1260--- lib/lp/registry/mail/notification.py 2015-03-13 19:05:50 +0000
1261+++ lib/lp/registry/mail/notification.py 2015-09-08 11:57:29 +0000
1262@@ -1,4 +1,4 @@
1263-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
1264+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
1265 # GNU Affero General Public License version 3 (see the file LICENSE).
1266
1267 """Event handlers that send email notifications."""
1268@@ -17,21 +17,16 @@
1269 getUtility,
1270 )
1271
1272-from lp.registry.enums import TeamMembershipPolicy
1273 from lp.registry.interfaces.mailinglist import IHeldMessageDetails
1274 from lp.registry.interfaces.person import IPersonSet
1275-from lp.registry.interfaces.teammembership import (
1276- ITeamMembershipSet,
1277- TeamMembershipStatus,
1278+from lp.registry.interfaces.persontransferjob import (
1279+ ITeamInvitationNotificationJobSource,
1280+ ITeamJoinNotificationJobSource,
1281 )
1282 from lp.services.config import config
1283 from lp.services.database.sqlbase import block_implicit_flushes
1284-from lp.services.mail.helpers import (
1285- get_contact_email_addresses,
1286- get_email_template,
1287- )
1288+from lp.services.mail.helpers import get_email_template
1289 from lp.services.mail.mailwrapper import MailWrapper
1290-from lp.services.mail.notificationrecipientset import NotificationRecipientSet
1291 from lp.services.mail.sendmail import (
1292 format_address,
1293 sendmail,
1294@@ -41,14 +36,7 @@
1295 IDirectEmailAuthorization,
1296 QuotaReachedError,
1297 )
1298-from lp.services.webapp.interfaces import ILaunchpadRoot
1299 from lp.services.webapp.publisher import canonical_url
1300-from lp.services.webapp.url import urlappend
1301-
1302-# Silence lint warnings.
1303-NotificationRecipientSet
1304-
1305-CC = "CC"
1306
1307
1308 @block_implicit_flushes
1309@@ -57,48 +45,9 @@
1310
1311 The notification will include a link to a page in which any team admin can
1312 accept the invitation.
1313-
1314- XXX: Guilherme Salgado 2007-05-08:
1315- At some point we may want to extend this functionality to allow invites
1316- to be sent to users as well, but for now we only use it for teams.
1317 """
1318- member = event.member
1319- assert member.is_team
1320- team = event.team
1321- membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1322- member, team)
1323- assert membership is not None
1324-
1325- reviewer = membership.proposed_by
1326- admin_addrs = member.getTeamAdminsEmailAddresses()
1327- from_addr = format_address(
1328- team.displayname, config.canonical.noreply_from_address)
1329- subject = 'Invitation for %s to join' % member.name
1330- templatename = 'membership-invitation.txt'
1331- template = get_email_template(templatename, app='registry')
1332- replacements = {
1333- 'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name),
1334- 'member': '%s (%s)' % (member.displayname, member.name),
1335- 'team': '%s (%s)' % (team.displayname, team.name),
1336- 'team_url': canonical_url(team),
1337- 'membership_invitations_url':
1338- "%s/+invitation/%s" % (canonical_url(member), team.name)}
1339- for address in admin_addrs:
1340- recipient = getUtility(IPersonSet).getByEmail(address)
1341- replacements['recipient_name'] = recipient.displayname
1342- msg = MailWrapper().format(template % replacements, force_wrap=True)
1343- simple_sendmail(from_addr, address, subject, msg)
1344-
1345-
1346-def send_team_email(from_addr, address, subject, template, replacements,
1347- rationale, headers=None):
1348- """Send a team message with a rationale."""
1349- if headers is None:
1350- headers = {}
1351- body = MailWrapper().format(template % replacements, force_wrap=True)
1352- footer = "-- \n%s" % rationale
1353- message = '%s\n\n%s' % (body, footer)
1354- simple_sendmail(from_addr, address, subject, message, headers)
1355+ getUtility(ITeamInvitationNotificationJobSource).create(
1356+ event.member, event.team)
1357
1358
1359 @block_implicit_flushes
1360@@ -109,130 +58,7 @@
1361 is pending approval. Otherwise it'll say that the person has joined the
1362 team and who added that person to the team.
1363 """
1364- person = event.person
1365- team = event.team
1366- membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1367- person, team)
1368- assert membership is not None
1369- approved, admin, proposed = [
1370- TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN,
1371- TeamMembershipStatus.PROPOSED]
1372- admin_addrs = team.getTeamAdminsEmailAddresses()
1373- from_addr = format_address(
1374- team.displayname, config.canonical.noreply_from_address)
1375-
1376- reviewer = membership.proposed_by
1377- if reviewer != person and membership.status in [approved, admin]:
1378- reviewer = membership.reviewed_by
1379- # Somebody added this person as a member, we better send a
1380- # notification to the person too.
1381- member_addrs = get_contact_email_addresses(person)
1382-
1383- headers = {}
1384- if person.is_team:
1385- templatename = 'new-member-notification-for-teams.txt'
1386- subject = '%s joined %s' % (person.name, team.name)
1387- header_rational = "Indirect member (%s)" % team.name
1388- footer_rationale = (
1389- "You received this email because "
1390- "%s is the new member." % person.name)
1391- else:
1392- templatename = 'new-member-notification.txt'
1393- subject = 'You have been added to %s' % team.name
1394- header_rational = "Member (%s)" % team.name
1395- footer_rationale = (
1396- "You received this email because you are the new member.")
1397-
1398- if team.mailing_list is not None:
1399- template = get_email_template(
1400- 'team-list-subscribe-block.txt', app='registry')
1401- editemails_url = urlappend(
1402- canonical_url(getUtility(ILaunchpadRoot)),
1403- 'people/+me/+editmailinglists')
1404- list_instructions = template % dict(editemails_url=editemails_url)
1405- else:
1406- list_instructions = ''
1407-
1408- template = get_email_template(templatename, app='registry')
1409- replacements = {
1410- 'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name),
1411- 'team_url': canonical_url(team),
1412- 'member': '%s (%s)' % (person.displayname, person.name),
1413- 'team': '%s (%s)' % (team.displayname, team.name),
1414- 'list_instructions': list_instructions,
1415- }
1416- headers = {'X-Launchpad-Message-Rationale': header_rational}
1417- for address in member_addrs:
1418- recipient = getUtility(IPersonSet).getByEmail(address)
1419- replacements['recipient_name'] = recipient.displayname
1420- send_team_email(
1421- from_addr, address, subject, template, replacements,
1422- footer_rationale, headers)
1423-
1424- # The member's email address may be in admin_addrs too; let's remove
1425- # it so the member don't get two notifications.
1426- admin_addrs = set(admin_addrs).difference(set(member_addrs))
1427-
1428- # Yes, we can have teams with no members; not even admins.
1429- if not admin_addrs:
1430- return
1431-
1432- # Open teams do not notify admins about new members.
1433- if team.membership_policy == TeamMembershipPolicy.OPEN:
1434- return
1435-
1436- replacements = {
1437- 'person_name': "%s (%s)" % (person.displayname, person.name),
1438- 'team_name': "%s (%s)" % (team.displayname, team.name),
1439- 'reviewer_name': "%s (%s)" % (reviewer.displayname, reviewer.name),
1440- 'url': canonical_url(membership)}
1441-
1442- headers = {}
1443- if membership.status in [approved, admin]:
1444- template = get_email_template(
1445- 'new-member-notification-for-admins.txt', app='registry')
1446- subject = '%s joined %s' % (person.name, team.name)
1447- elif membership.status == proposed:
1448- # In the UI, a user can only propose himself or a team he
1449- # admins. Some users of the REST API have a workflow, where
1450- # they propose users that are designated as mentees (Bug 498181).
1451- if reviewer != person:
1452- headers = {"Reply-To": reviewer.preferredemail.email}
1453- template = get_email_template(
1454- 'pending-membership-approval-for-third-party.txt',
1455- app='registry')
1456- else:
1457- headers = {"Reply-To": person.preferredemail.email}
1458- template = get_email_template(
1459- 'pending-membership-approval.txt', app='registry')
1460- subject = "%s wants to join" % person.name
1461- else:
1462- raise AssertionError(
1463- "Unexpected membership status: %s" % membership.status)
1464-
1465- for address in admin_addrs:
1466- recipient = getUtility(IPersonSet).getByEmail(address)
1467- replacements['recipient_name'] = recipient.displayname
1468- if recipient.is_team:
1469- header_rationale = 'Admin (%s via %s)' % (
1470- team.name, recipient.name)
1471- footer_rationale = (
1472- "you are an admin of the %s team\n"
1473- "via the %s team." % (
1474- team.displayname, recipient.displayname))
1475- elif recipient == team.teamowner:
1476- header_rationale = 'Owner (%s)' % team.name
1477- footer_rationale = (
1478- "you are the owner of the %s team." % team.displayname)
1479- else:
1480- header_rationale = 'Admin (%s)' % team.name
1481- footer_rationale = (
1482- "you are an admin of the %s team." % team.displayname)
1483- footer = 'You received this email because %s' % footer_rationale
1484- headers['X-Launchpad-Message-Rationale'] = header_rationale
1485- send_team_email(
1486- from_addr, address, subject, template, replacements,
1487- footer, headers)
1488+ getUtility(ITeamJoinNotificationJobSource).create(event.person, event.team)
1489
1490
1491 def notify_mailinglist_activated(mailinglist, event):
1492
1493=== added file 'lib/lp/registry/mail/teammembership.py'
1494--- lib/lp/registry/mail/teammembership.py 1970-01-01 00:00:00 +0000
1495+++ lib/lp/registry/mail/teammembership.py 2015-09-08 11:57:29 +0000
1496@@ -0,0 +1,435 @@
1497+# Copyright 2015 Canonical Ltd. This software is licensed under the
1498+# GNU Affero General Public License version 3 (see the file LICENSE).
1499+
1500+__metaclass__ = type
1501+__all__ = [
1502+ 'TeamMembershipMailer',
1503+ ]
1504+
1505+from collections import OrderedDict
1506+from datetime import datetime
1507+
1508+import pytz
1509+from zope.component import getUtility
1510+
1511+from lp.app.browser.tales import DurationFormatterAPI
1512+from lp.registry.enums import (
1513+ TeamMembershipPolicy,
1514+ TeamMembershipRenewalPolicy,
1515+ )
1516+from lp.registry.interfaces.teammembership import (
1517+ ITeamMembershipSet,
1518+ TeamMembershipStatus,
1519+ )
1520+from lp.registry.model.person import get_recipients
1521+from lp.services.config import config
1522+from lp.services.mail.basemailer import (
1523+ BaseMailer,
1524+ RecipientReason,
1525+ )
1526+from lp.services.mail.helpers import get_email_template
1527+from lp.services.mail.sendmail import format_address
1528+from lp.services.webapp.interfaces import ILaunchpadRoot
1529+from lp.services.webapp.publisher import canonical_url
1530+from lp.services.webapp.url import urlappend
1531+
1532+
1533+class TeamMembershipRecipientReason(RecipientReason):
1534+
1535+ @classmethod
1536+ def forInvitation(cls, admin, team, recipient, proposed_member, **kwargs):
1537+ header = cls.makeRationale(
1538+ "Invitation (%s)" % team.name, proposed_member)
1539+ reason = (
1540+ "You received this email because %%(lc_entity_is)s an admin of "
1541+ "the %s team." % proposed_member.displayname)
1542+ return cls(admin, recipient, header, reason, **kwargs)
1543+
1544+ @classmethod
1545+ def forMember(cls, member, team, recipient, **kwargs):
1546+ header = cls.makeRationale("Member (%s)" % team.name, member)
1547+ reason = (
1548+ "You received this email because %(lc_entity_is)s the affected "
1549+ "member.")
1550+ return cls(member, recipient, header, reason, **kwargs)
1551+
1552+ @classmethod
1553+ def forNewMember(cls, new_member, team, recipient, **kwargs):
1554+ # From a filtering point of view, this is identical to forMember;
1555+ # filtering on X-Launchpad-Notification-Type is more useful for
1556+ # determining the type of notification sent to a particular member.
1557+ # It's worth having a footer that makes a little more sense, though.
1558+ header = cls.makeRationale("Member (%s)" % team.name, new_member)
1559+ reason = (
1560+ "You received this email because %(lc_entity_is)s the new member.")
1561+ return cls(new_member, recipient, header, reason, **kwargs)
1562+
1563+ @classmethod
1564+ def forAdmin(cls, admin, team, recipient, **kwargs):
1565+ header = cls.makeRationale("Admin (%s)" % team.name, admin)
1566+ reason = (
1567+ "You received this email because %%(lc_entity_is)s an admin of "
1568+ "the %s team." % team.displayname)
1569+ return cls(admin, recipient, header, reason, **kwargs)
1570+
1571+ @classmethod
1572+ def forOwner(cls, owner, team, recipient, **kwargs):
1573+ header = cls.makeRationale("Owner (%s)" % team.name, owner)
1574+ reason = (
1575+ "You received this email because %%(lc_entity_is)s the owner "
1576+ "of the %s team." % team.displayname)
1577+ return cls(owner, recipient, header, reason, **kwargs)
1578+
1579+ def __init__(self, subscriber, recipient, mail_header, reason_template,
1580+ subject=None, template_name=None, reply_to=None,
1581+ recipient_class=None):
1582+ super(TeamMembershipRecipientReason, self).__init__(
1583+ subscriber, recipient, mail_header, reason_template)
1584+ self.subject = subject
1585+ self.template_name = template_name
1586+ self.reply_to = reply_to
1587+ self.recipient_class = recipient_class
1588+
1589+
1590+class TeamMembershipMailer(BaseMailer):
1591+
1592+ app = 'registry'
1593+
1594+ @classmethod
1595+ def forInvitationToJoinTeam(cls, member, team):
1596+ """Create a mailer for notifying about team joining invitations.
1597+
1598+ XXX: Guilherme Salgado 2007-05-08:
1599+ At some point we may want to extend this functionality to allow
1600+ invites to be sent to users as well, but for now we only use it for
1601+ teams.
1602+ """
1603+ assert member.is_team
1604+ membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1605+ member, team)
1606+ assert membership is not None
1607+ recipients = OrderedDict()
1608+ for admin in member.adminmembers:
1609+ for recipient in get_recipients(admin):
1610+ recipients[recipient] = TeamMembershipRecipientReason.forAdmin(
1611+ admin, member, recipient)
1612+ from_addr = format_address(
1613+ team.displayname, config.canonical.noreply_from_address)
1614+ subject = "Invitation for %s to join" % member.name
1615+ return cls(
1616+ subject, "membership-invitation.txt", recipients, from_addr,
1617+ "team-membership-invitation", member, team, membership.proposed_by,
1618+ membership=membership)
1619+
1620+ @classmethod
1621+ def forTeamJoin(cls, member, team):
1622+ """Create a mailer for notifying about a new member joining a team."""
1623+ membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1624+ member, team)
1625+ assert membership is not None
1626+ subject = None
1627+ template_name = None
1628+ notification_type = "team-membership-new"
1629+ recipients = OrderedDict()
1630+ reviewer = membership.proposed_by
1631+ if reviewer != member and membership.status in [
1632+ TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN]:
1633+ reviewer = membership.reviewed_by
1634+ # Somebody added this person as a member, we better send a
1635+ # notification to the person too.
1636+ if member.is_team:
1637+ template_name = "new-member-notification-for-teams.txt"
1638+ subject = "%s joined %s" % (member.name, team.name)
1639+ else:
1640+ template_name = "new-member-notification.txt"
1641+ subject = "You have been added to %s" % team.name
1642+ for recipient in get_recipients(member):
1643+ recipients[recipient] = (
1644+ TeamMembershipRecipientReason.forNewMember(
1645+ member, team, recipient, subject=subject,
1646+ template_name=template_name))
1647+ # Open teams do not notify admins about new members.
1648+ if team.membership_policy != TeamMembershipPolicy.OPEN:
1649+ reply_to = None
1650+ if membership.status in [
1651+ TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN]:
1652+ template_name = "new-member-notification-for-admins.txt"
1653+ subject = "%s joined %s" % (member.name, team.name)
1654+ elif membership.status == TeamMembershipStatus.PROPOSED:
1655+ # In the UI, a user can only propose themselves or a team
1656+ # they admin. Some users of the REST API have a workflow
1657+ # where they propose users that are designated as undergoing
1658+ # mentorship (Bug 498181).
1659+ if reviewer != member:
1660+ reply_to = reviewer.preferredemail.email
1661+ template_name = (
1662+ "pending-membership-approval-for-third-party.txt")
1663+ else:
1664+ reply_to = member.preferredemail.email
1665+ template_name = "pending-membership-approval.txt"
1666+ notification_type = "team-membership-pending"
1667+ subject = "%s wants to join" % member.name
1668+ else:
1669+ raise AssertionError(
1670+ "Unexpected membership status: %s" % membership.status)
1671+ for admin in team.adminmembers:
1672+ for recipient in get_recipients(admin):
1673+ # The new member may also be a team admin; don't send
1674+ # two notifications in that case.
1675+ if recipient not in recipients:
1676+ if recipient == team.teamowner:
1677+ reason_factory = (
1678+ TeamMembershipRecipientReason.forOwner)
1679+ else:
1680+ reason_factory = (
1681+ TeamMembershipRecipientReason.forAdmin)
1682+ recipients[recipient] = reason_factory(
1683+ admin, team, recipient, subject=subject,
1684+ template_name=template_name, reply_to=reply_to)
1685+ from_addr = format_address(
1686+ team.displayname, config.canonical.noreply_from_address)
1687+ return cls(
1688+ subject, template_name, recipients, from_addr, notification_type,
1689+ member, team, membership.proposed_by, membership=membership)
1690+
1691+ @classmethod
1692+ def forMembershipStatusChange(cls, member, team, reviewer,
1693+ old_status, new_status, last_change_comment):
1694+ """Create a mailer for a membership status change."""
1695+ template_name = 'membership-statuschange'
1696+ notification_type = 'team-membership-change'
1697+ subject = (
1698+ 'Membership change: %(member)s in %(team)s' %
1699+ {'member': member.name, 'team': team.name})
1700+ if new_status == TeamMembershipStatus.EXPIRED:
1701+ template_name = 'membership-expired'
1702+ notification_type = 'team-membership-expired'
1703+ subject = '%s expired from team' % member.name
1704+ elif (new_status == TeamMembershipStatus.APPROVED and
1705+ old_status != TeamMembershipStatus.ADMIN):
1706+ if old_status == TeamMembershipStatus.INVITED:
1707+ template_name = 'membership-invitation-accepted'
1708+ notification_type = 'team-membership-invitation-accepted'
1709+ subject = (
1710+ 'Invitation to %s accepted by %s' %
1711+ (member.name, reviewer.name))
1712+ elif old_status == TeamMembershipStatus.PROPOSED:
1713+ subject = '%s approved by %s' % (member.name, reviewer.name)
1714+ else:
1715+ subject = '%s added by %s' % (member.name, reviewer.name)
1716+ elif new_status == TeamMembershipStatus.INVITATION_DECLINED:
1717+ template_name = 'membership-invitation-declined'
1718+ notification_type = 'team-membership-invitation-declined'
1719+ subject = (
1720+ 'Invitation to %s declined by %s' %
1721+ (member.name, reviewer.name))
1722+ elif new_status == TeamMembershipStatus.DEACTIVATED:
1723+ subject = '%s deactivated by %s' % (member.name, reviewer.name)
1724+ elif new_status == TeamMembershipStatus.ADMIN:
1725+ subject = '%s made admin by %s' % (member.name, reviewer.name)
1726+ elif new_status == TeamMembershipStatus.DECLINED:
1727+ subject = '%s declined by %s' % (member.name, reviewer.name)
1728+ else:
1729+ # Use the default template and subject.
1730+ pass
1731+ template_name += "-%(recipient_class)s.txt"
1732+
1733+ if last_change_comment:
1734+ comment = "\n%s said:\n %s\n" % (
1735+ reviewer.displayname, last_change_comment.strip())
1736+ else:
1737+ comment = ""
1738+
1739+ recipients = OrderedDict()
1740+ if reviewer != member:
1741+ for recipient in get_recipients(member):
1742+ if member.is_team:
1743+ recipient_class = "bulk"
1744+ else:
1745+ recipient_class = "personal"
1746+ recipients[recipient] = (
1747+ TeamMembershipRecipientReason.forMember(
1748+ member, team, recipient,
1749+ recipient_class=recipient_class))
1750+ # Don't send admin notifications for open teams: they're
1751+ # unrestricted, so notifications on join/leave do not help the
1752+ # admins.
1753+ if team.membership_policy != TeamMembershipPolicy.OPEN:
1754+ for admin in team.adminmembers:
1755+ for recipient in get_recipients(admin):
1756+ # The new member may also be a team admin; don't send
1757+ # two notifications in that case.
1758+ if recipient not in recipients:
1759+ recipients[recipient] = (
1760+ TeamMembershipRecipientReason.forAdmin(
1761+ admin, team, recipient,
1762+ recipient_class="bulk"))
1763+
1764+ extra_params = {
1765+ "old_status": old_status,
1766+ "new_status": new_status,
1767+ "comment": comment,
1768+ }
1769+ from_addr = format_address(
1770+ team.displayname, config.canonical.noreply_from_address)
1771+ return cls(
1772+ subject, template_name, recipients, from_addr, notification_type,
1773+ member, team, reviewer, extra_params=extra_params)
1774+
1775+ @classmethod
1776+ def forExpiringMembership(cls, member, team, dateexpires):
1777+ """Create a mailer for warning about expiring membership."""
1778+ membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1779+ member, team)
1780+ assert membership is not None
1781+ if member.is_team:
1782+ target = member.teamowner
1783+ template_name = "membership-expiration-warning-bulk.txt"
1784+ subject = "%s will expire soon from %s" % (member.name, team.name)
1785+ else:
1786+ target = member
1787+ template_name = "membership-expiration-warning-personal.txt"
1788+ subject = "Your membership in %s is about to expire" % team.name
1789+
1790+ if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND:
1791+ how_to_renew = (
1792+ "If you want, you can renew this membership at\n"
1793+ "<%s/+expiringmembership/%s>" %
1794+ (canonical_url(member), team.name))
1795+ elif not membership.canChangeExpirationDate(target):
1796+ admins_names = []
1797+ admins = team.getDirectAdministrators()
1798+ assert admins.count() >= 1
1799+ if admins.count() == 1:
1800+ admin = admins[0]
1801+ how_to_renew = (
1802+ "To prevent this membership from expiring, you should "
1803+ "contact the\nteam's administrator, %s.\n<%s>"
1804+ % (admin.unique_displayname, canonical_url(admin)))
1805+ else:
1806+ for admin in admins:
1807+ admins_names.append(
1808+ "%s <%s>" % (
1809+ admin.unique_displayname, canonical_url(admin)))
1810+
1811+ how_to_renew = (
1812+ "To prevent this membership from expiring, you should "
1813+ "get in touch\nwith one of the team's administrators:\n")
1814+ how_to_renew += "\n".join(admins_names)
1815+ else:
1816+ how_to_renew = (
1817+ "To stay a member of this team you should extend your "
1818+ "membership at\n<%s/+member/%s>"
1819+ % (canonical_url(team), member.name))
1820+
1821+ recipients = OrderedDict()
1822+ for recipient in get_recipients(target):
1823+ recipients[recipient] = TeamMembershipRecipientReason.forMember(
1824+ member, team, recipient)
1825+
1826+ formatter = DurationFormatterAPI(dateexpires - datetime.now(pytz.UTC))
1827+ extra_params = {
1828+ "how_to_renew": how_to_renew,
1829+ "expiration_date": dateexpires.strftime("%Y-%m-%d"),
1830+ "approximate_duration": formatter.approximateduration(),
1831+ }
1832+
1833+ from_addr = format_address(
1834+ team.displayname, config.canonical.noreply_from_address)
1835+ return cls(
1836+ subject, template_name, recipients, from_addr,
1837+ "team-membership-expiration-warning", member, team,
1838+ membership.proposed_by, membership=membership,
1839+ extra_params=extra_params, wrap=False, force_wrap=False)
1840+
1841+ @classmethod
1842+ def forSelfRenewal(cls, member, team, dateexpires):
1843+ """Create a mailer for notifying about a self-renewal."""
1844+ assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND
1845+ template_name = "membership-member-renewed.txt"
1846+ subject = "%s extended their membership" % member.name
1847+ recipients = OrderedDict()
1848+ for admin in team.adminmembers:
1849+ for recipient in get_recipients(admin):
1850+ recipients[recipient] = TeamMembershipRecipientReason.forAdmin(
1851+ admin, team, recipient)
1852+ extra_params = {"dateexpires": dateexpires.strftime("%Y-%m-%d")}
1853+ from_addr = format_address(
1854+ team.displayname, config.canonical.noreply_from_address)
1855+ return cls(
1856+ subject, template_name, recipients, from_addr,
1857+ "team-membership-renewed", member, team, None,
1858+ extra_params=extra_params)
1859+
1860+ def __init__(self, subject, template_name, recipients, from_address,
1861+ notification_type, member, team, reviewer, membership=None,
1862+ extra_params={}, wrap=True, force_wrap=True):
1863+ """See `BaseMailer`."""
1864+ super(TeamMembershipMailer, self).__init__(
1865+ subject, template_name, recipients, from_address,
1866+ notification_type=notification_type, wrap=wrap,
1867+ force_wrap=force_wrap)
1868+ self.member = member
1869+ self.team = team
1870+ self.reviewer = reviewer
1871+ self.membership = membership
1872+ self.extra_params = extra_params
1873+
1874+ def _getSubject(self, email, recipient):
1875+ """See `BaseMailer`."""
1876+ reason, _ = self._recipients.getReason(email)
1877+ if reason.subject is not None:
1878+ subject_template = reason.subject
1879+ else:
1880+ subject_template = self._subject_template
1881+ return subject_template % self._getTemplateParams(email, recipient)
1882+
1883+ def _getReplyToAddress(self, email, recipient):
1884+ """See `BaseMailer`."""
1885+ reason, _ = self._recipients.getReason(email)
1886+ return reason.reply_to
1887+
1888+ def _getTemplateName(self, email, recipient):
1889+ """See `BaseMailer`."""
1890+ reason, _ = self._recipients.getReason(email)
1891+ if reason.template_name is not None:
1892+ template_name = reason.template_name
1893+ else:
1894+ template_name = self._template_name
1895+ return template_name % self._getTemplateParams(email, recipient)
1896+
1897+ def _getTemplateParams(self, email, recipient):
1898+ """See `BaseMailer`."""
1899+ params = super(TeamMembershipMailer, self)._getTemplateParams(
1900+ email, recipient)
1901+ params["recipient"] = recipient.displayname
1902+ reason, _ = self._recipients.getReason(email)
1903+ if reason.recipient_class is not None:
1904+ params["recipient_class"] = reason.recipient_class
1905+ params["member"] = self.member.unique_displayname
1906+ params["membership_invitations_url"] = "%s/+invitation/%s" % (
1907+ canonical_url(self.member), self.team.name)
1908+ params["team"] = self.team.unique_displayname
1909+ params["team_url"] = canonical_url(self.team)
1910+ if self.membership is not None:
1911+ params["membership_url"] = canonical_url(self.membership)
1912+ if reason.recipient_class == "bulk" and self.reviewer == self.member:
1913+ params["reviewer"] = "the user"
1914+ elif self.reviewer is not None:
1915+ params["reviewer"] = self.reviewer.unique_displayname
1916+ if self.team.mailing_list is not None:
1917+ template = get_email_template(
1918+ "team-list-subscribe-block.txt", app="registry")
1919+ editemails_url = urlappend(
1920+ canonical_url(getUtility(ILaunchpadRoot)),
1921+ "~/+editmailinglists")
1922+ list_instructions = template % {"editemails_url": editemails_url}
1923+ else:
1924+ list_instructions = ""
1925+ params["list_instructions"] = list_instructions
1926+ params.update(self.extra_params)
1927+ return params
1928+
1929+ def _getFooter(self, email, recipient, params):
1930+ """See `BaseMailer`."""
1931+ return "%(reason)s\n" % params
1932
1933=== modified file 'lib/lp/registry/model/persontransferjob.py'
1934--- lib/lp/registry/model/persontransferjob.py 2015-07-09 20:06:17 +0000
1935+++ lib/lp/registry/model/persontransferjob.py 2015-09-08 11:57:29 +0000
1936@@ -1,4 +1,4 @@
1937-# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
1938+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
1939 # GNU Affero General Public License version 3 (see the file LICENSE).
1940
1941 """Job classes related to PersonTransferJob."""
1942@@ -9,7 +9,10 @@
1943 'PersonTransferJob',
1944 ]
1945
1946+from datetime import datetime
1947+
1948 from lazr.delegates import delegate_to
1949+import pytz
1950 import simplejson
1951 from storm.expr import (
1952 And,
1953@@ -27,16 +30,15 @@
1954 )
1955
1956 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1957-from lp.registry.enums import (
1958- PersonTransferJobType,
1959- TeamMembershipPolicy,
1960- )
1961+from lp.registry.enums import PersonTransferJobType
1962 from lp.registry.interfaces.person import (
1963 IPerson,
1964 IPersonSet,
1965 ITeam,
1966 )
1967 from lp.registry.interfaces.persontransferjob import (
1968+ IExpiringMembershipNotificationJob,
1969+ IExpiringMembershipNotificationJobSource,
1970 IMembershipNotificationJob,
1971 IMembershipNotificationJobSource,
1972 IPersonDeactivateJob,
1973@@ -45,8 +47,15 @@
1974 IPersonMergeJobSource,
1975 IPersonTransferJob,
1976 IPersonTransferJobSource,
1977+ ISelfRenewalNotificationJob,
1978+ ISelfRenewalNotificationJobSource,
1979+ ITeamInvitationNotificationJob,
1980+ ITeamInvitationNotificationJobSource,
1981+ ITeamJoinNotificationJob,
1982+ ITeamJoinNotificationJobSource,
1983 )
1984 from lp.registry.interfaces.teammembership import TeamMembershipStatus
1985+from lp.registry.mail.teammembership import TeamMembershipMailer
1986 from lp.registry.model.person import Person
1987 from lp.registry.personmerge import merge_people
1988 from lp.services.config import config
1989@@ -62,17 +71,8 @@
1990 Job,
1991 )
1992 from lp.services.job.runner import BaseRunnableJob
1993-from lp.services.mail.helpers import (
1994- get_contact_email_addresses,
1995- get_email_template,
1996- )
1997-from lp.services.mail.mailwrapper import MailWrapper
1998-from lp.services.mail.sendmail import (
1999- format_address,
2000- format_address_for_person,
2001- simple_sendmail,
2002- )
2003-from lp.services.webapp import canonical_url
2004+from lp.services.mail.sendmail import format_address_for_person
2005+from lp.services.scripts import log
2006
2007
2008 @implementer(IPersonTransferJob)
2009@@ -182,6 +182,17 @@
2010 ])
2011 return vars
2012
2013+ _time_format = '%Y-%m-%d %H:%M:%S.%f'
2014+
2015+ @classmethod
2016+ def _serialiseDateTime(cls, dt):
2017+ return dt.strftime(cls._time_format)
2018+
2019+ @classmethod
2020+ def _deserialiseDateTime(cls, dt_str):
2021+ dt = datetime.strptime(dt_str, cls._time_format)
2022+ return dt.replace(tzinfo=pytz.UTC)
2023+
2024
2025 @implementer(IMembershipNotificationJob)
2026 @provider(IMembershipNotificationJobSource)
2027@@ -240,107 +251,9 @@
2028
2029 def run(self):
2030 """See `IMembershipNotificationJob`."""
2031- from lp.services.scripts import log
2032- from_addr = format_address(
2033- self.team.displayname, config.canonical.noreply_from_address)
2034- admin_emails = self.team.getTeamAdminsEmailAddresses()
2035- # person might be a self.team, so we can't rely on its preferredemail.
2036- self.member_email = get_contact_email_addresses(self.member)
2037- # Make sure we don't send the same notification twice to anybody.
2038- for email in self.member_email:
2039- if email in admin_emails:
2040- admin_emails.remove(email)
2041-
2042- if self.reviewer != self.member:
2043- self.reviewer_name = self.reviewer.unique_displayname
2044- else:
2045- self.reviewer_name = 'the user'
2046-
2047- if self.last_change_comment:
2048- comment = ("\n%s said:\n %s\n" % (
2049- self.reviewer.displayname, self.last_change_comment.strip()))
2050- else:
2051- comment = ""
2052-
2053- replacements = {
2054- 'member_name': self.member.unique_displayname,
2055- 'recipient_name': self.member.displayname,
2056- 'team_name': self.team.unique_displayname,
2057- 'team_url': canonical_url(self.team),
2058- 'old_status': self.old_status.title,
2059- 'new_status': self.new_status.title,
2060- 'reviewer_name': self.reviewer_name,
2061- 'comment': comment}
2062-
2063- template_name = 'membership-statuschange'
2064- subject = (
2065- 'Membership change: %(member)s in %(team)s'
2066- % {
2067- 'member': self.member.name,
2068- 'team': self.team.name,
2069- })
2070- if self.new_status == TeamMembershipStatus.EXPIRED:
2071- template_name = 'membership-expired'
2072- subject = '%s expired from team' % self.member.name
2073- elif (self.new_status == TeamMembershipStatus.APPROVED and
2074- self.old_status != TeamMembershipStatus.ADMIN):
2075- if self.old_status == TeamMembershipStatus.INVITED:
2076- subject = ('Invitation to %s accepted by %s'
2077- % (self.member.name, self.reviewer.name))
2078- template_name = 'membership-invitation-accepted'
2079- elif self.old_status == TeamMembershipStatus.PROPOSED:
2080- subject = '%s approved by %s' % (
2081- self.member.name, self.reviewer.name)
2082- else:
2083- subject = '%s added by %s' % (
2084- self.member.name, self.reviewer.name)
2085- elif self.new_status == TeamMembershipStatus.INVITATION_DECLINED:
2086- subject = ('Invitation to %s declined by %s'
2087- % (self.member.name, self.reviewer.name))
2088- template_name = 'membership-invitation-declined'
2089- elif self.new_status == TeamMembershipStatus.DEACTIVATED:
2090- subject = '%s deactivated by %s' % (
2091- self.member.name, self.reviewer.name)
2092- elif self.new_status == TeamMembershipStatus.ADMIN:
2093- subject = '%s made admin by %s' % (
2094- self.member.name, self.reviewer.name)
2095- elif self.new_status == TeamMembershipStatus.DECLINED:
2096- subject = '%s declined by %s' % (
2097- self.member.name, self.reviewer.name)
2098- else:
2099- # Use the default template and subject.
2100- pass
2101-
2102- # Must have someone to mail, and be a non-open team (because open
2103- # teams are unrestricted, notifications on join/ leave do not help the
2104- # admins.
2105- if (len(admin_emails) != 0 and
2106- self.team.membership_policy != TeamMembershipPolicy.OPEN):
2107- admin_template = get_email_template(
2108- "%s-bulk.txt" % template_name, app='registry')
2109- for address in admin_emails:
2110- recipient = getUtility(IPersonSet).getByEmail(address)
2111- replacements['recipient_name'] = recipient.displayname
2112- msg = MailWrapper().format(
2113- admin_template % replacements, force_wrap=True)
2114- simple_sendmail(from_addr, address, subject, msg)
2115-
2116- # The self.member can be a self.self.team without any
2117- # self.members, and in this case we won't have a single email
2118- # address to send this notification to.
2119- if self.member_email and self.reviewer != self.member:
2120- if self.member.is_team:
2121- template = '%s-bulk.txt' % template_name
2122- else:
2123- template = '%s-personal.txt' % template_name
2124- self.member_template = get_email_template(
2125- template, app='registry')
2126- for address in self.member_email:
2127- recipient = getUtility(IPersonSet).getByEmail(address)
2128- replacements['recipient_name'] = recipient.displayname
2129- msg = MailWrapper().format(
2130- self.member_template % replacements, force_wrap=True)
2131- simple_sendmail(from_addr, address, subject, msg)
2132+ TeamMembershipMailer.forMembershipStatusChange(
2133+ self.member, self.team, self.reviewer, self.old_status,
2134+ self.new_status, self.last_change_comment).sendAll()
2135 log.debug('MembershipNotificationJob sent email')
2136
2137 def __repr__(self):
2138@@ -430,7 +343,6 @@
2139 from_person_name = self.from_person.name
2140 to_person_name = self.to_person.name
2141
2142- from lp.services.scripts import log
2143 if self.metadata.get('delete', False):
2144 log.debug(
2145 "%s is about to delete ~%s", self.log_name,
2146@@ -511,7 +423,6 @@
2147
2148 def run(self):
2149 """Perform the merge."""
2150- from lp.services.scripts import log
2151 person_name = self.person.name
2152 log.debug('about to deactivate ~%s', person_name)
2153 self.person.deactivate(validate=False, pre_deactivate=False)
2154@@ -524,3 +435,160 @@
2155
2156 def getOperationDescription(self):
2157 return 'deactivating ~%s' % self.person.name
2158+
2159+
2160+@implementer(ITeamInvitationNotificationJob)
2161+@provider(ITeamInvitationNotificationJobSource)
2162+class TeamInvitationNotificationJob(PersonTransferJobDerived):
2163+ """A Job that sends a notification of an invitation to join a team."""
2164+
2165+ class_job_type = PersonTransferJobType.TEAM_INVITATION_NOTIFICATION
2166+
2167+ config = config.ITeamInvitationNotificationJobSource
2168+
2169+ @classmethod
2170+ def create(cls, member, team):
2171+ if not ITeam.providedBy(team):
2172+ raise TypeError('team must be ITeam: %s' % repr(team))
2173+ return super(TeamInvitationNotificationJob, cls).create(
2174+ minor_person=member, major_person=team, metadata={})
2175+
2176+ @property
2177+ def member(self):
2178+ return self.minor_person
2179+
2180+ @property
2181+ def team(self):
2182+ return self.major_person
2183+
2184+ def run(self):
2185+ """See `ITeamInvitationNotificationJob`."""
2186+ TeamMembershipMailer.forInvitationToJoinTeam(
2187+ self.member, self.team).sendAll()
2188+
2189+ def __repr__(self):
2190+ return (
2191+ "<{self.__class__.__name__} for invitation of "
2192+ "~{self.minor_person.name} to join ~{self.major_person.name}; "
2193+ "status={self.job.status}>").format(self=self)
2194+
2195+
2196+@implementer(ITeamJoinNotificationJob)
2197+@provider(ITeamJoinNotificationJobSource)
2198+class TeamJoinNotificationJob(PersonTransferJobDerived):
2199+ """A Job that sends a notification of a new member joining a team."""
2200+
2201+ class_job_type = PersonTransferJobType.TEAM_JOIN_NOTIFICATION
2202+
2203+ config = config.ITeamJoinNotificationJobSource
2204+
2205+ @classmethod
2206+ def create(cls, member, team):
2207+ if not ITeam.providedBy(team):
2208+ raise TypeError('team must be ITeam: %s' % repr(team))
2209+ return super(TeamJoinNotificationJob, cls).create(
2210+ minor_person=member, major_person=team, metadata={})
2211+
2212+ @property
2213+ def member(self):
2214+ return self.minor_person
2215+
2216+ @property
2217+ def team(self):
2218+ return self.major_person
2219+
2220+ def run(self):
2221+ """See `ITeamJoinNotificationJob`."""
2222+ TeamMembershipMailer.forTeamJoin(self.member, self.team).sendAll()
2223+
2224+ def __repr__(self):
2225+ return (
2226+ "<{self.__class__.__name__} for "
2227+ "~{self.minor_person.name} joining ~{self.major_person.name}; "
2228+ "status={self.job.status}>").format(self=self)
2229+
2230+
2231+@implementer(IExpiringMembershipNotificationJob)
2232+@provider(IExpiringMembershipNotificationJobSource)
2233+class ExpiringMembershipNotificationJob(PersonTransferJobDerived):
2234+ """A Job that sends a warning about expiring membership."""
2235+
2236+ class_job_type = PersonTransferJobType.EXPIRING_MEMBERSHIP_NOTIFICATION
2237+
2238+ config = config.IExpiringMembershipNotificationJobSource
2239+
2240+ @classmethod
2241+ def create(cls, member, team, dateexpires):
2242+ if not ITeam.providedBy(team):
2243+ raise TypeError('team must be ITeam: %s' % repr(team))
2244+ metadata = {
2245+ 'dateexpires': cls._serialiseDateTime(dateexpires),
2246+ }
2247+ return super(ExpiringMembershipNotificationJob, cls).create(
2248+ minor_person=member, major_person=team, metadata=metadata)
2249+
2250+ @property
2251+ def member(self):
2252+ return self.minor_person
2253+
2254+ @property
2255+ def team(self):
2256+ return self.major_person
2257+
2258+ @property
2259+ def dateexpires(self):
2260+ return self._deserialiseDateTime(self.metadata['dateexpires'])
2261+
2262+ def run(self):
2263+ """See `IExpiringMembershipNotificationJob`."""
2264+ TeamMembershipMailer.forExpiringMembership(
2265+ self.member, self.team, self.dateexpires).sendAll()
2266+
2267+ def __repr__(self):
2268+ return (
2269+ "<{self.__class__.__name__} for upcoming expiry of "
2270+ "~{self.minor_person.name} from ~{self.major_person.name}; "
2271+ "status={self.job.status}>").format(self=self)
2272+
2273+
2274+@implementer(ISelfRenewalNotificationJob)
2275+@provider(ISelfRenewalNotificationJobSource)
2276+class SelfRenewalNotificationJob(PersonTransferJobDerived):
2277+ """A Job that sends a notification of a self-renewal."""
2278+
2279+ class_job_type = PersonTransferJobType.SELF_RENEWAL_NOTIFICATION
2280+
2281+ config = config.ISelfRenewalNotificationJobSource
2282+
2283+ @classmethod
2284+ def create(cls, member, team, dateexpires):
2285+ if not ITeam.providedBy(team):
2286+ raise TypeError('team must be ITeam: %s' % repr(team))
2287+ metadata = {
2288+ 'dateexpires': cls._serialiseDateTime(dateexpires),
2289+ }
2290+ return super(SelfRenewalNotificationJob, cls).create(
2291+ minor_person=member, major_person=team, metadata=metadata)
2292+
2293+ @property
2294+ def member(self):
2295+ return self.minor_person
2296+
2297+ @property
2298+ def team(self):
2299+ return self.major_person
2300+
2301+ @property
2302+ def dateexpires(self):
2303+ return self._deserialiseDateTime(self.metadata['dateexpires'])
2304+
2305+ def run(self):
2306+ """See `ISelfRenewalNotificationJob`."""
2307+ TeamMembershipMailer.forSelfRenewal(
2308+ self.member, self.team, self.dateexpires).sendAll()
2309+
2310+ def __repr__(self):
2311+ return (
2312+ "<{self.__class__.__name__} for self-renewal of "
2313+ "~{self.minor_person.name} in ~{self.major_person.name}; "
2314+ "status={self.job.status}>").format(self=self)
2315
2316=== modified file 'lib/lp/registry/model/teammembership.py'
2317--- lib/lp/registry/model/teammembership.py 2015-07-08 16:05:11 +0000
2318+++ lib/lp/registry/model/teammembership.py 2015-09-08 11:57:29 +0000
2319@@ -1,4 +1,4 @@
2320-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
2321+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2322 # GNU Affero General Public License version 3 (see the file LICENSE).
2323
2324 __metaclass__ = type
2325@@ -24,7 +24,6 @@
2326 from zope.component import getUtility
2327 from zope.interface import implementer
2328
2329-from lp.app.browser.tales import DurationFormatterAPI
2330 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
2331 from lp.registry.enums import TeamMembershipRenewalPolicy
2332 from lp.registry.errors import (
2333@@ -32,12 +31,13 @@
2334 UserCannotChangeMembershipSilently,
2335 )
2336 from lp.registry.interfaces.person import (
2337- IPersonSet,
2338 validate_person,
2339 validate_public_person,
2340 )
2341 from lp.registry.interfaces.persontransferjob import (
2342+ IExpiringMembershipNotificationJobSource,
2343 IMembershipNotificationJobSource,
2344+ ISelfRenewalNotificationJobSource,
2345 )
2346 from lp.registry.interfaces.role import IPersonRoles
2347 from lp.registry.interfaces.sharingjob import (
2348@@ -52,7 +52,6 @@
2349 ITeamParticipation,
2350 TeamMembershipStatus,
2351 )
2352-from lp.services.config import config
2353 from lp.services.database.constants import UTC_NOW
2354 from lp.services.database.datetimecol import UtcDateTimeCol
2355 from lp.services.database.enumcol import EnumCol
2356@@ -63,16 +62,6 @@
2357 SQLBase,
2358 sqlvalues,
2359 )
2360-from lp.services.mail.helpers import (
2361- get_contact_email_addresses,
2362- get_email_template,
2363- )
2364-from lp.services.mail.mailwrapper import MailWrapper
2365-from lp.services.mail.sendmail import (
2366- format_address,
2367- simple_sendmail,
2368- )
2369-from lp.services.webapp import canonical_url
2370
2371
2372 @implementer(ITeamMembership)
2373@@ -132,26 +121,8 @@
2374
2375 def sendSelfRenewalNotification(self):
2376 """See `ITeamMembership`."""
2377- team = self.team
2378- member = self.person
2379- assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND
2380-
2381- from_addr = format_address(
2382- team.displayname, config.canonical.noreply_from_address)
2383- replacements = {'member_name': member.unique_displayname,
2384- 'team_name': team.unique_displayname,
2385- 'team_url': canonical_url(team),
2386- 'dateexpires': self.dateexpires.strftime('%Y-%m-%d')}
2387- subject = '%s extended their membership' % member.name
2388- template = get_email_template(
2389- 'membership-member-renewed.txt', app='registry')
2390- admins_addrs = self.team.getTeamAdminsEmailAddresses()
2391- for address in admins_addrs:
2392- recipient = getUtility(IPersonSet).getByEmail(address)
2393- replacements['recipient_name'] = recipient.displayname
2394- msg = MailWrapper().format(
2395- template % replacements, force_wrap=True)
2396- simple_sendmail(from_addr, address, subject, msg)
2397+ getUtility(ISelfRenewalNotificationJobSource).create(
2398+ self.person, self.team, self.dateexpires)
2399
2400 def canChangeStatusSilently(self, user):
2401 """Ensure that the user is in the Launchpad Administrators group.
2402@@ -194,68 +165,8 @@
2403 # there is nothing to do. The member will have received emails
2404 # from previous calls by flag-expired-memberships.py
2405 return
2406- member = self.person
2407- team = self.team
2408- if member.is_team:
2409- recipient = member.teamowner
2410- templatename = 'membership-expiration-warning-bulk.txt'
2411- subject = '%s will expire soon from %s' % (member.name, team.name)
2412- else:
2413- recipient = member
2414- templatename = 'membership-expiration-warning-personal.txt'
2415- subject = 'Your membership in %s is about to expire' % team.name
2416-
2417- if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND:
2418- how_to_renew = (
2419- "If you want, you can renew this membership at\n"
2420- "<%s/+expiringmembership/%s>"
2421- % (canonical_url(member), team.name))
2422- elif not self.canChangeExpirationDate(recipient):
2423- admins_names = []
2424- admins = team.getDirectAdministrators()
2425- assert admins.count() >= 1
2426- if admins.count() == 1:
2427- admin = admins[0]
2428- how_to_renew = (
2429- "To prevent this membership from expiring, you should "
2430- "contact the\nteam's administrator, %s.\n<%s>"
2431- % (admin.unique_displayname, canonical_url(admin)))
2432- else:
2433- for admin in admins:
2434- admins_names.append(
2435- "%s <%s>" % (admin.unique_displayname,
2436- canonical_url(admin)))
2437-
2438- how_to_renew = (
2439- "To prevent this membership from expiring, you should "
2440- "get in touch\nwith one of the team's administrators:\n")
2441- how_to_renew += "\n".join(admins_names)
2442- else:
2443- how_to_renew = (
2444- "To stay a member of this team you should extend your "
2445- "membership at\n<%s/+member/%s>"
2446- % (canonical_url(team), member.name))
2447-
2448- to_addrs = get_contact_email_addresses(recipient)
2449- if len(to_addrs) == 0:
2450- # The user does not have a preferred email address, he was
2451- # probably suspended.
2452- return
2453- formatter = DurationFormatterAPI(
2454- self.dateexpires - datetime.now(pytz.timezone('UTC')))
2455- replacements = {
2456- 'recipient_name': recipient.displayname,
2457- 'member_name': member.unique_displayname,
2458- 'team_url': canonical_url(team),
2459- 'how_to_renew': how_to_renew,
2460- 'team_name': team.unique_displayname,
2461- 'expiration_date': self.dateexpires.strftime('%Y-%m-%d'),
2462- 'approximate_duration': formatter.approximateduration()}
2463-
2464- msg = get_email_template(templatename, app='registry') % replacements
2465- from_addr = format_address(
2466- team.displayname, config.canonical.noreply_from_address)
2467- simple_sendmail(from_addr, to_addrs, subject, msg)
2468+ getUtility(IExpiringMembershipNotificationJobSource).create(
2469+ self.person, self.team, self.dateexpires)
2470
2471 def setStatus(self, status, user, comment=None, silent=False):
2472 """See `ITeamMembership`."""
2473
2474=== modified file 'lib/lp/registry/stories/person/xx-approve-members.txt'
2475--- lib/lp/registry/stories/person/xx-approve-members.txt 2010-10-06 21:28:02 +0000
2476+++ lib/lp/registry/stories/person/xx-approve-members.txt 2015-09-08 11:57:29 +0000
2477@@ -47,6 +47,7 @@
2478 ...
2479 Mark Shuttleworth said:
2480 Thanks for your interest
2481+ ...
2482
2483 As we can see, Andrew is now listed among the active members and Sample Person
2484 as an inactive one.
2485@@ -74,4 +75,3 @@
2486 Traceback (most recent call last):
2487 ...
2488 LinkNotFoundError
2489-
2490
2491=== modified file 'lib/lp/registry/tests/test_teammembership.py'
2492--- lib/lp/registry/tests/test_teammembership.py 2013-06-20 05:50:00 +0000
2493+++ lib/lp/registry/tests/test_teammembership.py 2015-09-08 11:57:29 +0000
2494@@ -1,4 +1,4 @@
2495-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2496+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2497 # GNU Affero General Public License version 3 (see the file LICENSE).
2498
2499 __metaclass__ = type
2500@@ -33,6 +33,10 @@
2501 IAccessArtifactSource,
2502 )
2503 from lp.registry.interfaces.person import IPersonSet
2504+from lp.registry.interfaces.persontransferjob import (
2505+ IExpiringMembershipNotificationJobSource,
2506+ ITeamJoinNotificationJobSource,
2507+ )
2508 from lp.registry.interfaces.teammembership import (
2509 CyclicalTeamMembershipError,
2510 ITeamMembershipSet,
2511@@ -59,8 +63,10 @@
2512 sqlvalues,
2513 )
2514 from lp.services.features.testing import FeatureFixture
2515+from lp.services.job.runner import JobRunner
2516 from lp.services.job.tests import block_on_job
2517 from lp.services.log.logger import BufferLogger
2518+from lp.services.mail.sendmail import format_address_for_person
2519 from lp.testing import (
2520 login,
2521 login_celebrity,
2522@@ -281,11 +287,17 @@
2523 class TestTeamParticipationQuery(TeamParticipationTestCase):
2524 """A test case for teammembership.test_find_team_participations."""
2525
2526+ def runMailJobs(self):
2527+ with dbuser("person-transfer-job"):
2528+ JobRunner.fromReady(
2529+ getUtility(ITeamJoinNotificationJobSource)).runAll()
2530+
2531 def test_find_team_participations(self):
2532 # The correct team participations are found and the query count is 1.
2533 self.team1.addMember(self.no_priv, self.foo_bar)
2534 self.team2.addMember(self.no_priv, self.foo_bar)
2535 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
2536+ self.runMailJobs()
2537
2538 people = [self.team1, self.team2]
2539 with StormStatementRecorder() as recorder:
2540@@ -301,9 +313,12 @@
2541 self.team1.addMember(self.no_priv, self.foo_bar)
2542 self.team2.addMember(self.no_priv, self.foo_bar)
2543 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
2544+ self.runMailJobs()
2545
2546 people = [self.foo_bar, self.team2]
2547 teams = [self.team1, self.team2]
2548+ # Repopulate Storm cache after running mail jobs.
2549+ [team.is_team for team in teams]
2550 with StormStatementRecorder() as recorder:
2551 people_teams = find_team_participations(people, teams)
2552 self.assertThat(recorder, HasQueryCount(Equals(1)))
2553@@ -1067,6 +1082,12 @@
2554 self.member, self.team)
2555 pop_notifications()
2556
2557+ def runMailJobs(self):
2558+ with dbuser("person-transfer-job"):
2559+ JobRunner.fromReady(
2560+ getUtility(IExpiringMembershipNotificationJobSource)).runAll()
2561+ return pop_notifications()
2562+
2563 def test_error_raised_when_no_expiration(self):
2564 # An exception is raised if the membership does not have an
2565 # expiration date.
2566@@ -1080,20 +1101,19 @@
2567 tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
2568 removeSecurityProxy(self.tm).dateexpires = tomorrow
2569 self.tm.sendExpirationWarningEmail()
2570- notifications = pop_notifications()
2571+ notifications = self.runMailJobs()
2572 self.assertEqual(1, len(notifications))
2573 message = notifications[0]
2574 self.assertEqual(
2575 'Your membership in red is about to expire', message['subject'])
2576- self.assertEqual(
2577- self.member.preferredemail.email, message['to'])
2578+ self.assertEqual(format_address_for_person(self.member), message['to'])
2579
2580 def test_no_message_sent_for_expired_memberships(self):
2581 # Members whose membership has expired do not get a message.
2582 yesterday = datetime.now(pytz.UTC) - timedelta(days=1)
2583 removeSecurityProxy(self.tm).dateexpires = yesterday
2584 self.tm.sendExpirationWarningEmail()
2585- notifications = pop_notifications()
2586+ notifications = self.runMailJobs()
2587 self.assertEqual(0, len(notifications))
2588
2589 def test_no_message_sent_for_non_active_users(self):
2590@@ -1104,7 +1124,7 @@
2591 now = datetime.now(pytz.UTC)
2592 removeSecurityProxy(self.tm).dateexpires = now + timedelta(days=1)
2593 self.tm.sendExpirationWarningEmail()
2594- notifications = pop_notifications()
2595+ notifications = self.runMailJobs()
2596 self.assertEqual(0, len(notifications))
2597
2598
2599
2600=== modified file 'lib/lp/services/config/schema-lazr.conf'
2601--- lib/lp/services/config/schema-lazr.conf 2015-09-08 10:05:33 +0000
2602+++ lib/lp/services/config/schema-lazr.conf 2015-09-08 11:57:29 +0000
2603@@ -1743,6 +1743,7 @@
2604 job_sources:
2605 IBranchModifiedMailJobSource,
2606 ICommercialExpiredJobSource,
2607+ IExpiringMembershipNotificationJobSource,
2608 IGitRepositoryModifiedMailJobSource,
2609 IMembershipNotificationJobSource,
2610 IPackageUploadNotificationJobSource,
2611@@ -1753,7 +1754,10 @@
2612 IQuestionEmailJobSource,
2613 IReclaimGitRepositorySpaceJobSource,
2614 IRemoveArtifactSubscriptionsJobSource,
2615+ ISelfRenewalNotificationJobSource,
2616 ISevenDayCommercialExpirationJobSource,
2617+ ITeamInvitationNotificationJobSource,
2618+ ITeamJoinNotificationJobSource,
2619 IThirtyDayCommercialExpirationJobSource
2620
2621 [IBranchMergeProposalJobSource]
2622@@ -1784,6 +1788,11 @@
2623 module: lp.soyuz.interfaces.distributionjob
2624 dbuser: distroseriesdifferencejob
2625
2626+[IExpiringMembershipNotificationJobSource]
2627+module: lp.registry.interfaces.persontransferjob
2628+dbuser: person-transfer-job
2629+crontab_group: MAIN
2630+
2631 [IGitRefScanJobSource]
2632 module: lp.code.interfaces.gitjob
2633 dbuser: branchscanner
2634@@ -1878,11 +1887,26 @@
2635 module: lp.code.interfaces.branchjob
2636 dbuser: translationsbranchscanner
2637
2638+[ISelfRenewalNotificationJobSource]
2639+module: lp.registry.interfaces.persontransferjob
2640+dbuser: person-transfer-job
2641+crontab_group: MAIN
2642+
2643 [ISevenDayCommercialExpirationJobSource]
2644 module: lp.registry.interfaces.productjob
2645 dbuser: product-job
2646 crontab_group: MAIN
2647
2648+[ITeamInvitationNotificationJobSource]
2649+module: lp.registry.interfaces.persontransferjob
2650+dbuser: person-transfer-job
2651+crontab_group: MAIN
2652+
2653+[ITeamJoinNotificationJobSource]
2654+module: lp.registry.interfaces.persontransferjob
2655+dbuser: person-transfer-job
2656+crontab_group: MAIN
2657+
2658 [IThirtyDayCommercialExpirationJobSource]
2659 module: lp.registry.interfaces.productjob
2660 dbuser: product-job
2661
2662=== modified file 'lib/lp/services/mail/basemailer.py'
2663--- lib/lp/services/mail/basemailer.py 2015-08-25 14:05:24 +0000
2664+++ lib/lp/services/mail/basemailer.py 2015-09-08 11:57:29 +0000
2665@@ -16,6 +16,7 @@
2666 from zope.error.interfaces import IErrorReportingUtility
2667
2668 from lp.services.mail.helpers import get_email_template
2669+from lp.services.mail.mailwrapper import MailWrapper
2670 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
2671 from lp.services.mail.sendmail import (
2672 append_footer,
2673@@ -39,7 +40,8 @@
2674
2675 def __init__(self, subject, template_name, recipients, from_address,
2676 delta=None, message_id=None, notification_type=None,
2677- mail_controller_class=None, request=None):
2678+ mail_controller_class=None, request=None, wrap=False,
2679+ force_wrap=False):
2680 """Constructor.
2681
2682 :param subject: A Python dict-replacement template for the subject
2683@@ -55,6 +57,8 @@
2684 use to send the mails. Defaults to `MailController`.
2685 :param request: An optional `IErrorReportRequest` to use when
2686 logging OOPSes.
2687+ :param wrap: Wrap body text using `MailWrapper`.
2688+ :param force_wrap: See `MailWrapper.format`.
2689 """
2690 self._subject_template = subject
2691 self._template_name = template_name
2692@@ -70,6 +74,8 @@
2693 mail_controller_class = MailController
2694 self._mail_controller_class = mail_controller_class
2695 self.request = request
2696+ self._wrap = wrap
2697+ self._force_wrap = force_wrap
2698
2699 def _getFromAddress(self, email, recipient):
2700 return self.from_address
2701@@ -108,7 +114,7 @@
2702 return (self._subject_template %
2703 self._getTemplateParams(email, recipient))
2704
2705- def _getReplyToAddress(self):
2706+ def _getReplyToAddress(self, email, recipient):
2707 """Return the address to use for the reply-to header."""
2708 return None
2709
2710@@ -119,7 +125,7 @@
2711 headers['X-Launchpad-Message-Rationale'] = reason.mail_header
2712 if self.notification_type is not None:
2713 headers['X-Launchpad-Notification-Type'] = self.notification_type
2714- reply_to = self._getReplyToAddress()
2715+ reply_to = self._getReplyToAddress(email, recipient)
2716 if reply_to is not None:
2717 headers['Reply-To'] = reply_to
2718 if self.message_id is not None:
2719@@ -158,6 +164,9 @@
2720 self._getTemplateName(email, recipient), app=self.app)
2721 params = self._getTemplateParams(email, recipient)
2722 body = template % params
2723+ if self._wrap:
2724+ body = MailWrapper().format(
2725+ body, force_wrap=self._force_wrap) + "\n"
2726 footer = self._getFooter(email, recipient, params)
2727 if footer is not None:
2728 body = append_footer(body, footer)
2729
2730=== modified file 'lib/lp/testing/mail_helpers.py'
2731--- lib/lp/testing/mail_helpers.py 2015-07-21 09:04:01 +0000
2732+++ lib/lp/testing/mail_helpers.py 2015-09-08 11:57:29 +0000
2733@@ -1,4 +1,4 @@
2734-# Copyright 2009 Canonical Ltd. This software is licensed under the
2735+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2736 # GNU Affero General Public License version 3 (see the file LICENSE).
2737
2738 """Helper functions dealing with emails in tests.
2739@@ -12,11 +12,17 @@
2740 from zope.component import getUtility
2741
2742 from lp.registry.interfaces.persontransferjob import (
2743+ IExpiringMembershipNotificationJobSource,
2744 IMembershipNotificationJobSource,
2745+ ISelfRenewalNotificationJobSource,
2746+ ITeamInvitationNotificationJobSource,
2747+ ITeamJoinNotificationJobSource,
2748 )
2749+from lp.services.config import config
2750 from lp.services.job.runner import JobRunner
2751 from lp.services.log.logger import DevNullLogger
2752 from lp.services.mail import stub
2753+from lp.testing.dbuser import dbuser
2754
2755
2756 def pop_notifications(sort_key=None, commit=True):
2757@@ -54,7 +60,8 @@
2758
2759
2760 def print_emails(include_reply_to=False, group_similar=False,
2761- include_rationale=False, notifications=None):
2762+ include_rationale=False, notifications=None,
2763+ include_notification_type=False):
2764 """Pop all messages from stub.test_emails and print them with
2765 their recipients.
2766
2767@@ -71,6 +78,8 @@
2768 header.
2769 :param notifications: Use the provided list of notifications instead of
2770 the stack.
2771+ :param include_notification_type: Include the
2772+ X-Launchpad-Notification-Type header.
2773 """
2774 distinct_bodies = {}
2775 if notifications is None:
2776@@ -99,16 +108,22 @@
2777 if include_rationale and rationale_header in message:
2778 print (
2779 '%s: %s' % (rationale_header, message[rationale_header]))
2780+ notification_type_header = 'X-Launchpad-Notification-Type'
2781+ if include_notification_type and notification_type_header in message:
2782+ print '%s: %s' % (
2783+ notification_type_header, message[notification_type_header])
2784 print 'Subject:', message['Subject']
2785 print body
2786 print "-" * 40
2787
2788
2789-def print_distinct_emails(include_reply_to=False, include_rationale=True):
2790+def print_distinct_emails(include_reply_to=False, include_rationale=True,
2791+ include_notification_type=True):
2792 """A convenient shortcut for `print_emails`(group_similar=True)."""
2793 return print_emails(group_similar=True,
2794 include_reply_to=include_reply_to,
2795- include_rationale=include_rationale)
2796+ include_rationale=include_rationale,
2797+ include_notification_type=include_notification_type)
2798
2799
2800 def run_mail_jobs():
2801@@ -121,7 +136,15 @@
2802 # Commit the transaction to make sure that the JobRunner can find
2803 # the queued jobs.
2804 transaction.commit()
2805- job_source = getUtility(IMembershipNotificationJobSource)
2806- logger = DevNullLogger()
2807- runner = JobRunner.fromReady(job_source, logger)
2808- runner.runAll()
2809+ for interface in (
2810+ IExpiringMembershipNotificationJobSource,
2811+ IMembershipNotificationJobSource,
2812+ ISelfRenewalNotificationJobSource,
2813+ ITeamInvitationNotificationJobSource,
2814+ ITeamJoinNotificationJobSource,
2815+ ):
2816+ job_source = getUtility(interface)
2817+ logger = DevNullLogger()
2818+ with dbuser(getattr(config, interface.__name__).dbuser):
2819+ runner = JobRunner.fromReady(job_source, logger)
2820+ runner.runAll()