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

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

Commit message

Convert team membership notifications to BaseMailer.

Description of the change

Convert team membership notifications to BaseMailer.

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

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

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

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

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2015-09-03 15:14:07 +0000
+++ database/schema/security.cfg 2015-09-08 11:57:29 +0000
@@ -2124,9 +2124,12 @@
2124public.account = SELECT2124public.account = SELECT
2125public.emailaddress = SELECT2125public.emailaddress = SELECT
2126public.job = SELECT, UPDATE2126public.job = SELECT, UPDATE
2127public.mailinglist = SELECT
2127public.person = SELECT2128public.person = SELECT
2129public.personsettings = SELECT
2128public.persontransferjob = SELECT2130public.persontransferjob = SELECT
2129public.teammembership = SELECT2131public.teammembership = SELECT
2132public.teamparticipation = SELECT
2130type=user2133type=user
21312134
2132[person-merge-job]2135[person-merge-job]
21332136
=== modified file 'lib/lp/code/mail/branchmergeproposal.py'
--- lib/lp/code/mail/branchmergeproposal.py 2015-09-01 17:10:46 +0000
+++ lib/lp/code/mail/branchmergeproposal.py 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Email notifications related to branch merge proposals."""4"""Email notifications related to branch merge proposals."""
@@ -96,7 +96,7 @@
96 merge_proposal, from_address, message_id=get_msgid(),96 merge_proposal, from_address, message_id=get_msgid(),
97 preview_diff=merge_proposal.preview_diff, direct_email=True)97 preview_diff=merge_proposal.preview_diff, direct_email=True)
9898
99 def _getReplyToAddress(self):99 def _getReplyToAddress(self, email, recipient):
100 """Return the address to use for the reply-to header."""100 """Return the address to use for the reply-to header."""
101 return self.merge_proposal.address101 return self.merge_proposal.address
102102
103103
=== modified file 'lib/lp/code/mail/tests/test_codereviewcomment.py'
--- lib/lp/code/mail/tests/test_codereviewcomment.py 2015-09-02 16:54:24 +0000
+++ lib/lp/code/mail/tests/test_codereviewcomment.py 2015-09-08 11:57:29 +0000
@@ -140,7 +140,10 @@
140 mailer, subscriber = self.makeMailer()140 mailer, subscriber = self.makeMailer()
141 merge_proposal = mailer.code_review_comment.branch_merge_proposal141 merge_proposal = mailer.code_review_comment.branch_merge_proposal
142 expected = 'mp+%d@code.launchpad.dev' % merge_proposal.id142 expected = 'mp+%d@code.launchpad.dev' % merge_proposal.id
143 self.assertEqual(expected, mailer._getReplyToAddress())143 self.assertEqual(
144 expected,
145 mailer._getReplyToAddress(
146 subscriber.preferredemail.email, subscriber))
144147
145 def test_generateEmail(self):148 def test_generateEmail(self):
146 """Ensure mailer's generateEmail method produces expected values."""149 """Ensure mailer's generateEmail method produces expected values."""
@@ -164,7 +167,8 @@
164 'X-Launchpad-Notification-Type': 'code-review',167 'X-Launchpad-Notification-Type': 'code-review',
165 'X-Launchpad-Project': source_branch.product.name,168 'X-Launchpad-Project': source_branch.product.name,
166 'Message-Id': message.rfc822msgid,169 'Message-Id': message.rfc822msgid,
167 'Reply-To': mailer._getReplyToAddress(),170 'Reply-To': mailer._getReplyToAddress(
171 subscriber.preferredemail.email, subscriber),
168 'In-Reply-To': message.parent.rfc822msgid}172 'In-Reply-To': message.parent.rfc822msgid}
169 for header, value in expected.items():173 for header, value in expected.items():
170 self.assertEqual(value, ctrl.headers[header], header)174 self.assertEqual(value, ctrl.headers[header], header)
171175
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2015-06-26 14:00:41 +0000
+++ lib/lp/registry/configure.zcml 2015-09-08 11:57:29 +0000
@@ -104,6 +104,30 @@
104 <allow interface=".interfaces.persontransferjob.IPersonDeactivateJobSource"/>104 <allow interface=".interfaces.persontransferjob.IPersonDeactivateJobSource"/>
105 </securedutility>105 </securedutility>
106106
107 <securedutility
108 component=".model.persontransferjob.TeamInvitationNotificationJob"
109 provides=".interfaces.persontransferjob.ITeamInvitationNotificationJobSource">
110 <allow interface=".interfaces.persontransferjob.ITeamInvitationNotificationJobSource"/>
111 </securedutility>
112
113 <securedutility
114 component=".model.persontransferjob.TeamJoinNotificationJob"
115 provides=".interfaces.persontransferjob.ITeamJoinNotificationJobSource">
116 <allow interface=".interfaces.persontransferjob.ITeamJoinNotificationJobSource"/>
117 </securedutility>
118
119 <securedutility
120 component=".model.persontransferjob.ExpiringMembershipNotificationJob"
121 provides=".interfaces.persontransferjob.IExpiringMembershipNotificationJobSource">
122 <allow interface=".interfaces.persontransferjob.IExpiringMembershipNotificationJobSource"/>
123 </securedutility>
124
125 <securedutility
126 component=".model.persontransferjob.SelfRenewalNotificationJob"
127 provides=".interfaces.persontransferjob.ISelfRenewalNotificationJobSource">
128 <allow interface=".interfaces.persontransferjob.ISelfRenewalNotificationJobSource"/>
129 </securedutility>
130
107 <class class=".model.persontransferjob.PersonTransferJob">131 <class class=".model.persontransferjob.PersonTransferJob">
108 <allow interface=".interfaces.persontransferjob.IPersonTransferJob"/>132 <allow interface=".interfaces.persontransferjob.IPersonTransferJob"/>
109 </class>133 </class>
@@ -120,6 +144,22 @@
120 <allow interface=".interfaces.persontransferjob.IPersonDeactivateJob"/>144 <allow interface=".interfaces.persontransferjob.IPersonDeactivateJob"/>
121 </class>145 </class>
122146
147 <class class=".model.persontransferjob.TeamInvitationNotificationJob">
148 <allow interface=".interfaces.persontransferjob.ITeamInvitationNotificationJob"/>
149 </class>
150
151 <class class=".model.persontransferjob.TeamJoinNotificationJob">
152 <allow interface=".interfaces.persontransferjob.ITeamJoinNotificationJob"/>
153 </class>
154
155 <class class=".model.persontransferjob.ExpiringMembershipNotificationJob">
156 <allow interface=".interfaces.persontransferjob.IExpiringMembershipNotificationJob"/>
157 </class>
158
159 <class class=".model.persontransferjob.SelfRenewalNotificationJob">
160 <allow interface=".interfaces.persontransferjob.ISelfRenewalNotificationJob"/>
161 </class>
162
123 <!-- IProductNotificationJob -->163 <!-- IProductNotificationJob -->
124 <securedutility164 <securedutility
125 component=".model.productjob.CommercialExpiredJob"165 component=".model.productjob.CommercialExpiredJob"
126166
=== modified file 'lib/lp/registry/doc/teammembership-email-notification.txt'
--- lib/lp/registry/doc/teammembership-email-notification.txt 2015-02-26 03:00:35 +0000
+++ lib/lp/registry/doc/teammembership-email-notification.txt 2015-09-08 11:57:29 +0000
@@ -84,10 +84,13 @@
8484
85 >>> print_distinct_emails(include_reply_to=True)85 >>> print_distinct_emails(include_reply_to=True)
86 From: Ubuntu Team <noreply@launchpad.net>86 From: Ubuntu Team <noreply@launchpad.net>
87 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,87 To: Alexander Limi <limi@plone.org>,
88 jeff.waugh@ubuntulinux.com, limi@plone.org88 Colin Watson <colin.watson@ubuntulinux.com>,
89 Foo Bar <foo.bar@canonical.com>,
90 Jeff Waugh <jeff.waugh@ubuntulinux.com>
89 Reply-To: robertc@robertcollins.net91 Reply-To: robertc@robertcollins.net
90 X-Launchpad-Message-Rationale: Admin (ubuntu-team)92 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
93 X-Launchpad-Notification-Type: team-membership-pending
91 Subject: lifeless wants to join94 Subject: lifeless wants to join
92 <BLANKLINE>95 <BLANKLINE>
93 Robert Collins (lifeless) wants to be a member of Ubuntu Team (ubuntu-96 Robert Collins (lifeless) wants to be a member of Ubuntu Team (ubuntu-
@@ -100,14 +103,17 @@
100 -- =103 -- =
101 <BLANKLINE>104 <BLANKLINE>
102 You received this email because you are an admin of the Ubuntu Team team.105 You received this email because you are an admin of the Ubuntu Team team.
106 <BLANKLINE>
103 ----------------------------------------107 ----------------------------------------
104 From: Ubuntu Team <noreply@launchpad.net>108 From: Ubuntu Team <noreply@launchpad.net>
105 To: mark@example.com109 To: Mark Shuttleworth <mark@example.com>
106 Reply-To: robertc@robertcollins.net110 Reply-To: robertc@robertcollins.net
107 X-Launchpad-Message-Rationale: Owner (ubuntu-team)111 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
112 X-Launchpad-Notification-Type: team-membership-pending
108 Subject: lifeless wants to join113 Subject: lifeless wants to join
109 ...114 ...
110 You received this email because you are the owner of the Ubuntu Team team.115 You received this email because you are the owner of the Ubuntu Team team.
116 <BLANKLINE>
111 ----------------------------------------117 ----------------------------------------
112118
113Declining a proposed member should generate notifications for both the119Declining a proposed member should generate notifications for both the
@@ -128,22 +134,39 @@
128134
129 >>> print_distinct_emails()135 >>> print_distinct_emails()
130 From: Ubuntu Team <noreply@launchpad.net>136 From: Ubuntu Team <noreply@launchpad.net>
131 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,137 To: Alexander Limi <limi@plone.org>,
132 jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com138 Colin Watson <colin.watson@ubuntulinux.com>,
139 Foo Bar <foo.bar@canonical.com>,
140 Jeff Waugh <jeff.waugh@ubuntulinux.com>,
141 Mark Shuttleworth <mark@example.com>
142 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
143 X-Launchpad-Notification-Type: team-membership-change
133 Subject: lifeless declined by mark144 Subject: lifeless declined by mark
134 <BLANKLINE>145 <BLANKLINE>
135 The membership status of Robert Collins (lifeless) in the team Ubuntu146 The membership status of Robert Collins (lifeless) in the team Ubuntu
136 Team (ubuntu-team) was changed by Mark Shuttleworth (mark) from147 Team (ubuntu-team) was changed by Mark Shuttleworth (mark) from
137 Proposed to Declined.148 Proposed to Declined.
138 <http://launchpad.dev/~ubuntu-team>149 <http://launchpad.dev/~ubuntu-team>
150 <BLANKLINE>
151 -- =
152 <BLANKLINE>
153 You received this email because you are an admin of the Ubuntu Team team.
154 <BLANKLINE>
139 ----------------------------------------155 ----------------------------------------
140 From: Ubuntu Team <noreply@launchpad.net>156 From: Ubuntu Team <noreply@launchpad.net>
141 To: robertc@robertcollins.net157 To: Robert Collins <robertc@robertcollins.net>
158 X-Launchpad-Message-Rationale: Member (ubuntu-team)
159 X-Launchpad-Notification-Type: team-membership-change
142 Subject: lifeless declined by mark160 Subject: lifeless declined by mark
143 <BLANKLINE>161 <BLANKLINE>
144 The status of your membership in the team Ubuntu Team (ubuntu-team) was162 The status of your membership in the team Ubuntu Team (ubuntu-team) was
145 changed by Mark Shuttleworth (mark) from Proposed to Declined.163 changed by Mark Shuttleworth (mark) from Proposed to Declined.
146 <http://launchpad.dev/~ubuntu-team>164 <http://launchpad.dev/~ubuntu-team>
165 <BLANKLINE>
166 -- =
167 <BLANKLINE>
168 You received this email because you are the affected member.
169 <BLANKLINE>
147 ----------------------------------------170 ----------------------------------------
148171
149The same goes for approving a proposed member.172The same goes for approving a proposed member.
@@ -157,7 +180,7 @@
157 # Remove notification of daf's membership pending approval from180 # Remove notification of daf's membership pending approval from
158 # stub.test_emails181 # stub.test_emails
159182
160 >>> transaction.commit()183 >>> run_mail_jobs()
161 >>> dummy = pop_notifications()184 >>> dummy = pop_notifications()
162185
163 >>> setStatus(daf_membership, TeamMembershipStatus.APPROVED,186 >>> setStatus(daf_membership, TeamMembershipStatus.APPROVED,
@@ -169,8 +192,13 @@
169192
170 >>> print_distinct_emails()193 >>> print_distinct_emails()
171 From: Ubuntu Team <noreply@launchpad.net>194 From: Ubuntu Team <noreply@launchpad.net>
172 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,195 To: Alexander Limi <limi@plone.org>,
173 jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com196 Colin Watson <colin.watson@ubuntulinux.com>,
197 Foo Bar <foo.bar@canonical.com>,
198 Jeff Waugh <jeff.waugh@ubuntulinux.com>,
199 Mark Shuttleworth <mark@example.com>
200 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
201 X-Launchpad-Notification-Type: team-membership-change
174 Subject: daf approved by mark202 Subject: daf approved by mark
175 <BLANKLINE>203 <BLANKLINE>
176 The membership status of Dafydd Harries (daf) in the team Ubuntu Team204 The membership status of Dafydd Harries (daf) in the team Ubuntu Team
@@ -180,9 +208,15 @@
180 <BLANKLINE>208 <BLANKLINE>
181 Mark Shuttleworth said:209 Mark Shuttleworth said:
182 This is a nice guy; I like him210 This is a nice guy; I like him
211 -- =
212 <BLANKLINE>
213 You received this email because you are an admin of the Ubuntu Team team.
214 <BLANKLINE>
183 ----------------------------------------215 ----------------------------------------
184 From: Ubuntu Team <noreply@launchpad.net>216 From: Ubuntu Team <noreply@launchpad.net>
185 To: daf@canonical.com217 To: Dafydd Harries <daf@canonical.com>
218 X-Launchpad-Message-Rationale: Member (ubuntu-team)
219 X-Launchpad-Notification-Type: team-membership-change
186 Subject: daf approved by mark220 Subject: daf approved by mark
187 <BLANKLINE>221 <BLANKLINE>
188 The status of your membership in the team Ubuntu Team (ubuntu-team) was222 The status of your membership in the team Ubuntu Team (ubuntu-team) was
@@ -191,6 +225,10 @@
191 <BLANKLINE>225 <BLANKLINE>
192 Mark Shuttleworth said:226 Mark Shuttleworth said:
193 This is a nice guy; I like him227 This is a nice guy; I like him
228 -- =
229 <BLANKLINE>
230 You received this email because you are the affected member.
231 <BLANKLINE>
194 ----------------------------------------232 ----------------------------------------
195233
196The same for deactivating a membership.234The same for deactivating a membership.
@@ -204,22 +242,37 @@
204242
205 >>> print_distinct_emails()243 >>> print_distinct_emails()
206 From: Ubuntu Team <noreply@launchpad.net>244 From: Ubuntu Team <noreply@launchpad.net>
207 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,245 To: Alexander Limi <limi@plone.org>,
208 jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com246 Colin Watson <colin.watson@ubuntulinux.com>,
247 Foo Bar <foo.bar@canonical.com>,
248 Jeff Waugh <jeff.waugh@ubuntulinux.com>,
249 Mark Shuttleworth <mark@example.com>
250 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
251 X-Launchpad-Notification-Type: team-membership-change
209 Subject: daf deactivated by mark252 Subject: daf deactivated by mark
210 <BLANKLINE>253 <BLANKLINE>
211 The membership status of Dafydd Harries (daf) in the team Ubuntu Team254 The membership status of Dafydd Harries (daf) in the team Ubuntu Team
212 (ubuntu-team) was changed by Mark Shuttleworth (mark) from Approved to255 (ubuntu-team) was changed by Mark Shuttleworth (mark) from Approved to
213 Deactivated.256 Deactivated.
214 <http://launchpad.dev/~ubuntu-team>257 <http://launchpad.dev/~ubuntu-team>
258 -- =
259 <BLANKLINE>
260 You received this email because you are an admin of the Ubuntu Team team.
261 <BLANKLINE>
215 ----------------------------------------262 ----------------------------------------
216 From: Ubuntu Team <noreply@launchpad.net>263 From: Ubuntu Team <noreply@launchpad.net>
217 To: daf@canonical.com264 To: Dafydd Harries <daf@canonical.com>
265 X-Launchpad-Message-Rationale: Member (ubuntu-team)
266 X-Launchpad-Notification-Type: team-membership-change
218 Subject: daf deactivated by mark267 Subject: daf deactivated by mark
219 <BLANKLINE>268 <BLANKLINE>
220 The status of your membership in the team Ubuntu Team (ubuntu-team) was269 The status of your membership in the team Ubuntu Team (ubuntu-team) was
221 changed by Mark Shuttleworth (mark) from Approved to Deactivated.270 changed by Mark Shuttleworth (mark) from Approved to Deactivated.
222 <http://launchpad.dev/~ubuntu-team>271 <http://launchpad.dev/~ubuntu-team>
272 -- =
273 <BLANKLINE>
274 You received this email because you are the affected member.
275 <BLANKLINE>
223 ----------------------------------------276 ----------------------------------------
224277
225Team admins can propose their teams using the join() method as well, but278Team admins can propose their teams using the join() method as well, but
@@ -235,10 +288,13 @@
235288
236 >>> print_distinct_emails(include_reply_to=True)289 >>> print_distinct_emails(include_reply_to=True)
237 From: Ubuntu Team <noreply@launchpad.net>290 From: Ubuntu Team <noreply@launchpad.net>
238 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,291 To: Alexander Limi <limi@plone.org>,
239 jeff.waugh@ubuntulinux.com, limi@plone.org292 Colin Watson <colin.watson@ubuntulinux.com>,
293 Foo Bar <foo.bar@canonical.com>,
294 Jeff Waugh <jeff.waugh@ubuntulinux.com>
240 Reply-To: mark@example.com295 Reply-To: mark@example.com
241 X-Launchpad-Message-Rationale: Admin (ubuntu-team)296 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
297 X-Launchpad-Notification-Type: team-membership-pending
242 Subject: admins wants to join298 Subject: admins wants to join
243 <BLANKLINE>299 <BLANKLINE>
244 Mark Shuttleworth (mark) wants to make Launchpad Administrators300 Mark Shuttleworth (mark) wants to make Launchpad Administrators
@@ -251,14 +307,17 @@
251 -- =307 -- =
252 <BLANKLINE>308 <BLANKLINE>
253 You received this email because you are an admin of the Ubuntu Team team.309 You received this email because you are an admin of the Ubuntu Team team.
310 <BLANKLINE>
254 ----------------------------------------311 ----------------------------------------
255 From: Ubuntu Team <noreply@launchpad.net>312 From: Ubuntu Team <noreply@launchpad.net>
256 To: mark@example.com313 To: Mark Shuttleworth <mark@example.com>
257 Reply-To: mark@example.com314 Reply-To: mark@example.com
258 X-Launchpad-Message-Rationale: Owner (ubuntu-team)315 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
316 X-Launchpad-Notification-Type: team-membership-pending
259 Subject: admins wants to join317 Subject: admins wants to join
260 ...318 ...
261 You received this email because you are the owner of the Ubuntu Team team.319 You received this email because you are the owner of the Ubuntu Team team.
320 <BLANKLINE>
262 ----------------------------------------321 ----------------------------------------
263322
264323
@@ -281,22 +340,27 @@
281340
282 >>> print_distinct_emails()341 >>> print_distinct_emails()
283 From: Ubuntu Team <noreply@launchpad.net>342 From: Ubuntu Team <noreply@launchpad.net>
284 To: marilize@hbd.com343 To: Marilize Coetzee <marilize@hbd.com>
285 X-Launchpad-Message-Rationale: Member (ubuntu-team)344 X-Launchpad-Message-Rationale: Member (ubuntu-team)
345 X-Launchpad-Notification-Type: team-membership-new
286 Subject: You have been added to ubuntu-team346 Subject: You have been added to ubuntu-team
287 <BLANKLINE>347 <BLANKLINE>
288 Celso Providelo (cprov) added you as a member of Ubuntu Team (ubuntu-348 Celso Providelo (cprov) added you as a member of Ubuntu Team (ubuntu-
289 team).349 team).
290 <http://launchpad.dev/~ubuntu-team>350 <http://launchpad.dev/~ubuntu-team>
291 <BLANKLINE>351 <BLANKLINE>
292 -- =352 -- =
293 <BLANKLINE>353 <BLANKLINE>
294 You received this email because you are the new member.354 You received this email because you are the new member.
355 <BLANKLINE>
295 ----------------------------------------356 ----------------------------------------
296 From: Ubuntu Team <noreply@launchpad.net>357 From: Ubuntu Team <noreply@launchpad.net>
297 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,358 To: Alexander Limi <limi@plone.org>,
298 jeff.waugh@ubuntulinux.com, limi@plone.org359 Colin Watson <colin.watson@ubuntulinux.com>,
360 Foo Bar <foo.bar@canonical.com>,
361 Jeff Waugh <jeff.waugh@ubuntulinux.com>
299 X-Launchpad-Message-Rationale: Admin (ubuntu-team)362 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
363 X-Launchpad-Notification-Type: team-membership-new
300 Subject: marilize joined ubuntu-team364 Subject: marilize joined ubuntu-team
301 <BLANKLINE>365 <BLANKLINE>
302 Marilize Coetzee (marilize) has been added as a member of Ubuntu Team366 Marilize Coetzee (marilize) has been added as a member of Ubuntu Team
@@ -308,13 +372,16 @@
308 -- =372 -- =
309 <BLANKLINE>373 <BLANKLINE>
310 You received this email because you are an admin of the Ubuntu Team team.374 You received this email because you are an admin of the Ubuntu Team team.
375 <BLANKLINE>
311 ----------------------------------------376 ----------------------------------------
312 From: Ubuntu Team <noreply@launchpad.net>377 From: Ubuntu Team <noreply@launchpad.net>
313 To: mark@example.com378 To: Mark Shuttleworth <mark@example.com>
314 X-Launchpad-Message-Rationale: Owner (ubuntu-team)379 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
380 X-Launchpad-Notification-Type: team-membership-new
315 Subject: marilize joined ubuntu-team381 Subject: marilize joined ubuntu-team
316 ...382 ...
317 You received this email because you are the owner of the Ubuntu Team team.383 You received this email because you are the owner of the Ubuntu Team team.
384 <BLANKLINE>
318 ----------------------------------------385 ----------------------------------------
319386
320By default, if the newly added member is actually a team, we'll only387By default, if the newly added member is actually a team, we'll only
@@ -332,7 +399,9 @@
332399
333 >>> print_distinct_emails()400 >>> print_distinct_emails()
334 From: Ubuntu Team <noreply@launchpad.net>401 From: Ubuntu Team <noreply@launchpad.net>
335 To: mark@example.com402 To: Mark Shuttleworth <mark@example.com>
403 X-Launchpad-Message-Rationale: Admin (ubuntu-mirror-admins)
404 X-Launchpad-Notification-Type: team-membership-invitation
336 Subject: Invitation for ubuntu-mirror-admins to join405 Subject: Invitation for ubuntu-mirror-admins to join
337 <BLANKLINE>406 <BLANKLINE>
338 Celso Providelo (cprov) has invited Mirror Administrators (ubuntu-407 Celso Providelo (cprov) has invited Mirror Administrators (ubuntu-
@@ -346,6 +415,13 @@
346 <BLANKLINE>415 <BLANKLINE>
347 Regards,416 Regards,
348 The Launchpad team417 The Launchpad team
418 <BLANKLINE>
419 -- =
420 <BLANKLINE>
421 You received this email because you are an admin of the Mirror
422 Administrato=
423 rs team.
424 <BLANKLINE>
349 ----------------------------------------425 ----------------------------------------
350426
351If one of the admins accept the invitation, then a notification is sent427If one of the admins accept the invitation, then a notification is sent
@@ -362,18 +438,48 @@
362438
363 >>> print_distinct_emails()439 >>> print_distinct_emails()
364 From: Ubuntu Team <noreply@launchpad.net>440 From: Ubuntu Team <noreply@launchpad.net>
365 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,441 To: Alexander Limi <limi@plone.org>,
366 jeff.waugh@ubuntulinux.com, karl@canonical.com, limi@plone.org,442 Colin Watson <colin.watson@ubuntulinux.com>,
367 mark@example.com443 Foo Bar <foo.bar@canonical.com>,
368 Subject: Invitation to ubuntu-mirror-admins accepted by mark444 Jeff Waugh <jeff.waugh@ubuntulinux.com>
369 <BLANKLINE>445 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
370 Mark Shuttleworth (mark) has accepted the invitation to make Mirror446 X-Launchpad-Notification-Type: team-membership-invitation-accepted
371 Administrators (ubuntu-mirror-admins) a member of Ubuntu Team (ubuntu-447 Subject: Invitation to ubuntu-mirror-admins accepted by mark
372 team).448 <BLANKLINE>
373 <http://launchpad.dev/~ubuntu-team>449 Mark Shuttleworth (mark) has accepted the invitation to make Mirror
374 <BLANKLINE>450 Administrators (ubuntu-mirror-admins) a member of Ubuntu Team (ubuntu-
375 Mark Shuttleworth said:451 team).
376 Of course I want to be part of ubuntu!452 <http://launchpad.dev/~ubuntu-team>
453 <BLANKLINE>
454 Mark Shuttleworth said:
455 Of course I want to be part of ubuntu!
456 <BLANKLINE>
457 -- =
458 <BLANKLINE>
459 You received this email because you are an admin of the Ubuntu Team team.
460 <BLANKLINE>
461 ----------------------------------------
462 From: Ubuntu Team <noreply@launchpad.net>
463 To: Karl Tilbury <karl@canonical.com>,
464 Mark Shuttleworth <mark@example.com>
465 X-Launchpad-Message-Rationale: Member (ubuntu-team) @ubuntu-mirror-admins
466 X-Launchpad-Notification-Type: team-membership-invitation-accepted
467 Subject: Invitation to ubuntu-mirror-admins accepted by mark
468 <BLANKLINE>
469 Mark Shuttleworth (mark) has accepted the invitation to make Mirror
470 Administrators (ubuntu-mirror-admins) a member of Ubuntu Team (ubuntu-
471 team).
472 <http://launchpad.dev/~ubuntu-team>
473 <BLANKLINE>
474 Mark Shuttleworth said:
475 Of course I want to be part of ubuntu!
476 <BLANKLINE>
477 -- =
478 <BLANKLINE>
479 You received this email because your team Mirror Administrators is the
480 affe=
481 cted member.
482 <BLANKLINE>
377 ----------------------------------------483 ----------------------------------------
378484
379Similarly, a notification is sent if the invitation is declined.485Similarly, a notification is sent if the invitation is declined.
@@ -384,7 +490,7 @@
384 # Reset stub.test_emails as we don't care about the notification triggered490 # Reset stub.test_emails as we don't care about the notification triggered
385 # by the addMember() call.491 # by the addMember() call.
386492
387 >>> transaction.commit()493 >>> run_mail_jobs()
388 >>> stub.test_emails = []494 >>> stub.test_emails = []
389495
390 >>> comment = "Landscape has nothing to do with ubuntu, unfortunately."496 >>> comment = "Landscape has nothing to do with ubuntu, unfortunately."
@@ -397,17 +503,47 @@
397503
398 >>> print_distinct_emails()504 >>> print_distinct_emails()
399 From: Ubuntu Team <noreply@launchpad.net>505 From: Ubuntu Team <noreply@launchpad.net>
400 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,506 To: Alexander Limi <limi@plone.org>,
401 guilherme.salgado@canonical.com, jeff.waugh@ubuntulinux.com,507 Colin Watson <colin.watson@ubuntulinux.com>,
402 limi@plone.org, mark@example.com, test@canonical.com508 Foo Bar <foo.bar@canonical.com>,
403 Subject: Invitation to landscape-developers declined by mark509 Jeff Waugh <jeff.waugh@ubuntulinux.com>,
404 <BLANKLINE>510 Mark Shuttleworth <mark@example.com>
405 Mark Shuttleworth (mark) has declined the invitation to make Landscape511 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
406 Developers (landscape-developers) a member of Ubuntu Team (ubuntu-team).512 X-Launchpad-Notification-Type: team-membership-invitation-declined
407 <http://launchpad.dev/~ubuntu-team>513 Subject: Invitation to landscape-developers declined by mark
408 <BLANKLINE>514 <BLANKLINE>
409 Mark Shuttleworth said:515 Mark Shuttleworth (mark) has declined the invitation to make Landscape
410 Landscape has nothing to do with ubuntu, unfortunately.516 Developers (landscape-developers) a member of Ubuntu Team (ubuntu-team).
517 <http://launchpad.dev/~ubuntu-team>
518 <BLANKLINE>
519 Mark Shuttleworth said:
520 Landscape has nothing to do with ubuntu, unfortunately.
521 <BLANKLINE>
522 -- =
523 <BLANKLINE>
524 You received this email because you are an admin of the Ubuntu Team team.
525 <BLANKLINE>
526 ----------------------------------------
527 From: Ubuntu Team <noreply@launchpad.net>
528 To: Guilherme Salgado <guilherme.salgado@canonical.com>,
529 Sample Person <test@canonical.com>
530 X-Launchpad-Message-Rationale: Member (ubuntu-team) @landscape-developers
531 X-Launchpad-Notification-Type: team-membership-invitation-declined
532 Subject: Invitation to landscape-developers declined by mark
533 <BLANKLINE>
534 Mark Shuttleworth (mark) has declined the invitation to make Landscape
535 Developers (landscape-developers) a member of Ubuntu Team (ubuntu-team).
536 <http://launchpad.dev/~ubuntu-team>
537 <BLANKLINE>
538 Mark Shuttleworth said:
539 Landscape has nothing to do with ubuntu, unfortunately.
540 <BLANKLINE>
541 -- =
542 <BLANKLINE>
543 You received this email because your team Landscape Developers is the
544 affec=
545 ted member.
546 <BLANKLINE>
411 ----------------------------------------547 ----------------------------------------
412548
413It's also possible to forcibly add a team as a member of another one, by549It's also possible to forcibly add a team as a member of another one, by
@@ -423,16 +559,22 @@
423559
424 >>> print_distinct_emails()560 >>> print_distinct_emails()
425 From: Ubuntu Team <noreply@launchpad.net>561 From: Ubuntu Team <noreply@launchpad.net>
426 To: foo.bar@canonical.com562 To: Foo Bar <foo.bar@canonical.com>
427 X-Launchpad-Message-Rationale: Indirect member (ubuntu-team)563 X-Launchpad-Message-Rationale: Member (ubuntu-team) @launchpad
564 X-Launchpad-Notification-Type: team-membership-new
428 Subject: launchpad joined ubuntu-team565 Subject: launchpad joined ubuntu-team
429 ...566 ...
430 You received this email because launchpad is the new member.567 You received this email because your team Launchpad Developers is the
568 new m=
569 ember.
570 <BLANKLINE>
431 ----------------------------------------571 ----------------------------------------
432 From: Ubuntu Team <noreply@launchpad.net>572 From: Ubuntu Team <noreply@launchpad.net>
433 To: colin.watson@ubuntulinux.com, jeff.waugh@ubuntulinux.com,573 To: Alexander Limi <limi@plone.org>,
434 limi@plone.org574 Colin Watson <colin.watson@ubuntulinux.com>,
575 Jeff Waugh <jeff.waugh@ubuntulinux.com>
435 X-Launchpad-Message-Rationale: Admin (ubuntu-team)576 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
577 X-Launchpad-Notification-Type: team-membership-new
436 Subject: launchpad joined ubuntu-team578 Subject: launchpad joined ubuntu-team
437 <BLANKLINE>579 <BLANKLINE>
438 Launchpad Developers (launchpad) has been added as a member of Ubuntu580 Launchpad Developers (launchpad) has been added as a member of Ubuntu
@@ -444,13 +586,16 @@
444 -- =586 -- =
445 <BLANKLINE>587 <BLANKLINE>
446 You received this email because you are an admin of the Ubuntu Team team.588 You received this email because you are an admin of the Ubuntu Team team.
589 <BLANKLINE>
447 ----------------------------------------590 ----------------------------------------
448 From: Ubuntu Team <noreply@launchpad.net>591 From: Ubuntu Team <noreply@launchpad.net>
449 To: mark@example.com592 To: Mark Shuttleworth <mark@example.com>
450 X-Launchpad-Message-Rationale: Owner (ubuntu-team)593 X-Launchpad-Message-Rationale: Owner (ubuntu-team)
594 X-Launchpad-Notification-Type: team-membership-new
451 Subject: launchpad joined ubuntu-team595 Subject: launchpad joined ubuntu-team
452 ...596 ...
453 You received this email because you are the owner of the Ubuntu Team team.597 You received this email because you are the owner of the Ubuntu Team team.
598 <BLANKLINE>
454 ----------------------------------------599 ----------------------------------------
455600
456601
@@ -480,16 +625,19 @@
480 ... utc_now + timedelta(days=9), mark)625 ... utc_now + timedelta(days=9), mark)
481 >>> flush_database_updates()626 >>> flush_database_updates()
482 >>> beta_testers_on_ubuntu_team.sendExpirationWarningEmail()627 >>> beta_testers_on_ubuntu_team.sendExpirationWarningEmail()
483 >>> transaction.commit()628 >>> run_mail_jobs()
484 >>> print_distinct_emails()629 >>> print_distinct_emails()
485 From: Ubuntu Team <noreply@launchpad.net>630 From: Ubuntu Team <noreply@launchpad.net>
486 To: beta-admin@launchpad.net631 To: Launchpad Beta Testers Owner <beta-admin@launchpad.net>
632 X-Launchpad-Message-Rationale: Member (ubuntu-team)
633 @launchpad-beta-testers
634 X-Launchpad-Notification-Type: team-membership-expiration-warning
487 Subject: launchpad-beta-testers will expire soon from ubuntu-team635 Subject: launchpad-beta-testers will expire soon from ubuntu-team
488 <BLANKLINE>636 <BLANKLINE>
489 On ..., 9 days from now, the membership637 On ..., 9 days from now, the membership
490 of Launchpad Beta Testers (launchpad-beta-testers) (which you are638 of Launchpad Beta Testers (launchpad-beta-testers) (which you are the
491 the owner of) in the Ubuntu Team (ubuntu-team) Launchpad team639 owner=
492 is due to expire.640 of) in the Ubuntu Team (ubuntu-team) Launchpad team is due to expire.
493 <http://launchpad.dev/~ubuntu-team>641 <http://launchpad.dev/~ubuntu-team>
494 <BLANKLINE>642 <BLANKLINE>
495 To prevent this membership from expiring, you should get in touch643 To prevent this membership from expiring, you should get in touch
@@ -505,6 +653,12 @@
505 <BLANKLINE>653 <BLANKLINE>
506 Thanks for using Launchpad!654 Thanks for using Launchpad!
507 <BLANKLINE>655 <BLANKLINE>
656 -- =
657 <BLANKLINE>
658 You received this email because your team Launchpad Beta Testers is the
659 aff=
660 ected member.
661 <BLANKLINE>
508 ----------------------------------------662 ----------------------------------------
509663
510If the team's renewal policy is ONDEMAND, though, the member is invited664If the team's renewal policy is ONDEMAND, though, the member is invited
@@ -518,10 +672,12 @@
518 ... utc_now + timedelta(days=9), mark)672 ... utc_now + timedelta(days=9), mark)
519 >>> flush_database_updates()673 >>> flush_database_updates()
520 >>> kamion_on_ubuntu_team.sendExpirationWarningEmail()674 >>> kamion_on_ubuntu_team.sendExpirationWarningEmail()
521 >>> transaction.commit()675 >>> run_mail_jobs()
522 >>> print_distinct_emails()676 >>> print_distinct_emails()
523 From: Ubuntu Team <noreply@launchpad.net>677 From: Ubuntu Team <noreply@launchpad.net>
524 To: colin.watson@ubuntulinux.com678 To: Colin Watson <colin.watson@ubuntulinux.com>
679 X-Launchpad-Message-Rationale: Member (ubuntu-team)
680 X-Launchpad-Notification-Type: team-membership-expiration-warning
525 Subject: Your membership in ubuntu-team is about to expire681 Subject: Your membership in ubuntu-team is about to expire
526 <BLANKLINE>682 <BLANKLINE>
527 On ..., 9 days from now, your membership683 On ..., 9 days from now, your membership
@@ -537,19 +693,26 @@
537 <BLANKLINE>693 <BLANKLINE>
538 Thanks for using Launchpad!694 Thanks for using Launchpad!
539 <BLANKLINE>695 <BLANKLINE>
696 -- =
697 <BLANKLINE>
698 You received this email because you are the affected member.
699 <BLANKLINE>
540 ----------------------------------------700 ----------------------------------------
541701
542 >>> beta_testers_on_ubuntu_team.sendExpirationWarningEmail()702 >>> beta_testers_on_ubuntu_team.sendExpirationWarningEmail()
543 >>> transaction.commit()703 >>> run_mail_jobs()
544 >>> print_distinct_emails()704 >>> print_distinct_emails()
545 From: Ubuntu Team <noreply@launchpad.net>705 From: Ubuntu Team <noreply@launchpad.net>
546 To: beta-admin@launchpad.net706 To: Launchpad Beta Testers Owner <beta-admin@launchpad.net>
707 X-Launchpad-Message-Rationale: Member (ubuntu-team)
708 @launchpad-beta-testers
709 X-Launchpad-Notification-Type: team-membership-expiration-warning
547 Subject: launchpad-beta-testers will expire soon from ubuntu-team710 Subject: launchpad-beta-testers will expire soon from ubuntu-team
548 <BLANKLINE>711 <BLANKLINE>
549 On ..., 9 days from now, the membership712 On ..., 9 days from now, the membership
550 of Launchpad Beta Testers (launchpad-beta-testers) (which you are713 of Launchpad Beta Testers (launchpad-beta-testers) (which you are the
551 the owner of) in the Ubuntu Team (ubuntu-team) Launchpad team714 owner=
552 is due to expire.715 of) in the Ubuntu Team (ubuntu-team) Launchpad team is due to expire.
553 <http://launchpad.dev/~ubuntu-team>716 <http://launchpad.dev/~ubuntu-team>
554 <BLANKLINE>717 <BLANKLINE>
555 If you want, you can renew this membership at718 If you want, you can renew this membership at
@@ -560,6 +723,12 @@
560 <BLANKLINE>723 <BLANKLINE>
561 Thanks for using Launchpad!724 Thanks for using Launchpad!
562 <BLANKLINE>725 <BLANKLINE>
726 -- =
727 <BLANKLINE>
728 You received this email because your team Launchpad Beta Testers is the
729 aff=
730 ected member.
731 <BLANKLINE>
563 ----------------------------------------732 ----------------------------------------
564733
565If the team's renewal policy is NONE but the member has the necessary734If the team's renewal policy is NONE but the member has the necessary
@@ -577,15 +746,18 @@
577 ... utc_now + timedelta(days=9), sampleperson)746 ... utc_now + timedelta(days=9), sampleperson)
578 >>> flush_database_updates()747 >>> flush_database_updates()
579 >>> sampleperson_on_landscape.sendExpirationWarningEmail()748 >>> sampleperson_on_landscape.sendExpirationWarningEmail()
580 >>> transaction.commit()749 >>> run_mail_jobs()
581 >>> print_distinct_emails()750 >>> print_distinct_emails()
582 From: Landscape Developers <noreply@launchpad.net>751 From: Landscape Developers <noreply@launchpad.net>
583 To: test@canonical.com752 To: Sample Person <test@canonical.com>
753 X-Launchpad-Message-Rationale: Member (landscape-developers)
754 X-Launchpad-Notification-Type: team-membership-expiration-warning
584 Subject: Your membership in landscape-developers is about to expire755 Subject: Your membership in landscape-developers is about to expire
585 <BLANKLINE>756 <BLANKLINE>
586 On ..., 9 days from now, your membership757 On ..., 9 days from now, your membership
587 in the Landscape Developers (landscape-developers) Launchpad team758 in the Landscape Developers (landscape-developers) Launchpad team is due
588 is due to expire.759 to=
760 expire.
589 <http://launchpad.dev/~landscape-developers>761 <http://launchpad.dev/~landscape-developers>
590 <BLANKLINE>762 <BLANKLINE>
591 To stay a member of this team you should extend your membership at763 To stay a member of this team you should extend your membership at
@@ -596,6 +768,10 @@
596 <BLANKLINE>768 <BLANKLINE>
597 Thanks for using Launchpad!769 Thanks for using Launchpad!
598 <BLANKLINE>770 <BLANKLINE>
771 -- =
772 <BLANKLINE>
773 You received this email because you are the affected member.
774 <BLANKLINE>
599 ----------------------------------------775 ----------------------------------------
600776
601777
@@ -634,7 +810,9 @@
634810
635 >>> print_distinct_emails()811 >>> print_distinct_emails()
636 From: Mirror Administrators <noreply@launchpad.net>812 From: Mirror Administrators <noreply@launchpad.net>
637 To: mark@example.com813 To: Mark Shuttleworth <mark@example.com>
814 X-Launchpad-Message-Rationale: Admin (ubuntu-mirror-admins)
815 X-Launchpad-Notification-Type: team-membership-renewed
638 Subject: karl extended their membership816 Subject: karl extended their membership
639 <BLANKLINE>817 <BLANKLINE>
640 Karl Tilbury (karl) renewed their own membership in the Mirror818 Karl Tilbury (karl) renewed their own membership in the Mirror
@@ -643,6 +821,13 @@
643 <BLANKLINE>821 <BLANKLINE>
644 Regards,822 Regards,
645 The Launchpad team823 The Launchpad team
824 <BLANKLINE>
825 -- =
826 <BLANKLINE>
827 You received this email because you are an admin of the Mirror
828 Administrato=
829 rs team.
830 <BLANKLINE>
646 ----------------------------------------831 ----------------------------------------
647832
648833
@@ -678,22 +863,37 @@
678863
679 >>> print_distinct_emails()864 >>> print_distinct_emails()
680 From: Ubuntu Team <noreply@launchpad.net>865 From: Ubuntu Team <noreply@launchpad.net>
681 To: colin.watson@ubuntulinux.com, foo.bar@canonical.com,866 To: Alexander Limi <limi@plone.org>,
682 jeff.waugh@ubuntulinux.com, limi@plone.org, mark@example.com867 Colin Watson <colin.watson@ubuntulinux.com>,
868 Foo Bar <foo.bar@canonical.com>,
869 Jeff Waugh <jeff.waugh@ubuntulinux.com>,
870 Mark Shuttleworth <mark@example.com>
871 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
872 X-Launchpad-Notification-Type: team-membership-change
683 Subject: cprov made admin by mark873 Subject: cprov made admin by mark
684 <BLANKLINE>874 <BLANKLINE>
685 The membership status of Celso Providelo (cprov) in the team Ubuntu Team875 The membership status of Celso Providelo (cprov) in the team Ubuntu Team
686 (ubuntu-team) was changed by Mark Shuttleworth (mark) from Approved to876 (ubuntu-team) was changed by Mark Shuttleworth (mark) from Approved to
687 Administrator.877 Administrator.
688 <http://launchpad.dev/~ubuntu-team>878 <http://launchpad.dev/~ubuntu-team>
879 <BLANKLINE>
880 -- =
881 You received this email because you are an admin of the Ubuntu Team team.
882 <BLANKLINE>
689 ----------------------------------------883 ----------------------------------------
690 From: Ubuntu Team <noreply@launchpad.net>884 From: Ubuntu Team <noreply@launchpad.net>
691 To: celso.providelo@canonical.com885 To: Celso Providelo <celso.providelo@canonical.com>
886 X-Launchpad-Message-Rationale: Member (ubuntu-team)
887 X-Launchpad-Notification-Type: team-membership-change
692 Subject: cprov made admin by mark888 Subject: cprov made admin by mark
693 <BLANKLINE>889 <BLANKLINE>
694 The status of your membership in the team Ubuntu Team (ubuntu-team) was890 The status of your membership in the team Ubuntu Team (ubuntu-team) was
695 changed by Mark Shuttleworth (mark) from Approved to Administrator.891 changed by Mark Shuttleworth (mark) from Approved to Administrator.
696 <http://launchpad.dev/~ubuntu-team>892 <http://launchpad.dev/~ubuntu-team>
893 <BLANKLINE>
894 -- =
895 You received this email because you are the affected member.
896 <BLANKLINE>
697 ----------------------------------------897 ----------------------------------------
698898
699If a team admin changes his own membership, the notification sent will899If a team admin changes his own membership, the notification sent will
@@ -710,14 +910,23 @@
710910
711 >>> print_distinct_emails()911 >>> print_distinct_emails()
712 From: Ubuntu Team <noreply@launchpad.net>912 From: Ubuntu Team <noreply@launchpad.net>
713 To: celso.providelo@canonical.com, colin.watson@ubuntulinux.com,913 To: Alexander Limi <limi@plone.org>,
714 foo.bar@canonical.com, limi@plone.org, mark@example.com914 Celso Providelo <celso.providelo@canonical.com>,
915 Colin Watson <colin.watson@ubuntulinux.com>,
916 Foo Bar <foo.bar@canonical.com>,
917 Mark Shuttleworth <mark@example.com>
918 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
919 X-Launchpad-Notification-Type: team-membership-change
715 Subject: Membership change: jdub in ubuntu-team920 Subject: Membership change: jdub in ubuntu-team
716 <BLANKLINE>921 <BLANKLINE>
717 The membership status of Jeff Waugh (jdub) in the team Ubuntu Team922 The membership status of Jeff Waugh (jdub) in the team Ubuntu Team
718 (ubuntu-team) was changed by the user from Administrator to923 (ubuntu-team) was changed by the user from Administrator to
719 Approved.924 Approved.
720 <http://launchpad.dev/~ubuntu-team>925 <http://launchpad.dev/~ubuntu-team>
926 <BLANKLINE>
927 -- =
928 You received this email because you are an admin of the Ubuntu Team team.
929 <BLANKLINE>
721 ----------------------------------------930 ----------------------------------------
722931
723Deactivating the membership of a team also generates notifications for932Deactivating the membership of a team also generates notifications for
@@ -736,15 +945,38 @@
736945
737 >>> print_distinct_emails()946 >>> print_distinct_emails()
738 From: Ubuntu Team <noreply@launchpad.net>947 From: Ubuntu Team <noreply@launchpad.net>
739 To: celso.providelo@canonical.com, colin.watson@ubuntulinux.com,948 To: Alexander Limi <limi@plone.org>,
740 foo.bar@canonical.com, karl@canonical.com, limi@plone.org,949 Celso Providelo <celso.providelo@canonical.com>,
741 mark@example.com950 Colin Watson <colin.watson@ubuntulinux.com>,
742 Subject: ubuntu-mirror-admins deactivated by mark951 Foo Bar <foo.bar@canonical.com>
743 <BLANKLINE>952 X-Launchpad-Message-Rationale: Admin (ubuntu-team)
744 The membership status of Mirror Administrators (ubuntu-mirror-admins) in953 X-Launchpad-Notification-Type: team-membership-change
745 the team Ubuntu Team (ubuntu-team) was changed by Mark Shuttleworth954 Subject: ubuntu-mirror-admins deactivated by mark
746 (mark) from Approved to Deactivated.955 <BLANKLINE>
747 <http://launchpad.dev/~ubuntu-team>956 The membership status of Mirror Administrators (ubuntu-mirror-admins) in
957 the team Ubuntu Team (ubuntu-team) was changed by Mark Shuttleworth
958 (mark) from Approved to Deactivated.
959 <http://launchpad.dev/~ubuntu-team>
960 <BLANKLINE>
961 -- =
962 You received this email because you are an admin of the Ubuntu Team team.
963 ----------------------------------------
964 From: Ubuntu Team <noreply@launchpad.net>
965 To: Karl Tilbury <karl@canonical.com>,
966 Mark Shuttleworth <mark@example.com>
967 X-Launchpad-Message-Rationale: Member (ubuntu-team) @ubuntu-mirror-admins
968 X-Launchpad-Notification-Type: team-membership-change
969 Subject: ubuntu-mirror-admins deactivated by mark
970 <BLANKLINE>
971 The membership status of Mirror Administrators (ubuntu-mirror-admins) in
972 the team Ubuntu Team (ubuntu-team) was changed by Mark Shuttleworth
973 (mark) from Approved to Deactivated.
974 <http://launchpad.dev/~ubuntu-team>
975 <BLANKLINE>
976 -- =
977 You received this email because your team Mirror Administrators is the
978 affe=
979 cted member.
748 ----------------------------------------980 ----------------------------------------
749981
750Deactivating memberships can also be done silently (no email982Deactivating memberships can also be done silently (no email
@@ -807,10 +1039,12 @@
807 >>> member = factory.makePerson(1039 >>> member = factory.makePerson(
808 ... name='team-member', email='team-member@example.com')1040 ... name='team-member', email='team-member@example.com')
809 >>> ignored = team_one.addMember(member, owner)1041 >>> ignored = team_one.addMember(member, owner)
1042 >>> run_mail_jobs()
810 >>> print_distinct_emails()1043 >>> print_distinct_emails()
811 From: Team One ...1044 From: Team One ...
812 To: team-member...1045 To: Team-member <team-member...>
813 X-Launchpad-Message-Rationale: Member (team-one)1046 X-Launchpad-Message-Rationale: Member (team-one)
1047 X-Launchpad-Notification-Type: team-membership-new
814 Subject: You have been added to team-one1048 Subject: You have been added to team-one
815 <BLANKLINE>1049 <BLANKLINE>
816 Team-owner (team-owner) added you as a member of Team One (team-one).1050 Team-owner (team-owner) added you as a member of Team One (team-one).
@@ -818,11 +1052,12 @@
818 <BLANKLINE>1052 <BLANKLINE>
819 If you would like to subscribe to the team list, use the link below1053 If you would like to subscribe to the team list, use the link below
820 to update your Mailing List Subscription preferences.1054 to update your Mailing List Subscription preferences.
821 <http://launchpad.dev/people/+me/+editmailinglists>1055 <http://launchpad.dev/~/+editmailinglists>
822 <BLANKLINE>1056 <BLANKLINE>
823 -- =1057 -- =
824 <BLANKLINE>1058 <BLANKLINE>
825 You received this email because you are the new member.1059 You received this email because you are the new member.
1060 <BLANKLINE>
826 ----------------------------------------1061 ----------------------------------------
8271062
828When a team join a team with a mailing list, the new member notification1063When a team join a team with a mailing list, the new member notification
@@ -831,10 +1066,12 @@
831 >>> team_two = factory.makeTeam(1066 >>> team_two = factory.makeTeam(
832 ... name='team-two', email='team-two@example.com', owner=owner)1067 ... name='team-two', email='team-two@example.com', owner=owner)
833 >>> ignored = team_one.addMember(team_two, owner, force_team_add=True)1068 >>> ignored = team_one.addMember(team_two, owner, force_team_add=True)
1069 >>> run_mail_jobs()
834 >>> print_distinct_emails()1070 >>> print_distinct_emails()
835 From: Team One ...1071 From: Team One ...
836 To: team-two...1072 To: Team Two <team-two...>
837 X-Launchpad-Message-Rationale: Indirect member (team-one)1073 X-Launchpad-Message-Rationale: Member (team-one) @team-two
1074 X-Launchpad-Notification-Type: team-membership-new
838 Subject: team-two joined team-one1075 Subject: team-two joined team-one
839 <BLANKLINE>1076 <BLANKLINE>
840 Team-owner (team-owner) added Team Two (team-two) (which you are a1077 Team-owner (team-owner) added Team Two (team-two) (which you are a
@@ -843,11 +1080,10 @@
843 <BLANKLINE>1080 <BLANKLINE>
844 If you would like to subscribe to the team list, use the link below1081 If you would like to subscribe to the team list, use the link below
845 to update your Mailing List Subscription preferences.1082 to update your Mailing List Subscription preferences.
846 <http://launchpad.dev/people/+me/+editmailinglists>1083 <http://launchpad.dev/~/+editmailinglists>
847 <BLANKLINE>1084 <BLANKLINE>
848 -- =1085 -- =
849 <BLANKLINE>1086 <BLANKLINE>
850 You received this email because team-two is the new member.1087 You received this email because your team Team Two is the new member.
1088 <BLANKLINE>
851 ----------------------------------------1089 ----------------------------------------
852
853
8541090
=== modified file 'lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt'
--- lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-expiration-warning-bulk.txt 2015-09-08 11:57:29 +0000
@@ -1,8 +1,7 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3On %(expiration_date)s, %(approximate_duration)s from now, the membership3On %(expiration_date)s, %(approximate_duration)s from now, the membership
4of %(member_name)s (which you are4of %(member)s (which you are the owner of) in the %(team)s Launchpad team
5the owner of) in the %(team_name)s Launchpad team
6is due to expire.5is due to expire.
7<%(team_url)s>6<%(team_url)s>
87
98
=== modified file 'lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt'
--- lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-expiration-warning-personal.txt 2015-09-08 11:57:29 +0000
@@ -1,8 +1,7 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3On %(expiration_date)s, %(approximate_duration)s from now, your membership3On %(expiration_date)s, %(approximate_duration)s from now, your membership
4in the %(team_name)s Launchpad team4in the %(team)s Launchpad team is due to expire.
5is due to expire.
6<%(team_url)s>5<%(team_url)s>
76
8%(how_to_renew)s7%(how_to_renew)s
98
=== modified file 'lib/lp/registry/emailtemplates/membership-expired-bulk.txt'
--- lib/lp/registry/emailtemplates/membership-expired-bulk.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-expired-bulk.txt 2015-09-08 11:57:29 +0000
@@ -1,6 +1,6 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3The membership of %(member_name)s in the %(team_name)s team has expired.3The membership of %(member)s in the %(team)s team has expired.
4<%(team_url)s>4<%(team_url)s>
55
6Regards,6Regards,
77
=== modified file 'lib/lp/registry/emailtemplates/membership-expired-personal.txt'
--- lib/lp/registry/emailtemplates/membership-expired-personal.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-expired-personal.txt 2015-09-08 11:57:29 +0000
@@ -1,6 +1,6 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3Your membership in the %(team_name)s team has expired.3Your membership in the %(team)s team has expired.
4<%(team_url)s>4<%(team_url)s>
55
6Regards,6Regards,
77
=== modified file 'lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt'
--- lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-invitation-accepted-bulk.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(reviewer_name)s has accepted the invitation to make %(member_name)s a member of %(team_name)s.3%(reviewer)s has accepted the invitation to make %(member)s a member of %(team)s.
4<%(team_url)s>4<%(team_url)s>
5%(comment)s5%(comment)s
66
=== modified file 'lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt'
--- lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-invitation-declined-bulk.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(reviewer_name)s has declined the invitation to make %(member_name)s a member of %(team_name)s.3%(reviewer)s has declined the invitation to make %(member)s a member of %(team)s.
4<%(team_url)s>4<%(team_url)s>
5%(comment)s5%(comment)s
66
=== modified file 'lib/lp/registry/emailtemplates/membership-invitation.txt'
--- lib/lp/registry/emailtemplates/membership-invitation.txt 2011-03-09 17:51:28 +0000
+++ lib/lp/registry/emailtemplates/membership-invitation.txt 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(reviewer)s has invited %(member)s (which you are an administrator of) to join %(team)s.3%(reviewer)s has invited %(member)s (which you are an administrator of) to join %(team)s.
4<%(team_url)s>4<%(team_url)s>
55
=== modified file 'lib/lp/registry/emailtemplates/membership-member-renewed.txt'
--- lib/lp/registry/emailtemplates/membership-member-renewed.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-member-renewed.txt 2015-09-08 11:57:29 +0000
@@ -1,6 +1,6 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(member_name)s renewed their own membership in the %(team_name)s team until %(dateexpires)s.3%(member)s renewed their own membership in the %(team)s team until %(dateexpires)s.
4<%(team_url)s>4<%(team_url)s>
55
6Regards,6Regards,
77
=== modified file 'lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt'
--- lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-statuschange-bulk.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3The 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.3The membership status of %(member)s in the team %(team)s was changed by %(reviewer)s from %(old_status)s to %(new_status)s.
4<%(team_url)s>4<%(team_url)s>
5%(comment)s5%(comment)s
66
=== modified file 'lib/lp/registry/emailtemplates/membership-statuschange-personal.txt'
--- lib/lp/registry/emailtemplates/membership-statuschange-personal.txt 2011-03-09 18:18:02 +0000
+++ lib/lp/registry/emailtemplates/membership-statuschange-personal.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3The status of your membership in the team %(team_name)s was changed by %(reviewer_name)s from %(old_status)s to %(new_status)s.3The status of your membership in the team %(team)s was changed by %(reviewer)s from %(old_status)s to %(new_status)s.
4<%(team_url)s>4<%(team_url)s>
5%(comment)s5%(comment)s
66
=== modified file 'lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt'
--- lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt 2011-03-09 17:51:28 +0000
+++ lib/lp/registry/emailtemplates/new-member-notification-for-admins.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(person_name)s has been added as a member of %(team_name)s by %(reviewer_name)s. Follow the link below for more details.3%(member)s has been added as a member of %(team)s by %(reviewer)s. Follow the link below for more details.
44
5 %(url)s5 %(membership_url)s
66
=== modified file 'lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt'
--- lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt 2011-03-09 17:51:28 +0000
+++ lib/lp/registry/emailtemplates/new-member-notification-for-teams.txt 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(reviewer)s added %(member)s (which you are a member of) as a member of %(team)s.3%(reviewer)s added %(member)s (which you are a member of) as a member of %(team)s.
4 <%(team_url)s>4 <%(team_url)s>
55
=== modified file 'lib/lp/registry/emailtemplates/new-member-notification.txt'
--- lib/lp/registry/emailtemplates/new-member-notification.txt 2011-03-09 17:51:28 +0000
+++ lib/lp/registry/emailtemplates/new-member-notification.txt 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(reviewer)s added you as a member of %(team)s.3%(reviewer)s added you as a member of %(team)s.
4 <%(team_url)s>4 <%(team_url)s>
55
=== modified file 'lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt'
--- lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt 2011-03-09 17:51:28 +0000
+++ lib/lp/registry/emailtemplates/pending-membership-approval-for-third-party.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(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.3%(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.
44
5 %(url)s5 %(membership_url)s
66
=== modified file 'lib/lp/registry/emailtemplates/pending-membership-approval.txt'
--- lib/lp/registry/emailtemplates/pending-membership-approval.txt 2011-03-09 17:51:28 +0000
+++ lib/lp/registry/emailtemplates/pending-membership-approval.txt 2015-09-08 11:57:29 +0000
@@ -1,5 +1,5 @@
1Hello %(recipient_name)s,1Hello %(recipient)s,
22
3%(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.3%(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.
44
5 %(url)s5 %(membership_url)s
66
=== modified file 'lib/lp/registry/enums.py'
--- lib/lp/registry/enums.py 2015-05-14 02:03:31 +0000
+++ lib/lp/registry/enums.py 2015-09-08 11:57:29 +0000
@@ -354,6 +354,31 @@
354 the user from teams.354 the user from teams.
355 """)355 """)
356356
357 TEAM_INVITATION_NOTIFICATION = DBItem(3, """
358 Notification of invitation to join team
359
360 Notify team admins that the team has been invited to join another
361 team.
362 """)
363
364 TEAM_JOIN_NOTIFICATION = DBItem(4, """
365 Notification of new member joining team
366
367 Notify that a new member has been added to a team.
368 """)
369
370 EXPIRING_MEMBERSHIP_NOTIFICATION = DBItem(5, """
371 Notification of expiring membership
372
373 Notify a member that their membership of a team is about to expire.
374 """)
375
376 SELF_RENEWAL_NOTIFICATION = DBItem(6, """
377 Notification of self-renewal
378
379 Notify team admins that a member renewed their own membership.
380 """)
381
357382
358class ProductJobType(DBEnumeratedType):383class ProductJobType(DBEnumeratedType):
359 """Values that IProductJob.job_type can take."""384 """Values that IProductJob.job_type can take."""
360385
=== modified file 'lib/lp/registry/interfaces/persontransferjob.py'
--- lib/lp/registry/interfaces/persontransferjob.py 2013-03-12 05:51:28 +0000
+++ lib/lp/registry/interfaces/persontransferjob.py 2015-09-08 11:57:29 +0000
@@ -1,10 +1,12 @@
1# Copyright 2010-2013 Canonical Ltd. This software is licensed under the1# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interface for the Jobs system to change memberships or merge persons."""4"""Interface for the Jobs system to change memberships or merge persons."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'IExpiringMembershipNotificationJob',
9 'IExpiringMembershipNotificationJobSource',
8 'IMembershipNotificationJob',10 'IMembershipNotificationJob',
9 'IMembershipNotificationJobSource',11 'IMembershipNotificationJobSource',
10 'IPersonDeactivateJob',12 'IPersonDeactivateJob',
@@ -13,6 +15,12 @@
13 'IPersonMergeJobSource',15 'IPersonMergeJobSource',
14 'IPersonTransferJob',16 'IPersonTransferJob',
15 'IPersonTransferJobSource',17 'IPersonTransferJobSource',
18 'ISelfRenewalNotificationJob',
19 'ISelfRenewalNotificationJobSource',
20 'ITeamInvitationNotificationJob',
21 'ITeamInvitationNotificationJobSource',
22 'ITeamJoinNotificationJob',
23 'ITeamJoinNotificationJobSource',
16 ]24 ]
1725
18from zope.interface import Attribute26from zope.interface import Attribute
@@ -161,3 +169,87 @@
161 :param person: Match jobs on `person`, or `None` to ignore.169 :param person: Match jobs on `person`, or `None` to ignore.
162 :return: A `ResultSet` yielding `IPersonDeactivateJob`.170 :return: A `ResultSet` yielding `IPersonDeactivateJob`.
163 """171 """
172
173
174class ITeamInvitationNotificationJob(IPersonTransferJob):
175 """A Job to notify about team joining invitations."""
176
177 member = PublicPersonChoice(
178 title=_('Alias for minor_person attribute'),
179 vocabulary='ValidPersonOrTeam',
180 required=True)
181
182 team = PublicPersonChoice(
183 title=_('Alias for major_person attribute'),
184 vocabulary='ValidPersonOrTeam',
185 required=True)
186
187
188class ITeamInvitationNotificationJobSource(IJobSource):
189 """An interface for acquiring ITeamInvitationNotificationJobs."""
190
191 def create(member, team):
192 """Create a new ITeamInvitationNotificationJob."""
193
194
195class ITeamJoinNotificationJob(IPersonTransferJob):
196 """A Job to notify about a new member joining a team."""
197
198 member = PublicPersonChoice(
199 title=_('Alias for minor_person attribute'),
200 vocabulary='ValidPersonOrTeam',
201 required=True)
202
203 team = PublicPersonChoice(
204 title=_('Alias for major_person attribute'),
205 vocabulary='ValidPersonOrTeam',
206 required=True)
207
208
209class ITeamJoinNotificationJobSource(IJobSource):
210 """An interface for acquiring ITeamJoinNotificationJobs."""
211
212 def create(member, team):
213 """Create a new ITeamJoinNotificationJob."""
214
215
216class IExpiringMembershipNotificationJob(IPersonTransferJob):
217 """A Job to send a warning about expiring membership."""
218
219 member = PublicPersonChoice(
220 title=_('Alias for minor_person attribute'),
221 vocabulary='ValidPersonOrTeam',
222 required=True)
223
224 team = PublicPersonChoice(
225 title=_('Alias for major_person attribute'),
226 vocabulary='ValidPersonOrTeam',
227 required=True)
228
229
230class IExpiringMembershipNotificationJobSource(IJobSource):
231 """An interface for acquiring IExpiringMembershipNotificationJobs."""
232
233 def create(member, team, dateexpires):
234 """Create a new IExpiringMembershipNotificationJob."""
235
236
237class ISelfRenewalNotificationJob(IPersonTransferJob):
238 """A Job to notify about a self-renewal."""
239
240 member = PublicPersonChoice(
241 title=_('Alias for minor_person attribute'),
242 vocabulary='ValidPersonOrTeam',
243 required=True)
244
245 team = PublicPersonChoice(
246 title=_('Alias for major_person attribute'),
247 vocabulary='ValidPersonOrTeam',
248 required=True)
249
250
251class ISelfRenewalNotificationJobSource(IJobSource):
252 """An interface for acquiring ISelfRenewalNotificationJobs."""
253
254 def create(member, team, dateexpires):
255 """Create a new ISelfRenewalNotificationJob."""
164256
=== modified file 'lib/lp/registry/mail/notification.py'
--- lib/lp/registry/mail/notification.py 2015-03-13 19:05:50 +0000
+++ lib/lp/registry/mail/notification.py 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Event handlers that send email notifications."""4"""Event handlers that send email notifications."""
@@ -17,21 +17,16 @@
17 getUtility,17 getUtility,
18 )18 )
1919
20from lp.registry.enums import TeamMembershipPolicy
21from lp.registry.interfaces.mailinglist import IHeldMessageDetails20from lp.registry.interfaces.mailinglist import IHeldMessageDetails
22from lp.registry.interfaces.person import IPersonSet21from lp.registry.interfaces.person import IPersonSet
23from lp.registry.interfaces.teammembership import (22from lp.registry.interfaces.persontransferjob import (
24 ITeamMembershipSet,23 ITeamInvitationNotificationJobSource,
25 TeamMembershipStatus,24 ITeamJoinNotificationJobSource,
26 )25 )
27from lp.services.config import config26from lp.services.config import config
28from lp.services.database.sqlbase import block_implicit_flushes27from lp.services.database.sqlbase import block_implicit_flushes
29from lp.services.mail.helpers import (28from lp.services.mail.helpers import get_email_template
30 get_contact_email_addresses,
31 get_email_template,
32 )
33from lp.services.mail.mailwrapper import MailWrapper29from lp.services.mail.mailwrapper import MailWrapper
34from lp.services.mail.notificationrecipientset import NotificationRecipientSet
35from lp.services.mail.sendmail import (30from lp.services.mail.sendmail import (
36 format_address,31 format_address,
37 sendmail,32 sendmail,
@@ -41,14 +36,7 @@
41 IDirectEmailAuthorization,36 IDirectEmailAuthorization,
42 QuotaReachedError,37 QuotaReachedError,
43 )38 )
44from lp.services.webapp.interfaces import ILaunchpadRoot
45from lp.services.webapp.publisher import canonical_url39from lp.services.webapp.publisher import canonical_url
46from lp.services.webapp.url import urlappend
47
48# Silence lint warnings.
49NotificationRecipientSet
50
51CC = "CC"
5240
5341
54@block_implicit_flushes42@block_implicit_flushes
@@ -57,48 +45,9 @@
5745
58 The notification will include a link to a page in which any team admin can46 The notification will include a link to a page in which any team admin can
59 accept the invitation.47 accept the invitation.
60
61 XXX: Guilherme Salgado 2007-05-08:
62 At some point we may want to extend this functionality to allow invites
63 to be sent to users as well, but for now we only use it for teams.
64 """48 """
65 member = event.member49 getUtility(ITeamInvitationNotificationJobSource).create(
66 assert member.is_team50 event.member, event.team)
67 team = event.team
68 membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
69 member, team)
70 assert membership is not None
71
72 reviewer = membership.proposed_by
73 admin_addrs = member.getTeamAdminsEmailAddresses()
74 from_addr = format_address(
75 team.displayname, config.canonical.noreply_from_address)
76 subject = 'Invitation for %s to join' % member.name
77 templatename = 'membership-invitation.txt'
78 template = get_email_template(templatename, app='registry')
79 replacements = {
80 'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name),
81 'member': '%s (%s)' % (member.displayname, member.name),
82 'team': '%s (%s)' % (team.displayname, team.name),
83 'team_url': canonical_url(team),
84 'membership_invitations_url':
85 "%s/+invitation/%s" % (canonical_url(member), team.name)}
86 for address in admin_addrs:
87 recipient = getUtility(IPersonSet).getByEmail(address)
88 replacements['recipient_name'] = recipient.displayname
89 msg = MailWrapper().format(template % replacements, force_wrap=True)
90 simple_sendmail(from_addr, address, subject, msg)
91
92
93def send_team_email(from_addr, address, subject, template, replacements,
94 rationale, headers=None):
95 """Send a team message with a rationale."""
96 if headers is None:
97 headers = {}
98 body = MailWrapper().format(template % replacements, force_wrap=True)
99 footer = "-- \n%s" % rationale
100 message = '%s\n\n%s' % (body, footer)
101 simple_sendmail(from_addr, address, subject, message, headers)
10251
10352
104@block_implicit_flushes53@block_implicit_flushes
@@ -109,130 +58,7 @@
109 is pending approval. Otherwise it'll say that the person has joined the58 is pending approval. Otherwise it'll say that the person has joined the
110 team and who added that person to the team.59 team and who added that person to the team.
111 """60 """
112 person = event.person61 getUtility(ITeamJoinNotificationJobSource).create(event.person, event.team)
113 team = event.team
114 membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
115 person, team)
116 assert membership is not None
117 approved, admin, proposed = [
118 TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN,
119 TeamMembershipStatus.PROPOSED]
120 admin_addrs = team.getTeamAdminsEmailAddresses()
121 from_addr = format_address(
122 team.displayname, config.canonical.noreply_from_address)
123
124 reviewer = membership.proposed_by
125 if reviewer != person and membership.status in [approved, admin]:
126 reviewer = membership.reviewed_by
127 # Somebody added this person as a member, we better send a
128 # notification to the person too.
129 member_addrs = get_contact_email_addresses(person)
130
131 headers = {}
132 if person.is_team:
133 templatename = 'new-member-notification-for-teams.txt'
134 subject = '%s joined %s' % (person.name, team.name)
135 header_rational = "Indirect member (%s)" % team.name
136 footer_rationale = (
137 "You received this email because "
138 "%s is the new member." % person.name)
139 else:
140 templatename = 'new-member-notification.txt'
141 subject = 'You have been added to %s' % team.name
142 header_rational = "Member (%s)" % team.name
143 footer_rationale = (
144 "You received this email because you are the new member.")
145
146 if team.mailing_list is not None:
147 template = get_email_template(
148 'team-list-subscribe-block.txt', app='registry')
149 editemails_url = urlappend(
150 canonical_url(getUtility(ILaunchpadRoot)),
151 'people/+me/+editmailinglists')
152 list_instructions = template % dict(editemails_url=editemails_url)
153 else:
154 list_instructions = ''
155
156 template = get_email_template(templatename, app='registry')
157 replacements = {
158 'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name),
159 'team_url': canonical_url(team),
160 'member': '%s (%s)' % (person.displayname, person.name),
161 'team': '%s (%s)' % (team.displayname, team.name),
162 'list_instructions': list_instructions,
163 }
164 headers = {'X-Launchpad-Message-Rationale': header_rational}
165 for address in member_addrs:
166 recipient = getUtility(IPersonSet).getByEmail(address)
167 replacements['recipient_name'] = recipient.displayname
168 send_team_email(
169 from_addr, address, subject, template, replacements,
170 footer_rationale, headers)
171
172 # The member's email address may be in admin_addrs too; let's remove
173 # it so the member don't get two notifications.
174 admin_addrs = set(admin_addrs).difference(set(member_addrs))
175
176 # Yes, we can have teams with no members; not even admins.
177 if not admin_addrs:
178 return
179
180 # Open teams do not notify admins about new members.
181 if team.membership_policy == TeamMembershipPolicy.OPEN:
182 return
183
184 replacements = {
185 'person_name': "%s (%s)" % (person.displayname, person.name),
186 'team_name': "%s (%s)" % (team.displayname, team.name),
187 'reviewer_name': "%s (%s)" % (reviewer.displayname, reviewer.name),
188 'url': canonical_url(membership)}
189
190 headers = {}
191 if membership.status in [approved, admin]:
192 template = get_email_template(
193 'new-member-notification-for-admins.txt', app='registry')
194 subject = '%s joined %s' % (person.name, team.name)
195 elif membership.status == proposed:
196 # In the UI, a user can only propose himself or a team he
197 # admins. Some users of the REST API have a workflow, where
198 # they propose users that are designated as mentees (Bug 498181).
199 if reviewer != person:
200 headers = {"Reply-To": reviewer.preferredemail.email}
201 template = get_email_template(
202 'pending-membership-approval-for-third-party.txt',
203 app='registry')
204 else:
205 headers = {"Reply-To": person.preferredemail.email}
206 template = get_email_template(
207 'pending-membership-approval.txt', app='registry')
208 subject = "%s wants to join" % person.name
209 else:
210 raise AssertionError(
211 "Unexpected membership status: %s" % membership.status)
212
213 for address in admin_addrs:
214 recipient = getUtility(IPersonSet).getByEmail(address)
215 replacements['recipient_name'] = recipient.displayname
216 if recipient.is_team:
217 header_rationale = 'Admin (%s via %s)' % (
218 team.name, recipient.name)
219 footer_rationale = (
220 "you are an admin of the %s team\n"
221 "via the %s team." % (
222 team.displayname, recipient.displayname))
223 elif recipient == team.teamowner:
224 header_rationale = 'Owner (%s)' % team.name
225 footer_rationale = (
226 "you are the owner of the %s team." % team.displayname)
227 else:
228 header_rationale = 'Admin (%s)' % team.name
229 footer_rationale = (
230 "you are an admin of the %s team." % team.displayname)
231 footer = 'You received this email because %s' % footer_rationale
232 headers['X-Launchpad-Message-Rationale'] = header_rationale
233 send_team_email(
234 from_addr, address, subject, template, replacements,
235 footer, headers)
23662
23763
238def notify_mailinglist_activated(mailinglist, event):64def notify_mailinglist_activated(mailinglist, event):
23965
=== added file 'lib/lp/registry/mail/teammembership.py'
--- lib/lp/registry/mail/teammembership.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/mail/teammembership.py 2015-09-08 11:57:29 +0000
@@ -0,0 +1,435 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5__all__ = [
6 'TeamMembershipMailer',
7 ]
8
9from collections import OrderedDict
10from datetime import datetime
11
12import pytz
13from zope.component import getUtility
14
15from lp.app.browser.tales import DurationFormatterAPI
16from lp.registry.enums import (
17 TeamMembershipPolicy,
18 TeamMembershipRenewalPolicy,
19 )
20from lp.registry.interfaces.teammembership import (
21 ITeamMembershipSet,
22 TeamMembershipStatus,
23 )
24from lp.registry.model.person import get_recipients
25from lp.services.config import config
26from lp.services.mail.basemailer import (
27 BaseMailer,
28 RecipientReason,
29 )
30from lp.services.mail.helpers import get_email_template
31from lp.services.mail.sendmail import format_address
32from lp.services.webapp.interfaces import ILaunchpadRoot
33from lp.services.webapp.publisher import canonical_url
34from lp.services.webapp.url import urlappend
35
36
37class TeamMembershipRecipientReason(RecipientReason):
38
39 @classmethod
40 def forInvitation(cls, admin, team, recipient, proposed_member, **kwargs):
41 header = cls.makeRationale(
42 "Invitation (%s)" % team.name, proposed_member)
43 reason = (
44 "You received this email because %%(lc_entity_is)s an admin of "
45 "the %s team." % proposed_member.displayname)
46 return cls(admin, recipient, header, reason, **kwargs)
47
48 @classmethod
49 def forMember(cls, member, team, recipient, **kwargs):
50 header = cls.makeRationale("Member (%s)" % team.name, member)
51 reason = (
52 "You received this email because %(lc_entity_is)s the affected "
53 "member.")
54 return cls(member, recipient, header, reason, **kwargs)
55
56 @classmethod
57 def forNewMember(cls, new_member, team, recipient, **kwargs):
58 # From a filtering point of view, this is identical to forMember;
59 # filtering on X-Launchpad-Notification-Type is more useful for
60 # determining the type of notification sent to a particular member.
61 # It's worth having a footer that makes a little more sense, though.
62 header = cls.makeRationale("Member (%s)" % team.name, new_member)
63 reason = (
64 "You received this email because %(lc_entity_is)s the new member.")
65 return cls(new_member, recipient, header, reason, **kwargs)
66
67 @classmethod
68 def forAdmin(cls, admin, team, recipient, **kwargs):
69 header = cls.makeRationale("Admin (%s)" % team.name, admin)
70 reason = (
71 "You received this email because %%(lc_entity_is)s an admin of "
72 "the %s team." % team.displayname)
73 return cls(admin, recipient, header, reason, **kwargs)
74
75 @classmethod
76 def forOwner(cls, owner, team, recipient, **kwargs):
77 header = cls.makeRationale("Owner (%s)" % team.name, owner)
78 reason = (
79 "You received this email because %%(lc_entity_is)s the owner "
80 "of the %s team." % team.displayname)
81 return cls(owner, recipient, header, reason, **kwargs)
82
83 def __init__(self, subscriber, recipient, mail_header, reason_template,
84 subject=None, template_name=None, reply_to=None,
85 recipient_class=None):
86 super(TeamMembershipRecipientReason, self).__init__(
87 subscriber, recipient, mail_header, reason_template)
88 self.subject = subject
89 self.template_name = template_name
90 self.reply_to = reply_to
91 self.recipient_class = recipient_class
92
93
94class TeamMembershipMailer(BaseMailer):
95
96 app = 'registry'
97
98 @classmethod
99 def forInvitationToJoinTeam(cls, member, team):
100 """Create a mailer for notifying about team joining invitations.
101
102 XXX: Guilherme Salgado 2007-05-08:
103 At some point we may want to extend this functionality to allow
104 invites to be sent to users as well, but for now we only use it for
105 teams.
106 """
107 assert member.is_team
108 membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
109 member, team)
110 assert membership is not None
111 recipients = OrderedDict()
112 for admin in member.adminmembers:
113 for recipient in get_recipients(admin):
114 recipients[recipient] = TeamMembershipRecipientReason.forAdmin(
115 admin, member, recipient)
116 from_addr = format_address(
117 team.displayname, config.canonical.noreply_from_address)
118 subject = "Invitation for %s to join" % member.name
119 return cls(
120 subject, "membership-invitation.txt", recipients, from_addr,
121 "team-membership-invitation", member, team, membership.proposed_by,
122 membership=membership)
123
124 @classmethod
125 def forTeamJoin(cls, member, team):
126 """Create a mailer for notifying about a new member joining a team."""
127 membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
128 member, team)
129 assert membership is not None
130 subject = None
131 template_name = None
132 notification_type = "team-membership-new"
133 recipients = OrderedDict()
134 reviewer = membership.proposed_by
135 if reviewer != member and membership.status in [
136 TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN]:
137 reviewer = membership.reviewed_by
138 # Somebody added this person as a member, we better send a
139 # notification to the person too.
140 if member.is_team:
141 template_name = "new-member-notification-for-teams.txt"
142 subject = "%s joined %s" % (member.name, team.name)
143 else:
144 template_name = "new-member-notification.txt"
145 subject = "You have been added to %s" % team.name
146 for recipient in get_recipients(member):
147 recipients[recipient] = (
148 TeamMembershipRecipientReason.forNewMember(
149 member, team, recipient, subject=subject,
150 template_name=template_name))
151 # Open teams do not notify admins about new members.
152 if team.membership_policy != TeamMembershipPolicy.OPEN:
153 reply_to = None
154 if membership.status in [
155 TeamMembershipStatus.APPROVED, TeamMembershipStatus.ADMIN]:
156 template_name = "new-member-notification-for-admins.txt"
157 subject = "%s joined %s" % (member.name, team.name)
158 elif membership.status == TeamMembershipStatus.PROPOSED:
159 # In the UI, a user can only propose themselves or a team
160 # they admin. Some users of the REST API have a workflow
161 # where they propose users that are designated as undergoing
162 # mentorship (Bug 498181).
163 if reviewer != member:
164 reply_to = reviewer.preferredemail.email
165 template_name = (
166 "pending-membership-approval-for-third-party.txt")
167 else:
168 reply_to = member.preferredemail.email
169 template_name = "pending-membership-approval.txt"
170 notification_type = "team-membership-pending"
171 subject = "%s wants to join" % member.name
172 else:
173 raise AssertionError(
174 "Unexpected membership status: %s" % membership.status)
175 for admin in team.adminmembers:
176 for recipient in get_recipients(admin):
177 # The new member may also be a team admin; don't send
178 # two notifications in that case.
179 if recipient not in recipients:
180 if recipient == team.teamowner:
181 reason_factory = (
182 TeamMembershipRecipientReason.forOwner)
183 else:
184 reason_factory = (
185 TeamMembershipRecipientReason.forAdmin)
186 recipients[recipient] = reason_factory(
187 admin, team, recipient, subject=subject,
188 template_name=template_name, reply_to=reply_to)
189 from_addr = format_address(
190 team.displayname, config.canonical.noreply_from_address)
191 return cls(
192 subject, template_name, recipients, from_addr, notification_type,
193 member, team, membership.proposed_by, membership=membership)
194
195 @classmethod
196 def forMembershipStatusChange(cls, member, team, reviewer,
197 old_status, new_status, last_change_comment):
198 """Create a mailer for a membership status change."""
199 template_name = 'membership-statuschange'
200 notification_type = 'team-membership-change'
201 subject = (
202 'Membership change: %(member)s in %(team)s' %
203 {'member': member.name, 'team': team.name})
204 if new_status == TeamMembershipStatus.EXPIRED:
205 template_name = 'membership-expired'
206 notification_type = 'team-membership-expired'
207 subject = '%s expired from team' % member.name
208 elif (new_status == TeamMembershipStatus.APPROVED and
209 old_status != TeamMembershipStatus.ADMIN):
210 if old_status == TeamMembershipStatus.INVITED:
211 template_name = 'membership-invitation-accepted'
212 notification_type = 'team-membership-invitation-accepted'
213 subject = (
214 'Invitation to %s accepted by %s' %
215 (member.name, reviewer.name))
216 elif old_status == TeamMembershipStatus.PROPOSED:
217 subject = '%s approved by %s' % (member.name, reviewer.name)
218 else:
219 subject = '%s added by %s' % (member.name, reviewer.name)
220 elif new_status == TeamMembershipStatus.INVITATION_DECLINED:
221 template_name = 'membership-invitation-declined'
222 notification_type = 'team-membership-invitation-declined'
223 subject = (
224 'Invitation to %s declined by %s' %
225 (member.name, reviewer.name))
226 elif new_status == TeamMembershipStatus.DEACTIVATED:
227 subject = '%s deactivated by %s' % (member.name, reviewer.name)
228 elif new_status == TeamMembershipStatus.ADMIN:
229 subject = '%s made admin by %s' % (member.name, reviewer.name)
230 elif new_status == TeamMembershipStatus.DECLINED:
231 subject = '%s declined by %s' % (member.name, reviewer.name)
232 else:
233 # Use the default template and subject.
234 pass
235 template_name += "-%(recipient_class)s.txt"
236
237 if last_change_comment:
238 comment = "\n%s said:\n %s\n" % (
239 reviewer.displayname, last_change_comment.strip())
240 else:
241 comment = ""
242
243 recipients = OrderedDict()
244 if reviewer != member:
245 for recipient in get_recipients(member):
246 if member.is_team:
247 recipient_class = "bulk"
248 else:
249 recipient_class = "personal"
250 recipients[recipient] = (
251 TeamMembershipRecipientReason.forMember(
252 member, team, recipient,
253 recipient_class=recipient_class))
254 # Don't send admin notifications for open teams: they're
255 # unrestricted, so notifications on join/leave do not help the
256 # admins.
257 if team.membership_policy != TeamMembershipPolicy.OPEN:
258 for admin in team.adminmembers:
259 for recipient in get_recipients(admin):
260 # The new member may also be a team admin; don't send
261 # two notifications in that case.
262 if recipient not in recipients:
263 recipients[recipient] = (
264 TeamMembershipRecipientReason.forAdmin(
265 admin, team, recipient,
266 recipient_class="bulk"))
267
268 extra_params = {
269 "old_status": old_status,
270 "new_status": new_status,
271 "comment": comment,
272 }
273 from_addr = format_address(
274 team.displayname, config.canonical.noreply_from_address)
275 return cls(
276 subject, template_name, recipients, from_addr, notification_type,
277 member, team, reviewer, extra_params=extra_params)
278
279 @classmethod
280 def forExpiringMembership(cls, member, team, dateexpires):
281 """Create a mailer for warning about expiring membership."""
282 membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
283 member, team)
284 assert membership is not None
285 if member.is_team:
286 target = member.teamowner
287 template_name = "membership-expiration-warning-bulk.txt"
288 subject = "%s will expire soon from %s" % (member.name, team.name)
289 else:
290 target = member
291 template_name = "membership-expiration-warning-personal.txt"
292 subject = "Your membership in %s is about to expire" % team.name
293
294 if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND:
295 how_to_renew = (
296 "If you want, you can renew this membership at\n"
297 "<%s/+expiringmembership/%s>" %
298 (canonical_url(member), team.name))
299 elif not membership.canChangeExpirationDate(target):
300 admins_names = []
301 admins = team.getDirectAdministrators()
302 assert admins.count() >= 1
303 if admins.count() == 1:
304 admin = admins[0]
305 how_to_renew = (
306 "To prevent this membership from expiring, you should "
307 "contact the\nteam's administrator, %s.\n<%s>"
308 % (admin.unique_displayname, canonical_url(admin)))
309 else:
310 for admin in admins:
311 admins_names.append(
312 "%s <%s>" % (
313 admin.unique_displayname, canonical_url(admin)))
314
315 how_to_renew = (
316 "To prevent this membership from expiring, you should "
317 "get in touch\nwith one of the team's administrators:\n")
318 how_to_renew += "\n".join(admins_names)
319 else:
320 how_to_renew = (
321 "To stay a member of this team you should extend your "
322 "membership at\n<%s/+member/%s>"
323 % (canonical_url(team), member.name))
324
325 recipients = OrderedDict()
326 for recipient in get_recipients(target):
327 recipients[recipient] = TeamMembershipRecipientReason.forMember(
328 member, team, recipient)
329
330 formatter = DurationFormatterAPI(dateexpires - datetime.now(pytz.UTC))
331 extra_params = {
332 "how_to_renew": how_to_renew,
333 "expiration_date": dateexpires.strftime("%Y-%m-%d"),
334 "approximate_duration": formatter.approximateduration(),
335 }
336
337 from_addr = format_address(
338 team.displayname, config.canonical.noreply_from_address)
339 return cls(
340 subject, template_name, recipients, from_addr,
341 "team-membership-expiration-warning", member, team,
342 membership.proposed_by, membership=membership,
343 extra_params=extra_params, wrap=False, force_wrap=False)
344
345 @classmethod
346 def forSelfRenewal(cls, member, team, dateexpires):
347 """Create a mailer for notifying about a self-renewal."""
348 assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND
349 template_name = "membership-member-renewed.txt"
350 subject = "%s extended their membership" % member.name
351 recipients = OrderedDict()
352 for admin in team.adminmembers:
353 for recipient in get_recipients(admin):
354 recipients[recipient] = TeamMembershipRecipientReason.forAdmin(
355 admin, team, recipient)
356 extra_params = {"dateexpires": dateexpires.strftime("%Y-%m-%d")}
357 from_addr = format_address(
358 team.displayname, config.canonical.noreply_from_address)
359 return cls(
360 subject, template_name, recipients, from_addr,
361 "team-membership-renewed", member, team, None,
362 extra_params=extra_params)
363
364 def __init__(self, subject, template_name, recipients, from_address,
365 notification_type, member, team, reviewer, membership=None,
366 extra_params={}, wrap=True, force_wrap=True):
367 """See `BaseMailer`."""
368 super(TeamMembershipMailer, self).__init__(
369 subject, template_name, recipients, from_address,
370 notification_type=notification_type, wrap=wrap,
371 force_wrap=force_wrap)
372 self.member = member
373 self.team = team
374 self.reviewer = reviewer
375 self.membership = membership
376 self.extra_params = extra_params
377
378 def _getSubject(self, email, recipient):
379 """See `BaseMailer`."""
380 reason, _ = self._recipients.getReason(email)
381 if reason.subject is not None:
382 subject_template = reason.subject
383 else:
384 subject_template = self._subject_template
385 return subject_template % self._getTemplateParams(email, recipient)
386
387 def _getReplyToAddress(self, email, recipient):
388 """See `BaseMailer`."""
389 reason, _ = self._recipients.getReason(email)
390 return reason.reply_to
391
392 def _getTemplateName(self, email, recipient):
393 """See `BaseMailer`."""
394 reason, _ = self._recipients.getReason(email)
395 if reason.template_name is not None:
396 template_name = reason.template_name
397 else:
398 template_name = self._template_name
399 return template_name % self._getTemplateParams(email, recipient)
400
401 def _getTemplateParams(self, email, recipient):
402 """See `BaseMailer`."""
403 params = super(TeamMembershipMailer, self)._getTemplateParams(
404 email, recipient)
405 params["recipient"] = recipient.displayname
406 reason, _ = self._recipients.getReason(email)
407 if reason.recipient_class is not None:
408 params["recipient_class"] = reason.recipient_class
409 params["member"] = self.member.unique_displayname
410 params["membership_invitations_url"] = "%s/+invitation/%s" % (
411 canonical_url(self.member), self.team.name)
412 params["team"] = self.team.unique_displayname
413 params["team_url"] = canonical_url(self.team)
414 if self.membership is not None:
415 params["membership_url"] = canonical_url(self.membership)
416 if reason.recipient_class == "bulk" and self.reviewer == self.member:
417 params["reviewer"] = "the user"
418 elif self.reviewer is not None:
419 params["reviewer"] = self.reviewer.unique_displayname
420 if self.team.mailing_list is not None:
421 template = get_email_template(
422 "team-list-subscribe-block.txt", app="registry")
423 editemails_url = urlappend(
424 canonical_url(getUtility(ILaunchpadRoot)),
425 "~/+editmailinglists")
426 list_instructions = template % {"editemails_url": editemails_url}
427 else:
428 list_instructions = ""
429 params["list_instructions"] = list_instructions
430 params.update(self.extra_params)
431 return params
432
433 def _getFooter(self, email, recipient, params):
434 """See `BaseMailer`."""
435 return "%(reason)s\n" % params
0436
=== modified file 'lib/lp/registry/model/persontransferjob.py'
--- lib/lp/registry/model/persontransferjob.py 2015-07-09 20:06:17 +0000
+++ lib/lp/registry/model/persontransferjob.py 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2013 Canonical Ltd. This software is licensed under the1# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Job classes related to PersonTransferJob."""4"""Job classes related to PersonTransferJob."""
@@ -9,7 +9,10 @@
9 'PersonTransferJob',9 'PersonTransferJob',
10 ]10 ]
1111
12from datetime import datetime
13
12from lazr.delegates import delegate_to14from lazr.delegates import delegate_to
15import pytz
13import simplejson16import simplejson
14from storm.expr import (17from storm.expr import (
15 And,18 And,
@@ -27,16 +30,15 @@
27 )30 )
2831
29from lp.app.interfaces.launchpad import ILaunchpadCelebrities32from lp.app.interfaces.launchpad import ILaunchpadCelebrities
30from lp.registry.enums import (33from lp.registry.enums import PersonTransferJobType
31 PersonTransferJobType,
32 TeamMembershipPolicy,
33 )
34from lp.registry.interfaces.person import (34from lp.registry.interfaces.person import (
35 IPerson,35 IPerson,
36 IPersonSet,36 IPersonSet,
37 ITeam,37 ITeam,
38 )38 )
39from lp.registry.interfaces.persontransferjob import (39from lp.registry.interfaces.persontransferjob import (
40 IExpiringMembershipNotificationJob,
41 IExpiringMembershipNotificationJobSource,
40 IMembershipNotificationJob,42 IMembershipNotificationJob,
41 IMembershipNotificationJobSource,43 IMembershipNotificationJobSource,
42 IPersonDeactivateJob,44 IPersonDeactivateJob,
@@ -45,8 +47,15 @@
45 IPersonMergeJobSource,47 IPersonMergeJobSource,
46 IPersonTransferJob,48 IPersonTransferJob,
47 IPersonTransferJobSource,49 IPersonTransferJobSource,
50 ISelfRenewalNotificationJob,
51 ISelfRenewalNotificationJobSource,
52 ITeamInvitationNotificationJob,
53 ITeamInvitationNotificationJobSource,
54 ITeamJoinNotificationJob,
55 ITeamJoinNotificationJobSource,
48 )56 )
49from lp.registry.interfaces.teammembership import TeamMembershipStatus57from lp.registry.interfaces.teammembership import TeamMembershipStatus
58from lp.registry.mail.teammembership import TeamMembershipMailer
50from lp.registry.model.person import Person59from lp.registry.model.person import Person
51from lp.registry.personmerge import merge_people60from lp.registry.personmerge import merge_people
52from lp.services.config import config61from lp.services.config import config
@@ -62,17 +71,8 @@
62 Job,71 Job,
63 )72 )
64from lp.services.job.runner import BaseRunnableJob73from lp.services.job.runner import BaseRunnableJob
65from lp.services.mail.helpers import (74from lp.services.mail.sendmail import format_address_for_person
66 get_contact_email_addresses,75from lp.services.scripts import log
67 get_email_template,
68 )
69from lp.services.mail.mailwrapper import MailWrapper
70from lp.services.mail.sendmail import (
71 format_address,
72 format_address_for_person,
73 simple_sendmail,
74 )
75from lp.services.webapp import canonical_url
7676
7777
78@implementer(IPersonTransferJob)78@implementer(IPersonTransferJob)
@@ -182,6 +182,17 @@
182 ])182 ])
183 return vars183 return vars
184184
185 _time_format = '%Y-%m-%d %H:%M:%S.%f'
186
187 @classmethod
188 def _serialiseDateTime(cls, dt):
189 return dt.strftime(cls._time_format)
190
191 @classmethod
192 def _deserialiseDateTime(cls, dt_str):
193 dt = datetime.strptime(dt_str, cls._time_format)
194 return dt.replace(tzinfo=pytz.UTC)
195
185196
186@implementer(IMembershipNotificationJob)197@implementer(IMembershipNotificationJob)
187@provider(IMembershipNotificationJobSource)198@provider(IMembershipNotificationJobSource)
@@ -240,107 +251,9 @@
240251
241 def run(self):252 def run(self):
242 """See `IMembershipNotificationJob`."""253 """See `IMembershipNotificationJob`."""
243 from lp.services.scripts import log254 TeamMembershipMailer.forMembershipStatusChange(
244 from_addr = format_address(255 self.member, self.team, self.reviewer, self.old_status,
245 self.team.displayname, config.canonical.noreply_from_address)256 self.new_status, self.last_change_comment).sendAll()
246 admin_emails = self.team.getTeamAdminsEmailAddresses()
247 # person might be a self.team, so we can't rely on its preferredemail.
248 self.member_email = get_contact_email_addresses(self.member)
249 # Make sure we don't send the same notification twice to anybody.
250 for email in self.member_email:
251 if email in admin_emails:
252 admin_emails.remove(email)
253
254 if self.reviewer != self.member:
255 self.reviewer_name = self.reviewer.unique_displayname
256 else:
257 self.reviewer_name = 'the user'
258
259 if self.last_change_comment:
260 comment = ("\n%s said:\n %s\n" % (
261 self.reviewer.displayname, self.last_change_comment.strip()))
262 else:
263 comment = ""
264
265 replacements = {
266 'member_name': self.member.unique_displayname,
267 'recipient_name': self.member.displayname,
268 'team_name': self.team.unique_displayname,
269 'team_url': canonical_url(self.team),
270 'old_status': self.old_status.title,
271 'new_status': self.new_status.title,
272 'reviewer_name': self.reviewer_name,
273 'comment': comment}
274
275 template_name = 'membership-statuschange'
276 subject = (
277 'Membership change: %(member)s in %(team)s'
278 % {
279 'member': self.member.name,
280 'team': self.team.name,
281 })
282 if self.new_status == TeamMembershipStatus.EXPIRED:
283 template_name = 'membership-expired'
284 subject = '%s expired from team' % self.member.name
285 elif (self.new_status == TeamMembershipStatus.APPROVED and
286 self.old_status != TeamMembershipStatus.ADMIN):
287 if self.old_status == TeamMembershipStatus.INVITED:
288 subject = ('Invitation to %s accepted by %s'
289 % (self.member.name, self.reviewer.name))
290 template_name = 'membership-invitation-accepted'
291 elif self.old_status == TeamMembershipStatus.PROPOSED:
292 subject = '%s approved by %s' % (
293 self.member.name, self.reviewer.name)
294 else:
295 subject = '%s added by %s' % (
296 self.member.name, self.reviewer.name)
297 elif self.new_status == TeamMembershipStatus.INVITATION_DECLINED:
298 subject = ('Invitation to %s declined by %s'
299 % (self.member.name, self.reviewer.name))
300 template_name = 'membership-invitation-declined'
301 elif self.new_status == TeamMembershipStatus.DEACTIVATED:
302 subject = '%s deactivated by %s' % (
303 self.member.name, self.reviewer.name)
304 elif self.new_status == TeamMembershipStatus.ADMIN:
305 subject = '%s made admin by %s' % (
306 self.member.name, self.reviewer.name)
307 elif self.new_status == TeamMembershipStatus.DECLINED:
308 subject = '%s declined by %s' % (
309 self.member.name, self.reviewer.name)
310 else:
311 # Use the default template and subject.
312 pass
313
314 # Must have someone to mail, and be a non-open team (because open
315 # teams are unrestricted, notifications on join/ leave do not help the
316 # admins.
317 if (len(admin_emails) != 0 and
318 self.team.membership_policy != TeamMembershipPolicy.OPEN):
319 admin_template = get_email_template(
320 "%s-bulk.txt" % template_name, app='registry')
321 for address in admin_emails:
322 recipient = getUtility(IPersonSet).getByEmail(address)
323 replacements['recipient_name'] = recipient.displayname
324 msg = MailWrapper().format(
325 admin_template % replacements, force_wrap=True)
326 simple_sendmail(from_addr, address, subject, msg)
327
328 # The self.member can be a self.self.team without any
329 # self.members, and in this case we won't have a single email
330 # address to send this notification to.
331 if self.member_email and self.reviewer != self.member:
332 if self.member.is_team:
333 template = '%s-bulk.txt' % template_name
334 else:
335 template = '%s-personal.txt' % template_name
336 self.member_template = get_email_template(
337 template, app='registry')
338 for address in self.member_email:
339 recipient = getUtility(IPersonSet).getByEmail(address)
340 replacements['recipient_name'] = recipient.displayname
341 msg = MailWrapper().format(
342 self.member_template % replacements, force_wrap=True)
343 simple_sendmail(from_addr, address, subject, msg)
344 log.debug('MembershipNotificationJob sent email')257 log.debug('MembershipNotificationJob sent email')
345258
346 def __repr__(self):259 def __repr__(self):
@@ -430,7 +343,6 @@
430 from_person_name = self.from_person.name343 from_person_name = self.from_person.name
431 to_person_name = self.to_person.name344 to_person_name = self.to_person.name
432345
433 from lp.services.scripts import log
434 if self.metadata.get('delete', False):346 if self.metadata.get('delete', False):
435 log.debug(347 log.debug(
436 "%s is about to delete ~%s", self.log_name,348 "%s is about to delete ~%s", self.log_name,
@@ -511,7 +423,6 @@
511423
512 def run(self):424 def run(self):
513 """Perform the merge."""425 """Perform the merge."""
514 from lp.services.scripts import log
515 person_name = self.person.name426 person_name = self.person.name
516 log.debug('about to deactivate ~%s', person_name)427 log.debug('about to deactivate ~%s', person_name)
517 self.person.deactivate(validate=False, pre_deactivate=False)428 self.person.deactivate(validate=False, pre_deactivate=False)
@@ -524,3 +435,160 @@
524435
525 def getOperationDescription(self):436 def getOperationDescription(self):
526 return 'deactivating ~%s' % self.person.name437 return 'deactivating ~%s' % self.person.name
438
439
440@implementer(ITeamInvitationNotificationJob)
441@provider(ITeamInvitationNotificationJobSource)
442class TeamInvitationNotificationJob(PersonTransferJobDerived):
443 """A Job that sends a notification of an invitation to join a team."""
444
445 class_job_type = PersonTransferJobType.TEAM_INVITATION_NOTIFICATION
446
447 config = config.ITeamInvitationNotificationJobSource
448
449 @classmethod
450 def create(cls, member, team):
451 if not ITeam.providedBy(team):
452 raise TypeError('team must be ITeam: %s' % repr(team))
453 return super(TeamInvitationNotificationJob, cls).create(
454 minor_person=member, major_person=team, metadata={})
455
456 @property
457 def member(self):
458 return self.minor_person
459
460 @property
461 def team(self):
462 return self.major_person
463
464 def run(self):
465 """See `ITeamInvitationNotificationJob`."""
466 TeamMembershipMailer.forInvitationToJoinTeam(
467 self.member, self.team).sendAll()
468
469 def __repr__(self):
470 return (
471 "<{self.__class__.__name__} for invitation of "
472 "~{self.minor_person.name} to join ~{self.major_person.name}; "
473 "status={self.job.status}>").format(self=self)
474
475
476@implementer(ITeamJoinNotificationJob)
477@provider(ITeamJoinNotificationJobSource)
478class TeamJoinNotificationJob(PersonTransferJobDerived):
479 """A Job that sends a notification of a new member joining a team."""
480
481 class_job_type = PersonTransferJobType.TEAM_JOIN_NOTIFICATION
482
483 config = config.ITeamJoinNotificationJobSource
484
485 @classmethod
486 def create(cls, member, team):
487 if not ITeam.providedBy(team):
488 raise TypeError('team must be ITeam: %s' % repr(team))
489 return super(TeamJoinNotificationJob, cls).create(
490 minor_person=member, major_person=team, metadata={})
491
492 @property
493 def member(self):
494 return self.minor_person
495
496 @property
497 def team(self):
498 return self.major_person
499
500 def run(self):
501 """See `ITeamJoinNotificationJob`."""
502 TeamMembershipMailer.forTeamJoin(self.member, self.team).sendAll()
503
504 def __repr__(self):
505 return (
506 "<{self.__class__.__name__} for "
507 "~{self.minor_person.name} joining ~{self.major_person.name}; "
508 "status={self.job.status}>").format(self=self)
509
510
511@implementer(IExpiringMembershipNotificationJob)
512@provider(IExpiringMembershipNotificationJobSource)
513class ExpiringMembershipNotificationJob(PersonTransferJobDerived):
514 """A Job that sends a warning about expiring membership."""
515
516 class_job_type = PersonTransferJobType.EXPIRING_MEMBERSHIP_NOTIFICATION
517
518 config = config.IExpiringMembershipNotificationJobSource
519
520 @classmethod
521 def create(cls, member, team, dateexpires):
522 if not ITeam.providedBy(team):
523 raise TypeError('team must be ITeam: %s' % repr(team))
524 metadata = {
525 'dateexpires': cls._serialiseDateTime(dateexpires),
526 }
527 return super(ExpiringMembershipNotificationJob, cls).create(
528 minor_person=member, major_person=team, metadata=metadata)
529
530 @property
531 def member(self):
532 return self.minor_person
533
534 @property
535 def team(self):
536 return self.major_person
537
538 @property
539 def dateexpires(self):
540 return self._deserialiseDateTime(self.metadata['dateexpires'])
541
542 def run(self):
543 """See `IExpiringMembershipNotificationJob`."""
544 TeamMembershipMailer.forExpiringMembership(
545 self.member, self.team, self.dateexpires).sendAll()
546
547 def __repr__(self):
548 return (
549 "<{self.__class__.__name__} for upcoming expiry of "
550 "~{self.minor_person.name} from ~{self.major_person.name}; "
551 "status={self.job.status}>").format(self=self)
552
553
554@implementer(ISelfRenewalNotificationJob)
555@provider(ISelfRenewalNotificationJobSource)
556class SelfRenewalNotificationJob(PersonTransferJobDerived):
557 """A Job that sends a notification of a self-renewal."""
558
559 class_job_type = PersonTransferJobType.SELF_RENEWAL_NOTIFICATION
560
561 config = config.ISelfRenewalNotificationJobSource
562
563 @classmethod
564 def create(cls, member, team, dateexpires):
565 if not ITeam.providedBy(team):
566 raise TypeError('team must be ITeam: %s' % repr(team))
567 metadata = {
568 'dateexpires': cls._serialiseDateTime(dateexpires),
569 }
570 return super(SelfRenewalNotificationJob, cls).create(
571 minor_person=member, major_person=team, metadata=metadata)
572
573 @property
574 def member(self):
575 return self.minor_person
576
577 @property
578 def team(self):
579 return self.major_person
580
581 @property
582 def dateexpires(self):
583 return self._deserialiseDateTime(self.metadata['dateexpires'])
584
585 def run(self):
586 """See `ISelfRenewalNotificationJob`."""
587 TeamMembershipMailer.forSelfRenewal(
588 self.member, self.team, self.dateexpires).sendAll()
589
590 def __repr__(self):
591 return (
592 "<{self.__class__.__name__} for self-renewal of "
593 "~{self.minor_person.name} in ~{self.major_person.name}; "
594 "status={self.job.status}>").format(self=self)
527595
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2015-07-08 16:05:11 +0000
+++ lib/lp/registry/model/teammembership.py 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -24,7 +24,6 @@
24from zope.component import getUtility24from zope.component import getUtility
25from zope.interface import implementer25from zope.interface import implementer
2626
27from lp.app.browser.tales import DurationFormatterAPI
28from lp.app.interfaces.launchpad import ILaunchpadCelebrities27from lp.app.interfaces.launchpad import ILaunchpadCelebrities
29from lp.registry.enums import TeamMembershipRenewalPolicy28from lp.registry.enums import TeamMembershipRenewalPolicy
30from lp.registry.errors import (29from lp.registry.errors import (
@@ -32,12 +31,13 @@
32 UserCannotChangeMembershipSilently,31 UserCannotChangeMembershipSilently,
33 )32 )
34from lp.registry.interfaces.person import (33from lp.registry.interfaces.person import (
35 IPersonSet,
36 validate_person,34 validate_person,
37 validate_public_person,35 validate_public_person,
38 )36 )
39from lp.registry.interfaces.persontransferjob import (37from lp.registry.interfaces.persontransferjob import (
38 IExpiringMembershipNotificationJobSource,
40 IMembershipNotificationJobSource,39 IMembershipNotificationJobSource,
40 ISelfRenewalNotificationJobSource,
41 )41 )
42from lp.registry.interfaces.role import IPersonRoles42from lp.registry.interfaces.role import IPersonRoles
43from lp.registry.interfaces.sharingjob import (43from lp.registry.interfaces.sharingjob import (
@@ -52,7 +52,6 @@
52 ITeamParticipation,52 ITeamParticipation,
53 TeamMembershipStatus,53 TeamMembershipStatus,
54 )54 )
55from lp.services.config import config
56from lp.services.database.constants import UTC_NOW55from lp.services.database.constants import UTC_NOW
57from lp.services.database.datetimecol import UtcDateTimeCol56from lp.services.database.datetimecol import UtcDateTimeCol
58from lp.services.database.enumcol import EnumCol57from lp.services.database.enumcol import EnumCol
@@ -63,16 +62,6 @@
63 SQLBase,62 SQLBase,
64 sqlvalues,63 sqlvalues,
65 )64 )
66from lp.services.mail.helpers import (
67 get_contact_email_addresses,
68 get_email_template,
69 )
70from lp.services.mail.mailwrapper import MailWrapper
71from lp.services.mail.sendmail import (
72 format_address,
73 simple_sendmail,
74 )
75from lp.services.webapp import canonical_url
7665
7766
78@implementer(ITeamMembership)67@implementer(ITeamMembership)
@@ -132,26 +121,8 @@
132121
133 def sendSelfRenewalNotification(self):122 def sendSelfRenewalNotification(self):
134 """See `ITeamMembership`."""123 """See `ITeamMembership`."""
135 team = self.team124 getUtility(ISelfRenewalNotificationJobSource).create(
136 member = self.person125 self.person, self.team, self.dateexpires)
137 assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND
138
139 from_addr = format_address(
140 team.displayname, config.canonical.noreply_from_address)
141 replacements = {'member_name': member.unique_displayname,
142 'team_name': team.unique_displayname,
143 'team_url': canonical_url(team),
144 'dateexpires': self.dateexpires.strftime('%Y-%m-%d')}
145 subject = '%s extended their membership' % member.name
146 template = get_email_template(
147 'membership-member-renewed.txt', app='registry')
148 admins_addrs = self.team.getTeamAdminsEmailAddresses()
149 for address in admins_addrs:
150 recipient = getUtility(IPersonSet).getByEmail(address)
151 replacements['recipient_name'] = recipient.displayname
152 msg = MailWrapper().format(
153 template % replacements, force_wrap=True)
154 simple_sendmail(from_addr, address, subject, msg)
155126
156 def canChangeStatusSilently(self, user):127 def canChangeStatusSilently(self, user):
157 """Ensure that the user is in the Launchpad Administrators group.128 """Ensure that the user is in the Launchpad Administrators group.
@@ -194,68 +165,8 @@
194 # there is nothing to do. The member will have received emails165 # there is nothing to do. The member will have received emails
195 # from previous calls by flag-expired-memberships.py166 # from previous calls by flag-expired-memberships.py
196 return167 return
197 member = self.person168 getUtility(IExpiringMembershipNotificationJobSource).create(
198 team = self.team169 self.person, self.team, self.dateexpires)
199 if member.is_team:
200 recipient = member.teamowner
201 templatename = 'membership-expiration-warning-bulk.txt'
202 subject = '%s will expire soon from %s' % (member.name, team.name)
203 else:
204 recipient = member
205 templatename = 'membership-expiration-warning-personal.txt'
206 subject = 'Your membership in %s is about to expire' % team.name
207
208 if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND:
209 how_to_renew = (
210 "If you want, you can renew this membership at\n"
211 "<%s/+expiringmembership/%s>"
212 % (canonical_url(member), team.name))
213 elif not self.canChangeExpirationDate(recipient):
214 admins_names = []
215 admins = team.getDirectAdministrators()
216 assert admins.count() >= 1
217 if admins.count() == 1:
218 admin = admins[0]
219 how_to_renew = (
220 "To prevent this membership from expiring, you should "
221 "contact the\nteam's administrator, %s.\n<%s>"
222 % (admin.unique_displayname, canonical_url(admin)))
223 else:
224 for admin in admins:
225 admins_names.append(
226 "%s <%s>" % (admin.unique_displayname,
227 canonical_url(admin)))
228
229 how_to_renew = (
230 "To prevent this membership from expiring, you should "
231 "get in touch\nwith one of the team's administrators:\n")
232 how_to_renew += "\n".join(admins_names)
233 else:
234 how_to_renew = (
235 "To stay a member of this team you should extend your "
236 "membership at\n<%s/+member/%s>"
237 % (canonical_url(team), member.name))
238
239 to_addrs = get_contact_email_addresses(recipient)
240 if len(to_addrs) == 0:
241 # The user does not have a preferred email address, he was
242 # probably suspended.
243 return
244 formatter = DurationFormatterAPI(
245 self.dateexpires - datetime.now(pytz.timezone('UTC')))
246 replacements = {
247 'recipient_name': recipient.displayname,
248 'member_name': member.unique_displayname,
249 'team_url': canonical_url(team),
250 'how_to_renew': how_to_renew,
251 'team_name': team.unique_displayname,
252 'expiration_date': self.dateexpires.strftime('%Y-%m-%d'),
253 'approximate_duration': formatter.approximateduration()}
254
255 msg = get_email_template(templatename, app='registry') % replacements
256 from_addr = format_address(
257 team.displayname, config.canonical.noreply_from_address)
258 simple_sendmail(from_addr, to_addrs, subject, msg)
259170
260 def setStatus(self, status, user, comment=None, silent=False):171 def setStatus(self, status, user, comment=None, silent=False):
261 """See `ITeamMembership`."""172 """See `ITeamMembership`."""
262173
=== modified file 'lib/lp/registry/stories/person/xx-approve-members.txt'
--- lib/lp/registry/stories/person/xx-approve-members.txt 2010-10-06 21:28:02 +0000
+++ lib/lp/registry/stories/person/xx-approve-members.txt 2015-09-08 11:57:29 +0000
@@ -47,6 +47,7 @@
47 ...47 ...
48 Mark Shuttleworth said:48 Mark Shuttleworth said:
49 Thanks for your interest49 Thanks for your interest
50 ...
5051
51As we can see, Andrew is now listed among the active members and Sample Person52As we can see, Andrew is now listed among the active members and Sample Person
52as an inactive one.53as an inactive one.
@@ -74,4 +75,3 @@
74 Traceback (most recent call last):75 Traceback (most recent call last):
75 ...76 ...
76 LinkNotFoundError77 LinkNotFoundError
77
7878
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2013-06-20 05:50:00 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -33,6 +33,10 @@
33 IAccessArtifactSource,33 IAccessArtifactSource,
34 )34 )
35from lp.registry.interfaces.person import IPersonSet35from lp.registry.interfaces.person import IPersonSet
36from lp.registry.interfaces.persontransferjob import (
37 IExpiringMembershipNotificationJobSource,
38 ITeamJoinNotificationJobSource,
39 )
36from lp.registry.interfaces.teammembership import (40from lp.registry.interfaces.teammembership import (
37 CyclicalTeamMembershipError,41 CyclicalTeamMembershipError,
38 ITeamMembershipSet,42 ITeamMembershipSet,
@@ -59,8 +63,10 @@
59 sqlvalues,63 sqlvalues,
60 )64 )
61from lp.services.features.testing import FeatureFixture65from lp.services.features.testing import FeatureFixture
66from lp.services.job.runner import JobRunner
62from lp.services.job.tests import block_on_job67from lp.services.job.tests import block_on_job
63from lp.services.log.logger import BufferLogger68from lp.services.log.logger import BufferLogger
69from lp.services.mail.sendmail import format_address_for_person
64from lp.testing import (70from lp.testing import (
65 login,71 login,
66 login_celebrity,72 login_celebrity,
@@ -281,11 +287,17 @@
281class TestTeamParticipationQuery(TeamParticipationTestCase):287class TestTeamParticipationQuery(TeamParticipationTestCase):
282 """A test case for teammembership.test_find_team_participations."""288 """A test case for teammembership.test_find_team_participations."""
283289
290 def runMailJobs(self):
291 with dbuser("person-transfer-job"):
292 JobRunner.fromReady(
293 getUtility(ITeamJoinNotificationJobSource)).runAll()
294
284 def test_find_team_participations(self):295 def test_find_team_participations(self):
285 # The correct team participations are found and the query count is 1.296 # The correct team participations are found and the query count is 1.
286 self.team1.addMember(self.no_priv, self.foo_bar)297 self.team1.addMember(self.no_priv, self.foo_bar)
287 self.team2.addMember(self.no_priv, self.foo_bar)298 self.team2.addMember(self.no_priv, self.foo_bar)
288 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)299 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
300 self.runMailJobs()
289301
290 people = [self.team1, self.team2]302 people = [self.team1, self.team2]
291 with StormStatementRecorder() as recorder:303 with StormStatementRecorder() as recorder:
@@ -301,9 +313,12 @@
301 self.team1.addMember(self.no_priv, self.foo_bar)313 self.team1.addMember(self.no_priv, self.foo_bar)
302 self.team2.addMember(self.no_priv, self.foo_bar)314 self.team2.addMember(self.no_priv, self.foo_bar)
303 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)315 self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
316 self.runMailJobs()
304317
305 people = [self.foo_bar, self.team2]318 people = [self.foo_bar, self.team2]
306 teams = [self.team1, self.team2]319 teams = [self.team1, self.team2]
320 # Repopulate Storm cache after running mail jobs.
321 [team.is_team for team in teams]
307 with StormStatementRecorder() as recorder:322 with StormStatementRecorder() as recorder:
308 people_teams = find_team_participations(people, teams)323 people_teams = find_team_participations(people, teams)
309 self.assertThat(recorder, HasQueryCount(Equals(1)))324 self.assertThat(recorder, HasQueryCount(Equals(1)))
@@ -1067,6 +1082,12 @@
1067 self.member, self.team)1082 self.member, self.team)
1068 pop_notifications()1083 pop_notifications()
10691084
1085 def runMailJobs(self):
1086 with dbuser("person-transfer-job"):
1087 JobRunner.fromReady(
1088 getUtility(IExpiringMembershipNotificationJobSource)).runAll()
1089 return pop_notifications()
1090
1070 def test_error_raised_when_no_expiration(self):1091 def test_error_raised_when_no_expiration(self):
1071 # An exception is raised if the membership does not have an1092 # An exception is raised if the membership does not have an
1072 # expiration date.1093 # expiration date.
@@ -1080,20 +1101,19 @@
1080 tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)1101 tomorrow = datetime.now(pytz.UTC) + timedelta(days=1)
1081 removeSecurityProxy(self.tm).dateexpires = tomorrow1102 removeSecurityProxy(self.tm).dateexpires = tomorrow
1082 self.tm.sendExpirationWarningEmail()1103 self.tm.sendExpirationWarningEmail()
1083 notifications = pop_notifications()1104 notifications = self.runMailJobs()
1084 self.assertEqual(1, len(notifications))1105 self.assertEqual(1, len(notifications))
1085 message = notifications[0]1106 message = notifications[0]
1086 self.assertEqual(1107 self.assertEqual(
1087 'Your membership in red is about to expire', message['subject'])1108 'Your membership in red is about to expire', message['subject'])
1088 self.assertEqual(1109 self.assertEqual(format_address_for_person(self.member), message['to'])
1089 self.member.preferredemail.email, message['to'])
10901110
1091 def test_no_message_sent_for_expired_memberships(self):1111 def test_no_message_sent_for_expired_memberships(self):
1092 # Members whose membership has expired do not get a message.1112 # Members whose membership has expired do not get a message.
1093 yesterday = datetime.now(pytz.UTC) - timedelta(days=1)1113 yesterday = datetime.now(pytz.UTC) - timedelta(days=1)
1094 removeSecurityProxy(self.tm).dateexpires = yesterday1114 removeSecurityProxy(self.tm).dateexpires = yesterday
1095 self.tm.sendExpirationWarningEmail()1115 self.tm.sendExpirationWarningEmail()
1096 notifications = pop_notifications()1116 notifications = self.runMailJobs()
1097 self.assertEqual(0, len(notifications))1117 self.assertEqual(0, len(notifications))
10981118
1099 def test_no_message_sent_for_non_active_users(self):1119 def test_no_message_sent_for_non_active_users(self):
@@ -1104,7 +1124,7 @@
1104 now = datetime.now(pytz.UTC)1124 now = datetime.now(pytz.UTC)
1105 removeSecurityProxy(self.tm).dateexpires = now + timedelta(days=1)1125 removeSecurityProxy(self.tm).dateexpires = now + timedelta(days=1)
1106 self.tm.sendExpirationWarningEmail()1126 self.tm.sendExpirationWarningEmail()
1107 notifications = pop_notifications()1127 notifications = self.runMailJobs()
1108 self.assertEqual(0, len(notifications))1128 self.assertEqual(0, len(notifications))
11091129
11101130
11111131
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2015-09-08 10:05:33 +0000
+++ lib/lp/services/config/schema-lazr.conf 2015-09-08 11:57:29 +0000
@@ -1743,6 +1743,7 @@
1743job_sources:1743job_sources:
1744 IBranchModifiedMailJobSource,1744 IBranchModifiedMailJobSource,
1745 ICommercialExpiredJobSource,1745 ICommercialExpiredJobSource,
1746 IExpiringMembershipNotificationJobSource,
1746 IGitRepositoryModifiedMailJobSource,1747 IGitRepositoryModifiedMailJobSource,
1747 IMembershipNotificationJobSource,1748 IMembershipNotificationJobSource,
1748 IPackageUploadNotificationJobSource,1749 IPackageUploadNotificationJobSource,
@@ -1753,7 +1754,10 @@
1753 IQuestionEmailJobSource,1754 IQuestionEmailJobSource,
1754 IReclaimGitRepositorySpaceJobSource,1755 IReclaimGitRepositorySpaceJobSource,
1755 IRemoveArtifactSubscriptionsJobSource,1756 IRemoveArtifactSubscriptionsJobSource,
1757 ISelfRenewalNotificationJobSource,
1756 ISevenDayCommercialExpirationJobSource,1758 ISevenDayCommercialExpirationJobSource,
1759 ITeamInvitationNotificationJobSource,
1760 ITeamJoinNotificationJobSource,
1757 IThirtyDayCommercialExpirationJobSource1761 IThirtyDayCommercialExpirationJobSource
17581762
1759[IBranchMergeProposalJobSource]1763[IBranchMergeProposalJobSource]
@@ -1784,6 +1788,11 @@
1784module: lp.soyuz.interfaces.distributionjob1788module: lp.soyuz.interfaces.distributionjob
1785dbuser: distroseriesdifferencejob1789dbuser: distroseriesdifferencejob
17861790
1791[IExpiringMembershipNotificationJobSource]
1792module: lp.registry.interfaces.persontransferjob
1793dbuser: person-transfer-job
1794crontab_group: MAIN
1795
1787[IGitRefScanJobSource]1796[IGitRefScanJobSource]
1788module: lp.code.interfaces.gitjob1797module: lp.code.interfaces.gitjob
1789dbuser: branchscanner1798dbuser: branchscanner
@@ -1878,11 +1887,26 @@
1878module: lp.code.interfaces.branchjob1887module: lp.code.interfaces.branchjob
1879dbuser: translationsbranchscanner1888dbuser: translationsbranchscanner
18801889
1890[ISelfRenewalNotificationJobSource]
1891module: lp.registry.interfaces.persontransferjob
1892dbuser: person-transfer-job
1893crontab_group: MAIN
1894
1881[ISevenDayCommercialExpirationJobSource]1895[ISevenDayCommercialExpirationJobSource]
1882module: lp.registry.interfaces.productjob1896module: lp.registry.interfaces.productjob
1883dbuser: product-job1897dbuser: product-job
1884crontab_group: MAIN1898crontab_group: MAIN
18851899
1900[ITeamInvitationNotificationJobSource]
1901module: lp.registry.interfaces.persontransferjob
1902dbuser: person-transfer-job
1903crontab_group: MAIN
1904
1905[ITeamJoinNotificationJobSource]
1906module: lp.registry.interfaces.persontransferjob
1907dbuser: person-transfer-job
1908crontab_group: MAIN
1909
1886[IThirtyDayCommercialExpirationJobSource]1910[IThirtyDayCommercialExpirationJobSource]
1887module: lp.registry.interfaces.productjob1911module: lp.registry.interfaces.productjob
1888dbuser: product-job1912dbuser: product-job
18891913
=== modified file 'lib/lp/services/mail/basemailer.py'
--- lib/lp/services/mail/basemailer.py 2015-08-25 14:05:24 +0000
+++ lib/lp/services/mail/basemailer.py 2015-09-08 11:57:29 +0000
@@ -16,6 +16,7 @@
16from zope.error.interfaces import IErrorReportingUtility16from zope.error.interfaces import IErrorReportingUtility
1717
18from lp.services.mail.helpers import get_email_template18from lp.services.mail.helpers import get_email_template
19from lp.services.mail.mailwrapper import MailWrapper
19from lp.services.mail.notificationrecipientset import NotificationRecipientSet20from lp.services.mail.notificationrecipientset import NotificationRecipientSet
20from lp.services.mail.sendmail import (21from lp.services.mail.sendmail import (
21 append_footer,22 append_footer,
@@ -39,7 +40,8 @@
3940
40 def __init__(self, subject, template_name, recipients, from_address,41 def __init__(self, subject, template_name, recipients, from_address,
41 delta=None, message_id=None, notification_type=None,42 delta=None, message_id=None, notification_type=None,
42 mail_controller_class=None, request=None):43 mail_controller_class=None, request=None, wrap=False,
44 force_wrap=False):
43 """Constructor.45 """Constructor.
4446
45 :param subject: A Python dict-replacement template for the subject47 :param subject: A Python dict-replacement template for the subject
@@ -55,6 +57,8 @@
55 use to send the mails. Defaults to `MailController`.57 use to send the mails. Defaults to `MailController`.
56 :param request: An optional `IErrorReportRequest` to use when58 :param request: An optional `IErrorReportRequest` to use when
57 logging OOPSes.59 logging OOPSes.
60 :param wrap: Wrap body text using `MailWrapper`.
61 :param force_wrap: See `MailWrapper.format`.
58 """62 """
59 self._subject_template = subject63 self._subject_template = subject
60 self._template_name = template_name64 self._template_name = template_name
@@ -70,6 +74,8 @@
70 mail_controller_class = MailController74 mail_controller_class = MailController
71 self._mail_controller_class = mail_controller_class75 self._mail_controller_class = mail_controller_class
72 self.request = request76 self.request = request
77 self._wrap = wrap
78 self._force_wrap = force_wrap
7379
74 def _getFromAddress(self, email, recipient):80 def _getFromAddress(self, email, recipient):
75 return self.from_address81 return self.from_address
@@ -108,7 +114,7 @@
108 return (self._subject_template %114 return (self._subject_template %
109 self._getTemplateParams(email, recipient))115 self._getTemplateParams(email, recipient))
110116
111 def _getReplyToAddress(self):117 def _getReplyToAddress(self, email, recipient):
112 """Return the address to use for the reply-to header."""118 """Return the address to use for the reply-to header."""
113 return None119 return None
114120
@@ -119,7 +125,7 @@
119 headers['X-Launchpad-Message-Rationale'] = reason.mail_header125 headers['X-Launchpad-Message-Rationale'] = reason.mail_header
120 if self.notification_type is not None:126 if self.notification_type is not None:
121 headers['X-Launchpad-Notification-Type'] = self.notification_type127 headers['X-Launchpad-Notification-Type'] = self.notification_type
122 reply_to = self._getReplyToAddress()128 reply_to = self._getReplyToAddress(email, recipient)
123 if reply_to is not None:129 if reply_to is not None:
124 headers['Reply-To'] = reply_to130 headers['Reply-To'] = reply_to
125 if self.message_id is not None:131 if self.message_id is not None:
@@ -158,6 +164,9 @@
158 self._getTemplateName(email, recipient), app=self.app)164 self._getTemplateName(email, recipient), app=self.app)
159 params = self._getTemplateParams(email, recipient)165 params = self._getTemplateParams(email, recipient)
160 body = template % params166 body = template % params
167 if self._wrap:
168 body = MailWrapper().format(
169 body, force_wrap=self._force_wrap) + "\n"
161 footer = self._getFooter(email, recipient, params)170 footer = self._getFooter(email, recipient, params)
162 if footer is not None:171 if footer is not None:
163 body = append_footer(body, footer)172 body = append_footer(body, footer)
164173
=== modified file 'lib/lp/testing/mail_helpers.py'
--- lib/lp/testing/mail_helpers.py 2015-07-21 09:04:01 +0000
+++ lib/lp/testing/mail_helpers.py 2015-09-08 11:57:29 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Helper functions dealing with emails in tests.4"""Helper functions dealing with emails in tests.
@@ -12,11 +12,17 @@
12from zope.component import getUtility12from zope.component import getUtility
1313
14from lp.registry.interfaces.persontransferjob import (14from lp.registry.interfaces.persontransferjob import (
15 IExpiringMembershipNotificationJobSource,
15 IMembershipNotificationJobSource,16 IMembershipNotificationJobSource,
17 ISelfRenewalNotificationJobSource,
18 ITeamInvitationNotificationJobSource,
19 ITeamJoinNotificationJobSource,
16 )20 )
21from lp.services.config import config
17from lp.services.job.runner import JobRunner22from lp.services.job.runner import JobRunner
18from lp.services.log.logger import DevNullLogger23from lp.services.log.logger import DevNullLogger
19from lp.services.mail import stub24from lp.services.mail import stub
25from lp.testing.dbuser import dbuser
2026
2127
22def pop_notifications(sort_key=None, commit=True):28def pop_notifications(sort_key=None, commit=True):
@@ -54,7 +60,8 @@
5460
5561
56def print_emails(include_reply_to=False, group_similar=False,62def print_emails(include_reply_to=False, group_similar=False,
57 include_rationale=False, notifications=None):63 include_rationale=False, notifications=None,
64 include_notification_type=False):
58 """Pop all messages from stub.test_emails and print them with65 """Pop all messages from stub.test_emails and print them with
59 their recipients.66 their recipients.
6067
@@ -71,6 +78,8 @@
71 header.78 header.
72 :param notifications: Use the provided list of notifications instead of79 :param notifications: Use the provided list of notifications instead of
73 the stack.80 the stack.
81 :param include_notification_type: Include the
82 X-Launchpad-Notification-Type header.
74 """83 """
75 distinct_bodies = {}84 distinct_bodies = {}
76 if notifications is None:85 if notifications is None:
@@ -99,16 +108,22 @@
99 if include_rationale and rationale_header in message:108 if include_rationale and rationale_header in message:
100 print (109 print (
101 '%s: %s' % (rationale_header, message[rationale_header]))110 '%s: %s' % (rationale_header, message[rationale_header]))
111 notification_type_header = 'X-Launchpad-Notification-Type'
112 if include_notification_type and notification_type_header in message:
113 print '%s: %s' % (
114 notification_type_header, message[notification_type_header])
102 print 'Subject:', message['Subject']115 print 'Subject:', message['Subject']
103 print body116 print body
104 print "-" * 40117 print "-" * 40
105118
106119
107def print_distinct_emails(include_reply_to=False, include_rationale=True):120def print_distinct_emails(include_reply_to=False, include_rationale=True,
121 include_notification_type=True):
108 """A convenient shortcut for `print_emails`(group_similar=True)."""122 """A convenient shortcut for `print_emails`(group_similar=True)."""
109 return print_emails(group_similar=True,123 return print_emails(group_similar=True,
110 include_reply_to=include_reply_to,124 include_reply_to=include_reply_to,
111 include_rationale=include_rationale)125 include_rationale=include_rationale,
126 include_notification_type=include_notification_type)
112127
113128
114def run_mail_jobs():129def run_mail_jobs():
@@ -121,7 +136,15 @@
121 # Commit the transaction to make sure that the JobRunner can find136 # Commit the transaction to make sure that the JobRunner can find
122 # the queued jobs.137 # the queued jobs.
123 transaction.commit()138 transaction.commit()
124 job_source = getUtility(IMembershipNotificationJobSource)139 for interface in (
125 logger = DevNullLogger()140 IExpiringMembershipNotificationJobSource,
126 runner = JobRunner.fromReady(job_source, logger)141 IMembershipNotificationJobSource,
127 runner.runAll()142 ISelfRenewalNotificationJobSource,
143 ITeamInvitationNotificationJobSource,
144 ITeamJoinNotificationJobSource,
145 ):
146 job_source = getUtility(interface)
147 logger = DevNullLogger()
148 with dbuser(getattr(config, interface.__name__).dbuser):
149 runner = JobRunner.fromReady(job_source, logger)
150 runner.runAll()