Merge lp:~edwin-grubbs/launchpad/bug-615654-queue-addmember-emails into lp:launchpad/db-devel
- bug-615654-queue-addmember-emails
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Edwin Grubbs |
Approved revision: | no longer in the source branch. |
Merged at revision: | 9873 |
Proposed branch: | lp:~edwin-grubbs/launchpad/bug-615654-queue-addmember-emails |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~edwin-grubbs/launchpad/bug-615654-registry-jobqueue-schema |
Diff against target: |
875 lines (+551/-99) 8 files modified
lib/lp/registry/configure.zcml (+17/-0) lib/lp/registry/doc/teammembership-email-notification.txt (+25/-11) lib/lp/registry/enum.py (+12/-0) lib/lp/registry/interfaces/persontransferjob.py (+70/-0) lib/lp/registry/model/persontransferjob.py (+334/-0) lib/lp/registry/model/teammembership.py (+7/-88) lib/lp/registry/tests/test_persontransferjob.py (+62/-0) lib/lp/testing/mail_helpers.py (+24/-0) |
To merge this branch: | bzr merge lp:~edwin-grubbs/launchpad/bug-615654-queue-addmember-emails |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email: mp+35862@code.launchpad.net |
Commit message
Description of the change
Summary
-------
This branch adds the MembershipNotif
after the team membership status has changed, e.g. proposed, approved,
admin, expired.
A follow-up branch will be needed in order to add a cronjob to actually
process the job queue. This branch can't land until then.
Implementation details
-------
lib/lp/
lib/
lib/
lib/
lib/
Tests:
lib/
lib/
lib/
Tests
-----
./bin/test -vv -t 'teammembership
Edwin Grubbs (edwin-grubbs) wrote : | # |
On Fri, Sep 17, 2010 at 3:12 PM, Brad Crittenden <email address hidden> wrote:
> Review: Approve code
> Hi Edwin,
>
> Very interesting branch. I've got some suggestions but overall it is great.
>
> --Brad
Thanks for the review.
>> === added file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -0,0 +1,322 @@
>> +# Copyright 2010 Canonical Ltd. This software is licensed under the
>> +# GNU Affero General Public License version 3 (see the file LICENSE).
>> +
>> +"""Job classes related to PersonTransferJob are in here."""
>
> s/ are in here//
Fixed. That's just odd. I'm blaming it on my rampant plagiarism in this branch.
>> +__metaclass__ = type
>> +__all__ = [
>> + 'MembershipNoti
>> + 'PersonTransfer
>> + ]
>> +
>> +from lazr.delegates import delegates
>> +import simplejson
>> +from sqlobject import SQLObjectNotFound
>> +from storm.base import Storm
>> +from storm.expr import And
>> +from storm.locals import (
>> + Int,
>> + Reference,
>> + Unicode,
>> + )
>> +from zope.component import getUtility
>> +from zope.interface import (
>> + classProvides,
>> + implements,
>> + )
>> +
>> +
>
> extra blank line
Fixed with henning's tool.
>> +from canonical.config import config
>> +from canonical.
>> +from canonical.
>> + get_contact_
>> + get_email_template,
>> + )
>> +from canonical.
>> + IMasterStore,
>> + IStore,
>> + )
>> +from canonical.
>> + format_address,
>> + simple_sendmail,
>> + )
>> +from canonical.
>> +from canonical.
>> +
>> +
>
> Hmmm, two of them. is there supposed to be two? did you do that
> manually or did henning's tool do it? if the latter, then keep it.
Fixed. I really need to update my vim plugin.
>> +from lp.registry.enum import PersonTransferJ
>> +from lp.registry.
>> + IPerson,
>> + IPersonSet,
>> + ITeam,
>> + )
>> +from lp.registry.
>> + IPersonTransferJob,
>> + IPersonTransfer
>> + IMembershipNoti
>> + IMembershipNoti
>> + )
>> +from lp.registry.
>> +from lp.registry.
>> +from lp.services.
>> +from lp.services.
>> +
>> +
>> +class PersonTransferJ
> [...]
>> + def __init__(self, minor_person, major_person, job_type, metadata):
>> + """Constructor.
>> +
>> + :param minor_person: The person or team being added to or removed
>> + from the major_person.
>> + :param major_person: The person or team that is receiving or losing
>> + the minor person.
>> + :param job_type: The specific membership action b...
Preview Diff
1 | === modified file 'lib/lp/registry/configure.zcml' | |||
2 | --- lib/lp/registry/configure.zcml 2010-09-14 10:51:25 +0000 | |||
3 | +++ lib/lp/registry/configure.zcml 2010-09-17 22:26:48 +0000 | |||
4 | @@ -84,6 +84,23 @@ | |||
5 | 84 | __call__"/> | 84 | __call__"/> |
6 | 85 | </class> | 85 | </class> |
7 | 86 | 86 | ||
8 | 87 | <!-- IPersonTransferJob --> | ||
9 | 88 | <securedutility | ||
10 | 89 | component="lp.registry.model.persontransferjob.MembershipNotificationJob" | ||
11 | 90 | provides="lp.registry.interfaces.persontransferjob.IMembershipNotificationJobSource"> | ||
12 | 91 | <allow | ||
13 | 92 | interface="lp.registry.interfaces.persontransferjob.IMembershipNotificationJobSource"/> | ||
14 | 93 | </securedutility> | ||
15 | 94 | |||
16 | 95 | <class class="lp.registry.model.persontransferjob.PersonTransferJob"> | ||
17 | 96 | <allow interface="lp.registry.interfaces.persontransferjob.IPersonTransferJob"/> | ||
18 | 97 | </class> | ||
19 | 98 | |||
20 | 99 | <class class="lp.registry.model.persontransferjob.MembershipNotificationJob"> | ||
21 | 100 | <allow | ||
22 | 101 | interface="lp.registry.interfaces.persontransferjob.IMembershipNotificationJob"/> | ||
23 | 102 | </class> | ||
24 | 103 | |||
25 | 87 | <!-- Location --> | 104 | <!-- Location --> |
26 | 88 | 105 | ||
27 | 89 | <class | 106 | <class |
28 | 90 | 107 | ||
29 | === modified file 'lib/lp/registry/doc/teammembership-email-notification.txt' | |||
30 | --- lib/lp/registry/doc/teammembership-email-notification.txt 2010-03-26 00:45:36 +0000 | |||
31 | +++ lib/lp/registry/doc/teammembership-email-notification.txt 2010-09-17 22:26:48 +0000 | |||
32 | @@ -24,7 +24,7 @@ | |||
33 | 24 | 24 | ||
34 | 25 | >>> from lp.services.mail import stub | 25 | >>> from lp.services.mail import stub |
35 | 26 | >>> from lp.testing.mail_helpers import ( | 26 | >>> from lp.testing.mail_helpers import ( |
37 | 27 | ... pop_notifications, print_distinct_emails) | 27 | ... pop_notifications, print_distinct_emails, run_mail_jobs) |
38 | 28 | 28 | ||
39 | 29 | >>> from zope.component import getUtility | 29 | >>> from zope.component import getUtility |
40 | 30 | >>> from canonical.launchpad.interfaces import ( | 30 | >>> from canonical.launchpad.interfaces import ( |
41 | @@ -49,7 +49,7 @@ | |||
42 | 49 | >>> membership.status.title | 49 | >>> membership.status.title |
43 | 50 | 'Proposed' | 50 | 'Proposed' |
44 | 51 | 51 | ||
46 | 52 | >>> transaction.commit() | 52 | >>> run_mail_jobs() |
47 | 53 | >>> len(stub.test_emails) | 53 | >>> len(stub.test_emails) |
48 | 54 | 5 | 54 | 5 |
49 | 55 | >>> print_distinct_emails(include_reply_to=True) | 55 | >>> print_distinct_emails(include_reply_to=True) |
50 | @@ -87,6 +87,11 @@ | |||
51 | 87 | # that team. | 87 | # that team. |
52 | 88 | >>> login('mark@example.com') | 88 | >>> login('mark@example.com') |
53 | 89 | >>> setStatus(membership, TeamMembershipStatus.DECLINED, reviewer=mark) | 89 | >>> setStatus(membership, TeamMembershipStatus.DECLINED, reviewer=mark) |
54 | 90 | |||
55 | 91 | addMember() has queued up a job to send out the emails. We'll run the | ||
56 | 92 | job now. | ||
57 | 93 | |||
58 | 94 | >>> run_mail_jobs() | ||
59 | 90 | >>> len(stub.test_emails) | 95 | >>> len(stub.test_emails) |
60 | 91 | 6 | 96 | 6 |
61 | 92 | 97 | ||
62 | @@ -125,6 +130,7 @@ | |||
63 | 125 | 130 | ||
64 | 126 | >>> setStatus(daf_membership, TeamMembershipStatus.APPROVED, | 131 | >>> setStatus(daf_membership, TeamMembershipStatus.APPROVED, |
65 | 127 | ... reviewer=mark, comment='This is a nice guy; I like him') | 132 | ... reviewer=mark, comment='This is a nice guy; I like him') |
66 | 133 | >>> run_mail_jobs() | ||
67 | 128 | >>> stub.test_emails.sort(by_to_addrs) | 134 | >>> stub.test_emails.sort(by_to_addrs) |
68 | 129 | >>> len(stub.test_emails) | 135 | >>> len(stub.test_emails) |
69 | 130 | 6 | 136 | 6 |
70 | @@ -158,6 +164,7 @@ | |||
71 | 158 | 164 | ||
72 | 159 | >>> setStatus(daf_membership, TeamMembershipStatus.DEACTIVATED, | 165 | >>> setStatus(daf_membership, TeamMembershipStatus.DEACTIVATED, |
73 | 160 | ... reviewer=mark) | 166 | ... reviewer=mark) |
74 | 167 | >>> run_mail_jobs() | ||
75 | 161 | >>> stub.test_emails.sort(by_to_addrs) | 168 | >>> stub.test_emails.sort(by_to_addrs) |
76 | 162 | >>> len(stub.test_emails) | 169 | >>> len(stub.test_emails) |
77 | 163 | 6 | 170 | 6 |
78 | @@ -187,7 +194,7 @@ | |||
79 | 187 | 194 | ||
80 | 188 | >>> admins = personset.getByName('admins') | 195 | >>> admins = personset.getByName('admins') |
81 | 189 | >>> admins.join(ubuntu_team, requester=mark) | 196 | >>> admins.join(ubuntu_team, requester=mark) |
83 | 190 | >>> transaction.commit() | 197 | >>> run_mail_jobs() |
84 | 191 | >>> len(stub.test_emails) | 198 | >>> len(stub.test_emails) |
85 | 192 | 5 | 199 | 5 |
86 | 193 | >>> print_distinct_emails(include_reply_to=True) | 200 | >>> print_distinct_emails(include_reply_to=True) |
87 | @@ -228,7 +235,10 @@ | |||
88 | 228 | >>> cprov = personset.getByName('cprov') | 235 | >>> cprov = personset.getByName('cprov') |
89 | 229 | >>> marilize = personset.getByName('marilize') | 236 | >>> marilize = personset.getByName('marilize') |
90 | 230 | >>> ignored = ubuntu_team.addMember(marilize, reviewer=cprov) | 237 | >>> ignored = ubuntu_team.addMember(marilize, reviewer=cprov) |
92 | 231 | >>> transaction.commit() | 238 | >>> run_mail_jobs() |
93 | 239 | |||
94 | 240 | Now, the emails have been sent. | ||
95 | 241 | |||
96 | 232 | >>> len(stub.test_emails) | 242 | >>> len(stub.test_emails) |
97 | 233 | 6 | 243 | 6 |
98 | 234 | >>> print_distinct_emails() | 244 | >>> print_distinct_emails() |
99 | @@ -277,7 +287,7 @@ | |||
100 | 277 | >>> mirror_admins.getTeamAdminsEmailAddresses() | 287 | >>> mirror_admins.getTeamAdminsEmailAddresses() |
101 | 278 | ['mark@example.com'] | 288 | ['mark@example.com'] |
102 | 279 | >>> ignored = ubuntu_team.addMember(mirror_admins, reviewer=cprov) | 289 | >>> ignored = ubuntu_team.addMember(mirror_admins, reviewer=cprov) |
104 | 280 | >>> transaction.commit() | 290 | >>> run_mail_jobs() |
105 | 281 | >>> len(stub.test_emails) | 291 | >>> len(stub.test_emails) |
106 | 282 | 1 | 292 | 1 |
107 | 283 | >>> print_distinct_emails() | 293 | >>> print_distinct_emails() |
108 | @@ -304,7 +314,7 @@ | |||
109 | 304 | >>> comment = "Of course I want to be part of ubuntu!" | 314 | >>> comment = "Of course I want to be part of ubuntu!" |
110 | 305 | >>> mirror_admins.acceptInvitationToBeMemberOf(ubuntu_team, comment) | 315 | >>> mirror_admins.acceptInvitationToBeMemberOf(ubuntu_team, comment) |
111 | 306 | >>> flush_database_updates() | 316 | >>> flush_database_updates() |
113 | 307 | >>> transaction.commit() | 317 | >>> run_mail_jobs() |
114 | 308 | 318 | ||
115 | 309 | >>> len(stub.test_emails) | 319 | >>> len(stub.test_emails) |
116 | 310 | 6 | 320 | 6 |
117 | @@ -337,7 +347,7 @@ | |||
118 | 337 | >>> comment = "Landscape has nothing to do with ubuntu, unfortunately." | 347 | >>> comment = "Landscape has nothing to do with ubuntu, unfortunately." |
119 | 338 | >>> landscape.declineInvitationToBeMemberOf(ubuntu_team, comment) | 348 | >>> landscape.declineInvitationToBeMemberOf(ubuntu_team, comment) |
120 | 339 | >>> flush_database_updates() | 349 | >>> flush_database_updates() |
122 | 340 | >>> transaction.commit() | 350 | >>> run_mail_jobs() |
123 | 341 | 351 | ||
124 | 342 | >>> len(stub.test_emails) | 352 | >>> len(stub.test_emails) |
125 | 343 | 7 | 353 | 7 |
126 | @@ -363,7 +373,7 @@ | |||
127 | 363 | >>> ignored = ubuntu_team.addMember( | 373 | >>> ignored = ubuntu_team.addMember( |
128 | 364 | ... launchpad, reviewer=cprov, force_team_add=True) | 374 | ... launchpad, reviewer=cprov, force_team_add=True) |
129 | 365 | >>> flush_database_updates() | 375 | >>> flush_database_updates() |
131 | 366 | >>> transaction.commit() | 376 | >>> run_mail_jobs() |
132 | 367 | >>> len(stub.test_emails) | 377 | >>> len(stub.test_emails) |
133 | 368 | 5 | 378 | 5 |
134 | 369 | >>> print_distinct_emails() | 379 | >>> print_distinct_emails() |
135 | @@ -610,7 +620,7 @@ | |||
136 | 610 | >>> membershipset.handleMembershipsExpiringToday( | 620 | >>> membershipset.handleMembershipsExpiringToday( |
137 | 611 | ... reviewer=getUtility(ILaunchpadCelebrities).janitor) | 621 | ... reviewer=getUtility(ILaunchpadCelebrities).janitor) |
138 | 612 | >>> flush_database_updates() | 622 | >>> flush_database_updates() |
140 | 613 | >>> transaction.commit() | 623 | >>> run_mail_jobs() |
141 | 614 | 624 | ||
142 | 615 | >>> len(stub.test_emails) | 625 | >>> len(stub.test_emails) |
143 | 616 | 8 | 626 | 8 |
144 | @@ -690,7 +700,7 @@ | |||
145 | 690 | 700 | ||
146 | 691 | >>> login_person(karl) | 701 | >>> login_person(karl) |
147 | 692 | >>> karl.renewTeamMembership(mirror_admins) | 702 | >>> karl.renewTeamMembership(mirror_admins) |
149 | 693 | >>> transaction.commit() | 703 | >>> run_mail_jobs() |
150 | 694 | >>> len(stub.test_emails) | 704 | >>> len(stub.test_emails) |
151 | 695 | 1 | 705 | 1 |
152 | 696 | 706 | ||
153 | @@ -713,7 +723,7 @@ | |||
154 | 713 | approved to admin, but he won't get a notification of that. | 723 | approved to admin, but he won't get a notification of that. |
155 | 714 | 724 | ||
156 | 715 | >>> team = personset.newTeam(mark, 'testteam', 'Test') | 725 | >>> team = personset.newTeam(mark, 'testteam', 'Test') |
158 | 716 | >>> transaction.commit() | 726 | >>> run_mail_jobs() |
159 | 717 | >>> len(stub.test_emails) | 727 | >>> len(stub.test_emails) |
160 | 718 | 0 | 728 | 0 |
161 | 719 | 729 | ||
162 | @@ -730,6 +740,7 @@ | |||
163 | 730 | >>> login('mark@example.com') | 740 | >>> login('mark@example.com') |
164 | 731 | >>> setStatus( | 741 | >>> setStatus( |
165 | 732 | ... cprov_membership, TeamMembershipStatus.ADMIN, reviewer=mark) | 742 | ... cprov_membership, TeamMembershipStatus.ADMIN, reviewer=mark) |
166 | 743 | >>> run_mail_jobs() | ||
167 | 733 | >>> len(stub.test_emails) | 744 | >>> len(stub.test_emails) |
168 | 734 | 6 | 745 | 6 |
169 | 735 | >>> print_distinct_emails() | 746 | >>> print_distinct_emails() |
170 | @@ -760,6 +771,7 @@ | |||
171 | 760 | >>> jdub_membership = membershipset.getByPersonAndTeam(jdub, ubuntu_team) | 771 | >>> jdub_membership = membershipset.getByPersonAndTeam(jdub, ubuntu_team) |
172 | 761 | >>> setStatus(jdub_membership, TeamMembershipStatus.APPROVED, | 772 | >>> setStatus(jdub_membership, TeamMembershipStatus.APPROVED, |
173 | 762 | ... reviewer=jdub) | 773 | ... reviewer=jdub) |
174 | 774 | >>> run_mail_jobs() | ||
175 | 763 | >>> len(stub.test_emails) | 775 | >>> len(stub.test_emails) |
176 | 764 | 5 | 776 | 5 |
177 | 765 | >>> print_distinct_emails() | 777 | >>> print_distinct_emails() |
178 | @@ -784,6 +796,7 @@ | |||
179 | 784 | ... mirror_admins, ubuntu_team) | 796 | ... mirror_admins, ubuntu_team) |
180 | 785 | >>> setStatus(mirror_admins_membership, TeamMembershipStatus.DEACTIVATED, | 797 | >>> setStatus(mirror_admins_membership, TeamMembershipStatus.DEACTIVATED, |
181 | 786 | ... reviewer=mark, silent=False) | 798 | ... reviewer=mark, silent=False) |
182 | 799 | >>> run_mail_jobs() | ||
183 | 787 | >>> len(stub.test_emails) | 800 | >>> len(stub.test_emails) |
184 | 788 | 6 | 801 | 6 |
185 | 789 | 802 | ||
186 | @@ -811,6 +824,7 @@ | |||
187 | 811 | Approved | 824 | Approved |
188 | 812 | >>> setStatus(dumper_hwdb_membership, | 825 | >>> setStatus(dumper_hwdb_membership, |
189 | 813 | ... TeamMembershipStatus.DEACTIVATED, reviewer=mark, silent=True) | 826 | ... TeamMembershipStatus.DEACTIVATED, reviewer=mark, silent=True) |
190 | 827 | >>> run_mail_jobs() | ||
191 | 814 | >>> len(stub.test_emails) | 828 | >>> len(stub.test_emails) |
192 | 815 | 0 | 829 | 0 |
193 | 816 | >>> print dumper_hwdb_membership.status.title | 830 | >>> print dumper_hwdb_membership.status.title |
194 | 817 | 831 | ||
195 | === modified file 'lib/lp/registry/enum.py' | |||
196 | --- lib/lp/registry/enum.py 2010-09-10 10:08:58 +0000 | |||
197 | +++ lib/lp/registry/enum.py 2010-09-17 22:26:48 +0000 | |||
198 | @@ -5,6 +5,7 @@ | |||
199 | 5 | 5 | ||
200 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
201 | 7 | __all__ = [ | 7 | __all__ = [ |
202 | 8 | 'PersonTransferJobType', | ||
203 | 8 | 'BugNotificationLevel', | 9 | 'BugNotificationLevel', |
204 | 9 | 'DistroSeriesDifferenceStatus', | 10 | 'DistroSeriesDifferenceStatus', |
205 | 10 | 'DistroSeriesDifferenceType', | 11 | 'DistroSeriesDifferenceType', |
206 | @@ -82,6 +83,7 @@ | |||
207 | 82 | This difference has been resolved and versions are now equal. | 83 | This difference has been resolved and versions are now equal. |
208 | 83 | """) | 84 | """) |
209 | 84 | 85 | ||
210 | 86 | |||
211 | 85 | class DistroSeriesDifferenceType(DBEnumeratedType): | 87 | class DistroSeriesDifferenceType(DBEnumeratedType): |
212 | 86 | """Distribution series difference type.""" | 88 | """Distribution series difference type.""" |
213 | 87 | 89 | ||
214 | @@ -104,3 +106,13 @@ | |||
215 | 104 | 106 | ||
216 | 105 | This package is present in both series with different versions. | 107 | This package is present in both series with different versions. |
217 | 106 | """) | 108 | """) |
218 | 109 | |||
219 | 110 | |||
220 | 111 | class PersonTransferJobType(DBEnumeratedType): | ||
221 | 112 | """Values that IPersonTransferJob.job_type can take.""" | ||
222 | 113 | |||
223 | 114 | MEMBERSHIP_NOTIFICATION = DBItem(0, """ | ||
224 | 115 | Add-member notification | ||
225 | 116 | |||
226 | 117 | Notify affected users of new team membership. | ||
227 | 118 | """) | ||
228 | 107 | 119 | ||
229 | === added file 'lib/lp/registry/interfaces/persontransferjob.py' | |||
230 | --- lib/lp/registry/interfaces/persontransferjob.py 1970-01-01 00:00:00 +0000 | |||
231 | +++ lib/lp/registry/interfaces/persontransferjob.py 2010-09-17 22:26:48 +0000 | |||
232 | @@ -0,0 +1,70 @@ | |||
233 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
234 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
235 | 3 | |||
236 | 4 | """Interface for the Jobs system to change memberships or merge persons.""" | ||
237 | 5 | |||
238 | 6 | __metaclass__ = type | ||
239 | 7 | __all__ = [ | ||
240 | 8 | 'IMembershipNotificationJob', | ||
241 | 9 | 'IMembershipNotificationJobSource', | ||
242 | 10 | 'IPersonTransferJob', | ||
243 | 11 | 'IPersonTransferJobSource', | ||
244 | 12 | ] | ||
245 | 13 | |||
246 | 14 | from zope.interface import Attribute | ||
247 | 15 | from zope.schema import ( | ||
248 | 16 | Int, | ||
249 | 17 | Object, | ||
250 | 18 | ) | ||
251 | 19 | |||
252 | 20 | from canonical.launchpad import _ | ||
253 | 21 | from lp.services.fields import PublicPersonChoice | ||
254 | 22 | from lp.services.job.interfaces.job import ( | ||
255 | 23 | IJob, | ||
256 | 24 | IJobSource, | ||
257 | 25 | IRunnableJob, | ||
258 | 26 | ) | ||
259 | 27 | |||
260 | 28 | |||
261 | 29 | class IPersonTransferJob(IRunnableJob): | ||
262 | 30 | """A Job related to team membership or a person merge.""" | ||
263 | 31 | |||
264 | 32 | id = Int( | ||
265 | 33 | title=_('DB ID'), required=True, readonly=True, | ||
266 | 34 | description=_("The tracking number for this job.")) | ||
267 | 35 | |||
268 | 36 | job = Object( | ||
269 | 37 | title=_('The common Job attributes'), | ||
270 | 38 | schema=IJob, | ||
271 | 39 | required=True) | ||
272 | 40 | |||
273 | 41 | minor_person = PublicPersonChoice( | ||
274 | 42 | title=_('The person being added to the major person/team'), | ||
275 | 43 | vocabulary='ValidPersonOrTeam', | ||
276 | 44 | required=True) | ||
277 | 45 | |||
278 | 46 | major_person = PublicPersonChoice( | ||
279 | 47 | title=_('The person or team receiving the minor person'), | ||
280 | 48 | vocabulary='ValidPersonOrTeam', | ||
281 | 49 | required=True) | ||
282 | 50 | |||
283 | 51 | metadata = Attribute('A dict of data about the job.') | ||
284 | 52 | |||
285 | 53 | |||
286 | 54 | class IPersonTransferJobSource(IJobSource): | ||
287 | 55 | """An interface for acquiring IPersonTransferJobs.""" | ||
288 | 56 | |||
289 | 57 | def create(minor_person, major_person, metadata): | ||
290 | 58 | """Create a new IPersonTransferJob.""" | ||
291 | 59 | |||
292 | 60 | |||
293 | 61 | class IMembershipNotificationJob(IPersonTransferJob): | ||
294 | 62 | """A Job to notify new members of a team of that change.""" | ||
295 | 63 | |||
296 | 64 | |||
297 | 65 | class IMembershipNotificationJobSource(IJobSource): | ||
298 | 66 | """An interface for acquiring IMembershipNotificationJobs.""" | ||
299 | 67 | |||
300 | 68 | def create(member, team, reviewer, old_status, new_status, | ||
301 | 69 | last_change_comment=None): | ||
302 | 70 | """Create a new IMembershipNotificationJob.""" | ||
303 | 0 | 71 | ||
304 | === added file 'lib/lp/registry/model/persontransferjob.py' | |||
305 | --- lib/lp/registry/model/persontransferjob.py 1970-01-01 00:00:00 +0000 | |||
306 | +++ lib/lp/registry/model/persontransferjob.py 2010-09-17 22:26:48 +0000 | |||
307 | @@ -0,0 +1,334 @@ | |||
308 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
309 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
310 | 3 | |||
311 | 4 | """Job classes related to PersonTransferJob.""" | ||
312 | 5 | |||
313 | 6 | __metaclass__ = type | ||
314 | 7 | __all__ = [ | ||
315 | 8 | 'MembershipNotificationJob', | ||
316 | 9 | 'PersonTransferJob', | ||
317 | 10 | ] | ||
318 | 11 | |||
319 | 12 | from lazr.delegates import delegates | ||
320 | 13 | import simplejson | ||
321 | 14 | from sqlobject import SQLObjectNotFound | ||
322 | 15 | from storm.base import Storm | ||
323 | 16 | from storm.expr import And | ||
324 | 17 | from storm.locals import ( | ||
325 | 18 | Int, | ||
326 | 19 | Reference, | ||
327 | 20 | Unicode, | ||
328 | 21 | ) | ||
329 | 22 | from zope.component import getUtility | ||
330 | 23 | from zope.interface import ( | ||
331 | 24 | classProvides, | ||
332 | 25 | implements, | ||
333 | 26 | ) | ||
334 | 27 | |||
335 | 28 | from canonical.config import config | ||
336 | 29 | from canonical.database.enumcol import EnumCol | ||
337 | 30 | from canonical.launchpad.helpers import ( | ||
338 | 31 | get_contact_email_addresses, | ||
339 | 32 | get_email_template, | ||
340 | 33 | ) | ||
341 | 34 | from canonical.launchpad.interfaces.lpstorm import IMasterStore | ||
342 | 35 | from canonical.launchpad.mail import ( | ||
343 | 36 | format_address, | ||
344 | 37 | simple_sendmail, | ||
345 | 38 | ) | ||
346 | 39 | from canonical.launchpad.mailnotification import MailWrapper | ||
347 | 40 | from canonical.launchpad.webapp import canonical_url | ||
348 | 41 | from lp.registry.enum import PersonTransferJobType | ||
349 | 42 | from lp.registry.interfaces.person import ( | ||
350 | 43 | IPerson, | ||
351 | 44 | IPersonSet, | ||
352 | 45 | ITeam, | ||
353 | 46 | ) | ||
354 | 47 | from lp.registry.interfaces.persontransferjob import ( | ||
355 | 48 | IMembershipNotificationJob, | ||
356 | 49 | IMembershipNotificationJobSource, | ||
357 | 50 | IPersonTransferJob, | ||
358 | 51 | IPersonTransferJobSource, | ||
359 | 52 | ) | ||
360 | 53 | from lp.registry.interfaces.teammembership import TeamMembershipStatus | ||
361 | 54 | from lp.registry.model.person import Person | ||
362 | 55 | from lp.services.job.model.job import Job | ||
363 | 56 | from lp.services.job.runner import BaseRunnableJob | ||
364 | 57 | |||
365 | 58 | |||
366 | 59 | class PersonTransferJob(Storm): | ||
367 | 60 | """Base class for team membership and person merge jobs.""" | ||
368 | 61 | |||
369 | 62 | implements(IPersonTransferJob) | ||
370 | 63 | |||
371 | 64 | __storm_table__ = 'PersonTransferJob' | ||
372 | 65 | |||
373 | 66 | id = Int(primary=True) | ||
374 | 67 | |||
375 | 68 | job_id = Int(name='job') | ||
376 | 69 | job = Reference(job_id, Job.id) | ||
377 | 70 | |||
378 | 71 | major_person_id = Int(name='major_person') | ||
379 | 72 | major_person = Reference(major_person_id, Person.id) | ||
380 | 73 | |||
381 | 74 | minor_person_id = Int(name='minor_person') | ||
382 | 75 | minor_person = Reference(minor_person_id, Person.id) | ||
383 | 76 | |||
384 | 77 | job_type = EnumCol(enum=PersonTransferJobType, notNull=True) | ||
385 | 78 | |||
386 | 79 | _json_data = Unicode('json_data') | ||
387 | 80 | |||
388 | 81 | @property | ||
389 | 82 | def metadata(self): | ||
390 | 83 | return simplejson.loads(self._json_data) | ||
391 | 84 | |||
392 | 85 | def __init__(self, minor_person, major_person, job_type, metadata): | ||
393 | 86 | """Constructor. | ||
394 | 87 | |||
395 | 88 | :param minor_person: The person or team being added to or removed | ||
396 | 89 | from the major_person. | ||
397 | 90 | :param major_person: The person or team that is receiving or losing | ||
398 | 91 | the minor person. | ||
399 | 92 | :param job_type: The specific membership action being performed. | ||
400 | 93 | :param metadata: The type-specific variables, as a JSON-compatible | ||
401 | 94 | dict. | ||
402 | 95 | """ | ||
403 | 96 | super(PersonTransferJob, self).__init__() | ||
404 | 97 | self.job = Job() | ||
405 | 98 | self.job_type = job_type | ||
406 | 99 | self.major_person = major_person | ||
407 | 100 | self.minor_person = minor_person | ||
408 | 101 | |||
409 | 102 | json_data = simplejson.dumps(metadata) | ||
410 | 103 | # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring, | ||
411 | 104 | # but the DB representation is unicode. | ||
412 | 105 | self._json_data = json_data.decode('utf-8') | ||
413 | 106 | |||
414 | 107 | @classmethod | ||
415 | 108 | def get(cls, key): | ||
416 | 109 | """Return the instance of this class whose key is supplied.""" | ||
417 | 110 | store = IMasterStore(PersonTransferJob) | ||
418 | 111 | instance = store.get(cls, key) | ||
419 | 112 | if instance is None: | ||
420 | 113 | raise SQLObjectNotFound( | ||
421 | 114 | 'No occurrence of %s has key %s' % (cls.__name__, key)) | ||
422 | 115 | return instance | ||
423 | 116 | |||
424 | 117 | |||
425 | 118 | class PersonTransferJobDerived(BaseRunnableJob): | ||
426 | 119 | """Intermediate class for deriving from PersonTransferJob. | ||
427 | 120 | |||
428 | 121 | Storm classes can't simply be subclassed or you can end up with | ||
429 | 122 | multiple objects referencing the same row in the db. This class uses | ||
430 | 123 | lazr.delegates, which is a little bit simpler than storm's | ||
431 | 124 | infoheritance solution to the problem. Subclasses need to override | ||
432 | 125 | the run() method. | ||
433 | 126 | """ | ||
434 | 127 | |||
435 | 128 | delegates(IPersonTransferJob) | ||
436 | 129 | classProvides(IPersonTransferJobSource) | ||
437 | 130 | |||
438 | 131 | def __init__(self, job): | ||
439 | 132 | self.context = job | ||
440 | 133 | |||
441 | 134 | def __repr__(self): | ||
442 | 135 | return ( | ||
443 | 136 | '<%(job_type)s branch job (%(id)s) for %(minor_person)s ' | ||
444 | 137 | 'as part of %(major_person)s. status=%(status)s>' % { | ||
445 | 138 | 'job_type': self.context.job_type.name, | ||
446 | 139 | 'id': self.context.id, | ||
447 | 140 | 'minor_person': self.minor_person.name, | ||
448 | 141 | 'major_person': self.major_person.name, | ||
449 | 142 | 'status': self.job.status, | ||
450 | 143 | }) | ||
451 | 144 | |||
452 | 145 | @classmethod | ||
453 | 146 | def create(cls, minor_person, major_person, metadata): | ||
454 | 147 | """See `IPersonTransferJob`.""" | ||
455 | 148 | if not IPerson.providedBy(minor_person): | ||
456 | 149 | raise TypeError("minor_person must be IPerson: %s" | ||
457 | 150 | % repr(minor_person)) | ||
458 | 151 | if not IPerson.providedBy(major_person): | ||
459 | 152 | raise TypeError("major_person must be IPerson: %s" | ||
460 | 153 | % repr(major_person)) | ||
461 | 154 | job = PersonTransferJob( | ||
462 | 155 | minor_person=minor_person, | ||
463 | 156 | major_person=major_person, | ||
464 | 157 | job_type=cls.class_job_type, | ||
465 | 158 | metadata=metadata) | ||
466 | 159 | return cls(job) | ||
467 | 160 | |||
468 | 161 | @classmethod | ||
469 | 162 | def iterReady(cls): | ||
470 | 163 | """Iterate through all ready PersonTransferJobs.""" | ||
471 | 164 | store = IMasterStore(PersonTransferJob) | ||
472 | 165 | jobs = store.find( | ||
473 | 166 | PersonTransferJob, | ||
474 | 167 | And(PersonTransferJob.job_type == cls.class_job_type, | ||
475 | 168 | PersonTransferJob.job_id.is_in(Job.ready_jobs))) | ||
476 | 169 | return (cls(job) for job in jobs) | ||
477 | 170 | |||
478 | 171 | def getOopsVars(self): | ||
479 | 172 | """See `IRunnableJob`.""" | ||
480 | 173 | vars = BaseRunnableJob.getOopsVars(self) | ||
481 | 174 | vars.extend([ | ||
482 | 175 | ('major_person_name', self.context.major_person.name), | ||
483 | 176 | ('minor_person_name', self.context.minor_person.name), | ||
484 | 177 | ]) | ||
485 | 178 | return vars | ||
486 | 179 | |||
487 | 180 | |||
488 | 181 | class MembershipNotificationJob(PersonTransferJobDerived): | ||
489 | 182 | """A Job that sends notifications about team membership changes.""" | ||
490 | 183 | |||
491 | 184 | implements(IMembershipNotificationJob) | ||
492 | 185 | classProvides(IMembershipNotificationJobSource) | ||
493 | 186 | |||
494 | 187 | class_job_type = PersonTransferJobType.MEMBERSHIP_NOTIFICATION | ||
495 | 188 | |||
496 | 189 | @classmethod | ||
497 | 190 | def create(cls, member, team, reviewer, old_status, new_status, | ||
498 | 191 | last_change_comment=None): | ||
499 | 192 | if not ITeam.providedBy(team): | ||
500 | 193 | raise TypeError('team must be ITeam: %s' % repr(team)) | ||
501 | 194 | if not IPerson.providedBy(reviewer): | ||
502 | 195 | raise TypeError('reviewer must be IPerson: %s' % repr(reviewer)) | ||
503 | 196 | if old_status not in TeamMembershipStatus: | ||
504 | 197 | raise TypeError("old_status must be TeamMembershipStatus: %s" | ||
505 | 198 | % repr(old_status)) | ||
506 | 199 | if new_status not in TeamMembershipStatus: | ||
507 | 200 | raise TypeError("new_status must be TeamMembershipStatus: %s" | ||
508 | 201 | % repr(new_status)) | ||
509 | 202 | metadata = { | ||
510 | 203 | 'reviewer': reviewer.id, | ||
511 | 204 | 'old_status': old_status.name, | ||
512 | 205 | 'new_status': new_status.name, | ||
513 | 206 | 'last_change_comment': last_change_comment, | ||
514 | 207 | } | ||
515 | 208 | return super(MembershipNotificationJob, cls).create( | ||
516 | 209 | minor_person=member, major_person=team, metadata=metadata) | ||
517 | 210 | |||
518 | 211 | @property | ||
519 | 212 | def member(self): | ||
520 | 213 | return self.minor_person | ||
521 | 214 | |||
522 | 215 | @property | ||
523 | 216 | def team(self): | ||
524 | 217 | return self.major_person | ||
525 | 218 | |||
526 | 219 | @property | ||
527 | 220 | def reviewer(self): | ||
528 | 221 | return getUtility(IPersonSet).get(self.metadata['reviewer']) | ||
529 | 222 | |||
530 | 223 | @property | ||
531 | 224 | def old_status(self): | ||
532 | 225 | return TeamMembershipStatus.items[self.metadata['old_status']] | ||
533 | 226 | |||
534 | 227 | @property | ||
535 | 228 | def new_status(self): | ||
536 | 229 | return TeamMembershipStatus.items[self.metadata['new_status']] | ||
537 | 230 | |||
538 | 231 | @property | ||
539 | 232 | def last_change_comment(self): | ||
540 | 233 | return self.metadata['last_change_comment'] | ||
541 | 234 | |||
542 | 235 | def run(self): | ||
543 | 236 | """See `IMembershipNotificationJob`.""" | ||
544 | 237 | from canonical.launchpad.scripts import log | ||
545 | 238 | from_addr = format_address( | ||
546 | 239 | self.team.displayname, config.canonical.noreply_from_address) | ||
547 | 240 | admin_emails = self.team.getTeamAdminsEmailAddresses() | ||
548 | 241 | # person might be a self.team, so we can't rely on its preferredemail. | ||
549 | 242 | self.member_email = get_contact_email_addresses(self.member) | ||
550 | 243 | # Make sure we don't send the same notification twice to anybody. | ||
551 | 244 | for email in self.member_email: | ||
552 | 245 | if email in admin_emails: | ||
553 | 246 | admin_emails.remove(email) | ||
554 | 247 | |||
555 | 248 | if self.reviewer != self.member: | ||
556 | 249 | self.reviewer_name = self.reviewer.unique_displayname | ||
557 | 250 | else: | ||
558 | 251 | # The user himself changed his self.membership. | ||
559 | 252 | self.reviewer_name = 'the user himself' | ||
560 | 253 | |||
561 | 254 | if self.last_change_comment: | ||
562 | 255 | comment = ("\n%s said:\n %s\n" % ( | ||
563 | 256 | self.reviewer.displayname, self.last_change_comment.strip())) | ||
564 | 257 | else: | ||
565 | 258 | comment = "" | ||
566 | 259 | |||
567 | 260 | replacements = { | ||
568 | 261 | 'member_name': self.member.unique_displayname, | ||
569 | 262 | 'recipient_name': self.member.displayname, | ||
570 | 263 | 'team_name': self.team.unique_displayname, | ||
571 | 264 | 'team_url': canonical_url(self.team), | ||
572 | 265 | 'old_status': self.old_status.title, | ||
573 | 266 | 'new_status': self.new_status.title, | ||
574 | 267 | 'reviewer_name': self.reviewer_name, | ||
575 | 268 | 'comment': comment} | ||
576 | 269 | |||
577 | 270 | template_name = 'membership-statuschange' | ||
578 | 271 | subject = ( | ||
579 | 272 | 'Membership change: %(member)s in %(team)s' | ||
580 | 273 | % { | ||
581 | 274 | 'member': self.member.name, | ||
582 | 275 | 'team': self.team.name, | ||
583 | 276 | }) | ||
584 | 277 | if self.new_status == TeamMembershipStatus.EXPIRED: | ||
585 | 278 | template_name = 'membership-expired' | ||
586 | 279 | subject = '%s expired from team' % self.member.name | ||
587 | 280 | elif (self.new_status == TeamMembershipStatus.APPROVED and | ||
588 | 281 | self.old_status != TeamMembershipStatus.ADMIN): | ||
589 | 282 | if self.old_status == TeamMembershipStatus.INVITED: | ||
590 | 283 | subject = ('Invitation to %s accepted by %s' | ||
591 | 284 | % (self.member.name, self.reviewer.name)) | ||
592 | 285 | template_name = 'membership-invitation-accepted' | ||
593 | 286 | elif self.old_status == TeamMembershipStatus.PROPOSED: | ||
594 | 287 | subject = '%s approved by %s' % ( | ||
595 | 288 | self.member.name, self.reviewer.name) | ||
596 | 289 | else: | ||
597 | 290 | subject = '%s added by %s' % ( | ||
598 | 291 | self.member.name, self.reviewer.name) | ||
599 | 292 | elif self.new_status == TeamMembershipStatus.INVITATION_DECLINED: | ||
600 | 293 | subject = ('Invitation to %s declined by %s' | ||
601 | 294 | % (self.member.name, self.reviewer.name)) | ||
602 | 295 | template_name = 'membership-invitation-declined' | ||
603 | 296 | elif self.new_status == TeamMembershipStatus.DEACTIVATED: | ||
604 | 297 | subject = '%s deactivated by %s' % ( | ||
605 | 298 | self.member.name, self.reviewer.name) | ||
606 | 299 | elif self.new_status == TeamMembershipStatus.ADMIN: | ||
607 | 300 | subject = '%s made admin by %s' % ( | ||
608 | 301 | self.member.name, self.reviewer.name) | ||
609 | 302 | elif self.new_status == TeamMembershipStatus.DECLINED: | ||
610 | 303 | subject = '%s declined by %s' % ( | ||
611 | 304 | self.member.name, self.reviewer.name) | ||
612 | 305 | else: | ||
613 | 306 | # Use the default template and subject. | ||
614 | 307 | pass | ||
615 | 308 | |||
616 | 309 | if len(admin_emails) != 0: | ||
617 | 310 | admin_template = get_email_template( | ||
618 | 311 | "%s-bulk.txt" % template_name) | ||
619 | 312 | for address in admin_emails: | ||
620 | 313 | recipient = getUtility(IPersonSet).getByEmail(address) | ||
621 | 314 | replacements['recipient_name'] = recipient.displayname | ||
622 | 315 | msg = MailWrapper().format( | ||
623 | 316 | admin_template % replacements, force_wrap=True) | ||
624 | 317 | simple_sendmail(from_addr, address, subject, msg) | ||
625 | 318 | |||
626 | 319 | # The self.member can be a self.self.team without any | ||
627 | 320 | # self.members, and in this case we won't have a single email | ||
628 | 321 | # address to send this notification to. | ||
629 | 322 | if self.member_email and self.reviewer != self.member: | ||
630 | 323 | if self.member.isTeam(): | ||
631 | 324 | template = '%s-bulk.txt' % template_name | ||
632 | 325 | else: | ||
633 | 326 | template = '%s-personal.txt' % template_name | ||
634 | 327 | self.member_template = get_email_template(template) | ||
635 | 328 | for address in self.member_email: | ||
636 | 329 | recipient = getUtility(IPersonSet).getByEmail(address) | ||
637 | 330 | replacements['recipient_name'] = recipient.displayname | ||
638 | 331 | msg = MailWrapper().format( | ||
639 | 332 | self.member_template % replacements, force_wrap=True) | ||
640 | 333 | simple_sendmail(from_addr, address, subject, msg) | ||
641 | 334 | log.debug('MembershipNotificationJob sent email') | ||
642 | 0 | 335 | ||
643 | === modified file 'lib/lp/registry/model/teammembership.py' | |||
644 | --- lib/lp/registry/model/teammembership.py 2010-08-20 20:31:18 +0000 | |||
645 | +++ lib/lp/registry/model/teammembership.py 2010-09-17 22:26:48 +0000 | |||
646 | @@ -5,6 +5,7 @@ | |||
647 | 5 | 5 | ||
648 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
649 | 7 | __all__ = [ | 7 | __all__ = [ |
650 | 8 | 'sendStatusChangeNotification', | ||
651 | 8 | 'TeamMembership', | 9 | 'TeamMembership', |
652 | 9 | 'TeamMembershipSet', | 10 | 'TeamMembershipSet', |
653 | 10 | 'TeamParticipation', | 11 | 'TeamParticipation', |
654 | @@ -51,6 +52,9 @@ | |||
655 | 51 | TeamMembershipRenewalPolicy, | 52 | TeamMembershipRenewalPolicy, |
656 | 52 | validate_public_person, | 53 | validate_public_person, |
657 | 53 | ) | 54 | ) |
658 | 55 | from lp.registry.interfaces.persontransferjob import ( | ||
659 | 56 | IMembershipNotificationJobSource, | ||
660 | 57 | ) | ||
661 | 54 | from lp.registry.interfaces.teammembership import ( | 58 | from lp.registry.interfaces.teammembership import ( |
662 | 55 | CyclicalTeamMembershipError, | 59 | CyclicalTeamMembershipError, |
663 | 56 | DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT, | 60 | DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT, |
664 | @@ -400,96 +404,11 @@ | |||
665 | 400 | """Send a status change notification to all team admins and the | 404 | """Send a status change notification to all team admins and the |
666 | 401 | member whose membership's status changed. | 405 | member whose membership's status changed. |
667 | 402 | """ | 406 | """ |
668 | 403 | team = self.team | ||
669 | 404 | member = self.person | ||
670 | 405 | reviewer = self.last_changed_by | 407 | reviewer = self.last_changed_by |
671 | 406 | from_addr = format_address( | ||
672 | 407 | team.displayname, config.canonical.noreply_from_address) | ||
673 | 408 | new_status = self.status | 408 | new_status = self.status |
758 | 409 | admins_emails = team.getTeamAdminsEmailAddresses() | 409 | getUtility(IMembershipNotificationJobSource).create( |
759 | 410 | # self.person might be a team, so we can't rely on its preferredemail. | 410 | self.person, self.team, reviewer, old_status, new_status, |
760 | 411 | member_email = get_contact_email_addresses(member) | 411 | self.last_change_comment) |
677 | 412 | # Make sure we don't send the same notification twice to anybody. | ||
678 | 413 | for email in member_email: | ||
679 | 414 | if email in admins_emails: | ||
680 | 415 | admins_emails.remove(email) | ||
681 | 416 | |||
682 | 417 | if reviewer != member: | ||
683 | 418 | reviewer_name = reviewer.unique_displayname | ||
684 | 419 | else: | ||
685 | 420 | # The user himself changed his membership. | ||
686 | 421 | reviewer_name = 'the user himself' | ||
687 | 422 | |||
688 | 423 | if self.last_change_comment: | ||
689 | 424 | comment = ("\n%s said:\n %s\n" % ( | ||
690 | 425 | reviewer.displayname, self.last_change_comment.strip())) | ||
691 | 426 | else: | ||
692 | 427 | comment = "" | ||
693 | 428 | |||
694 | 429 | replacements = { | ||
695 | 430 | 'member_name': member.unique_displayname, | ||
696 | 431 | 'recipient_name': member.displayname, | ||
697 | 432 | 'team_name': team.unique_displayname, | ||
698 | 433 | 'team_url': canonical_url(team), | ||
699 | 434 | 'old_status': old_status.title, | ||
700 | 435 | 'new_status': new_status.title, | ||
701 | 436 | 'reviewer_name': reviewer_name, | ||
702 | 437 | 'comment': comment} | ||
703 | 438 | |||
704 | 439 | template_name = 'membership-statuschange' | ||
705 | 440 | subject = ('Membership change: %(member)s in %(team)s' | ||
706 | 441 | % {'member': member.name, 'team': team.name}) | ||
707 | 442 | if new_status == TeamMembershipStatus.EXPIRED: | ||
708 | 443 | template_name = 'membership-expired' | ||
709 | 444 | subject = '%s expired from team' % member.name | ||
710 | 445 | elif (new_status == TeamMembershipStatus.APPROVED and | ||
711 | 446 | old_status != TeamMembershipStatus.ADMIN): | ||
712 | 447 | if old_status == TeamMembershipStatus.INVITED: | ||
713 | 448 | subject = ('Invitation to %s accepted by %s' | ||
714 | 449 | % (member.name, reviewer.name)) | ||
715 | 450 | template_name = 'membership-invitation-accepted' | ||
716 | 451 | elif old_status == TeamMembershipStatus.PROPOSED: | ||
717 | 452 | subject = '%s approved by %s' % (member.name, reviewer.name) | ||
718 | 453 | else: | ||
719 | 454 | subject = '%s added by %s' % (member.name, reviewer.name) | ||
720 | 455 | elif new_status == TeamMembershipStatus.INVITATION_DECLINED: | ||
721 | 456 | subject = ('Invitation to %s declined by %s' | ||
722 | 457 | % (member.name, reviewer.name)) | ||
723 | 458 | template_name = 'membership-invitation-declined' | ||
724 | 459 | elif new_status == TeamMembershipStatus.DEACTIVATED: | ||
725 | 460 | subject = '%s deactivated by %s' % (member.name, reviewer.name) | ||
726 | 461 | elif new_status == TeamMembershipStatus.ADMIN: | ||
727 | 462 | subject = '%s made admin by %s' % (member.name, reviewer.name) | ||
728 | 463 | elif new_status == TeamMembershipStatus.DECLINED: | ||
729 | 464 | subject = '%s declined by %s' % (member.name, reviewer.name) | ||
730 | 465 | else: | ||
731 | 466 | # Use the default template and subject. | ||
732 | 467 | pass | ||
733 | 468 | |||
734 | 469 | if admins_emails: | ||
735 | 470 | admins_template = get_email_template( | ||
736 | 471 | "%s-bulk.txt" % template_name) | ||
737 | 472 | for address in admins_emails: | ||
738 | 473 | recipient = getUtility(IPersonSet).getByEmail(address) | ||
739 | 474 | replacements['recipient_name'] = recipient.displayname | ||
740 | 475 | msg = MailWrapper().format( | ||
741 | 476 | admins_template % replacements, force_wrap=True) | ||
742 | 477 | simple_sendmail(from_addr, address, subject, msg) | ||
743 | 478 | |||
744 | 479 | # The member can be a team without any members, and in this case we | ||
745 | 480 | # won't have a single email address to send this notification to. | ||
746 | 481 | if member_email and reviewer != member: | ||
747 | 482 | if member.isTeam(): | ||
748 | 483 | template = '%s-bulk.txt' % template_name | ||
749 | 484 | else: | ||
750 | 485 | template = '%s-personal.txt' % template_name | ||
751 | 486 | member_template = get_email_template(template) | ||
752 | 487 | for address in member_email: | ||
753 | 488 | recipient = getUtility(IPersonSet).getByEmail(address) | ||
754 | 489 | replacements['recipient_name'] = recipient.displayname | ||
755 | 490 | msg = MailWrapper().format( | ||
756 | 491 | member_template % replacements, force_wrap=True) | ||
757 | 492 | simple_sendmail(from_addr, address, subject, msg) | ||
761 | 493 | 412 | ||
762 | 494 | 413 | ||
763 | 495 | class TeamMembershipSet: | 414 | class TeamMembershipSet: |
764 | 496 | 415 | ||
765 | === added file 'lib/lp/registry/tests/test_persontransferjob.py' | |||
766 | --- lib/lp/registry/tests/test_persontransferjob.py 1970-01-01 00:00:00 +0000 | |||
767 | +++ lib/lp/registry/tests/test_persontransferjob.py 2010-09-17 22:26:48 +0000 | |||
768 | @@ -0,0 +1,62 @@ | |||
769 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
770 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
771 | 3 | |||
772 | 4 | """Tests for PersonTransferJobs.""" | ||
773 | 5 | |||
774 | 6 | __metaclass__ = type | ||
775 | 7 | |||
776 | 8 | from canonical.testing import LaunchpadZopelessLayer | ||
777 | 9 | from lp.registry.enum import PersonTransferJobType | ||
778 | 10 | from lp.registry.model.persontransferjob import ( | ||
779 | 11 | PersonTransferJob, | ||
780 | 12 | PersonTransferJobDerived, | ||
781 | 13 | ) | ||
782 | 14 | from lp.testing import TestCaseWithFactory | ||
783 | 15 | |||
784 | 16 | |||
785 | 17 | class PersonTransferJobTestCase(TestCaseWithFactory): | ||
786 | 18 | """Test case for basic PersonTransferJob class.""" | ||
787 | 19 | |||
788 | 20 | layer = LaunchpadZopelessLayer | ||
789 | 21 | |||
790 | 22 | def test_instantiate(self): | ||
791 | 23 | # PersonTransferJob.__init__() instantiates a | ||
792 | 24 | # PersonTransferJob instance. | ||
793 | 25 | person = self.factory.makePerson() | ||
794 | 26 | team = self.factory.makeTeam() | ||
795 | 27 | |||
796 | 28 | metadata = ('some', 'arbitrary', 'metadata') | ||
797 | 29 | person_transfer_job = PersonTransferJob( | ||
798 | 30 | person, | ||
799 | 31 | team, | ||
800 | 32 | PersonTransferJobType.MEMBERSHIP_NOTIFICATION, | ||
801 | 33 | metadata) | ||
802 | 34 | |||
803 | 35 | self.assertEqual(person, person_transfer_job.minor_person) | ||
804 | 36 | self.assertEqual(team, person_transfer_job.major_person) | ||
805 | 37 | self.assertEqual( | ||
806 | 38 | PersonTransferJobType.MEMBERSHIP_NOTIFICATION, | ||
807 | 39 | person_transfer_job.job_type) | ||
808 | 40 | |||
809 | 41 | # When we actually access the PersonTransferJob's metadata it | ||
810 | 42 | # gets unserialized from JSON, so the representation returned by | ||
811 | 43 | # person_transfer_job.metadata will be different from what we | ||
812 | 44 | # originally passed in. | ||
813 | 45 | metadata_expected = [u'some', u'arbitrary', u'metadata'] | ||
814 | 46 | self.assertEqual(metadata_expected, person_transfer_job.metadata) | ||
815 | 47 | |||
816 | 48 | |||
817 | 49 | class PersonTransferJobDerivedTestCase(TestCaseWithFactory): | ||
818 | 50 | """Test case for the PersonTransferJobDerived class.""" | ||
819 | 51 | |||
820 | 52 | layer = LaunchpadZopelessLayer | ||
821 | 53 | |||
822 | 54 | def test_create_explodes(self): | ||
823 | 55 | # PersonTransferJobDerived.create() will blow up because it | ||
824 | 56 | # needs to be subclassed to work properly. | ||
825 | 57 | person = self.factory.makePerson() | ||
826 | 58 | team = self.factory.makeTeam() | ||
827 | 59 | metadata = {'foo': 'bar'} | ||
828 | 60 | self.assertRaises( | ||
829 | 61 | AttributeError, | ||
830 | 62 | PersonTransferJobDerived.create, person, team, metadata) | ||
831 | 0 | 63 | ||
832 | === modified file 'lib/lp/testing/mail_helpers.py' | |||
833 | --- lib/lp/testing/mail_helpers.py 2010-08-20 20:31:18 +0000 | |||
834 | +++ lib/lp/testing/mail_helpers.py 2010-09-17 22:26:48 +0000 | |||
835 | @@ -10,7 +10,14 @@ | |||
836 | 10 | 10 | ||
837 | 11 | import transaction | 11 | import transaction |
838 | 12 | 12 | ||
839 | 13 | from zope.component import getUtility | ||
840 | 14 | |||
841 | 15 | from lp.registry.interfaces.persontransferjob import ( | ||
842 | 16 | IMembershipNotificationJobSource, | ||
843 | 17 | ) | ||
844 | 18 | from lp.services.job.runner import JobRunner | ||
845 | 13 | from lp.services.mail import stub | 19 | from lp.services.mail import stub |
846 | 20 | from lp.testing.logger import MockLogger | ||
847 | 14 | 21 | ||
848 | 15 | 22 | ||
849 | 16 | def pop_notifications(sort_key=None, commit=True): | 23 | def pop_notifications(sort_key=None, commit=True): |
850 | @@ -91,8 +98,25 @@ | |||
851 | 91 | print body | 98 | print body |
852 | 92 | print "-"*40 | 99 | print "-"*40 |
853 | 93 | 100 | ||
854 | 101 | |||
855 | 94 | def print_distinct_emails(include_reply_to=False, include_rationale=True): | 102 | def print_distinct_emails(include_reply_to=False, include_rationale=True): |
856 | 95 | """A convenient shortcut for `print_emails`(group_similar=True).""" | 103 | """A convenient shortcut for `print_emails`(group_similar=True).""" |
857 | 96 | return print_emails(group_similar=True, | 104 | return print_emails(group_similar=True, |
858 | 97 | include_reply_to=include_reply_to, | 105 | include_reply_to=include_reply_to, |
859 | 98 | include_rationale=include_rationale) | 106 | include_rationale=include_rationale) |
860 | 107 | |||
861 | 108 | |||
862 | 109 | def run_mail_jobs(): | ||
863 | 110 | """Process job queues that send out emails. | ||
864 | 111 | |||
865 | 112 | If a new job type is added that sends emails, this function can be | ||
866 | 113 | extended to run those jobs, so that testing emails doesn't require a | ||
867 | 114 | bunch of different function calls to process different queues. | ||
868 | 115 | """ | ||
869 | 116 | # Commit the transaction to make sure that the JobRunner can find | ||
870 | 117 | # the queued jobs. | ||
871 | 118 | transaction.commit() | ||
872 | 119 | job_source = getUtility(IMembershipNotificationJobSource) | ||
873 | 120 | logger = MockLogger() | ||
874 | 121 | runner = JobRunner.fromReady(job_source, logger) | ||
875 | 122 | runner.runAll() |
Hi Edwin,
Very interesting branch. I've got some suggestions but overall it is great.
--Brad
> === added file 'lib/lp/ registry/ model/persontra nsferjob. py' registry/ model/persontra nsferjob. py 1970-01-01 00:00:00 +0000 registry/ model/persontra nsferjob. py 2010-09-17 18:59:05 +0000
> --- lib/lp/
> +++ lib/lp/
> @@ -0,0 +1,322 @@
> +# Copyright 2010 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Job classes related to PersonTransferJob are in here."""
s/ are in here//
> +__metaclass__ = type ficationJob' , Job',
> +__all__ = [
> + 'MembershipNoti
> + 'PersonTransfer
> + ]
> +
> +from lazr.delegates import delegates
> +import simplejson
> +from sqlobject import SQLObjectNotFound
> +from storm.base import Storm
> +from storm.expr import And
> +from storm.locals import (
> + Int,
> + Reference,
> + Unicode,
> + )
> +from zope.component import getUtility
> +from zope.interface import (
> + classProvides,
> + implements,
> + )
> +
> +
extra blank line
> +from canonical.config import config database. enumcol import EnumCol launchpad. helpers import ( email_addresses , launchpad. interfaces. lpstorm import ( launchpad. mail import ( launchpad. mailnotificatio n import MailWrapper launchpad. webapp import canonical_url
> +from canonical.
> +from canonical.
> + get_contact_
> + get_email_template,
> + )
> +from canonical.
> + IMasterStore,
> + IStore,
> + )
> +from canonical.
> + format_address,
> + simple_sendmail,
> + )
> +from canonical.
> +from canonical.
> +
> +
Hmmm, two of them. is there supposed to be two? did you do that
manually or did henning's tool do it? if the latter, then keep it.
> +from lp.registry.enum import PersonTransferJ obType interfaces. person import ( interfaces. persontransferj ob import ( JobSource, ficationJob, ficationJobSour ce, interfaces. teammembership import TeamMembershipS tatus model.person import Person job.model. job import Job job.runner import BaseRunnableJob ob(Storm) :
> +from lp.registry.
> + IPerson,
> + IPersonSet,
> + ITeam,
> + )
> +from lp.registry.
> + IPersonTransferJob,
> + IPersonTransfer
> + IMembershipNoti
> + IMembershipNoti
> + )
> +from lp.registry.
> +from lp.registry.
> +from lp.services.
> +from lp.services.
> +
> +
> +class PersonTransferJ
[...]
> + def __init__(self, minor_person, major_person, job_type, metadata):
> + """Constructor.
> +
> + :param minor_person: The person or team being added to or removed
> + from the major_person.
> + :param major_person: The person or team that is receiving or losing
> + the minor person.
> + :param job_type: The specific membership action being performed.
> + :param metadata: The type-specific variables, as a JSON-compatible
> + dict.
Indent to match.
> + """ nsferJob, self).__init__()
> + super(PersonTra
> + self.job = Job()
> + self.job_type = job_type
> + self.major_person = major_person
> + self.minor_person = minor_person
> +
> + json_data = simplejson.dump...