Merge lp:~danilo/launchpad/bug-548-use-preference into lp:launchpad/db-devel
- bug-548-use-preference
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Brad Crittenden |
Approved revision: | no longer in the source branch. |
Merged at revision: | 10178 |
Proposed branch: | lp:~danilo/launchpad/bug-548-use-preference |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~gary/launchpad/bug548-db-2 |
Diff against target: |
756 lines (+254/-148) 8 files modified
database/schema/security.cfg (+7/-0) lib/lp/app/widgets/__init__.py (+2/-0) lib/lp/bugs/doc/bugnotification-sending.txt (+91/-118) lib/lp/bugs/scripts/bugnotification.py (+11/-5) lib/lp/bugs/tests/test_bugchanges.py (+59/-15) lib/lp/registry/model/person.py (+37/-7) lib/lp/scripts/garbo.py (+7/-3) lib/lp/testing/dbuser.py (+40/-0) |
To merge this branch: | bzr merge lp:~danilo/launchpad/bug-548-use-preference |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email:
|
Commit message
Mute self-generated bug notifications for accounts that set that option (iow, mute themselves).
Description of the change
= Mute self-generated bug notifications =
For users who choose to not receive any emails resulting from their own activity in the bug tracker we should stop sending emails. This is part of our effort to solve bug 548. It still depends on unlanded work by Gary, and we expect to see another branch which enables setting this through a web page.
== Implementation details ==
I've discussed the implementation with Gavin and Deryck. My initial efforts were focused on not even creating a BugNotification record for a user who decides not to receive these emails, but because of potential indirect subscriptions through team membership, we had to move this to the code actually producing email notifications and "flattening" out recipients based on team membership. This means that it's all in one place now (all 3 lines of code changes), and should have less chances of conflicting with ongoing refactoring work by Gavin (which touches IBug.addChangeN
== Tests ==
bin/test -cvvt no_self_email
== Demo & QA ==
To QA this, we need to set person.
It's very nice that "cronscripts/
Preview Diff
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2011-02-04 17:56:21 +0000 |
3 | +++ database/schema/security.cfg 2011-02-04 17:56:23 +0000 |
4 | @@ -578,7 +578,9 @@ |
5 | public.milestone = SELECT |
6 | public.packagebugsupervisor = SELECT |
7 | public.person = SELECT, INSERT, UPDATE |
8 | +public.personsettings = SELECT, INSERT |
9 | public.personlanguage = SELECT |
10 | +public.personsettings = SELECT, INSERT |
11 | public.product = SELECT, UPDATE |
12 | public.productseries = SELECT |
13 | public.project = SELECT, UPDATE |
14 | @@ -1093,6 +1095,7 @@ |
15 | public.packageselection = SELECT, INSERT, UPDATE |
16 | public.packaging = SELECT, INSERT, UPDATE |
17 | public.person = SELECT, INSERT, UPDATE |
18 | +public.personsettings = SELECT, INSERT, UPDATE |
19 | public.personlanguage = SELECT, INSERT, UPDATE |
20 | public.pocketchroot = SELECT, INSERT, UPDATE |
21 | public.pocomment = SELECT, INSERT, UPDATE |
22 | @@ -1211,6 +1214,7 @@ |
23 | public.account = SELECT, INSERT |
24 | public.accountpassword = SELECT, INSERT |
25 | public.person = SELECT, INSERT, UPDATE |
26 | +public.personsettings = SELECT, INSERT |
27 | public.emailaddress = SELECT, INSERT, UPDATE |
28 | public.teamparticipation = SELECT, INSERT |
29 | public.teammembership = SELECT |
30 | @@ -1323,6 +1327,7 @@ |
31 | # Announce handling |
32 | public.account = SELECT, INSERT |
33 | public.person = SELECT, INSERT |
34 | +public.personsettings = SELECT, INSERT |
35 | public.emailaddress = SELECT, INSERT |
36 | public.teamparticipation = SELECT, INSERT |
37 | public.teammembership = SELECT |
38 | @@ -1763,6 +1768,7 @@ |
39 | public.mailinglist = SELECT, INSERT, UPDATE |
40 | public.mailinglistsubscription = SELECT, INSERT, UPDATE |
41 | public.person = SELECT, INSERT, UPDATE |
42 | +public.personsettings = SELECT, INSERT |
43 | public.teammembership = SELECT, INSERT, UPDATE |
44 | public.teamparticipation = SELECT, INSERT, UPDATE |
45 | |
46 | @@ -1944,6 +1950,7 @@ |
47 | groups=script |
48 | public.account = SELECT, INSERT, UPDATE |
49 | public.person = SELECT, INSERT |
50 | +public.personsettings = SELECT, INSERT |
51 | public.product = SELECT, INSERT, UPDATE |
52 | public.productseries = SELECT, INSERT |
53 | public.productlicense = SELECT, INSERT |
54 | |
55 | === added file 'lib/lp/app/widgets/__init__.py' |
56 | --- lib/lp/app/widgets/__init__.py 1970-01-01 00:00:00 +0000 |
57 | +++ lib/lp/app/widgets/__init__.py 2011-02-02 15:37:35 +0000 |
58 | @@ -0,0 +1,2 @@ |
59 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
60 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
61 | |
62 | === modified file 'lib/lp/bugs/doc/bugnotification-sending.txt' |
63 | --- lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-04 17:56:21 +0000 |
64 | +++ lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-04 17:56:23 +0000 |
65 | @@ -30,20 +30,9 @@ |
66 | ... print email_notification.get_payload(decode=True) |
67 | ... print "-" * 70 |
68 | |
69 | -We'll also define some helper functions to help us with database users. |
70 | - |
71 | - >>> from canonical.config import config |
72 | - >>> from canonical.database.sqlbase import commit |
73 | - >>> from canonical.testing.layers import LaunchpadZopelessLayer |
74 | - |
75 | - >>> def switch_db_to_launchpad(): |
76 | - ... commit() |
77 | - ... LaunchpadZopelessLayer.switchDbUser('launchpad') |
78 | - |
79 | - >>> def switch_db_to_bugnotification(): |
80 | - ... commit() |
81 | - ... LaunchpadZopelessLayer.switchDbUser( |
82 | - ... config.malone.bugnotification_dbuser) |
83 | +We'll also import a helper function to help us with database users. |
84 | + |
85 | + >>> from lp.testing.dbuser import lp_dbuser |
86 | |
87 | You'll note that we are printing out an X-Launchpad-Message-Rationale |
88 | header. This header is a simple string that allows people to filter |
89 | @@ -356,16 +345,13 @@ |
90 | ... print member.preferredemail.email |
91 | marilize@hbd.com |
92 | |
93 | - >>> switch_db_to_launchpad() |
94 | - >>> bug_one.subscribe(shipit_admins, shipit_admins) |
95 | - <...> |
96 | - |
97 | - >>> comment = getUtility(IMessageSet).fromText( |
98 | - ... 'subject', 'a comment.', sample_person, |
99 | - ... datecreated=ten_minutes_ago) |
100 | - >>> bug_one.addCommentNotification(comment) |
101 | - |
102 | - >>> switch_db_to_bugnotification() |
103 | + >>> with lp_dbuser(): |
104 | + ... ignored = bug_one.subscribe(shipit_admins, shipit_admins) |
105 | + ... comment = getUtility(IMessageSet).fromText( |
106 | + ... 'subject', 'a comment.', sample_person, |
107 | + ... datecreated=ten_minutes_ago) |
108 | + ... bug_one.addCommentNotification(comment) |
109 | + |
110 | >>> pending_notifications = getUtility( |
111 | ... IBugNotificationSet).getNotificationsToSend() |
112 | >>> len(pending_notifications) |
113 | @@ -398,17 +384,15 @@ |
114 | >>> params = CreateBugParams( |
115 | ... msg=description, owner=sample_person, title='new bug') |
116 | |
117 | - >>> switch_db_to_launchpad() |
118 | - >>> new_bug = ubuntu.createBug(params) |
119 | - >>> switch_db_to_bugnotification() |
120 | - >>> flush_notifications() |
121 | + >>> with lp_dbuser(): |
122 | + ... new_bug = ubuntu.createBug(params) |
123 | |
124 | If a bug is a duplicate of another bug, a marker gets inserted at the |
125 | top of the email: |
126 | |
127 | - >>> switch_db_to_launchpad() |
128 | - >>> new_bug.markAsDuplicate(bug_one) |
129 | - >>> switch_db_to_bugnotification() |
130 | + >>> flush_notifications() |
131 | + >>> with lp_dbuser(): |
132 | + ... new_bug.markAsDuplicate(bug_one) |
133 | >>> comment = getUtility(IMessageSet).fromText( |
134 | ... 'subject', 'a comment.', sample_person, |
135 | ... datecreated=ten_minutes_ago) |
136 | @@ -474,12 +458,11 @@ |
137 | ... 'Zero-day on Frobulator', 'Woah.', sample_person, |
138 | ... datecreated=ten_minutes_ago) |
139 | |
140 | - >>> switch_db_to_launchpad() |
141 | - >>> sec_vuln_bug = ubuntu.createBug(CreateBugParams( |
142 | + >>> with lp_dbuser(): |
143 | + ... sec_vuln_bug = ubuntu.createBug(CreateBugParams( |
144 | ... msg=sec_vuln_description, owner=sample_person, |
145 | ... title='Zero-day on Frobulator', |
146 | ... security_related=True, private=True)) |
147 | - >>> switch_db_to_bugnotification() |
148 | |
149 | >>> sec_vuln_bug.security_related |
150 | True |
151 | @@ -730,10 +713,8 @@ |
152 | The tags will be space-separated to allow the list to be wrapped if it |
153 | gets over-long. |
154 | |
155 | - >>> switch_db_to_launchpad() |
156 | - >>> bug_three.tags = [u'layout-test', u'another-tag', u'yet-another'] |
157 | - |
158 | - >>> switch_db_to_bugnotification() |
159 | + >>> with lp_dbuser(): |
160 | + ... bug_three.tags = [u'layout-test', u'another-tag', u'yet-another'] |
161 | |
162 | >>> bug_three = getUtility(IBugSet).get(3) |
163 | >>> for message in trigger_and_get_email_messages(bug_three): |
164 | @@ -743,15 +724,13 @@ |
165 | If we remove the tags from the bug, the X-Launchpad-Bug-Tags header |
166 | won't be included. |
167 | |
168 | - >>> switch_db_to_launchpad() |
169 | - >>> bug_three.tags = [] |
170 | - >>> switch_db_to_bugnotification() |
171 | + >>> with lp_dbuser(): |
172 | + ... bug_three.tags = [] |
173 | |
174 | >>> bug_three = getUtility(IBugSet).get(3) |
175 | >>> for message in trigger_and_get_email_messages(bug_three): |
176 | ... message.get_all('X-Launchpad-Bug-Tags') |
177 | |
178 | - >>> switch_db_to_launchpad() |
179 | >>> #bug_three.unsubscribe(sample_person, sample_person) |
180 | |
181 | |
182 | @@ -771,7 +750,8 @@ |
183 | |
184 | Predictably, private bugs are sent with a slightly different header: |
185 | |
186 | - >>> bug_three.setPrivate(True, sample_person) |
187 | + >>> with lp_dbuser(): |
188 | + ... bug_three.setPrivate(True, sample_person) |
189 | True |
190 | >>> bug_three.private |
191 | True |
192 | @@ -799,7 +779,8 @@ |
193 | The presence of the security flag on a bug is, surprise, denoted by a |
194 | simple "yes": |
195 | |
196 | - >>> bug_three.setSecurityRelated(True) |
197 | + >>> with lp_dbuser(): |
198 | + ... bug_three.setSecurityRelated(True) |
199 | True |
200 | >>> bug_three.security_related |
201 | True |
202 | @@ -824,9 +805,9 @@ |
203 | >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com') |
204 | |
205 | >>> from lp.bugs.interfaces.bugmessage import IBugMessageSet |
206 | - >>> getUtility(IBugMessageSet).createMessage( |
207 | - ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
208 | - <BugMessage ...> |
209 | + >>> with lp_dbuser(): |
210 | + ... ignored = getUtility(IBugMessageSet).createMessage( |
211 | + ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
212 | |
213 | >>> for message in trigger_and_get_email_messages(bug_three): |
214 | ... print message.get('X-Launchpad-Bug-Commenters') |
215 | @@ -835,9 +816,9 @@ |
216 | It only lists each user once, no matter how many comments they've |
217 | made. |
218 | |
219 | - >>> getUtility(IBugMessageSet).createMessage( |
220 | - ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
221 | - <BugMessage ...> |
222 | + >>> with lp_dbuser(): |
223 | + ... ignored = getUtility(IBugMessageSet).createMessage( |
224 | + ... 'Hungry', bug_three, foo_bar, "Make me a sandwich.") |
225 | |
226 | >>> for message in trigger_and_get_email_messages(bug_three): |
227 | ... print message.get('X-Launchpad-Bug-Commenters') |
228 | @@ -868,63 +849,58 @@ |
229 | Concise Person does not. We'll also create teams and give them members |
230 | with different verbose_bugnotifications settings. |
231 | |
232 | - >>> switch_db_to_launchpad() |
233 | - >>> bug = factory.makeBug( |
234 | - ... product=factory.makeProduct(title='Foo'), |
235 | - ... title='In the beginning, the universe was created. This ' |
236 | - ... 'has made a lot of people very angry and has been ' |
237 | - ... 'widely regarded as a bad move', |
238 | - ... description="This is a long description of the bug, which " |
239 | - ... "will be automatically wrapped by the BugNotification " |
240 | - ... "machinery. Ain't technology great?") |
241 | - |
242 | - >>> verbose_person = factory.makePerson( |
243 | - ... displayname='Verbose Person', email='verbose@example.com') |
244 | - >>> verbose_person.verbose_bugnotifications = True |
245 | - >>> bug.subscribe(verbose_person, verbose_person) |
246 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
247 | - |
248 | - >>> concise_person = factory.makePerson( |
249 | - ... displayname='Concise Person', email='concise@example.com') |
250 | - >>> concise_person.verbose_bugnotifications = False |
251 | - >>> bug.subscribe(concise_person, concise_person) |
252 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
253 | + >>> with lp_dbuser(): |
254 | + ... bug = factory.makeBug( |
255 | + ... product=factory.makeProduct(title='Foo'), |
256 | + ... title='In the beginning, the universe was created. This ' |
257 | + ... 'has made a lot of people very angry and has been ' |
258 | + ... 'widely regarded as a bad move', |
259 | + ... description="This is a long description of the bug, which " |
260 | + ... "will be automatically wrapped by the BugNotification " |
261 | + ... "machinery. Ain't technology great?") |
262 | + ... verbose_person = factory.makePerson( |
263 | + ... displayname='Verbose Person', email='verbose@example.com') |
264 | + ... verbose_person.verbose_bugnotifications = True |
265 | + ... ignored = bug.subscribe(verbose_person, verbose_person) |
266 | + ... concise_person = factory.makePerson( |
267 | + ... displayname='Concise Person', email='concise@example.com') |
268 | + ... concise_person.verbose_bugnotifications = False |
269 | + ... ignored = bug.subscribe(concise_person, concise_person) |
270 | |
271 | |
272 | Concise Team doesn't want verbose notifications, while Concise Team |
273 | Person, a member, does. |
274 | |
275 | - >>> concise_team = factory.makeTeam( |
276 | - ... name='conciseteam', displayname='Concise Team') |
277 | - >>> concise_team.verbose_bugnotifications = False |
278 | - >>> concise_team_person = factory.makePerson( |
279 | - ... displayname='Concise Team Person', |
280 | - ... email='conciseteam@example.com') |
281 | - >>> concise_team_person.verbose_bugnotifications = True |
282 | - >>> ignored = concise_team.addMember( |
283 | - ... concise_team_person, concise_team_person) |
284 | - >>> bug.subscribe(concise_team, concise_team_person) |
285 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
286 | + >>> with lp_dbuser(): |
287 | + ... concise_team = factory.makeTeam( |
288 | + ... name='conciseteam', displayname='Concise Team') |
289 | + ... concise_team.verbose_bugnotifications = False |
290 | + ... concise_team_person = factory.makePerson( |
291 | + ... displayname='Concise Team Person', |
292 | + ... email='conciseteam@example.com') |
293 | + ... concise_team_person.verbose_bugnotifications = True |
294 | + ... ignored = concise_team.addMember( |
295 | + ... concise_team_person, concise_team_person) |
296 | + ... ignored = bug.subscribe(concise_team, concise_team_person) |
297 | |
298 | Verbose Team wants verbose notifications, while Verbose Team Person, a |
299 | member, does not. |
300 | |
301 | - >>> verbose_team = factory.makeTeam( |
302 | - ... name='verboseteam', displayname='Verbose Team') |
303 | - >>> verbose_team.verbose_bugnotifications = True |
304 | - >>> verbose_team_person = factory.makePerson( |
305 | - ... displayname='Verbose Team Person', |
306 | - ... email='verboseteam@example.com') |
307 | - >>> verbose_team_person.verbose_bugnotifications = False |
308 | - >>> ignored = verbose_team.addMember( |
309 | - ... verbose_team_person, verbose_team_person) |
310 | - >>> bug.subscribe(verbose_team, verbose_team_person) |
311 | - <lp.bugs.model.bugsubscription.BugSubscription ...> |
312 | + >>> with lp_dbuser(): |
313 | + ... verbose_team = factory.makeTeam( |
314 | + ... name='verboseteam', displayname='Verbose Team') |
315 | + ... verbose_team.verbose_bugnotifications = True |
316 | + ... verbose_team_person = factory.makePerson( |
317 | + ... displayname='Verbose Team Person', |
318 | + ... email='verboseteam@example.com') |
319 | + ... verbose_team_person.verbose_bugnotifications = False |
320 | + ... ignored = verbose_team.addMember( |
321 | + ... verbose_team_person, verbose_team_person) |
322 | + ... ignored = bug.subscribe(verbose_team, verbose_team_person) |
323 | |
324 | We'll expire all existing notifications since we're not interested in |
325 | them: |
326 | |
327 | - >>> switch_db_to_bugnotification() |
328 | >>> notifications = getUtility( |
329 | ... IBugNotificationSet).getNotificationsToSend() |
330 | >>> len(notifications) |
331 | @@ -1103,19 +1079,22 @@ |
332 | |
333 | People (not teams) will have the choice to receive notifications from actions |
334 | they generated. For now, everyone receives these notifications whether they |
335 | -want them or not. However, we can show that the attribute exists for now. |
336 | +want them or not. |
337 | |
338 | - >>> flush_notifications() |
339 | - >>> switch_db_to_launchpad() |
340 | >>> verbose_person.selfgenerated_bugnotifications |
341 | True |
342 | + >>> with lp_dbuser(): |
343 | + ... verbose_person.selfgenerated_bugnotifications = False |
344 | |
345 | -Teams do not implement this attribute. |
346 | +Teams provide this attribute read-only. |
347 | |
348 | >>> verbose_team.selfgenerated_bugnotifications |
349 | + True |
350 | + >>> with lp_dbuser(): |
351 | + ... verbose_team.selfgenerated_bugnotifications = False |
352 | Traceback (most recent call last): |
353 | ... |
354 | - NotImplementedError: Teams do not support this attribute. |
355 | + NotImplementedError: Teams do not support changing this attribute. |
356 | |
357 | Notification Recipients |
358 | ----------------------- |
359 | @@ -1125,16 +1104,14 @@ |
360 | notification level of the subscription. |
361 | |
362 | >>> flush_notifications() |
363 | - >>> switch_db_to_launchpad() |
364 | |
365 | >>> from lp.bugs.enum import BugNotificationLevel |
366 | >>> from lp.registry.interfaces.product import IProductSet |
367 | >>> firefox = getUtility(IProductSet).getByName('firefox') |
368 | >>> mr_no_privs = getUtility(IPersonSet).getByName('no-priv') |
369 | - >>> subscription_no_priv = firefox.addBugSubscription( |
370 | - ... mr_no_privs, mr_no_privs) |
371 | - |
372 | - >>> switch_db_to_bugnotification() |
373 | + >>> with lp_dbuser(): |
374 | + ... subscription_no_priv = firefox.addBugSubscription( |
375 | + ... mr_no_privs, mr_no_privs) |
376 | |
377 | The notifications generated by addCommentNotification() are sent only to |
378 | structural subscribers with no filters, or with the notification level |
379 | @@ -1200,10 +1177,9 @@ |
380 | |
381 | |
382 | >>> flush_notifications() |
383 | - >>> switch_db_to_launchpad() |
384 | - >>> filter = subscription_no_priv.newBugFilter() |
385 | - >>> filter.bug_notification_level = BugNotificationLevel.COMMENTS |
386 | - >>> switch_db_to_bugnotification() |
387 | + >>> with lp_dbuser(): |
388 | + ... filter = subscription_no_priv.newBugFilter() |
389 | + ... filter.bug_notification_level = BugNotificationLevel.COMMENTS |
390 | |
391 | >>> comment = getUtility(IMessageSet).fromText( |
392 | ... 'subject', 'another comment.', sample_person, |
393 | @@ -1261,9 +1237,8 @@ |
394 | no comment notifications. |
395 | |
396 | >>> flush_notifications() |
397 | - >>> switch_db_to_launchpad() |
398 | - >>> filter.bug_notification_level = BugNotificationLevel.METADATA |
399 | - >>> switch_db_to_bugnotification() |
400 | + >>> with lp_dbuser(): |
401 | + ... filter.bug_notification_level = BugNotificationLevel.METADATA |
402 | |
403 | >>> comment = getUtility(IMessageSet).fromText( |
404 | ... 'subject', 'no comment for no-priv.', sample_person, |
405 | @@ -1373,9 +1348,8 @@ |
406 | no notifications created by addChangeNotification(). |
407 | |
408 | >>> flush_notifications() |
409 | - >>> switch_db_to_launchpad() |
410 | - >>> filter.bug_notification_level = BugNotificationLevel.LIFECYCLE |
411 | - >>> switch_db_to_bugnotification() |
412 | + >>> with lp_dbuser(): |
413 | + ... filter.bug_notification_level = BugNotificationLevel.LIFECYCLE |
414 | |
415 | >>> bug_one.addChangeNotification('** Summary changed to: something.', |
416 | ... sample_person, when=ten_minutes_ago) |
417 | @@ -1433,10 +1407,9 @@ |
418 | after all. |
419 | |
420 | >>> flush_notifications() |
421 | - >>> switch_db_to_launchpad() |
422 | - >>> filter2 = subscription_no_priv.newBugFilter() |
423 | - >>> filter2.bug_notification_level = BugNotificationLevel.METADATA |
424 | - >>> switch_db_to_bugnotification() |
425 | + >>> with lp_dbuser(): |
426 | + ... filter2 = subscription_no_priv.newBugFilter() |
427 | + ... filter2.bug_notification_level = BugNotificationLevel.METADATA |
428 | |
429 | >>> bug_one.addChangeNotification('** Summary changed to: whatever.', |
430 | ... sample_person, when=ten_minutes_ago) |
431 | |
432 | === modified file 'lib/lp/bugs/scripts/bugnotification.py' |
433 | --- lib/lp/bugs/scripts/bugnotification.py 2011-01-06 14:43:46 +0000 |
434 | +++ lib/lp/bugs/scripts/bugnotification.py 2011-02-04 17:56:23 +0000 |
435 | @@ -39,7 +39,7 @@ |
436 | """ |
437 | first_notification = bug_notifications[0] |
438 | bug = first_notification.bug |
439 | - person = first_notification.message.owner |
440 | + person_causing_change = first_notification.message.owner |
441 | subject = first_notification.message.subject |
442 | |
443 | comment = None |
444 | @@ -49,12 +49,17 @@ |
445 | recipients = {} |
446 | for notification in bug_notifications: |
447 | for recipient in notification.recipients: |
448 | - for email_person in emailPeople(recipient.person): |
449 | + email_people = emailPeople(recipient.person) |
450 | + if (not person_causing_change.selfgenerated_bugnotifications and |
451 | + person_causing_change in email_people): |
452 | + email_people.remove(person_causing_change) |
453 | + for email_person in email_people: |
454 | recipients[email_person] = recipient |
455 | |
456 | for notification in bug_notifications: |
457 | assert notification.bug == bug, bug.id |
458 | - assert notification.message.owner == person, person.id |
459 | + assert notification.message.owner == person_causing_change, ( |
460 | + person_causing_change.id) |
461 | if notification.is_comment: |
462 | assert comment is None, ( |
463 | "Only one of the notifications is allowed to be a comment.") |
464 | @@ -99,8 +104,9 @@ |
465 | messages = [] |
466 | mail_wrapper = MailWrapper(width=72) |
467 | content = '\n\n'.join(text_notifications) |
468 | - from_address = get_bugmail_from_address(person, bug) |
469 | - bug_notification_builder = BugNotificationBuilder(bug, person) |
470 | + from_address = get_bugmail_from_address(person_causing_change, bug) |
471 | + bug_notification_builder = BugNotificationBuilder( |
472 | + bug, person_causing_change) |
473 | sorted_recipients = sorted( |
474 | recipients.items(), key=lambda t: t[0].preferredemail.email) |
475 | for email_person, recipient in sorted_recipients: |
476 | |
477 | === modified file 'lib/lp/bugs/tests/test_bugchanges.py' |
478 | --- lib/lp/bugs/tests/test_bugchanges.py 2011-02-04 01:15:13 +0000 |
479 | +++ lib/lp/bugs/tests/test_bugchanges.py 2011-02-04 17:56:23 +0000 |
480 | @@ -3,8 +3,6 @@ |
481 | |
482 | """Tests for recording changes done to a bug.""" |
483 | |
484 | -import unittest |
485 | - |
486 | from lazr.lifecycle.event import ( |
487 | ObjectCreatedEvent, |
488 | ObjectModifiedEvent, |
489 | @@ -16,7 +14,6 @@ |
490 | |
491 | from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias |
492 | from lp.bugs.model.bugnotification import BugNotification |
493 | -from canonical.launchpad.ftests import login |
494 | from canonical.launchpad.webapp.interfaces import ILaunchBag |
495 | from canonical.launchpad.webapp.publisher import canonical_url |
496 | from canonical.testing.layers import LaunchpadFunctionalLayer |
497 | @@ -27,18 +24,20 @@ |
498 | BugTaskStatus, |
499 | ) |
500 | from lp.bugs.interfaces.cve import ICveSet |
501 | -from lp.testing.factory import LaunchpadObjectFactory |
502 | -from lp.testing import person_logged_in |
503 | - |
504 | - |
505 | -class TestBugChanges(unittest.TestCase): |
506 | +from lp.bugs.scripts.bugnotification import construct_email_notifications |
507 | +from lp.testing import ( |
508 | + person_logged_in, |
509 | + TestCaseWithFactory, |
510 | + ) |
511 | + |
512 | + |
513 | +class TestBugChanges(TestCaseWithFactory): |
514 | |
515 | layer = LaunchpadFunctionalLayer |
516 | |
517 | def setUp(self): |
518 | - login('foo.bar@canonical.com') |
519 | + super(TestBugChanges, self).setUp('foo.bar@canonical.com') |
520 | self.admin_user = getUtility(ILaunchBag).user |
521 | - self.factory = LaunchpadObjectFactory() |
522 | self.user = self.factory.makePerson(displayname='Arthur Dent') |
523 | self.product = self.factory.makeProduct( |
524 | owner=self.user, official_malone=True) |
525 | @@ -106,6 +105,16 @@ |
526 | |
527 | return getattr(obj_before_modification, attribute) |
528 | |
529 | + def getNewNotifications(self, bug=None): |
530 | + if bug is None: |
531 | + bug = self.bug |
532 | + bug_notifications = BugNotification.selectBy( |
533 | + bug=bug, orderBy='id') |
534 | + new_notifications = [ |
535 | + notification for notification in bug_notifications |
536 | + if notification.id not in self.old_notification_ids] |
537 | + return new_notifications |
538 | + |
539 | def assertRecordedChange(self, expected_activity=None, |
540 | expected_notification=None, bug=None): |
541 | """Assert that things were recorded as expected.""" |
542 | @@ -114,11 +123,7 @@ |
543 | new_activities = [ |
544 | activity for activity in bug.activity |
545 | if activity not in self.old_activities] |
546 | - bug_notifications = BugNotification.selectBy( |
547 | - bug=bug, orderBy='id') |
548 | - new_notifications = [ |
549 | - notification for notification in bug_notifications |
550 | - if notification.id not in self.old_notification_ids] |
551 | + new_notifications = self.getNewNotifications(bug) |
552 | |
553 | if expected_activity is None: |
554 | self.assertEqual(len(new_activities), 0) |
555 | @@ -173,6 +178,16 @@ |
556 | for recipient in added_notification.recipients), |
557 | set(expected_recipients)) |
558 | |
559 | + def assertRecipients(self, expected_recipients): |
560 | + notifications = self.getNewNotifications() |
561 | + notifications, messages = construct_email_notifications(notifications) |
562 | + recipients = set(message['to'] for message in messages) |
563 | + |
564 | + self.assertEqual( |
565 | + set(recipient.preferredemail.email |
566 | + for recipient in expected_recipients), |
567 | + recipients) |
568 | + |
569 | def test_subscribe(self): |
570 | # Subscribing someone to a bug adds an item to the activity log, |
571 | # but doesn't send an e-mail notification. |
572 | @@ -1586,3 +1601,32 @@ |
573 | expected_activity=expected_activity, |
574 | expected_notification=expected_notification, |
575 | bug=new_bug) |
576 | + |
577 | + def test_description_changed_no_self_email(self): |
578 | + # Users who have selfgenerated_bugnotifications set to False |
579 | + # do not get any bug email that they generated themselves. |
580 | + self.user.selfgenerated_bugnotifications = False |
581 | + |
582 | + old_description = self.changeAttribute( |
583 | + self.bug, 'description', 'New description') |
584 | + |
585 | + # self.user is not included among the recipients. |
586 | + self.assertRecipients( |
587 | + [self.product_metadata_subscriber]) |
588 | + |
589 | + def test_description_changed_no_self_email_indirect(self): |
590 | + # Users who have selfgenerated_bugnotifications set to False |
591 | + # do not get any bug email that they generated themselves, |
592 | + # even if a subscription is through a team membership. |
593 | + team = self.factory.makeTeam() |
594 | + team.addMember(self.user, team.teamowner) |
595 | + self.bug.subscribe(team, self.user) |
596 | + |
597 | + self.user.selfgenerated_bugnotifications = False |
598 | + |
599 | + old_description = self.changeAttribute( |
600 | + self.bug, 'description', 'New description') |
601 | + |
602 | + # self.user is not included among the recipients. |
603 | + self.assertRecipients( |
604 | + [self.product_metadata_subscriber, team.teamowner]) |
605 | |
606 | === modified file 'lib/lp/registry/model/person.py' |
607 | --- lib/lp/registry/model/person.py 2011-02-04 17:56:21 +0000 |
608 | +++ lib/lp/registry/model/person.py 2011-02-04 17:56:23 +0000 |
609 | @@ -84,8 +84,10 @@ |
610 | from zope.event import notify |
611 | from zope.interface import ( |
612 | alsoProvides, |
613 | + directlyProvides, |
614 | implementer, |
615 | implements, |
616 | + providedBy, |
617 | ) |
618 | from zope.lifecycleevent import ObjectCreatedEvent |
619 | from zope.publisher.interfaces import Unauthorized |
620 | @@ -372,6 +374,35 @@ |
621 | selfgenerated_bugnotifications = BoolCol(notNull=True, default=True) |
622 | |
623 | |
624 | +class UnwritableStub: |
625 | + |
626 | + def __init__(self, error_msg, *interfaces): |
627 | + self._error_msg = error_msg |
628 | + directlyProvides(self, *interfaces) |
629 | + |
630 | + def __getattr__(self, name): |
631 | + # If the attribute doesn't exist, we'll get a None, which will not |
632 | + # have "default" and thus will generate an AttributeError, which is |
633 | + # correct for a __getattr__. If the attribute does exist but is not |
634 | + # a field with a default (such as a method), we will also get a |
635 | + # contractually-acceptable AttributeError. Therefore, we can be |
636 | + # simple here. |
637 | + return providedBy(self).get(name).default |
638 | + |
639 | + def __setattr__(self, name, value): |
640 | + if name.startswith('_'): |
641 | + # The attributes we want to protect from writing are the ones on |
642 | + # the interface, which should not be "underwear" attributes. We |
643 | + # need to be able to write these, particularly so that |
644 | + # zope.interface machinery works. |
645 | + self.__dict__[name] = value |
646 | + else: |
647 | + raise NotImplementedError(self._error_msg) |
648 | + |
649 | + |
650 | +_unwritable_person_settings_stub = UnwritableStub( |
651 | + 'Teams do not support changing this attribute.', IPersonSettings) |
652 | + |
653 | class Person( |
654 | SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin, |
655 | HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin): |
656 | @@ -391,13 +422,11 @@ |
657 | @cachedproperty |
658 | def _person_settings(self): |
659 | if self.is_team: |
660 | - # Hopefully no-one ever encounters this. If someone does, |
661 | - # that means that the code is trying to look at |
662 | - # person-specific attributes on a team, and we should warn |
663 | - # about that explicitly to give a hint about what is wrong |
664 | - # (rather than merely returning None). |
665 | - raise NotImplementedError( |
666 | - 'Teams do not support this attribute.') |
667 | + # Teams need to provide these attributes for reading in order for |
668 | + # things like snapshots to work, but they are not actually |
669 | + # pertinent to teams, so they are not actually implemented for |
670 | + # writes. |
671 | + return _unwritable_person_settings_stub |
672 | else: |
673 | # This is a person. |
674 | return IStore(PersonSettings).find( |
675 | @@ -2112,6 +2141,7 @@ |
676 | ('logintoken', 'requester'), |
677 | ('personlanguage', 'person'), |
678 | ('personlocation', 'person'), |
679 | + ('personsettings', 'person'), |
680 | ('persontransferjob', 'minor_person'), |
681 | ('persontransferjob', 'major_person'), |
682 | ('signedcodeofconduct', 'owner'), |
683 | |
684 | === modified file 'lib/lp/scripts/garbo.py' |
685 | --- lib/lp/scripts/garbo.py 2011-01-31 09:24:13 +0000 |
686 | +++ lib/lp/scripts/garbo.py 2011-02-04 17:56:23 +0000 |
687 | @@ -431,10 +431,13 @@ |
688 | for (from_table, from_column, to_table, to_column, uflag, dflag) in ( |
689 | postgresql.listReferences(cursor(), 'person', 'id')): |
690 | # Skip things that don't link to Person.id or that link to it from |
691 | - # TeamParticipation or EmailAddress, as all Person entries will be |
692 | - # linked to from these tables. |
693 | + # TeamParticipation, EmailAddress, as all Person entries will be |
694 | + # linked to from these tables. Similarly, PersonSettings can |
695 | + # simply be deleted if it exists, because it has a 1 (or 0) to 1 |
696 | + # relationship with Person. |
697 | if (to_table != 'person' or to_column != 'id' |
698 | - or from_table in ('teamparticipation', 'emailaddress')): |
699 | + or from_table in ('teamparticipation', 'emailaddress', |
700 | + 'personsettings')): |
701 | continue |
702 | self.log.debug( |
703 | "Populating LinkedPeople from %s.%s" |
704 | @@ -510,6 +513,7 @@ |
705 | UPDATE EmailAddress SET person=NULL |
706 | WHERE person IN (%s) |
707 | """ % people_ids) |
708 | + # This cascade deletes any PersonSettings records. |
709 | self.store.execute(""" |
710 | DELETE FROM Person |
711 | WHERE id IN (%s) |
712 | |
713 | === added file 'lib/lp/testing/dbuser.py' |
714 | --- lib/lp/testing/dbuser.py 1970-01-01 00:00:00 +0000 |
715 | +++ lib/lp/testing/dbuser.py 2011-02-04 17:56:23 +0000 |
716 | @@ -0,0 +1,40 @@ |
717 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
718 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
719 | + |
720 | +"""Provides a context manager to run parts of a test as a different dbuser.""" |
721 | + |
722 | +__metaclass__ = type |
723 | +__all__ = [ |
724 | + 'dbuser', |
725 | + 'lp_dbuser', |
726 | + ] |
727 | + |
728 | +from contextlib import contextmanager |
729 | + |
730 | +from canonical.database.sqlbase import commit |
731 | +from canonical.testing.layers import LaunchpadZopelessLayer |
732 | + |
733 | +@contextmanager |
734 | +def dbuser(temporary_name): |
735 | + """A context manager that temporarily changes the dbuser. |
736 | + |
737 | + Use with the LaunchpadZopelessLayer layer and subclasses. |
738 | + |
739 | + temporary_name is the name of the dbuser that should be in place for the |
740 | + code in the "with" block. |
741 | + """ |
742 | + restore_name = LaunchpadZopelessLayer.txn._dbuser |
743 | + commit() |
744 | + # Note that this will raise an assertion error if the |
745 | + # LaunchpadZopelessLayer is not already set up. |
746 | + LaunchpadZopelessLayer.switchDbUser(temporary_name) |
747 | + yield |
748 | + commit() |
749 | + LaunchpadZopelessLayer.switchDbUser(restore_name) |
750 | + |
751 | +def lp_dbuser(): |
752 | + """A context manager that temporarily changes to the launchpad dbuser. |
753 | + |
754 | + Use with the LaunchpadZopelessLayer layer and subclasses. |
755 | + """ |
756 | + return dbuser('launchpad') |
The change looks good Danilo. As we discussed on IRC please change the variable 'person' in bugnotification.py to something more descriptive that indicates that person is the one who took the action generating the notification. I suggested 'instigator' but there may be something better.