Merge lp:~danilo/launchpad/bug-548-use-preference into lp:launchpad/db-devel

Proposed by Данило Шеган
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
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+48616@code.launchpad.net

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.addChangeNotification).

== Tests ==

bin/test -cvvt no_self_email

== Demo & QA ==

To QA this, we need to set person.selfgenerated_bugnotifications to False (perhaps through a query like "UPDATE PersonSettings SET selfgenerated_bugnotifications=FALSE WHERE person IN (SELECT id FROM Person WHERE name='your-name')"), and then we need to try modifying any bug, and after running cronscripts/send-bug-notifications.py, no notification should go out for our own account.

It's very nice that "cronscripts/send-bug-notifications.py -vv" actually prints out all outgoing notifications, so that will hugely help with QA (no need to have to chase out notifications in the staging inbox).

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

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.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 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')

Subscribers

People subscribed via source and target branches

to status/vote changes: