Merge lp:~cjwatson/launchpad/team-mail into lp:launchpad
- team-mail
- Merge into devel
Proposed by
Colin Watson
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
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-
To post a comment you must log in.
Revision history for this message
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() |
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-membershi p-new", "team-membershi p-pending" etc. so people can easily blackhole all notifications about a particularly boring team's membership.
I'd also call the new thing TeamMembershipM ailer, and put it in teammembership.py, as there are other types of team emails.