Merge lp:~sinzui/launchpad/question-email-3 into lp:launchpad

Proposed by Curtis Hovey on 2011-04-30
Status: Merged
Merged at revision: 12976
Proposed branch: lp:~sinzui/launchpad/question-email-3
Merge into: lp:launchpad
Diff against target: 2719 lines (+622/-934) 14 files modified
To merge this branch: bzr merge lp:~sinzui/launchpad/question-email-3
Reviewer Review Type Date Requested Status
j.c.sackett Approve on 2011-05-02
Benji York 2011-04-30 Abstain on 2011-05-02
Review via email: mp+59574@code.launchpad.net

Description of the Change

Send question email asynchronously.

    Launchpad bug:
        https://bugs.launchpad.net/bugs/550954
        https://bugs.launchpad.net/bugs/608037
        https://bugs.launchpad.net/bugs/618390
    Pre-implementation: jcsackett, lifeless

Change QuestionNotification to queue emails instead of sending them.
This addresses four kinds of timeouts that happen when a question needs
email sent to many users.

--------------------------------------------------------------------

RULES

    * Replace QuestionNotification's send() method with a enqueue() method.
    * Remove unneeded methods and tests that were used to send email.
    * Update the notification doctests to to use questionemailjobs instead
      of sent email.

QA

    * Create a question on qastaging.
    * Ask an admin to run cronscripts/process-job-source-groups.py on
      qastaging
    * Verify the email arrives in the staging inbox.

LINT

    database/schema/security.cfg
    lib/lp/answers/notification.py
    lib/lp/answers/doc/notifications.txt
    lib/lp/answers/tests/test_question_notifications.py
    lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt
    lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt

TEST

    ./bin/test -vv -t answers.*notification

IMPLEMENTATION

Gave all processes that could update a bug or question access to questionjob.
    database/schema/security.cfg

Added the enqueue() method and the recipient_set attribute.
Removes send(), getRecipients(), buildBody(), getFromAddress() methods.
    lib/lp/answers/notification.py
    lib/lp/answers/tests/test_question_notifications.py

/o\ More than 1000 lines of the diff is dedicated to updating the existing
doctests to check questionemailjob instead of sent emails. 900 lines are
just notifications.txt. For all three tests, I added a utility that behaves
like pop_notification, but questionemailjobs are not emails. I deleted all
tests for email addresses and reason footers, because those are provided by
recipientsets, which are already tested in question.txt and elsewhere. The
same is true the the two chunks about team email, I deleted them
since Question, not QuestionNotification, determines that. There were a few
cases where the counts of notifications changed. This is because notification
were implicitly created when the test question was created; the tests were
were somewhat wrong. Since the sort rules changed, email addresses versus
recipientset types, The order of question asker and subscriber changed.
    lib/lp/answers/doc/notifications.txt
    lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt
    lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt

To post a comment you must log in.
Benji York (benji) wrote :

Someone else is already lined up to do this review.

review: Abstain
j.c.sackett (jcsackett) wrote :

Curtis--

This mostly looks solid.

I have only one minor quibble. Names like
{{{
    class TestQuestionNotification(QuestionNotification)
}}}
strike me as somewhat confusing, since they follow our naming convention for testcases. I think something like FakeQuestionNotification or MockQuestionNotification would make it clearer. This is a nonblocker for approval, but something perhaps worth considering.

This (and the other branches in this set of work) are really great. Thanks for them.

review: Approve
12973. By Curtis Hovey on 2011-05-02

merged devel.

12974. By Curtis Hovey on 2011-05-02

Merged getErrorREcipients fix.

12975. By Curtis Hovey on 2011-05-02

Rename the fake notification classes.

12976. By Curtis Hovey on 2011-05-02

Fixed the getErrorRecipients() method.
getIterReady() uses a question that did not implicitly create an email job.

12977. By Curtis Hovey on 2011-05-02

Remove the recipient's email address after removing the person and reason.

12978. By Curtis Hovey on 2011-05-02

Added doctest test harness.

12979. By Curtis Hovey on 2011-05-02

Remove unneeded message.

12980. By Curtis Hovey on 2011-05-02

Removed mail assertions from tests that do not exercise notifications.

12981. By Curtis Hovey on 2011-05-02

Hush lint.

12982. By Curtis Hovey on 2011-05-03

Updated test_notifications_for_question_subscribers to test for question email jobs.

12983. By Curtis Hovey on 2011-05-03

Merged devel.

12984. By Curtis Hovey on 2011-05-04

Merged devel and backout security cfg changes because the diff is nasty. It is easier to reinsert the changes and rerun ec2.

12985. By Curtis Hovey on 2011-05-04

Fixed long lines.

12986. By Curtis Hovey on 2011-05-04

Restored teh questionjob db permissions.

Preview Diff

1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-05-03 12:52:05 +0000
3+++ database/schema/security.cfg 2011-05-04 17:17:01 +0000
4@@ -583,6 +583,7 @@
5 public.productseries = SELECT
6 public.project = SELECT, UPDATE
7 public.question = SELECT
8+public.questionjob = SELECT, INSERT
9 public.questionbug = SELECT
10 public.questionsubscription = SELECT
11 public.section = SELECT
12@@ -599,56 +600,56 @@
13
14 [branchscanner]
15 groups=write, script
16-public.account = SELECT, INSERT
17-public.accountpassword = SELECT, INSERT
18-public.branch = SELECT, UPDATE
19-public.branchjob = SELECT, INSERT, UPDATE, DELETE
20-public.branchmergeproposal = SELECT, UPDATE
21-public.branchmergeproposaljob = SELECT, INSERT
22-public.branchrevision = SELECT, INSERT, UPDATE, DELETE
23-public.branchsubscription = SELECT
24-public.branchvisibilitypolicy = SELECT
25-public.bugactivity = SELECT, INSERT
26-public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
27-public.bugbranch = SELECT, INSERT, UPDATE
28-public.bugnotification = SELECT, INSERT
29-public.bugnotificationfilter = SELECT, INSERT
30-public.bugnotificationrecipient = SELECT, INSERT
31-public.bugsubscription = SELECT
32-public.bugsubscriptionfilter = SELECT
33-public.bugsubscriptionfilterimportance = SELECT
34-public.bugsubscriptionfilterstatus = SELECT
35-public.bugsubscriptionfiltertag = SELECT
36-public.bugtag = SELECT
37-public.codereviewmessage = SELECT
38-public.codereviewvote = SELECT
39-public.diff = SELECT, INSERT, DELETE
40-public.distribution = SELECT
41-public.distributionsourcepackage = SELECT, UPDATE
42-public.distroseries = SELECT
43-public.emailaddress = SELECT
44-public.incrementaldiff = SELECT
45-public.job = SELECT, INSERT, UPDATE, DELETE
46-public.karma = SELECT, INSERT
47-public.karmaaction = SELECT
48-public.message = SELECT, INSERT
49-public.messagechunk = SELECT, INSERT
50-public.person = SELECT
51-public.revision = SELECT, INSERT, UPDATE
52-public.revisionauthor = SELECT, INSERT, UPDATE
53-public.revisioncache = SELECT, INSERT
54-public.revisionparent = SELECT, INSERT
55-public.revisionproperty = SELECT, INSERT
56-public.seriessourcepackagebranch = SELECT
57-public.sourcepackagename = SELECT
58-public.sourcepackagerecipe = SELECT, UPDATE
59-public.sourcepackagerecipedata = SELECT
60+public.account = SELECT, INSERT
61+public.accountpassword = SELECT, INSERT
62+public.branch = SELECT, UPDATE
63+public.branchjob = SELECT, INSERT, UPDATE, DELETE
64+public.branchmergeproposal = SELECT, UPDATE
65+public.branchmergeproposaljob = SELECT, INSERT
66+public.branchrevision = SELECT, INSERT, UPDATE, DELETE
67+public.branchsubscription = SELECT
68+public.branchvisibilitypolicy = SELECT
69+public.bugactivity = SELECT, INSERT
70+public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
71+public.bugbranch = SELECT, INSERT, UPDATE
72+public.bugnotification = SELECT, INSERT
73+public.bugnotificationfilter = SELECT, INSERT
74+public.bugnotificationrecipient = SELECT, INSERT
75+public.bugsubscription = SELECT
76+public.bugsubscriptionfilter = SELECT
77+public.bugsubscriptionfilterimportance = SELECT
78+public.bugsubscriptionfilterstatus = SELECT
79+public.bugsubscriptionfiltertag = SELECT
80+public.bugtag = SELECT
81+public.codereviewmessage = SELECT
82+public.codereviewvote = SELECT
83+public.diff = SELECT, INSERT, DELETE
84+public.distribution = SELECT
85+public.distributionsourcepackage = SELECT, UPDATE
86+public.distroseries = SELECT
87+public.emailaddress = SELECT
88+public.incrementaldiff = SELECT
89+public.job = SELECT, INSERT, UPDATE, DELETE
90+public.karma = SELECT, INSERT
91+public.karmaaction = SELECT
92+public.message = SELECT, INSERT
93+public.messagechunk = SELECT, INSERT
94+public.person = SELECT
95+public.revision = SELECT, INSERT, UPDATE
96+public.revisionauthor = SELECT, INSERT, UPDATE
97+public.revisioncache = SELECT, INSERT
98+public.revisionparent = SELECT, INSERT
99+public.revisionproperty = SELECT, INSERT
100+public.seriessourcepackagebranch = SELECT
101+public.sourcepackagename = SELECT
102+public.sourcepackagerecipe = SELECT, UPDATE
103+public.sourcepackagerecipedata = SELECT
104 public.sourcepackagerecipedatainstruction = SELECT
105-public.staticdiff = SELECT, INSERT, DELETE
106-public.structuralsubscription = SELECT
107-public.translationtemplatesbuild = SELECT, INSERT
108-public.validpersoncache = SELECT
109-public.validpersonorteamcache = SELECT
110+public.staticdiff = SELECT, INSERT, DELETE
111+public.structuralsubscription = SELECT
112+public.translationtemplatesbuild = SELECT, INSERT
113+public.validpersoncache = SELECT
114+public.validpersonorteamcache = SELECT
115 type=user
116
117 [branch-distro]
118@@ -682,36 +683,36 @@
119
120 [distributionmirror]
121 groups=script
122-public.account = SELECT
123-public.archive = SELECT
124-public.archivearch = SELECT
125-public.binarypackagebuild = SELECT
126-public.binarypackagefile = SELECT
127-public.binarypackagename = SELECT
128-public.binarypackagepublishinghistory = SELECT
129-public.binarypackagerelease = SELECT
130-public.buildfarmjob = SELECT
131-public.component = SELECT
132-public.componentselection = SELECT
133-public.distribution = SELECT
134-public.distributionmirror = SELECT, UPDATE
135-public.distroarchseries = SELECT
136-public.distroseries = SELECT
137-public.emailaddress = SELECT
138-public.libraryfilealias = SELECT, INSERT
139-public.libraryfilecontent = SELECT, INSERT
140-public.mirrorcdimagedistroseries = SELECT, INSERT, UPDATE, DELETE
141-public.mirrordistroarchseries = SELECT, UPDATE, DELETE, INSERT
142-public.mirrordistroseriessource = SELECT, UPDATE, DELETE, INSERT
143-public.mirrorproberecord = SELECT, INSERT
144-public.packagebuild = SELECT
145-public.person = SELECT
146-public.processorfamily = SELECT
147-public.sourcepackagename = SELECT
148-public.sourcepackagepublishinghistory = SELECT
149-public.sourcepackagerelease = SELECT
150-public.sourcepackagereleasefile = SELECT
151-public.teammembership = SELECT
152+public.account = SELECT
153+public.archive = SELECT
154+public.archivearch = SELECT
155+public.binarypackagebuild = SELECT
156+public.binarypackagefile = SELECT
157+public.binarypackagename = SELECT
158+public.binarypackagepublishinghistory = SELECT
159+public.binarypackagerelease = SELECT
160+public.buildfarmjob = SELECT
161+public.component = SELECT
162+public.componentselection = SELECT
163+public.distribution = SELECT
164+public.distributionmirror = SELECT, UPDATE
165+public.distroarchseries = SELECT
166+public.distroseries = SELECT
167+public.emailaddress = SELECT
168+public.libraryfilealias = SELECT, INSERT
169+public.libraryfilecontent = SELECT, INSERT
170+public.mirrorcdimagedistroseries = SELECT, INSERT, UPDATE, DELETE
171+public.mirrordistroarchseries = SELECT, UPDATE, DELETE, INSERT
172+public.mirrordistroseriessource = SELECT, UPDATE, DELETE, INSERT
173+public.mirrorproberecord = SELECT, INSERT
174+public.packagebuild = SELECT
175+public.person = SELECT
176+public.processorfamily = SELECT
177+public.sourcepackagename = SELECT
178+public.sourcepackagepublishinghistory = SELECT
179+public.sourcepackagerelease = SELECT
180+public.sourcepackagereleasefile = SELECT
181+public.teammembership = SELECT
182 type=user
183
184 [teammembership]
185@@ -726,16 +727,16 @@
186
187 [karma]
188 groups=script
189-public.emailaddress = SELECT
190-public.karma = SELECT
191-public.karmaaction = SELECT
192-public.karmacache = SELECT, INSERT, UPDATE, DELETE
193-public.karmacategory = SELECT
194-public.karmatotalcache = SELECT, INSERT, UPDATE, DELETE
195-public.person = SELECT
196-public.product = SELECT
197-public.validpersoncache = SELECT
198-public.validpersonorteamcache = SELECT
199+public.emailaddress = SELECT
200+public.karma = SELECT
201+public.karmaaction = SELECT
202+public.karmacache = SELECT, INSERT, UPDATE, DELETE
203+public.karmacategory = SELECT
204+public.karmatotalcache = SELECT, INSERT, UPDATE, DELETE
205+public.person = SELECT
206+public.product = SELECT
207+public.validpersoncache = SELECT
208+public.validpersonorteamcache = SELECT
209 type=user
210
211 [request-daily-builds]
212@@ -783,33 +784,33 @@
213
214 [cve]
215 groups=script
216-public.cve = SELECT, INSERT, UPDATE
217-public.cvereference = SELECT, INSERT, UPDATE, DELETE
218+public.cve = SELECT, INSERT, UPDATE
219+public.cvereference = SELECT, INSERT, UPDATE, DELETE
220 type=user
221
222 [gina]
223 groups=write,script
224-public.account = SELECT, INSERT
225-public.accountpassword = SELECT, INSERT
226-public.archive = SELECT, UPDATE
227-public.archivearch = SELECT, UPDATE
228-public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
229-public.distribution = SELECT
230-public.distributionjob = SELECT, INSERT
231-public.distributionsourcepackage = SELECT, INSERT
232-public.packagediff = SELECT, INSERT, UPDATE
233-public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
234+public.account = SELECT, INSERT
235+public.accountpassword = SELECT, INSERT
236+public.archive = SELECT, UPDATE
237+public.archivearch = SELECT, UPDATE
238+public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
239+public.distribution = SELECT
240+public.distributionjob = SELECT, INSERT
241+public.distributionsourcepackage = SELECT, INSERT
242+public.packagediff = SELECT, INSERT, UPDATE
243+public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
244 type=user
245
246 [archivepublisher]
247 groups=write,script
248 public.answercontact = SELECT
249-public.archive = SELECT, UPDATE
250-public.archivearch = SELECT
251-public.archiveauthtoken = SELECT, UPDATE
252-public.archivepermission = SELECT, INSERT
253-public.archivesubscriber = SELECT, UPDATE
254-public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
255+public.archive = SELECT, UPDATE
256+public.archivearch = SELECT
257+public.archiveauthtoken = SELECT, UPDATE
258+public.archivepermission = SELECT, INSERT
259+public.archivesubscriber = SELECT, UPDATE
260+public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
261 public.bug = SELECT, UPDATE
262 public.bugactivity = SELECT, INSERT
263 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
264@@ -830,10 +831,11 @@
265 public.bugtrackeralias = SELECT, INSERT
266 public.bugwatch = SELECT, INSERT
267 public.cve = SELECT, INSERT
268-public.distributionjob = SELECT, INSERT, DELETE
269-public.distributionsourcepackage = SELECT, INSERT, UPDATE
270-public.flatpackagesetinclusion = SELECT, INSERT, UPDATE, DELETE
271-public.gpgkey = SELECT, INSERT, UPDATE
272+public.distributionjob = SELECT, INSERT, DELETE
273+public.distributionsourcepackage = SELECT, INSERT, UPDATE
274+public.flatpackagesetinclusion = SELECT, INSERT, UPDATE, DELETE
275+public.gpgkey = SELECT, INSERT, UPDATE
276+public.job = SELECT, INSERT, UPDATE
277 public.karma = SELECT, INSERT
278 public.karmaaction = SELECT
279 public.language = SELECT
280@@ -841,21 +843,22 @@
281 public.messagechunk = SELECT, INSERT
282 public.milestone = SELECT
283 public.packagebugsupervisor = SELECT
284-public.packagecopyrequest = SELECT, INSERT, UPDATE
285-public.packagediff = SELECT, INSERT, UPDATE
286-public.packageset = SELECT, INSERT
287-public.packagesetgroup = SELECT
288-public.packagesetinclusion = SELECT, INSERT, UPDATE, DELETE
289-public.packagesetsources = SELECT, INSERT, UPDATE, DELETE
290+public.packagecopyrequest = SELECT, INSERT, UPDATE
291+public.packagediff = SELECT, INSERT, UPDATE
292+public.packageset = SELECT, INSERT
293+public.packagesetgroup = SELECT
294+public.packagesetinclusion = SELECT, INSERT, UPDATE, DELETE
295+public.packagesetsources = SELECT, INSERT, UPDATE, DELETE
296 public.personlanguage = SELECT
297 public.product = SELECT
298 public.productseries = SELECT
299 public.project = SELECT
300-public.publisherconfig = SELECT, INSERT
301+public.publisherconfig = SELECT, INSERT
302 public.question = SELECT
303+public.questionjob = SELECT, INSERT
304 public.questionbug = SELECT
305 public.questionsubscription = SELECT
306-public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
307+public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
308 public.structuralsubscription = SELECT
309 public.validpersoncache = SELECT
310 public.validpersonorteamcache = SELECT
311@@ -863,58 +866,58 @@
312
313 [fiera]
314 groups=script,translations_approval
315-public.account = SELECT
316-public.archive = SELECT, UPDATE
317-public.archivearch = SELECT, UPDATE
318-public.archivedependency = SELECT
319-public.binarypackagebuild = SELECT, INSERT, UPDATE
320-public.binarypackagefile = SELECT
321-public.binarypackagename = SELECT
322-public.binarypackagepublishinghistory = SELECT
323-public.binarypackagerelease = SELECT
324-public.branch = SELECT
325-public.branchjob = SELECT, DELETE
326-public.builder = SELECT, INSERT, UPDATE
327-public.buildfarmjob = SELECT, INSERT, UPDATE
328-public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE
329-public.buildqueue = SELECT, INSERT, UPDATE, DELETE
330-public.component = SELECT
331-public.distribution = SELECT, UPDATE
332-public.distroarchseries = SELECT, UPDATE
333-public.distroseries = SELECT, UPDATE
334-public.emailaddress = SELECT
335-public.flatpackagesetinclusion = SELECT
336-public.gpgkey = SELECT
337-public.job = SELECT, INSERT, UPDATE, DELETE
338-public.libraryfilealias = SELECT, INSERT
339-public.libraryfilecontent = SELECT, INSERT
340-public.packagebuild = SELECT, INSERT, UPDATE
341-public.packageset = SELECT
342-public.packagesetgroup = SELECT
343-public.packagesetinclusion = SELECT
344-public.packagesetsources = SELECT
345-public.person = SELECT
346-public.pocketchroot = SELECT, INSERT, UPDATE
347-public.processor = SELECT
348-public.processorfamily = SELECT
349-public.product = SELECT
350-public.productseries = SELECT
351-public.publisherconfig = SELECT
352-public.section = SELECT
353-public.seriessourcepackagebranch = SELECT
354-public.sourcepackagename = SELECT
355-public.sourcepackagepublishinghistory = SELECT
356-public.sourcepackagerecipe = SELECT
357-public.sourcepackagerecipebuild = SELECT, UPDATE
358-public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE, DELETE
359-public.sourcepackagerecipedata = SELECT
360-public.sourcepackagerecipedatainstruction = SELECT
361-public.sourcepackagerelease = SELECT
362-public.sourcepackagereleasefile = SELECT
363-public.teammembership = SELECT
364-public.teamparticipation = SELECT
365-public.translationimportqueueentry = SELECT, INSERT, UPDATE
366-public.translationtemplatesbuild = SELECT, INSERT
367+public.account = SELECT
368+public.archive = SELECT, UPDATE
369+public.archivearch = SELECT, UPDATE
370+public.archivedependency = SELECT
371+public.binarypackagebuild = SELECT, INSERT, UPDATE
372+public.binarypackagefile = SELECT
373+public.binarypackagename = SELECT
374+public.binarypackagepublishinghistory = SELECT
375+public.binarypackagerelease = SELECT
376+public.branch = SELECT
377+public.branchjob = SELECT, DELETE
378+public.builder = SELECT, INSERT, UPDATE
379+public.buildfarmjob = SELECT, INSERT, UPDATE
380+public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE
381+public.buildqueue = SELECT, INSERT, UPDATE, DELETE
382+public.component = SELECT
383+public.distribution = SELECT, UPDATE
384+public.distroarchseries = SELECT, UPDATE
385+public.distroseries = SELECT, UPDATE
386+public.emailaddress = SELECT
387+public.flatpackagesetinclusion = SELECT
388+public.gpgkey = SELECT
389+public.job = SELECT, INSERT, UPDATE, DELETE
390+public.libraryfilealias = SELECT, INSERT
391+public.libraryfilecontent = SELECT, INSERT
392+public.packagebuild = SELECT, INSERT, UPDATE
393+public.packageset = SELECT
394+public.packagesetgroup = SELECT
395+public.packagesetinclusion = SELECT
396+public.packagesetsources = SELECT
397+public.person = SELECT
398+public.pocketchroot = SELECT, INSERT, UPDATE
399+public.processor = SELECT
400+public.processorfamily = SELECT
401+public.product = SELECT
402+public.productseries = SELECT
403+public.publisherconfig = SELECT
404+public.section = SELECT
405+public.seriessourcepackagebranch = SELECT
406+public.sourcepackagename = SELECT
407+public.sourcepackagepublishinghistory = SELECT
408+public.sourcepackagerecipe = SELECT
409+public.sourcepackagerecipebuild = SELECT, UPDATE
410+public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE, DELETE
411+public.sourcepackagerecipedata = SELECT
412+public.sourcepackagerecipedatainstruction = SELECT
413+public.sourcepackagerelease = SELECT
414+public.sourcepackagereleasefile = SELECT
415+public.teammembership = SELECT
416+public.teamparticipation = SELECT
417+public.translationimportqueueentry = SELECT, INSERT, UPDATE
418+public.translationtemplatesbuild = SELECT, INSERT
419 type=user
420
421 [ppa-apache-log-parser]
422@@ -973,45 +976,45 @@
423
424 [sync_packages]
425 groups=script
426-public.archive = SELECT
427-public.archivepermission = SELECT, INSERT
428-public.binarypackagebuild = SELECT, INSERT
429-public.binarypackagefile = SELECT, INSERT
430-public.binarypackagename = SELECT
431-public.binarypackagepublishinghistory = SELECT, INSERT
432-public.binarypackagerelease = SELECT
433-public.buildfarmjob = SELECT, INSERT
434-public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE
435-public.buildqueue = SELECT, INSERT, UPDATE
436-public.component = SELECT
437-public.componentselection = SELECT, INSERT
438-public.distribution = SELECT
439-public.distributionjob = SELECT
440-public.distroarchseries = SELECT, INSERT
441-public.distroseries = SELECT, UPDATE
442-public.flatpackagesetinclusion = SELECT, INSERT
443-public.gpgkey = SELECT
444-public.job = SELECT, INSERT, UPDATE, DELETE
445-public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE
446-public.libraryfilecontent = SELECT, INSERT
447-public.packagebuild = SELECT, INSERT
448-public.packageset = SELECT, INSERT
449-public.packagesetgroup = SELECT, INSERT
450-public.packagesetinclusion = SELECT, INSERT
451-public.packagesetsources = SELECT, INSERT
452-public.packageupload = SELECT
453-public.packaging = SELECT, INSERT
454-public.person = SELECT
455-public.pocketchroot = SELECT
456-public.processor = SELECT
457-public.processorfamily = SELECT
458-public.section = SELECT
459-public.sectionselection = SELECT, INSERT
460-public.sourcepackageformatselection = SELECT, INSERT
461-public.sourcepackagename = SELECT
462-public.sourcepackagepublishinghistory = SELECT, INSERT
463-public.sourcepackagerelease = SELECT
464-public.sourcepackagereleasefile = SELECT, INSERT, UPDATE
465+public.archive = SELECT
466+public.archivepermission = SELECT, INSERT
467+public.binarypackagebuild = SELECT, INSERT
468+public.binarypackagefile = SELECT, INSERT
469+public.binarypackagename = SELECT
470+public.binarypackagepublishinghistory = SELECT, INSERT
471+public.binarypackagerelease = SELECT
472+public.buildfarmjob = SELECT, INSERT
473+public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE
474+public.buildqueue = SELECT, INSERT, UPDATE
475+public.component = SELECT
476+public.componentselection = SELECT, INSERT
477+public.distribution = SELECT
478+public.distributionjob = SELECT
479+public.distroarchseries = SELECT, INSERT
480+public.distroseries = SELECT, UPDATE
481+public.flatpackagesetinclusion = SELECT, INSERT
482+public.gpgkey = SELECT
483+public.job = SELECT, INSERT, UPDATE, DELETE
484+public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE
485+public.libraryfilecontent = SELECT, INSERT
486+public.packagebuild = SELECT, INSERT
487+public.packageset = SELECT, INSERT
488+public.packagesetgroup = SELECT, INSERT
489+public.packagesetinclusion = SELECT, INSERT
490+public.packagesetsources = SELECT, INSERT
491+public.packageupload = SELECT
492+public.packaging = SELECT, INSERT
493+public.person = SELECT
494+public.pocketchroot = SELECT
495+public.processor = SELECT
496+public.processorfamily = SELECT
497+public.section = SELECT
498+public.sectionselection = SELECT, INSERT
499+public.sourcepackageformatselection = SELECT, INSERT
500+public.sourcepackagename = SELECT
501+public.sourcepackagepublishinghistory = SELECT, INSERT
502+public.sourcepackagerelease = SELECT
503+public.sourcepackagereleasefile = SELECT, INSERT, UPDATE
504 type=user
505
506 [distroseriesdifferencejob]
507@@ -1183,7 +1186,7 @@
508 public.distribution = SELECT
509 public.emailaddress = SELECT
510 public.faq = SELECT
511-public.job = SELECT, UPDATE
512+public.job = SELECT, INSERT, UPDATE
513 public.language = SELECT
514 public.message = SELECT, INSERT
515 public.messagechunk = SELECT, INSERT
516@@ -1192,7 +1195,7 @@
517 public.product = SELECT
518 public.question = SELECT, UPDATE
519 public.questionbug = SELECT
520-public.questionjob = SELECT
521+public.questionjob = SELECT, INSERT
522 public.questionmessage = SELECT, INSERT
523 public.questionsubscription = SELECT
524 public.sourcepackagename = SELECT
525@@ -1285,6 +1288,7 @@
526 public.project = SELECT, UPDATE
527 public.question = SELECT
528 public.questionbug = SELECT
529+public.questionjob = SELECT, INSERT
530 public.questionsubscription = SELECT
531 public.section = SELECT, INSERT
532 public.sectionselection = SELECT
533@@ -1389,6 +1393,7 @@
534 public.publisherconfig = SELECT
535 public.question = SELECT
536 public.questionbug = SELECT
537+public.questionjob = SELECT, INSERT
538 public.questionsubscription = SELECT
539 public.section = SELECT
540 public.sectionselection = SELECT
541@@ -1464,6 +1469,7 @@
542 public.project = SELECT, UPDATE
543 public.question = SELECT
544 public.questionbug = SELECT
545+public.questionjob = SELECT, INSERT
546 public.questionsubscription = SELECT
547 public.section = SELECT
548 public.sourcepackagename = SELECT
549@@ -1667,6 +1673,7 @@
550 public.project = SELECT, UPDATE
551 public.question = SELECT, UPDATE
552 public.questionbug = SELECT
553+public.questionjob = SELECT, INSERT
554 public.questionmessage = SELECT, INSERT
555 public.questionsubscription = SELECT
556 public.section = SELECT
557@@ -2026,6 +2033,7 @@
558 public.project = SELECT, UPDATE
559 public.pushmirroraccess = SELECT, UPDATE
560 public.question = SELECT, UPDATE
561+public.questionjob = SELECT, UPDATE
562 public.questionreopening = SELECT, UPDATE
563 public.questionsubscription = SELECT, UPDATE, DELETE
564 public.revisionauthor = SELECT, UPDATE
565
566=== modified file 'lib/lp/answers/browser/tests/views.txt'
567--- lib/lp/answers/browser/tests/views.txt 2011-04-27 13:59:57 +0000
568+++ lib/lp/answers/browser/tests/views.txt 2011-05-04 17:17:01 +0000
569@@ -19,22 +19,6 @@
570 >>> firefox_question.subscribe(firefox_question.owner)
571 <QuestionSubscription...>
572
573- # Let's define a helper function which commits the transaction, so
574- # that the notifications are queued in stub.test_emails and pops these
575- # notifications from the queue.
576-
577- >>> from lp.services.mail import stub
578- >>> import email
579- >>> import transaction
580- >>> def pop_notifications():
581- ... transaction.commit()
582- ... notifications = [
583- ... email.message_from_string(raw_message)
584- ... for fromaddr, toaddrs, raw_message in sorted(stub.test_emails)
585- ... ]
586- ... stub.test_emails = []
587- ... return notifications
588-
589
590 QuestionSubscriptionView
591 ------------------------
592@@ -84,11 +68,6 @@
593 >>> view.request.response.getHeader('Location')
594 '.../+question/3'
595
596-These two actions didn't generate any notification mails:
597-
598- >>> len(pop_notifications())
599- 0
600-
601
602 QuestionWorkflowView
603 --------------------
604@@ -155,13 +134,6 @@
605 >>> workflow_harness.redirectionTarget()
606 '.../+question/2'
607
608-Workflow actions like these will send out notifications to subscribers.
609-(Complete notifications testing will be found in answer-tracker-
610-notifications.txt)
611-
612- >>> len(pop_notifications())
613- 1
614-
615 The available actions for that other user are still comment, give an
616 answer or request more information:
617
618@@ -347,10 +319,6 @@
619 >>> workflow_harness.redirectionTarget()
620 '.../+question/2'
621
622- # Clear all notifications.
623-
624- >>> notifications = pop_notifications()
625-
626
627 QuestionMakeBugView
628 -------------------
629@@ -359,9 +327,6 @@
630 question. In addition to creating a bug, this operation will also link
631 the bug to the question.
632
633-If the user creates a bug, a "Linked to bug" notification is sent and
634-the user is subscribed to the bug.
635-
636 >>> login('foo.bar@canonical.com')
637 >>> request = LaunchpadTestRequest(
638 ... form={'field.actions.create': 'Create',
639@@ -393,19 +358,6 @@
640 >>> 'Bug #%s created.' % new_bug_id in message[0]
641 True
642
643- >>> notifications = pop_notifications()
644- >>> len(notifications)
645- 1
646-
647- >>> print notifications[0].get_payload(decode=True)
648- Your question #3...
649- ...
650- Linked to bug: #...
651-
652- http://bugs.launchpad.dev/bugs/...
653- "Bug title"
654- ...
655-
656 If the question already has bugs linked to it, no new bug can be
657 created.
658
659@@ -471,10 +423,6 @@
660 >>> print firefox_question.status.title
661 Solved
662
663- # Clear the notification.
664-
665- >>> notifications = pop_notifications()
666-
667
668 QuestionEditView
669 ----------------
670@@ -586,10 +534,6 @@
671 >>> print question_three.product.name
672 firefox
673
674- # Clear out the pending notifications.
675-
676- >>> notifications = pop_notifications()
677-
678 # Reassign back the question to ubuntu
679
680 >>> question_three.target = ubuntu
681
682=== modified file 'lib/lp/answers/doc/notifications.txt'
683--- lib/lp/answers/doc/notifications.txt 2011-04-27 13:59:57 +0000
684+++ lib/lp/answers/doc/notifications.txt 2011-05-04 17:17:01 +0000
685@@ -7,9 +7,9 @@
686 notification looks like:
687
688 >>> from zope.event import notify
689- >>> from lazr.lifecycle.event import ObjectCreatedEvent
690+ >>> from lp.answers.tests.test_question_notifications import (
691+ ... pop_questionemailjobs)
692 >>> from lp.registry.interfaces.distribution import IDistributionSet
693- >>> from lp.testing.mail_helpers import pop_notifications
694 >>> login('test@canonical.com')
695 >>> sample_person = getUtility(ILaunchBag).user
696 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
697@@ -29,7 +29,7 @@
698 >>> [sub.person.displayname for sub in ubuntu_question.subscriptions]
699 [u'Sample Person']
700
701- >>> notifications = pop_notifications()
702+ >>> notifications = pop_questionemailjobs()
703 >>> len(notifications)
704 1
705
706@@ -41,44 +41,26 @@
707 Danilo have a story worth telling.
708
709 >>> add_notification = notifications[0]
710- >>> add_notification['From']
711- 'Sample Person <question...@answers.launchpad.net>'
712-
713- >>> add_notification['Reply-To']
714- 'question...@answers.launchpad.net'
715-
716- >>> add_notification['To']
717- 'test@canonical.com'
718-
719- >>> add_notification['Subject']
720- "[Question #...]: Can't install Ubuntu"
721+
722+ >>> print add_notification.subject
723+ [Question #...]: Can't install Ubuntu
724
725 Like all Launchpad notifications should, the message contain in the
726 footer the reason why the user is receiving the notification.
727
728- >>> notification_body = add_notification.get_payload(decode=True)
729- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
730+ >>> print add_notification.body
731 New question #... on Ubuntu:
732 http://.../ubuntu/+question/...
733 <BLANKLINE>
734 I insert the install CD in the CD-ROM drive, but it won't boot.
735- <BLANKLINE>
736- --...
737- You received this question notification because you asked the question.
738
739 The notification also includes a 'X-Launchpad-Question' header that
740 contains information about the question.
741
742- >>> print add_notification['X-Launchpad-Question']
743+ >>> print add_notification.headers['X-Launchpad-Question']
744 distribution=ubuntu; sourcepackage=None; status=Open;
745 assignee=None; priority=Normal; language=en
746
747-As well as the standard 'X-Launchpad-Message-Rationale' header that
748-contains in short format the reason for the user to be contacted.
749-
750- >>> print add_notification['X-Launchpad-Message-Rationale']
751- Asker
752-
753 Register the Ubuntu Team as Ubuntu's answer contact, so that they get
754 notified about the changes as well:
755
756@@ -124,17 +106,12 @@
757 Three copies of the notification got sent, one to Sample Person, one to
758 Foo Bar, and one to Ubuntu Team:
759
760- >>> from operator import itemgetter
761- >>> notifications = sorted(pop_notifications(), key=itemgetter('To'))
762- >>> [notification['To'] for notification in notifications]
763- ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
764-
765- >>> edit_notification = notifications[0]
766- >>> notification_body = edit_notification.get_payload(decode=True)
767- >>> print edit_notification['Subject']
768+ >>> notifications = pop_questionemailjobs()
769+ >>> edit_notification = notifications[1]
770+ >>> print edit_notification.subject
771 Re: [Question #...]: Installer doesn't work on a Mac
772
773- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
774+ >>> print edit_notification.body
775 Question #... libstdc++ in Ubuntu changed:
776 http://.../ubuntu/+source/libstdc++/+question/...
777 <BLANKLINE>
778@@ -148,10 +125,6 @@
779 drive, but it won't boot.
780 <BLANKLINE>
781 It boots straight into MacOS 9.
782- <BLANKLINE>
783- --...
784- You received this question notification because you are the assignee for
785- this question.
786
787 # XXX flacoste 2006-09-19: Add checks for notification of change to #
788 status whiteboard, priority. For example, if a question is # transferred
789@@ -163,21 +136,13 @@
790 >>> ubuntu_question.target = ubuntu
791 >>> notify(ObjectModifiedEvent(
792 ... ubuntu_question, unmodified_question, ['target']))
793- >>> notifications = sorted(pop_notifications(), key=itemgetter('To'))
794- >>> [notification['To'] for notification in notifications]
795- ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
796-
797- >>> edit_notification = notifications[0]
798- >>> notification_body = edit_notification.get_payload(decode=True)
799- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
800+ >>> notifications = pop_questionemailjobs()
801+ >>> edit_notification = notifications[1]
802+ >>> print edit_notification.body
803 Question #... Ubuntu changed:
804 http://.../ubuntu/+question/...
805 <BLANKLINE>
806 Project: libstdc++ in Ubuntu => Ubuntu
807- <BLANKLINE>
808- --...
809- You received this question notification because you are the assignee for
810- this question.
811
812 Changing the assignee will trigger a notification.
813
814@@ -187,21 +152,13 @@
815 >>> ubuntu_question.assignee = no_priv
816 >>> notify(ObjectModifiedEvent(
817 ... ubuntu_question, unmodified_question, ['assignee']))
818- >>> notifications = sorted(pop_notifications(), key=itemgetter('To'))
819- >>> [notification['To'] for notification in notifications]
820- ['no-priv@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
821-
822- >>> edit_notification = notifications[0]
823- >>> notification_body = edit_notification.get_payload(decode=True)
824- >>> print notification_body
825+ >>> notifications = pop_questionemailjobs()
826+ >>> edit_notification = notifications[1]
827+ >>> print edit_notification.body
828 Question #... Ubuntu changed:
829 http://.../ubuntu/+question/...
830 <BLANKLINE>
831 Assignee: Foo Bar => No Privileges Person
832- <BLANKLINE>
833- --...
834- You received this question notification because you are the assignee for
835- this question.
836
837 If we trigger a modification event when no changes worth notifying about
838 was made, no notification is sent:
839@@ -211,7 +168,7 @@
840 >>> notify(ObjectModifiedEvent(
841 ... ubuntu_question, unmodified_question, ['status']))
842
843- >>> notifications = pop_notifications()
844+ >>> notifications = pop_questionemailjobs()
845 >>> len(notifications)
846 0
847
848@@ -245,23 +202,18 @@
849 >>> notify(ObjectModifiedEvent(
850 ... ubuntu_question, unmodified_question, ['bugs']))
851
852- >>> notifications = pop_notifications()
853+ >>> notifications = pop_questionemailjobs()
854 >>> len(notifications)
855 2
856
857- >>> edit_notification = notifications[0]
858- >>> notification_body = edit_notification.get_payload(decode=True)
859- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
860+ >>> edit_notification = notifications[1]
861+ >>> print edit_notification.body
862 Question #... on Ubuntu changed:
863 http://.../ubuntu/+question/...
864 <BLANKLINE>
865 Linked to bug: #...
866 http://.../bugs/...
867 "Installer fails on a Mac PPC"
868- <BLANKLINE>
869- --...
870- You received this question notification because you are a member of
871- Ubuntu Team, which is an answer contact for Ubuntu.
872
873
874 Bug Unlinked Notification
875@@ -277,23 +229,18 @@
876 >>> notify(ObjectModifiedEvent(
877 ... ubuntu_question, unmodified_question, ['bugs']))
878
879- >>> notifications = pop_notifications()
880+ >>> notifications = pop_questionemailjobs()
881 >>> len(notifications)
882 2
883
884- >>> edit_notification = notifications[0]
885- >>> notification_body = edit_notification.get_payload(decode=True)
886- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
887+ >>> edit_notification = notifications[1]
888+ >>> print edit_notification.body
889 Question #... on Ubuntu changed:
890 http://.../ubuntu/+question/...
891 <BLANKLINE>
892 Removed link to bug: #...
893 http://.../bugs/...
894 "Installer fails on a Mac PPC"
895- <BLANKLINE>
896- --...
897- You received this question notification because you are a member of
898- Ubuntu Team, which is an answer contact for Ubuntu.
899
900
901 Linked Bug Status Changed Notification
902@@ -314,13 +261,10 @@
903 >>> request_message = ubuntu_question.requestInfo(
904 ... no_priv, "What is your Mac model?")
905
906- >>> notifications = pop_notifications()
907- >>> [email_msg['To'] for email_msg in notifications]
908- ['support@ubuntu.com', 'test@canonical.com']
909-
910- >>> support_notification = notifications[0]
911- >>> support_notification['Subject']
912- "Re: [Question #...]: Installer doesn't work on a Mac"
913+ >>> notifications = pop_questionemailjobs()
914+ >>> support_notification = notifications[1]
915+ >>> print support_notification.subject
916+ Re: [Question #...]: Installer doesn't work on a Mac
917
918 For workflow notifications, the content of the notification is slightly
919 different based on whether you are the question owner or somebody else.
920@@ -328,8 +272,7 @@
921 For example, the notification to the answer contacts and every other
922 subscribers except the question owner will look like this:
923
924- >>> notification_body = support_notification.get_payload(decode=True)
925- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
926+ >>> print support_notification.body
927 Question #... on Ubuntu changed:
928 http://.../ubuntu/+question/...
929 <BLANKLINE>
930@@ -337,16 +280,11 @@
931 <BLANKLINE>
932 No Privileges Person requested more information:
933 What is your Mac model?
934- <BLANKLINE>
935- --...
936- You received this question notification because you are a member of
937- Ubuntu Team, which is an answer contact for Ubuntu.
938
939 But the owner notification has a slightly different preamble and has an
940 extra footer.
941
942- >>> notification_body = notifications[1].get_payload(decode=True)
943- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
944+ >>> print notifications[0].body
945 Your question #... on Ubuntu changed:
946 http://.../ubuntu/+question/...
947 <BLANKLINE>
948@@ -359,8 +297,6 @@
949 To answer this request for more information, you can either reply to
950 this email or enter your reply at the following page:
951 http://.../ubuntu/+question/...
952- <BLANKLINE>
953- You received this question notification because you asked the question.
954
955 Of course, if the owner unsubscribe from the question, he won't receives
956 a notification.
957@@ -369,12 +305,8 @@
958 >>> ubuntu_question.unsubscribe(sample_person)
959 >>> message = ubuntu_question.giveInfo('A PowerMac 7200.')
960
961- >>> notifications = pop_notifications()
962- >>> [email_msg['To'] for email_msg in notifications]
963- ['support@ubuntu.com']
964-
965- >>> notification_body = notifications[0].get_payload(decode=True)
966- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
967+ >>> notifications = pop_questionemailjobs()
968+ >>> print notifications[1].body
969 Question #... on Ubuntu changed:
970 http://.../ubuntu/+question/...
971 <BLANKLINE>
972@@ -382,15 +314,11 @@
973 <BLANKLINE>
974 Sample Person gave more information on the question:
975 A PowerMac 7200.
976- <BLANKLINE>
977- --...
978- You received this question notification because you are a member of
979- Ubuntu Team, which is an answer contact for Ubuntu.
980
981 The notification for new messages on the question contain a 'References'
982 header to the previous message for threading purpose.
983
984- >>> references = notifications[0]['References']
985+ >>> references = notifications[0].headers['References']
986 >>> print references
987 <...>
988
989@@ -413,15 +341,11 @@
990 >>> login('no-priv@canonical.com')
991 >>> message = ubuntu_question.expireQuestion(
992 ... no_priv, "Expired because of no recent activity.")
993-
994- >>> notifications = pop_notifications()
995- >>> [email_msg['To'] for email_msg in notifications]
996- ['support@ubuntu.com', 'test@canonical.com']
997+ >>> notifications = pop_questionemailjobs()
998
999 Default notification when the question is expired:
1000
1001- >>> notification_body = notifications[0].get_payload(decode=True)
1002- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1003+ >>> print notifications[1].body
1004 Question #... on Ubuntu changed:
1005 http://.../ubuntu/+question/...
1006 <BLANKLINE>
1007@@ -430,14 +354,10 @@
1008 No Privileges Person expired the question:
1009 Expired because of no recent activity.
1010 <BLANKLINE>
1011- --...
1012- You received this question notification because you are a member of
1013- Ubuntu Team, which is an answer contact for Ubuntu.
1014
1015 Notification received by the owner:
1016
1017- >>> notification_body = notifications[1].get_payload(decode=True)
1018- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1019+ >>> print notifications[0].body
1020 Your question #... on Ubuntu changed:
1021 http://.../ubuntu/+question/...
1022 <BLANKLINE>
1023@@ -451,8 +371,6 @@
1024 by replying to this email or by going to the following page and
1025 entering more information about your problem:
1026 http://.../ubuntu/+question/...
1027- <BLANKLINE>
1028- You received this question notification because you asked the question.
1029
1030
1031 Notifications for reopen()
1032@@ -473,20 +391,16 @@
1033 ... "newbie."),
1034 ... owner=sample_person)
1035 >>> message = ubuntu_question.reopen(email_msg)
1036-
1037- >>> notifications = pop_notifications()
1038- >>> [email_msg['To'] for email_msg in notifications]
1039- ['support@ubuntu.com', 'test@canonical.com']
1040+ >>> notifications = pop_questionemailjobs()
1041
1042 Notice also how the 'Re' handling is handled nicely:
1043
1044- >>> print notifications[0]['Subject']
1045+ >>> print notifications[0].subject
1046 Re: [Question #...]: Installer doesn't work on a Mac
1047
1048 Default notification when the owner reopens the question:
1049
1050- >>> notification_body = notifications[0].get_payload(decode=True)
1051- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1052+ >>> print notifications[1].body
1053 Question #... on Ubuntu changed:
1054 http://.../ubuntu/+question/...
1055 <BLANKLINE>
1056@@ -497,15 +411,10 @@
1057 useful.
1058 <BLANKLINE>
1059 Please provide some help to a newbie.
1060- <BLANKLINE>
1061- --...
1062- You received this question notification because you are a member of
1063- Ubuntu Team, which is an answer contact for Ubuntu.
1064
1065 Notification received by the owner:
1066
1067- >>> notification_body = notifications[1].get_payload(decode=True)
1068- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1069+ >>> print notifications[0].body
1070 Your question #... on Ubuntu changed:
1071 http://.../ubuntu/+question/...
1072 <BLANKLINE>
1073@@ -516,9 +425,6 @@
1074 useful.
1075 <BLANKLINE>
1076 Please provide some help to a newbie.
1077- <BLANKLINE>
1078- --...
1079- You received this question notification because you asked the question.
1080
1081
1082 Notifications for giveAnswer()
1083@@ -533,14 +439,11 @@
1084 ... "https://help.ubuntu.com/community/Installation/OldWorldMacs "
1085 ... "for all the details.")
1086
1087- >>> notifications = pop_notifications()
1088- >>> [email_msg['To'] for email_msg in notifications]
1089- ['support@ubuntu.com', 'test@canonical.com']
1090+ >>> notifications = pop_questionemailjobs()
1091
1092 Default notification when an answer is proposed:
1093
1094- >>> notification_body = notifications[0].get_payload(decode=True)
1095- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1096+ >>> print notifications[1].body
1097 Question #... on Ubuntu changed:
1098 http://.../ubuntu/+question/...
1099 <BLANKLINE>
1100@@ -553,15 +456,10 @@
1101 <BLANKLINE>
1102 Consult https://help.ubuntu.com/community/Installation/OldWorldMacs for
1103 all the details.
1104- <BLANKLINE>
1105- --...
1106- You received this question notification because you are a member of
1107- Ubuntu Team, which is an answer contact for Ubuntu.
1108
1109 Notification received by the owner:
1110
1111- >>> notification_body = notifications[1].get_payload(decode=True)
1112- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1113+ >>> print notifications[0].body
1114 Your question #... on Ubuntu changed:
1115 http://.../ubuntu/+question/...
1116 <BLANKLINE>
1117@@ -583,8 +481,6 @@
1118 If you still need help, you can reply to this email or go to the
1119 following page to enter your feedback:
1120 http://.../ubuntu/+question/...
1121- <BLANKLINE>
1122- You received this question notification because you asked the question.
1123
1124
1125 Notifications for confirm()
1126@@ -595,14 +491,11 @@
1127 ... "I've installed BootX and the installer CD is now booting. "
1128 ... "Thanks!", answer=answer_message)
1129
1130- >>> notifications = pop_notifications()
1131- >>> [email_msg['To'] for email_msg in notifications]
1132- ['support@ubuntu.com', 'test@canonical.com']
1133+ >>> notifications = pop_questionemailjobs()
1134
1135 Default notification when the owner confirms an answer:
1136
1137- >>> notification_body = notifications[0].get_payload(decode=True)
1138- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1139+ >>> print notifications[1].body
1140 Question #... on Ubuntu changed:
1141 http://.../ubuntu/+question/...
1142 <BLANKLINE>
1143@@ -610,15 +503,10 @@
1144 <BLANKLINE>
1145 Sample Person confirmed that the question is solved:
1146 I've installed BootX and the installer CD is now booting. Thanks!
1147- <BLANKLINE>
1148- --...
1149- You received this question notification because you are a member of
1150- Ubuntu Team, which is an answer contact for Ubuntu.
1151
1152 Notification received by the owner:
1153
1154- >>> notification_body = notifications[1].get_payload(decode=True)
1155- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1156+ >>> print notifications[0].body
1157 Your question #... on Ubuntu changed:
1158 http://.../ubuntu/+question/...
1159 <BLANKLINE>
1160@@ -626,9 +514,6 @@
1161 <BLANKLINE>
1162 You confirmed that the question is solved:
1163 I've installed BootX and the installer CD is now booting. Thanks!
1164- <BLANKLINE>
1165- --...
1166- You received this question notification because you asked the question.
1167
1168
1169 Notifications for addComment()
1170@@ -639,38 +524,27 @@
1171 ... no_priv, "Unless you have lots of RAM... and even then, the "
1172 ... "system will probably be very slow.")
1173
1174- >>> notifications = pop_notifications()
1175- >>> [email_msg['To'] for email_msg in notifications]
1176- ['support@ubuntu.com', 'test@canonical.com']
1177+ >>> notifications = pop_questionemailjobs()
1178
1179 Default notification when a comment is posted:
1180
1181- >>> notification_body = notifications[0].get_payload(decode=True)
1182- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1183+ >>> print notifications[1].body
1184 Question #... on Ubuntu changed:
1185 http://.../ubuntu/+question/...
1186 <BLANKLINE>
1187 No Privileges Person posted a new comment:
1188 Unless you have lots of RAM... and even then, the system will probably
1189 be very slow.
1190- <BLANKLINE>
1191- --...
1192- You received this question notification because you are a member of
1193- Ubuntu Team, which is an answer contact for Ubuntu.
1194
1195 Notification received by the owner:
1196
1197- >>> notification_body = notifications[1].get_payload(decode=True)
1198- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1199+ >>> print notifications[0].body
1200 Your question #... on Ubuntu changed:
1201 http://.../ubuntu/+question/...
1202 <BLANKLINE>
1203 No Privileges Person posted a new comment:
1204 Unless you have lots of RAM... and even then, the system will probably
1205 be very slow.
1206- <BLANKLINE>
1207- --...
1208- You received this question notification because you asked the question.
1209
1210
1211 Notifications for reject()
1212@@ -681,14 +555,11 @@
1213 >>> message = ubuntu_question.reject(
1214 ... foo_bar, "Yeah! It will be awfully slow.")
1215
1216- >>> notifications = pop_notifications()
1217- >>> [email_msg['To'] for email_msg in notifications]
1218- ['support@ubuntu.com', 'test@canonical.com']
1219+ >>> notifications = pop_questionemailjobs()
1220
1221 Default notification when the question is rejected:
1222
1223- >>> notification_body = notifications[0].get_payload(decode=True)
1224- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1225+ >>> print notifications[1].body
1226 Question #... on Ubuntu changed:
1227 http://.../ubuntu/+question/...
1228 <BLANKLINE>
1229@@ -697,14 +568,10 @@
1230 Foo Bar rejected the question:
1231 Yeah! It will be awfully slow.
1232 <BLANKLINE>
1233- --...
1234- You received this question notification because you are a member of
1235- Ubuntu Team, which is an answer contact for Ubuntu.
1236
1237 Notification received by the owner:
1238
1239- >>> notification_body = notifications[1].get_payload(decode=True)
1240- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1241+ >>> print notifications[0].body
1242 Your question #... on Ubuntu changed:
1243 http://.../ubuntu/+question/...
1244 <BLANKLINE>
1245@@ -718,8 +585,6 @@
1246 explaining your point of view either by replying to this email or at
1247 the following page:
1248 http://.../ubuntu/+question/...
1249- <BLANKLINE>
1250- You received this question notification because you asked the question.
1251
1252
1253 Notifications for setStatus()
1254@@ -730,14 +595,11 @@
1255 >>> message = ubuntu_question.setStatus(
1256 ... foo_bar, QuestionStatus.SOLVED, "The rejection was a mistake.")
1257
1258- >>> notifications = pop_notifications()
1259- >>> [email_msg['To'] for email_msg in notifications]
1260- ['support@ubuntu.com', 'test@canonical.com']
1261+ >>> notifications = pop_questionemailjobs()
1262
1263 Default notification when somebody changes the status:
1264
1265- >>> notification_body = notifications[0].get_payload(decode=True)
1266- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1267+ >>> print notifications[1].body
1268 Question #... on Ubuntu changed:
1269 http://.../ubuntu/+question/...
1270 <BLANKLINE>
1271@@ -745,15 +607,10 @@
1272 <BLANKLINE>
1273 Foo Bar changed the question status:
1274 The rejection was a mistake.
1275- <BLANKLINE>
1276- --...
1277- You received this question notification because you are a member of
1278- Ubuntu Team, which is an answer contact for Ubuntu.
1279
1280 Notification received by the owner:
1281
1282- >>> notification_body = notifications[1].get_payload(decode=True)
1283- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1284+ >>> print notifications[0].body
1285 Your question #... on Ubuntu changed:
1286 http://.../ubuntu/+question/...
1287 <BLANKLINE>
1288@@ -761,9 +618,6 @@
1289 <BLANKLINE>
1290 Foo Bar changed the question status:
1291 The rejection was a mistake.
1292- <BLANKLINE>
1293- --...
1294- You received this question notification because you asked the question.
1295
1296
1297 Notifications for linkFAQ()
1298@@ -777,10 +631,7 @@
1299 >>> firefox = getUtility(IProductSet).getByName('firefox')
1300 >>> firefox_question = firefox.newQuestion(
1301 ... no_priv, 'How can I play Flash?', 'I want Flash!')
1302-
1303- # Discard notifications.
1304-
1305- >>> notifications = pop_notifications()
1306+ >>> ignore = pop_questionemailjobs()
1307
1308 >>> login('test@canonical.com')
1309 >>> firefox_faq = firefox.getFAQ(10)
1310@@ -789,13 +640,9 @@
1311
1312 >>> message = firefox_question.linkFAQ(
1313 ... sample_person, firefox_faq, "Read the FAQ.")
1314-
1315- >>> notifications = pop_notifications()
1316- >>> [email_msg['To'] for email_msg in notifications]
1317- ['no-priv@canonical.com']
1318-
1319- >>> notification_body = notifications[0].get_payload(decode=True)
1320- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1321+ >>> notifications = pop_questionemailjobs()
1322+
1323+ >>> print notifications[0].body
1324 Your question #... on Mozilla Firefox changed:
1325 http://answers.launchpad.dev/firefox/+question/...
1326 <BLANKLINE>
1327@@ -814,13 +661,9 @@
1328
1329 >>> message = firefox_question.linkFAQ(
1330 ... sample_person, None, "Sorry, this wasn't so useful.")
1331-
1332- >>> notifications = pop_notifications()
1333- >>> [email_msg['To'] for email_msg in notifications]
1334- ['no-priv@canonical.com']
1335-
1336- >>> notification_body = notifications[0].get_payload(decode=True)
1337- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1338+ >>> notifications = pop_questionemailjobs()
1339+
1340+ >>> print notifications[0].body
1341 Your question #... on Mozilla Firefox changed:
1342 http://answers.launchpad.dev/firefox/+question/...
1343 <BLANKLINE>
1344@@ -841,49 +684,9 @@
1345 from bugs just like when a question is normally created.
1346
1347 >>> bug_question = ubuntu.createQuestionFromBug(bug)
1348- >>> notifications = pop_notifications()
1349+ >>> notifications = pop_questionemailjobs()
1350 >>> len(notifications)
1351- 4
1352-
1353- >>> [email_msg['To'] for email_msg in notifications]
1354- ['no-priv@canonical.com', 'no-priv@canonical.com',
1355- 'support@ubuntu.com', 'support@ubuntu.com']
1356-
1357-
1358-Notifications and Teams
1359------------------------
1360-
1361-When a team is subscribed to a question, there are two cases two
1362-consider. The first one is if the team has an email address set, a
1363-notification will only be sent to that address. (That email address is
1364-assumed to be a mailing list reaching all the team members.) We already
1365-saw an example of that case with the Ubuntu Team in the examples above.
1366-
1367-The other case is when the team doesn't have an email address set. In
1368-that case, all the team members will be notified individually.
1369-
1370- >>> launchpad_devs = getUtility(IPersonSet).getByName('launchpad')
1371- >>> ubuntu_question.subscribe(launchpad_devs)
1372- <QuestionSubscription...>
1373-
1374- >>> login('test@canonical.com')
1375- >>> message = ubuntu_question.addComment(sample_person, 'A comment.')
1376-
1377- >>> notifications = pop_notifications()
1378- >>> [email_msg['To'] for email_msg in notifications]
1379- ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
1380-
1381-Of course, if the user is also individually subscribed to the question,
1382-he will receives only one notification:
1383-
1384- >>> ubuntu_question.subscribe(foo_bar)
1385- <QuestionSubscription...>
1386-
1387- >>> message = ubuntu_question.addComment(sample_person, 'A comment.')
1388-
1389- >>> notifications = pop_notifications()
1390- >>> [email_msg['To'] for email_msg in notifications]
1391- ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
1392+ 3
1393
1394
1395 Notifications and Localized Questions
1396@@ -915,13 +718,10 @@
1397 ... u'corretamente e mostra a minha versao do java. No entanto, '
1398 ... u'mover o mouse na pagina faz com que o firefox quebre.'),
1399 ... language=getUtility(ILanguageSet)['pt_BR'])
1400- >>> notifications = pop_notifications()
1401- >>> [email_msg['To'] for email_msg in notifications]
1402- ['guilherme.salgado@canonical.com', 'test@canonical.com']
1403+ >>> notifications = pop_questionemailjobs()
1404
1405- >>> from email.Header import decode_header, make_header
1406- >>> unicode(make_header(decode_header(notifications[0]['Subject'])))
1407- u'[Question #...]: Abrir uma p\xe1gina que requer java quebra o firefox'
1408+ >>> print notifications[0].subject.encode('ASCII', 'backslashreplace')
1409+ [Question #...]: Abrir uma p\xe1gina que requer java quebra o firefox
1410
1411 Similarly, when a question in a non-English language is modified or its
1412 status changed, only the subscribers speaking that language will receive
1413@@ -931,9 +731,7 @@
1414 ... "Veja o screenshot: http://tinyurl.com/y8jq8z")
1415 <QuestionMessage...>
1416
1417- >>> notifications = pop_notifications()
1418- >>> [email_msg['To'] for email_msg in notifications]
1419- ['guilherme.salgado@canonical.com', 'test@canonical.com']
1420+ >>> ignore = pop_questionemailjobs()
1421
1422 The exception to these general rules is that when a question is created
1423 in language spoken by none of the answer contacts, each one will receive
1424@@ -949,43 +747,35 @@
1425 ... sample_person, title="Impossible d'installer Ubuntu",
1426 ... description=u"Le CD ne semble pas fonctionn\xe9.",
1427 ... language=french)
1428- >>> notifications = pop_notifications()
1429- >>> [email_msg['To'] for email_msg in notifications]
1430- ['guilherme.salgado@canonical.com', 'support@ubuntu.com',
1431- 'test@canonical.com']
1432+ >>> notifications = pop_questionemailjobs()
1433
1434- >>> notifications[0]['Subject']
1435- "[Question #...]: (French) Impossible d'installer Ubuntu"
1436+ >>> print notifications[1].subject
1437+ [Question #...]: (French) Impossible d'installer Ubuntu
1438
1439 # Define a function that will replace non-ascii character with
1440 # its unicoded encoded value.
1441 # Effectively replace u'\xe9' by '\\e9'.
1442
1443- >>> def escape_utf8_payload(message):
1444- ... charset = message.get_content_charset()
1445- ... content = unicode(message.get_payload(decode=True), charset)
1446- ... return content.encode('us-ascii', 'backslashreplace')
1447+ >>> def recode_text(notification):
1448+ ... return notification.body.encode('ASCII', 'backslashreplace')
1449
1450- >>> notification_body = escape_utf8_payload(notifications[0])
1451- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1452+ >>> notification_body = recode_text(notifications[1])
1453+ >>> print notification_body
1454 A question was asked in a language (French) spoken by
1455 none of the registered Ubuntu answer contacts.
1456 <BLANKLINE>
1457 http://.../ubuntu/+question/...
1458 <BLANKLINE>
1459 Le CD ne semble pas fonctionn\xe9...
1460- --...
1461- You received this question notification because you are an answer
1462- contact for Ubuntu.
1463
1464 The notification received by the question owner contain a warning that
1465 the question is in a language spoken by none of the answer contacts:
1466
1467- >>> notifications[-1]['Subject']
1468- "[Question #...]: Impossible d'installer Ubuntu"
1469+ >>> print notifications[0].subject
1470+ [Question #...]: Impossible d'installer Ubuntu
1471
1472- >>> notification_body = escape_utf8_payload(notifications[-1])
1473- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1474+ >>> notification_body = recode_text(notifications[0])
1475+ >>> print notification_body
1476 New question #... on Ubuntu:
1477 http://.../ubuntu/+question/...
1478 <BLANKLINE>
1479@@ -993,9 +783,6 @@
1480 <BLANKLINE>
1481 WARNING: This question is asked in a language (French)
1482 spoken by none of the registered Ubuntu answer contacts.
1483- <BLANKLINE>
1484- --...
1485- You received this question notification because you asked the question.
1486
1487 No notification will be sent to the answer contacts when this question
1488 is modified. Only the owner will receive a modification notification
1489@@ -1006,13 +793,10 @@
1490 >>> french_question.title = u"CD d'Ubuntu ne d\xe9marre pas"
1491 >>> notify(ObjectModifiedEvent(
1492 ... french_question, unmodified_question, ['title']))
1493-
1494- >>> notifications = pop_notifications()
1495- >>> [email_msg['To'] for email_msg in notifications]
1496- ['test@canonical.com']
1497-
1498- >>> notification_body = escape_utf8_payload(notifications[0])
1499- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1500+ >>> notifications = pop_questionemailjobs()
1501+
1502+ >>> notification_body = recode_text(notifications[0])
1503+ >>> print notification_body
1504 Your question #... on Ubuntu changed:
1505 http://.../ubuntu/+question/...
1506 <BLANKLINE>
1507@@ -1021,89 +805,3 @@
1508 <BLANKLINE>
1509 WARNING: This question is asked in a language (French)
1510 spoken by none of the registered Ubuntu answer contacts.
1511- <BLANKLINE>
1512- --...
1513- You received this question notification because you asked the question.
1514-
1515-
1516-Localized Questions and Teams
1517-.............................
1518-
1519-We will notify the team only if the question language is in one of the
1520-team's preferred languages. The languages spoken by the team members is
1521-unimportant.
1522-
1523-For example, the rosetta admins team becomes an Answer contact for
1524-English questions. Carlos speaks Spanish, and he is an answer contact
1525-for Ubuntu. He is also a member of the rosetta admins team. The team
1526-wont receive emails because of his membership when they become answer
1527-contacts too.
1528-
1529- >>> rosetta_admins = getUtility(IPersonSet).getByName('rosetta-admins')
1530- >>> [lang.code for lang in rosetta_admins.languages]
1531- []
1532-
1533- >>> rosetta_admins.addLanguage(getUtility(ILanguageSet)['en'])
1534- >>> carlos = getUtility(IPersonSet).getByName('carlos')
1535- >>> carlos.inTeam(rosetta_admins)
1536- True
1537-
1538- >>> spanish = getUtility(ILanguageSet)['es']
1539- >>> spanish in carlos.languages
1540- True
1541-
1542- >>> ubuntu.addAnswerContact(carlos)
1543- True
1544-
1545- >>> ubuntu.addAnswerContact(rosetta_admins)
1546- True
1547-
1548- >>> spanish_question = ubuntu.newQuestion(
1549- ... sample_person, title="Necesidad ayuda con Firefox",
1550- ... description="No puedo acceso al Internet en Firefox.",
1551- ... language=spanish)
1552- >>> notifications = pop_notifications()
1553- >>> [email_msg['To'] for email_msg in notifications]
1554- ['carlos@canonical.com', 'test@canonical.com']
1555-
1556- >>> ubuntu.removeAnswerContact(carlos)
1557- True
1558-
1559-But if the team languages attribute is set, this set of languages will
1560-be used. So, if the team only officially speaks French, it will only
1561-receive notifications about French (and English) questions.
1562-
1563- >>> rosetta_admins.addLanguage(french)
1564-
1565- # Resend the new message notification
1566-
1567- >>> notify(ObjectCreatedEvent(french_question))
1568- >>> notifications = pop_notifications()
1569- >>> [email_msg['To'] for email_msg in notifications]
1570- ['rosetta@launchpad.net', 'test@canonical.com']
1571-
1572-When the team doesn't use an explicit address. All team members will be
1573-contacted if the question language is supported. For example, the
1574-Launchpad Developers team doesn't have any preferred email address set.
1575-Its only member, Foo Bar will receive a notification if the team
1576-supported languages includes the question language:
1577-
1578- >>> launchpad_devs = getUtility(IPersonSet).getByName('launchpad')
1579- >>> list(launchpad_devs.languages)
1580- []
1581-
1582- >>> [member.name for member in launchpad_devs.activemembers]
1583- [u'name16']
1584-
1585- >>> launchpad_devs.addLanguage(spanish)
1586- >>> ubuntu.addAnswerContact(launchpad_devs)
1587- True
1588-
1589- # Resend the new message notification
1590-
1591- >>> notify(ObjectCreatedEvent(spanish_question))
1592- >>> notifications = pop_notifications()
1593- >>> [email_msg['To'] for email_msg in notifications]
1594- ['foo.bar@canonical.com', 'test@canonical.com']
1595-
1596-
1597
1598=== modified file 'lib/lp/answers/model/questionjob.py'
1599--- lib/lp/answers/model/questionjob.py 2011-04-28 18:40:45 +0000
1600+++ lib/lp/answers/model/questionjob.py 2011-05-04 17:17:01 +0000
1601@@ -51,6 +51,7 @@
1602 from lp.services.job.runner import BaseRunnableJob
1603 from lp.services.mail.mailwrapper import MailWrapper
1604 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
1605+from lp.services.mail.sendmail import format_address_for_person
1606 from lp.services.propertycache import cachedproperty
1607
1608
1609@@ -170,7 +171,7 @@
1610
1611 def getErrorRecipients(self):
1612 """See `IRunnableJob`."""
1613- return self.user
1614+ return [format_address_for_person(self.user)]
1615
1616 @property
1617 def from_address(self):
1618
1619=== modified file 'lib/lp/answers/notification.py'
1620--- lib/lp/answers/notification.py 2011-04-27 13:59:57 +0000
1621+++ lib/lp/answers/notification.py 2011-05-04 17:17:01 +0000
1622@@ -10,16 +10,17 @@
1623
1624 import os
1625
1626+from zope.component import getUtility
1627+
1628 from canonical.config import config
1629-from canonical.launchpad.mail import (
1630- format_address,
1631- simple_sendmail,
1632+from canonical.launchpad.webapp.publisher import canonical_url
1633+from lp.answers.enums import (
1634+ QuestionAction,
1635+ QuestionRecipientSet,
1636 )
1637-from canonical.launchpad.webapp.publisher import canonical_url
1638-from lp.answers.enums import QuestionAction
1639+from lp.answers.interfaces.questionjob import IQuestionEmailJobSource
1640 from lp.registry.interfaces.person import IPerson
1641 from lp.services.mail.mailwrapper import MailWrapper
1642-from lp.services.mail.notificationrecipientset import NotificationRecipientSet
1643 from lp.services.propertycache import cachedproperty
1644
1645
1646@@ -41,6 +42,8 @@
1647 QuestionNotification can be registered as event subscribers.
1648 """
1649
1650+ recipient_set = QuestionRecipientSet.ASKER_SUBSCRIBER
1651+
1652 def __init__(self, question, event):
1653 """Base constructor.
1654
1655@@ -51,25 +54,15 @@
1656 self.event = event
1657 self._user = IPerson(self.event.user)
1658 self.initialize()
1659+ self.job = None
1660 if self.shouldNotify():
1661- self.send()
1662+ self.job = self.enqueue()
1663
1664 @property
1665 def user(self):
1666 """Return the user from the event. """
1667 return self._user
1668
1669- def getFromAddress(self):
1670- """Return a formatted email address suitable for user in the From
1671- header of the question notification.
1672-
1673- Default is Event Person Display Name <question#@answertracker_domain>
1674- """
1675- return format_address(
1676- self.user.displayname,
1677- 'question%s@%s' % (
1678- self.question.id, config.answertracker.email_domain))
1679-
1680 def getSubject(self):
1681 """Return the subject of the notification.
1682
1683@@ -114,18 +107,6 @@
1684
1685 return headers
1686
1687- def getRecipients(self):
1688- """Return the recipient of the notification.
1689-
1690- Default to the question's subscribers that speaks the request
1691- languages. If the question owner is subscribed, he's always consider
1692- to speak the language.
1693-
1694- :return: A `INotificationRecipientSet` containing the recipients and
1695- rationale.
1696- """
1697- return self.question.getRecipients()
1698-
1699 def initialize(self):
1700 """Initialization hook for subclasses.
1701
1702@@ -144,32 +125,16 @@
1703 """
1704 return True
1705
1706- def buildBody(self, body, rationale):
1707- """Wrap the body and ensure the rationale is is separated."""
1708- wrapper = MailWrapper()
1709- body_parts = [body, wrapper.format(rationale)]
1710- if '\n-- ' not in body:
1711- body_parts.insert(1, '-- ')
1712- return '\n'.join(body_parts)
1713-
1714- def send(self):
1715- """Sends the notification to all the notification recipients.
1716-
1717- This method takes care of adding the rationale for contacting each
1718- recipient and also sets the X-Launchpad-Message-Rationale header on
1719- each message.
1720- """
1721- from_address = self.getFromAddress()
1722+ def enqueue(self):
1723+ """Create a job to send email about the event."""
1724 subject = self.getSubject()
1725 body = self.getBody()
1726 headers = self.getHeaders()
1727- recipients = self.getRecipients()
1728- for email in recipients.getEmails():
1729- rationale, header = recipients.getReason(email)
1730- headers['X-Launchpad-Message-Rationale'] = header
1731- formatted_body = self.buildBody(body, rationale)
1732- simple_sendmail(
1733- from_address, email, subject, formatted_body, headers)
1734+ job_source = getUtility(IQuestionEmailJobSource)
1735+ job = job_source.create(
1736+ self.question, self.user, self.recipient_set,
1737+ subject, body, headers)
1738+ return job
1739
1740 @property
1741 def unsupported_language(self):
1742@@ -215,6 +180,7 @@
1743 class QuestionModifiedDefaultNotification(QuestionNotification):
1744 """Base implementation of a notification when a question is modified."""
1745
1746+ recipient_set = QuestionRecipientSet.SUBSCRIBER
1747 # Email template used to render the body.
1748 body_template = "question-modified-notification.txt"
1749
1750@@ -347,18 +313,6 @@
1751
1752 return get_email_template(self.body_template) % replacements
1753
1754- def getRecipients(self):
1755- """The default notification goes to all question subscribers that
1756- speak the request language, except the owner.
1757- """
1758- original_recipients = QuestionNotification.getRecipients(self)
1759- recipients = NotificationRecipientSet()
1760- for person in original_recipients:
1761- if person != self.question.owner:
1762- rationale, header = original_recipients.getReason(person)
1763- recipients.add(person, rationale, header)
1764- return recipients
1765-
1766 # Header template used when a new message is added to the question.
1767 action_header_template = {
1768 QuestionAction.REQUESTINFO:
1769@@ -397,6 +351,7 @@
1770 class QuestionModifiedOwnerNotification(QuestionModifiedDefaultNotification):
1771 """Notification sent to the owner when his question is modified."""
1772
1773+ recipient_set = QuestionRecipientSet.ASKER
1774 # These actions will be done by the owner, so use the second person.
1775 action_header_template = dict(
1776 QuestionModifiedDefaultNotification.action_header_template)
1777@@ -426,16 +381,6 @@
1778 self.body_template = self.body_template_by_action.get(
1779 self.new_message.action, self.body_template)
1780
1781- def getRecipients(self):
1782- """Return the owner of the question if he's still subscribed."""
1783- recipients = NotificationRecipientSet()
1784- owner = self.question.owner
1785- original_recipients = self.question.direct_recipients
1786- if owner in self.question.direct_recipients:
1787- rationale, header = original_recipients.getReason(owner)
1788- recipients.add(owner, rationale, header)
1789- return recipients
1790-
1791 def getBody(self):
1792 """See QuestionNotification."""
1793 body = QuestionModifiedDefaultNotification.getBody(self)
1794@@ -447,6 +392,8 @@
1795 class QuestionUnsupportedLanguageNotification(QuestionNotification):
1796 """Notification sent to answer contacts for unsupported languages."""
1797
1798+ recipient_set = QuestionRecipientSet.CONTACT
1799+
1800 def getSubject(self):
1801 """See QuestionNotification."""
1802 return '[Question #%s]: (%s) %s' % (
1803@@ -457,10 +404,6 @@
1804 """Return True when the question is in an unsupported language."""
1805 return self.unsupported_language
1806
1807- def getRecipients(self):
1808- """Notify only the answer contacts."""
1809- return self.question.target.getAnswerContactRecipients(None)
1810-
1811 def getBody(self):
1812 """See QuestionNotification."""
1813 question = self.question
1814
1815=== removed file 'lib/lp/answers/stories/question-confirm-url.txt'
1816--- lib/lp/answers/stories/question-confirm-url.txt 2009-11-11 22:17:17 +0000
1817+++ lib/lp/answers/stories/question-confirm-url.txt 1970-01-01 00:00:00 +0000
1818@@ -1,105 +0,0 @@
1819-= Confirming an Answer using the Link in the Notification Email =
1820-
1821-When an answer is posted on a question, its owner will usually receive a
1822-notification by email. That email includes a link that can be used by
1823-the owner to confirm that the answer solved his problem.
1824-
1825- # We will use one browser objects for the owner, and one for the user
1826- # providing support, 'No Privileges Person' here.
1827-
1828- >>> owner_browser = setupBrowser(auth='Basic test@canonical.com:test')
1829- >>> support_browser = setupBrowser(
1830- ... auth='Basic no-priv@canonical.com:test')
1831-
1832-When the URL is used when the question isn't in the right state, the user
1833-will be redirected to the question page and a notification will be
1834-displayed:
1835-
1836- >>> owner_browser.open(
1837- ... 'http://launchpad.dev/firefox/+question/2/+confirm?'
1838- ... 'answer_id=1')
1839- >>> owner_browser.url
1840- 'http://.../firefox/+question/2'
1841-
1842- >>> soup = find_main_content(owner_browser.contents)
1843- >>> print soup.first('div', 'error message').renderContents()
1844- The question is not in a state where you can confirm an
1845- answer.
1846-
1847-Posting an answer on the question will send an email notification
1848-containing a link to confirm that answer.
1849-
1850- # First subscribe the owner, so that he receives the notification.
1851- >>> owner_browser.open(
1852- ... 'http://launchpad.dev/firefox/+question/2/+subscribe')
1853- >>> owner_browser.getControl('Subscribe').click()
1854-
1855- # Post the answer...
1856- >>> support_browser.open('http://launchpad.dev/firefox/+question/2')
1857- >>> support_browser.getControl('Message').value = (
1858- ... 'SVG is supported out of the box in recent versions of Firefox. '
1859- ... 'I suggest you upgrade your browser.')
1860- >>> support_browser.getControl('Add Answer').click()
1861-
1862- # ... and get the confirmation URL from the notification
1863- >>> import email
1864- >>> import re
1865- >>> from lp.services.mail import stub
1866- >>> notification = email.message_from_string(stub.test_emails[-1][2])
1867- >>> urls = re.findall('(http:[^\s]+)+', notification.get_payload())
1868- >>> confirm_url = urls[-2].decode('quoted-printable')
1869- >>> print confirm_url
1870- http://answers.launchpad.dev/firefox/+question/2/+confirm?answer_id=...
1871-
1872-If a cropped URL or forged URL is used, an UnexpectedFormData error will
1873-be displayed. In the following example, the answer_id parameter refers
1874-to an answer not part of that question, it mimics a badly handcrafted
1875-URL:
1876-
1877- >>> owner_browser.open(
1878- ... 'http://launchpad.dev/firefox/+question/2/+confirm?'
1879- ... 'answer_id=3')
1880- Traceback (most recent call last):
1881- ...
1882- UnexpectedFormData...
1883-
1884- >>> owner_browser.open(
1885- ... 'http://launchpad.dev/firefox/+question/2/+confirm')
1886- Traceback (most recent call last):
1887- ...
1888- UnexpectedFormData...
1889-
1890-The page is only accessible to the question owner:
1891-
1892- >>> support_browser.open(confirm_url)
1893- Traceback (most recent call last):
1894- ...
1895- Unauthorized...
1896-
1897-On the confirmation page, the user can see the answer that he is
1898-confirming.
1899-
1900- >>> owner_browser.open(confirm_url)
1901- >>> soup = find_main_content(owner_browser.contents)
1902- >>> for comment in soup.fetch('div', 'boardCommentBody'):
1903- ... print comment.renderContents()
1904- <p>SVG is supported out of the box in recent versions of Firefox. I
1905- suggest you upgrade your browser.</p>
1906-
1907-To confirm the answer, he needs to click the 'This Solved My Problem'
1908-button. He can enter an optional message along his confirmation.
1909-
1910- >>> owner_browser.getControl('Message').value = (
1911- ... "Thanks! This indeed solved the problem.")
1912- >>> owner_browser.getControl('This Solved My Problem').click()
1913-
1914-This adds his comment to the question and mark it as 'Solved.'
1915-
1916- >>> print extract_text(
1917- ... find_tag_by_id(owner_browser.contents, 'question-status'))
1918- Status: Solved ...
1919- >>> print find_tags_by_class(
1920- ... owner_browser.contents, 'boardCommentBody')[-1].renderContents()
1921- <p>Thanks! This indeed solved the problem.</p>
1922-
1923-
1924
1925=== modified file 'lib/lp/answers/tests/test_question_notifications.py'
1926--- lib/lp/answers/tests/test_question_notifications.py 2011-04-23 01:31:22 +0000
1927+++ lib/lp/answers/tests/test_question_notifications.py 2011-05-04 17:17:01 +0000
1928@@ -5,18 +5,43 @@
1929
1930 __metaclass__ = type
1931
1932+__all__ = [
1933+ 'pop_questionemailjobs',
1934+ ]
1935+
1936 from unittest import TestCase
1937
1938+from zope.component import getUtility
1939 from zope.interface import implements
1940+from zope.security.proxy import removeSecurityProxy
1941
1942+from canonical.testing import DatabaseFunctionalLayer
1943+from lp.answers.enums import QuestionRecipientSet
1944+from lp.answers.interfaces.questioncollection import IQuestionSet
1945+from lp.answers.model.questionjob import QuestionEmailJob
1946 from lp.answers.notification import (
1947 QuestionAddedNotification,
1948 QuestionModifiedDefaultNotification,
1949+ QuestionModifiedOwnerNotification,
1950+ QuestionNotification,
1951+ QuestionUnsupportedLanguageNotification,
1952 )
1953 from lp.registry.interfaces.person import IPerson
1954-
1955-
1956-class TestQuestionModifiedNotification(QuestionModifiedDefaultNotification):
1957+from lp.services.worlddata.interfaces.language import ILanguageSet
1958+from lp.testing import TestCaseWithFactory
1959+
1960+
1961+def pop_questionemailjobs():
1962+ jobs = sorted(
1963+ QuestionEmailJob.iterReady(),
1964+ key=lambda job: job.metadata["recipient_set"])
1965+ for job in jobs:
1966+ job.start()
1967+ job.complete()
1968+ return jobs
1969+
1970+
1971+class FakeQuestionModifiedNotification(QuestionModifiedDefaultNotification):
1972 """Subclass that do not send emails and with simpler initialization.
1973
1974 Since notifications are handlers that accomplish their action on
1975@@ -39,6 +64,7 @@
1976 self.id = id
1977 self.title = title
1978 self.owner = FakeUser()
1979+ self.messages = []
1980
1981
1982 class StubQuestionMessage:
1983@@ -56,6 +82,7 @@
1984 class FakeEvent:
1985 """A fake event."""
1986 user = FakeUser()
1987+ object_before_modification = StubQuestion()
1988
1989
1990 class QuestionModifiedDefaultNotificationTestCase(TestCase):
1991@@ -63,22 +90,13 @@
1992
1993 def setUp(self):
1994 """Create a notification with a fake question."""
1995- self.notification = TestQuestionModifiedNotification(
1996+ self.notification = FakeQuestionModifiedNotification(
1997 StubQuestion(), FakeEvent())
1998
1999- def test_buildBody_with_separator(self):
2000- # A body with a separator is preserved.
2001- formatted_body = self.notification.buildBody(
2002- "body\n-- ", "rationale")
2003- self.assertEqual(
2004- "body\n-- \nrationale", formatted_body)
2005-
2006- def test_buildBody_without_separator(self):
2007- # A separator will added to body if one is not present.
2008- formatted_body = self.notification.buildBody(
2009- "body -- mdash", "rationale")
2010- self.assertEqual(
2011- "body -- mdash\n-- \nrationale", formatted_body)
2012+ def test_recipient_set(self):
2013+ self.assertEqual(
2014+ QuestionRecipientSet.SUBSCRIBER,
2015+ self.notification.recipient_set)
2016
2017 def test_getSubject(self):
2018 """getSubject() when there is no message added to the question."""
2019@@ -90,25 +108,120 @@
2020 """The notification user is always the event user."""
2021 question = StubQuestion()
2022 event = FakeEvent()
2023- notification = TestQuestionModifiedNotification(question, event)
2024+ notification = FakeQuestionModifiedNotification(question, event)
2025 self.assertEqual(event.user, notification.user)
2026 self.assertNotEqual(question.owner, notification.user)
2027
2028
2029-class TestQuestionAddedNotification(QuestionAddedNotification):
2030- """A subclass that does not send emails."""
2031-
2032- def shouldNotify(self):
2033- return False
2034-
2035-
2036-class QuestionCreatedTestCase(TestCase):
2037+class FakeQuestionModifiedOwnerNotification(
2038+ QuestionModifiedOwnerNotification):
2039+ """A subclass that does not send emails."""
2040+
2041+ def shouldNotify(self):
2042+ return False
2043+
2044+
2045+class QuestionModifiedOwnerNotificationTestCase(TestCase):
2046+ """Test cases for mail notifications about owner modified questions."""
2047+
2048+ def setUp(self):
2049+ self.question = StubQuestion()
2050+ self.event = FakeEvent()
2051+ self.notification = FakeQuestionModifiedOwnerNotification(
2052+ self.question, self.event)
2053+
2054+ def test_recipient_set(self):
2055+ self.assertEqual(
2056+ QuestionRecipientSet.ASKER,
2057+ self.notification.recipient_set)
2058+
2059+
2060+class FakeQuestionAddedNotification(QuestionAddedNotification):
2061+ """A subclass that does not send emails."""
2062+
2063+ def shouldNotify(self):
2064+ return False
2065+
2066+
2067+class QuestionAddedNotificationTestCase(TestCase):
2068 """Test cases for mail notifications about created questions."""
2069
2070+ def setUp(self):
2071+ self.question = StubQuestion()
2072+ self.event = FakeEvent()
2073+ self.notification = FakeQuestionAddedNotification(
2074+ self.question, self.event)
2075+
2076+ def test_recipient_set(self):
2077+ self.assertEqual(
2078+ QuestionRecipientSet.ASKER_SUBSCRIBER,
2079+ self.notification.recipient_set)
2080+
2081 def test_user_is_question_owner(self):
2082 """The notification user is always the question owner."""
2083- question = StubQuestion()
2084+ self.assertEqual(self.question.owner, self.notification.user)
2085+ self.assertNotEqual(self.event.user, self.notification.user)
2086+
2087+
2088+class FakeQuestionUnsupportedLanguageNotification(
2089+ QuestionUnsupportedLanguageNotification):
2090+ """A subclass that does not send emails."""
2091+
2092+ def shouldNotify(self):
2093+ return False
2094+
2095+
2096+class QuestionUnsupportedLanguageNotificationTestCase(TestCase):
2097+ """Test notifications about questions with unsupported languages."""
2098+
2099+ def setUp(self):
2100+ self.question = StubQuestion()
2101+ self.event = FakeEvent()
2102+ self.notification = FakeQuestionUnsupportedLanguageNotification(
2103+ self.question, self.event)
2104+
2105+ def test_recipient_set(self):
2106+ self.assertEqual(
2107+ QuestionRecipientSet.CONTACT,
2108+ self.notification.recipient_set)
2109+
2110+
2111+class FakeQuestionNotification(QuestionNotification):
2112+ """A subclass to exercise question notifcations."""
2113+
2114+ recipient_set = QuestionRecipientSet.ASKER_SUBSCRIBER
2115+
2116+ def getBody(self):
2117+ return 'body'
2118+
2119+
2120+class QuestionNotificationTestCase(TestCaseWithFactory):
2121+ """Test common question notification behavior."""
2122+
2123+ layer = DatabaseFunctionalLayer
2124+
2125+ def makeQuestion(self):
2126+ """Create question that does not trigger a notification."""
2127+ asker = self.factory.makePerson()
2128+ product = self.factory.makeProduct()
2129+ naked_question_set = removeSecurityProxy(getUtility(IQuestionSet))
2130+ question = naked_question_set.new(
2131+ title='title', description='description', owner=asker,
2132+ language=getUtility(ILanguageSet)['en'],
2133+ product=product, distribution=None, sourcepackagename=None)
2134+ return question
2135+
2136+ def test_init_enqueue(self):
2137+ # Creating a question notification creates a queation email job.
2138+ question = self.makeQuestion()
2139 event = FakeEvent()
2140- notification = TestQuestionAddedNotification(question, event)
2141- self.assertEqual(question.owner, notification.user)
2142- self.assertNotEqual(event.user, notification.user)
2143+ event.user = self.factory.makePerson()
2144+ notification = FakeQuestionNotification(question, event)
2145+ self.assertEqual(
2146+ notification.recipient_set.name,
2147+ notification.job.metadata['recipient_set'])
2148+ self.assertEqual(notification.question, notification.job.question)
2149+ self.assertEqual(notification.user, notification.job.user)
2150+ self.assertEqual(notification.getSubject(), notification.job.subject)
2151+ self.assertEqual(notification.getBody(), notification.job.body)
2152+ self.assertEqual(notification.getHeaders(), notification.job.headers)
2153
2154=== modified file 'lib/lp/answers/tests/test_questionjob.py'
2155--- lib/lp/answers/tests/test_questionjob.py 2011-04-28 22:25:45 +0000
2156+++ lib/lp/answers/tests/test_questionjob.py 2011-05-04 17:17:01 +0000
2157@@ -11,6 +11,7 @@
2158 from testtools.content_type import UTF8_TEXT
2159
2160 from zope.component import getUtility
2161+from zope.security.proxy import removeSecurityProxy
2162
2163 from canonical.launchpad.interfaces.lpstorm import IStore
2164 from canonical.launchpad.mail import format_address
2165@@ -20,6 +21,7 @@
2166 QuestionJobType,
2167 QuestionRecipientSet,
2168 )
2169+from lp.answers.interfaces.questioncollection import IQuestionSet
2170 from lp.answers.interfaces.questionjob import IQuestionEmailJobSource
2171 from lp.answers.model.questionjob import (
2172 QuestionJob,
2173@@ -28,6 +30,7 @@
2174 from lp.services.job.interfaces.job import JobStatus
2175 from lp.services.log.logger import BufferLogger
2176 from lp.services.mail import stub
2177+from lp.services.mail.sendmail import format_address_for_person
2178 from lp.services.worlddata.interfaces.language import ILanguageSet
2179 from lp.testing import (
2180 run_script,
2181@@ -107,7 +110,14 @@
2182
2183 def test_iterReady(self):
2184 # Jobs in the ready state are returned by the iterator.
2185- question = self.factory.makeQuestion()
2186+ # Creating a question implicitly created an question email job.
2187+ asker = self.factory.makePerson()
2188+ product = self.factory.makeProduct()
2189+ naked_question_set = removeSecurityProxy(getUtility(IQuestionSet))
2190+ question = naked_question_set.new(
2191+ title='title', description='description', owner=asker,
2192+ language=getUtility(ILanguageSet)['en'],
2193+ product=product, distribution=None, sourcepackagename=None)
2194 user, subject, ignore, headers = self.makeUserSubjectBodyHeaders()
2195 job_1 = QuestionEmailJob.create(
2196 question, user, QuestionRecipientSet.SUBSCRIBER,
2197@@ -198,7 +208,8 @@
2198 job = QuestionEmailJob.create(
2199 question, user, QuestionRecipientSet.SUBSCRIBER,
2200 subject, body, headers)
2201- self.assertEqual(user, job.getErrorRecipients())
2202+ self.assertEqual(
2203+ [format_address_for_person(job.user)], job.getErrorRecipients())
2204
2205 def test_recipients_asker(self):
2206 # The recipients property contains the question owner.
2207@@ -323,8 +334,18 @@
2208 def test_run_cronscript(self):
2209 # The cronscript is configured: schema-lazr.conf and security.cfg.
2210 question = self.factory.makeQuestion()
2211+ with person_logged_in(question.target.owner):
2212+ question.linkBug(self.factory.makeBug(product=question.target))
2213+ question.linkFAQ(
2214+ question.target.owner,
2215+ self.factory.makeFAQ(target=question.target),
2216+ 'test FAQ link')
2217 self.addAnswerContact(question)
2218 user, subject, body, headers = self.makeUserSubjectBodyHeaders()
2219+ with person_logged_in(user):
2220+ lang_set = getUtility(ILanguageSet)
2221+ user.addLanguage(lang_set['en'])
2222+ question.target.addAnswerContact(user)
2223 job = QuestionEmailJob.create(
2224 question, user, QuestionRecipientSet.ASKER_SUBSCRIBER,
2225 subject, body, headers)
2226@@ -336,6 +357,8 @@
2227 self.addDetail("stdout", Content(UTF8_TEXT, lambda: out))
2228 self.addDetail("stderr", Content(UTF8_TEXT, lambda: err))
2229 self.assertEqual(0, exit_code)
2230+ self.assertTrue(
2231+ 'Traceback (most recent call last)' not in err)
2232 message = (
2233 'QuestionEmailJob has sent email for question %s.' % question.id)
2234 self.assertTrue(
2235
2236=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
2237--- lib/lp/bugs/tests/test_bugnotification.py 2011-04-05 22:34:35 +0000
2238+++ lib/lp/bugs/tests/test_bugnotification.py 2011-05-04 17:17:01 +0000
2239@@ -6,6 +6,7 @@
2240 __metaclass__ = type
2241
2242 from itertools import chain
2243+import transaction
2244 import unittest
2245
2246 from lazr.lifecycle.event import ObjectModifiedEvent
2247@@ -24,6 +25,7 @@
2248 LaunchpadFunctionalLayer,
2249 LaunchpadZopelessLayer,
2250 )
2251+from lp.answers.tests.test_question_notifications import pop_questionemailjobs
2252 from lp.bugs.interfaces.bugtask import (
2253 BugTaskStatus,
2254 IUpstreamBugTask,
2255@@ -36,7 +38,6 @@
2256 from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute
2257 from lp.testing import TestCaseWithFactory
2258 from lp.testing.factory import LaunchpadObjectFactory
2259-from lp.testing.mail_helpers import pop_notifications
2260 from lp.testing.matchers import Contains
2261
2262
2263@@ -120,8 +121,9 @@
2264 self.subscriber = self.factory.makePerson()
2265 question.subscribe(self.subscriber)
2266 question.linkBug(self.bug)
2267- # Flush pending notifications for question creation.
2268- pop_notifications()
2269+ # Flush pending jobs for question creation.
2270+ pop_questionemailjobs()
2271+ transaction.commit()
2272 self.layer.switchDbUser(config.malone.expiration_dbuser)
2273
2274 def test_notifications_for_question_subscribers(self):
2275@@ -134,10 +136,10 @@
2276 bug_modified = ObjectModifiedEvent(
2277 bugtask, bugtask_before_modification, ["status"])
2278 notify(bug_modified)
2279+ recipients = [
2280+ job.metadata['recipient_set'] for job in pop_questionemailjobs()]
2281 self.assertContentEqual(
2282- [self.product.owner.preferredemail.email,
2283- self.subscriber.preferredemail.email],
2284- [mail['To'] for mail in pop_notifications()])
2285+ ['ASKER_SUBSCRIBER'], recipients)
2286
2287
2288 class TestNotificationsLinkToFilters(TestCaseWithFactory):
2289@@ -254,7 +256,7 @@
2290 self.assertEqual(
2291 {self.subscriber: {'sources': sources,
2292 'filter descriptions': []},
2293- subscriber2: {'sources': sources2,
2294+ subscriber2: {'sources': sources2,
2295 'filter descriptions': [u'Special Filter!']}},
2296 BugNotificationSet().getRecipientFilterData(
2297 {self.subscriber: sources, subscriber2: sources2},
2298@@ -278,7 +280,7 @@
2299 # Perform the test.
2300 sources = list(self.notification.recipients)
2301 sources.extend(self.notification2.recipients)
2302- assert(len(sources)==2)
2303+ assert(len(sources) == 2)
2304 self.assertEqual(
2305 {self.subscriber: {'sources': sources,
2306 'filter descriptions': ['Another Filter!', 'Special Filter!']}},
2307@@ -316,7 +318,7 @@
2308 sources = list(self.notification.recipients)
2309 sources2 = list(notification2.recipients)
2310 self.assertEqual(
2311- {subscriber2: {'sources': sources2,
2312+ {subscriber2: {'sources': sources2,
2313 'filter descriptions': [u'Special Filter!']}},
2314 BugNotificationSet().getRecipientFilterData(
2315 {self.subscriber: sources, subscriber2: sources2},
2316
2317=== modified file 'lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt'
2318--- lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt 2010-10-18 22:24:59 +0000
2319+++ lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt 2011-05-04 17:17:01 +0000
2320@@ -1,4 +1,5 @@
2321-= Linked Bug Status Changed Notification =
2322+Linked Bug Status Changed Notification
2323+======================================
2324
2325 While a bug is linked to a question , its subscribers will be notified
2326 of changes to the bug status:
2327@@ -7,6 +8,8 @@
2328 >>> from zope.interface import providedBy
2329 >>> from lazr.lifecycle.event import ObjectModifiedEvent
2330 >>> from lazr.lifecycle.snapshot import Snapshot
2331+ >>> from lp.answers.tests.test_question_notifications import (
2332+ ... pop_questionemailjobs)
2333 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
2334 >>> from lp.registry.interfaces.person import IPersonSet
2335
2336@@ -15,20 +18,22 @@
2337 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
2338 >>> bugtask.transitionToStatus(BugTaskStatus.CONFIRMED, no_priv)
2339 >>> bugtask.statusexplanation = 'This bug really happened to me.'
2340+ >>> ignore = pop_questionemailjobs()
2341 >>> notify(ObjectModifiedEvent(
2342 ... bugtask, original_bugtask, ['status', 'statusexplanation'],
2343 ... user=no_priv))
2344
2345- >>> from lp.testing.mail_helpers import pop_notifications
2346- >>> notifications = pop_notifications()
2347+ >>> notifications = pop_questionemailjobs()
2348 >>> len(notifications)
2349- 2
2350- >>> [notification['To'] for notification in notifications]
2351- ['support@ubuntu.com', 'test@canonical.com']
2352- >>> notification_body = notifications[0].get_payload(decode=True)
2353- >>> print notifications[0]['Subject']
2354+ 1
2355+
2356+ >>> print notifications[0].metadata['recipient_set']
2357+ ASKER_SUBSCRIBER
2358+
2359+ >>> print notifications[0].subject
2360 [Question #...]: Status of bug #... changed to 'Confirmed' in Ubuntu
2361- >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
2362+
2363+ >>> print notifications[0].body
2364 Bug #... status changed in Ubuntu:
2365 <BLANKLINE>
2366 New => Confirmed
2367@@ -43,15 +48,12 @@
2368 This bug is linked to #15.
2369 Can't install Ubuntu
2370 http://.../ubuntu/+question/...
2371- <BLANKLINE>
2372- --...
2373- You received this question notification because you are a member of
2374- Ubuntu Team, which is an answer contact for Ubuntu.
2375
2376 Only a change in status triggers a notification.
2377
2378 >>> from lp.testing import login_person
2379- >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
2380+ >>> sample_person = getUtility(IPersonSet).getByEmail(
2381+ ... 'test@canonical.com')
2382 >>> login_person(sample_person)
2383 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
2384 >>> bugtask.transitionToAssignee(sample_person)
2385@@ -59,6 +61,5 @@
2386 ... bugtask, original_bugtask, ['assignee', 'dateassigned'],
2387 ... user=sample_person))
2388
2389- >>> len(pop_notifications())
2390+ >>> len(pop_questionemailjobs())
2391 0
2392-
2393
2394=== modified file 'lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt'
2395--- lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt 2010-10-10 15:39:28 +0000
2396+++ lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt 2011-05-04 17:17:01 +0000
2397@@ -1,4 +1,5 @@
2398-= Linked Bug Status Changed Notification (Private) =
2399+Linked Bug Status Changed Notification (Private)
2400+================================================
2401
2402 See `answer-tracker-notifications-linked-bug.txt` for public bug behavior.
2403
2404@@ -9,9 +10,10 @@
2405 >>> from zope.interface import providedBy
2406 >>> from lazr.lifecycle.event import ObjectModifiedEvent
2407 >>> from lazr.lifecycle.snapshot import Snapshot
2408+ >>> from lp.answers.tests.test_question_notifications import (
2409+ ... pop_questionemailjobs)
2410 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
2411 >>> from lp.registry.interfaces.person import IPersonSet
2412- >>> from lp.testing.mail_helpers import pop_notifications
2413
2414 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
2415 >>> bugtask = get_bugtask_linked_to_question()
2416@@ -20,8 +22,9 @@
2417 True
2418 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
2419 >>> bugtask.transitionToStatus(BugTaskStatus.FIXCOMMITTED, no_priv)
2420+ >>> ignore = pop_questionemailjobs()
2421 >>> notify(ObjectModifiedEvent(
2422 ... bugtask, original_bugtask, ['status'], user=no_priv))
2423- >>> notifications = pop_notifications()
2424+ >>> notifications = pop_questionemailjobs()
2425 >>> len(notifications)
2426 0
2427
2428=== added directory 'lib/lp/services/mail/doc'
2429=== renamed file 'lib/canonical/launchpad/doc/notification-recipient-set.txt' => 'lib/lp/services/mail/doc/notification-recipient-set.txt'
2430--- lib/canonical/launchpad/doc/notification-recipient-set.txt 2010-12-06 22:10:11 +0000
2431+++ lib/lp/services/mail/doc/notification-recipient-set.txt 2011-05-04 17:17:01 +0000
2432@@ -1,8 +1,9 @@
2433-= INotificationRecipientSet =
2434+INotificationRecipientSet
2435+=========================
2436
2437 It is part of Launchpad policy that all email notifications contain in
2438-the footer an explanation of why the email was sent. A simpler string
2439-is also usually added to a X-Launchpad-Message-Rationale header to allow
2440+the footer an explanation of why the email was sent. A simpler string is
2441+also usually added to a X-Launchpad-Message-Rationale header to allow
2442 easy filtering.
2443
2444 The easiest way to implement that policy is for methods returning a list
2445@@ -11,11 +12,12 @@
2446 recipient lists with the rationale for contacting them.
2447
2448 There is a base implementation of the interface available as
2449-canonical.launchpad.mailnotification.NotificationRecipientSet.
2450-You can use it as is or derive from it
2451-(see bugnotificationrecipients.txt for an example of a derivation).
2452+canonical.launchpad.mailnotification.NotificationRecipientSet. You can
2453+use it as is or derive from it (see bugnotificationrecipients.txt for an
2454+example of a derivation).
2455
2456- >>> from canonical.launchpad.interfaces.launchpad import INotificationRecipientSet
2457+ >>> from canonical.launchpad.interfaces.launchpad import (
2458+ ... INotificationRecipientSet)
2459 >>> from canonical.launchpad.webapp.testing import verifyObject
2460 >>> from canonical.launchpad.mailnotification import (
2461 ... NotificationRecipientSet)
2462@@ -24,10 +26,12 @@
2463 >>> verifyObject(INotificationRecipientSet, recipients)
2464 True
2465
2466-== Populating the set ==
2467-
2468-You add recipients to the set using the add() method. The method takes the
2469-IPerson to add along the notification rationale and header code.
2470+
2471+Populating the set
2472+------------------
2473+
2474+You add recipients to the set using the add() method. The method takes
2475+the IPerson to add along the notification rationale and header code.
2476
2477 >>> from lp.registry.interfaces.person import IPersonSet
2478 >>> person_set = getUtility(IPersonSet)
2479@@ -43,7 +47,8 @@
2480 value is only used as an example. In practice, you should try to reuse
2481 existing values if they apply to your context.
2482
2483-The getPersons() method returns the list of recipients sorted by display name.
2484+The getPersons() method returns the list of recipients sorted by display
2485+name.
2486
2487 >>> [person.displayname for person in recipients.getRecipients()]
2488 [u'Celso Providelo', u'Sample Person']
2489@@ -55,19 +60,21 @@
2490 Celso Providelo
2491 Sample Person
2492
2493-The getEmails() methods return the emails of all the recipients, also sorted
2494-alphabetically:
2495+The getEmails() methods return the emails of all the recipients, also
2496+sorted alphabetically:
2497
2498 >>> recipients.getEmails()
2499 ['celso.providelo@canonical.com', 'test@canonical.com']
2500
2501-You can test if an IPerson or an email is part of the recipients using the
2502-standard `in` operator:
2503+You can test if an IPerson or an email is part of the recipients using
2504+the standard `in` operator:
2505
2506 >>> cprov in recipients
2507 True
2508+
2509 >>> 'celso.providelo@canonical.com' in recipients
2510 True
2511+
2512 >>> u'test@canonical.com' in recipients
2513 True
2514
2515@@ -85,9 +92,12 @@
2516 >>> bool(NotificationRecipientSet())
2517 False
2518
2519-== Obtaining the rationale ==
2520-
2521-You can obtain the rationale, header tuple by using the getReason() method:
2522+
2523+Obtaining the rationale
2524+-----------------------
2525+
2526+You can obtain the rationale, header tuple by using the getReason()
2527+method:
2528
2529 >>> recipients.getReason(cprov)
2530 ('You are notified for no reason.', 'Why not')
2531@@ -117,24 +127,29 @@
2532 ...
2533 AssertionError: ...
2534
2535-== Team as recipient ==
2536-
2537-Adding a team with a preferred email address works like adding any
2538-other person:
2539+
2540+Team as recipient
2541+-----------------
2542+
2543+Adding a team with a preferred email address works like adding any other
2544+person:
2545
2546 >>> ubuntu_team = person_set.getByName('ubuntu-team')
2547 >>> login_person(ubuntu_team.teamowner)
2548 >>> print ubuntu_team.preferredemail.email
2549 support@ubuntu.com
2550+
2551 >>> recipients.add(ubuntu_team, 'You are notified for fun.', 'Fun')
2552
2553 >>> ubuntu_team in recipients
2554 True
2555+
2556 >>> 'support@ubuntu.com' in recipients
2557 True
2558
2559 >>> [person.displayname for person in recipients]
2560 [u'Celso Providelo', u'Sample Person', u'Ubuntu Team']
2561+
2562 >>> recipients.getEmails()
2563 ['celso.providelo@canonical.com', 'support@ubuntu.com',
2564 'test@canonical.com']
2565@@ -146,11 +161,13 @@
2566 >>> ubuntu_gnome_team = person_set.getByName('name18')
2567 >>> print ubuntu_gnome_team.preferredemail
2568 None
2569+
2570 >>> recipients.add(
2571 ... ubuntu_gnome_team,
2572 ... 'Notified because a member of the team', 'Team')
2573 >>> ubuntu_gnome_team in recipients
2574 True
2575+
2576 >>> recipients.getEmails()
2577 ['andrew.bennetts@ubuntulinux.com', 'foo.bar@canonical.com',
2578 'limi@plone.org', 'steve.alexander@ubuntulinux.com',
2579@@ -162,21 +179,25 @@
2580 [u'Ubuntu Gnome Team']
2581
2582 So Sample Person is not in the recipients list, even if his email will
2583-be notified for he's a member of Warty Security Team, itself a member
2584-of Ubuntu Gnome Team:
2585+be notified for he's a member of Warty Security Team, itself a member of
2586+Ubuntu Gnome Team:
2587
2588 >>> warty_security_team = person_set.getByName('name20')
2589 >>> print warty_security_team.displayname
2590 Warty Security Team
2591+
2592 >>> sample_person.inTeam(warty_security_team)
2593 True
2594+
2595 >>> warty_security_team.inTeam(ubuntu_gnome_team)
2596 True
2597+
2598 >>> sample_person in ubuntu_gnome_team.activemembers
2599 False
2600
2601 >>> sample_person in recipients
2602 False
2603+
2604 >>> 'test@canonical.com' in recipients
2605 True
2606
2607@@ -184,10 +205,13 @@
2608
2609 >>> recipients.getReason(ubuntu_gnome_team)
2610 ('Notified because a member of the team', 'Team')
2611+
2612 >>> recipients.getReason('test@canonical.com')
2613 ('Notified because a member of the team', 'Team')
2614
2615-== Adding many persons at the same time ==
2616+
2617+Adding many persons at the same time
2618+------------------------------------
2619
2620 If you pass an iterable sequence to the add() method, all members will
2621 be added with the same rationale:
2622@@ -200,27 +224,37 @@
2623
2624 >>> recipients.getReason(no_priv)
2625 ('Notified for fun.', 'Fun')
2626+
2627 >>> recipients.getReason(sample_person)
2628 ('Notified for fun.', 'Fun')
2629
2630-== Removing recipients ==
2631-
2632-It is also possible to remove a person from the NotificationRecipientSet():
2633+
2634+Removing recipients
2635+-------------------
2636+
2637+It is also possible to remove a person from the
2638+NotificationRecipientSet():
2639
2640 >>> recipients = NotificationRecipientSet()
2641 >>> recipients.add(
2642 ... [sample_person, no_priv, cprov], 'Notified for fun.', 'Fun')
2643 >>> [person.displayname for person in recipients.getRecipients()]
2644 [u'Celso Providelo', u'No Privileges Person', u'Sample Person']
2645+
2646 >>> recipients.remove([sample_person, cprov])
2647 >>> [person.displayname for person in recipients.getRecipients()]
2648 [u'No Privileges Person']
2649
2650-== A person's first impression sticks ==
2651-
2652-In general, the most specific rationale is used for a given email.
2653-A rationale given for a person is considered more
2654-specific than one obtained through team membership.
2655+ >>> recipients.getEmails()
2656+ ['no-priv@canonical.com']
2657+
2658+
2659+A person's first impression sticks
2660+----------------------------------
2661+
2662+In general, the most specific rationale is used for a given email. A
2663+rationale given for a person is considered more specific than one
2664+obtained through team membership.
2665
2666 So, if a person is added more than once to the set, the first reason
2667 will be the one returned.
2668@@ -265,7 +299,8 @@
2669 ('Sample Person', 'Person')
2670
2671
2672-== Merging recipients set ==
2673+Merging recipients set
2674+----------------------
2675
2676 You can merge two recipients set by using the update() method. It will
2677 add all the recipients in the second set along their rationale. If the
2678@@ -283,3 +318,5 @@
2679 Celso Providelo: B (Reason B)
2680 No Privileges Person: B (Reason B)
2681 Sample Person: A (Reason A)
2682+
2683+
2684
2685=== modified file 'lib/lp/services/mail/notificationrecipientset.py'
2686--- lib/lp/services/mail/notificationrecipientset.py 2011-03-30 20:08:42 +0000
2687+++ lib/lp/services/mail/notificationrecipientset.py 2011-05-04 17:17:01 +0000
2688@@ -131,6 +131,7 @@
2689 removed_person.preferredemail)
2690 email = str(preferred_email.email)
2691 self._receiving_people.discard((email, removed_person))
2692+ del self._emailToPerson[email]
2693
2694 def update(self, recipient_set):
2695 """See `INotificationRecipientSet`."""
2696
2697=== added file 'lib/lp/services/mail/tests/test_doc.py'
2698--- lib/lp/services/mail/tests/test_doc.py 1970-01-01 00:00:00 +0000
2699+++ lib/lp/services/mail/tests/test_doc.py 2011-05-04 17:17:01 +0000
2700@@ -0,0 +1,19 @@
2701+# Copyright 2011 Canonical Ltd. This software is licensed under the
2702+# GNU Affero General Public License version 3 (see the file LICENSE).
2703+
2704+"""Test mail documentation."""
2705+
2706+__metaclass__ = type
2707+
2708+import os
2709+
2710+from canonical.testing.layers import DatabaseFunctionalLayer
2711+from lp.services.testing import build_test_suite
2712+
2713+
2714+here = os.path.dirname(os.path.realpath(__file__))
2715+
2716+
2717+def test_suite():
2718+ suite = build_test_suite(here, {}, layer=DatabaseFunctionalLayer)
2719+ return suite