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

Proposed by Curtis Hovey
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
database/schema/security.cfg (+220/-212)
lib/lp/answers/browser/tests/views.txt (+0/-56)
lib/lp/answers/doc/notifications.txt (+84/-386)
lib/lp/answers/model/questionjob.py (+2/-1)
lib/lp/answers/notification.py (+22/-79)
lib/lp/answers/stories/question-confirm-url.txt (+0/-105)
lib/lp/answers/tests/test_question_notifications.py (+143/-30)
lib/lp/answers/tests/test_questionjob.py (+25/-2)
lib/lp/bugs/tests/test_bugnotification.py (+11/-9)
lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt (+17/-16)
lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt (+6/-3)
lib/lp/services/mail/doc/notification-recipient-set.txt (+72/-35)
lib/lp/services/mail/notificationrecipientset.py (+1/-0)
lib/lp/services/mail/tests/test_doc.py (+19/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/question-email-3
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Benji York (community) Abstain
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.
Revision history for this message
Benji York (benji) wrote :

Someone else is already lined up to do this review.

review: Abstain
Revision history for this message
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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2011-05-03 12:52:05 +0000
+++ database/schema/security.cfg 2011-05-04 17:17:01 +0000
@@ -583,6 +583,7 @@
583public.productseries = SELECT583public.productseries = SELECT
584public.project = SELECT, UPDATE584public.project = SELECT, UPDATE
585public.question = SELECT585public.question = SELECT
586public.questionjob = SELECT, INSERT
586public.questionbug = SELECT587public.questionbug = SELECT
587public.questionsubscription = SELECT588public.questionsubscription = SELECT
588public.section = SELECT589public.section = SELECT
@@ -599,56 +600,56 @@
599600
600[branchscanner]601[branchscanner]
601groups=write, script602groups=write, script
602public.account = SELECT, INSERT603public.account = SELECT, INSERT
603public.accountpassword = SELECT, INSERT604public.accountpassword = SELECT, INSERT
604public.branch = SELECT, UPDATE605public.branch = SELECT, UPDATE
605public.branchjob = SELECT, INSERT, UPDATE, DELETE606public.branchjob = SELECT, INSERT, UPDATE, DELETE
606public.branchmergeproposal = SELECT, UPDATE607public.branchmergeproposal = SELECT, UPDATE
607public.branchmergeproposaljob = SELECT, INSERT608public.branchmergeproposaljob = SELECT, INSERT
608public.branchrevision = SELECT, INSERT, UPDATE, DELETE609public.branchrevision = SELECT, INSERT, UPDATE, DELETE
609public.branchsubscription = SELECT610public.branchsubscription = SELECT
610public.branchvisibilitypolicy = SELECT611public.branchvisibilitypolicy = SELECT
611public.bugactivity = SELECT, INSERT612public.bugactivity = SELECT, INSERT
612public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE613public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
613public.bugbranch = SELECT, INSERT, UPDATE614public.bugbranch = SELECT, INSERT, UPDATE
614public.bugnotification = SELECT, INSERT615public.bugnotification = SELECT, INSERT
615public.bugnotificationfilter = SELECT, INSERT616public.bugnotificationfilter = SELECT, INSERT
616public.bugnotificationrecipient = SELECT, INSERT617public.bugnotificationrecipient = SELECT, INSERT
617public.bugsubscription = SELECT618public.bugsubscription = SELECT
618public.bugsubscriptionfilter = SELECT619public.bugsubscriptionfilter = SELECT
619public.bugsubscriptionfilterimportance = SELECT620public.bugsubscriptionfilterimportance = SELECT
620public.bugsubscriptionfilterstatus = SELECT621public.bugsubscriptionfilterstatus = SELECT
621public.bugsubscriptionfiltertag = SELECT622public.bugsubscriptionfiltertag = SELECT
622public.bugtag = SELECT623public.bugtag = SELECT
623public.codereviewmessage = SELECT624public.codereviewmessage = SELECT
624public.codereviewvote = SELECT625public.codereviewvote = SELECT
625public.diff = SELECT, INSERT, DELETE626public.diff = SELECT, INSERT, DELETE
626public.distribution = SELECT627public.distribution = SELECT
627public.distributionsourcepackage = SELECT, UPDATE628public.distributionsourcepackage = SELECT, UPDATE
628public.distroseries = SELECT629public.distroseries = SELECT
629public.emailaddress = SELECT630public.emailaddress = SELECT
630public.incrementaldiff = SELECT631public.incrementaldiff = SELECT
631public.job = SELECT, INSERT, UPDATE, DELETE632public.job = SELECT, INSERT, UPDATE, DELETE
632public.karma = SELECT, INSERT633public.karma = SELECT, INSERT
633public.karmaaction = SELECT634public.karmaaction = SELECT
634public.message = SELECT, INSERT635public.message = SELECT, INSERT
635public.messagechunk = SELECT, INSERT636public.messagechunk = SELECT, INSERT
636public.person = SELECT637public.person = SELECT
637public.revision = SELECT, INSERT, UPDATE638public.revision = SELECT, INSERT, UPDATE
638public.revisionauthor = SELECT, INSERT, UPDATE639public.revisionauthor = SELECT, INSERT, UPDATE
639public.revisioncache = SELECT, INSERT640public.revisioncache = SELECT, INSERT
640public.revisionparent = SELECT, INSERT641public.revisionparent = SELECT, INSERT
641public.revisionproperty = SELECT, INSERT642public.revisionproperty = SELECT, INSERT
642public.seriessourcepackagebranch = SELECT643public.seriessourcepackagebranch = SELECT
643public.sourcepackagename = SELECT644public.sourcepackagename = SELECT
644public.sourcepackagerecipe = SELECT, UPDATE645public.sourcepackagerecipe = SELECT, UPDATE
645public.sourcepackagerecipedata = SELECT646public.sourcepackagerecipedata = SELECT
646public.sourcepackagerecipedatainstruction = SELECT647public.sourcepackagerecipedatainstruction = SELECT
647public.staticdiff = SELECT, INSERT, DELETE648public.staticdiff = SELECT, INSERT, DELETE
648public.structuralsubscription = SELECT649public.structuralsubscription = SELECT
649public.translationtemplatesbuild = SELECT, INSERT650public.translationtemplatesbuild = SELECT, INSERT
650public.validpersoncache = SELECT651public.validpersoncache = SELECT
651public.validpersonorteamcache = SELECT652public.validpersonorteamcache = SELECT
652type=user653type=user
653654
654[branch-distro]655[branch-distro]
@@ -682,36 +683,36 @@
682683
683[distributionmirror]684[distributionmirror]
684groups=script685groups=script
685public.account = SELECT686public.account = SELECT
686public.archive = SELECT687public.archive = SELECT
687public.archivearch = SELECT688public.archivearch = SELECT
688public.binarypackagebuild = SELECT689public.binarypackagebuild = SELECT
689public.binarypackagefile = SELECT690public.binarypackagefile = SELECT
690public.binarypackagename = SELECT691public.binarypackagename = SELECT
691public.binarypackagepublishinghistory = SELECT692public.binarypackagepublishinghistory = SELECT
692public.binarypackagerelease = SELECT693public.binarypackagerelease = SELECT
693public.buildfarmjob = SELECT694public.buildfarmjob = SELECT
694public.component = SELECT695public.component = SELECT
695public.componentselection = SELECT696public.componentselection = SELECT
696public.distribution = SELECT697public.distribution = SELECT
697public.distributionmirror = SELECT, UPDATE698public.distributionmirror = SELECT, UPDATE
698public.distroarchseries = SELECT699public.distroarchseries = SELECT
699public.distroseries = SELECT700public.distroseries = SELECT
700public.emailaddress = SELECT701public.emailaddress = SELECT
701public.libraryfilealias = SELECT, INSERT702public.libraryfilealias = SELECT, INSERT
702public.libraryfilecontent = SELECT, INSERT703public.libraryfilecontent = SELECT, INSERT
703public.mirrorcdimagedistroseries = SELECT, INSERT, UPDATE, DELETE704public.mirrorcdimagedistroseries = SELECT, INSERT, UPDATE, DELETE
704public.mirrordistroarchseries = SELECT, UPDATE, DELETE, INSERT705public.mirrordistroarchseries = SELECT, UPDATE, DELETE, INSERT
705public.mirrordistroseriessource = SELECT, UPDATE, DELETE, INSERT706public.mirrordistroseriessource = SELECT, UPDATE, DELETE, INSERT
706public.mirrorproberecord = SELECT, INSERT707public.mirrorproberecord = SELECT, INSERT
707public.packagebuild = SELECT708public.packagebuild = SELECT
708public.person = SELECT709public.person = SELECT
709public.processorfamily = SELECT710public.processorfamily = SELECT
710public.sourcepackagename = SELECT711public.sourcepackagename = SELECT
711public.sourcepackagepublishinghistory = SELECT712public.sourcepackagepublishinghistory = SELECT
712public.sourcepackagerelease = SELECT713public.sourcepackagerelease = SELECT
713public.sourcepackagereleasefile = SELECT714public.sourcepackagereleasefile = SELECT
714public.teammembership = SELECT715public.teammembership = SELECT
715type=user716type=user
716717
717[teammembership]718[teammembership]
@@ -726,16 +727,16 @@
726727
727[karma]728[karma]
728groups=script729groups=script
729public.emailaddress = SELECT730public.emailaddress = SELECT
730public.karma = SELECT731public.karma = SELECT
731public.karmaaction = SELECT732public.karmaaction = SELECT
732public.karmacache = SELECT, INSERT, UPDATE, DELETE733public.karmacache = SELECT, INSERT, UPDATE, DELETE
733public.karmacategory = SELECT734public.karmacategory = SELECT
734public.karmatotalcache = SELECT, INSERT, UPDATE, DELETE735public.karmatotalcache = SELECT, INSERT, UPDATE, DELETE
735public.person = SELECT736public.person = SELECT
736public.product = SELECT737public.product = SELECT
737public.validpersoncache = SELECT738public.validpersoncache = SELECT
738public.validpersonorteamcache = SELECT739public.validpersonorteamcache = SELECT
739type=user740type=user
740741
741[request-daily-builds]742[request-daily-builds]
@@ -783,33 +784,33 @@
783784
784[cve]785[cve]
785groups=script786groups=script
786public.cve = SELECT, INSERT, UPDATE787public.cve = SELECT, INSERT, UPDATE
787public.cvereference = SELECT, INSERT, UPDATE, DELETE788public.cvereference = SELECT, INSERT, UPDATE, DELETE
788type=user789type=user
789790
790[gina]791[gina]
791groups=write,script792groups=write,script
792public.account = SELECT, INSERT793public.account = SELECT, INSERT
793public.accountpassword = SELECT, INSERT794public.accountpassword = SELECT, INSERT
794public.archive = SELECT, UPDATE795public.archive = SELECT, UPDATE
795public.archivearch = SELECT, UPDATE796public.archivearch = SELECT, UPDATE
796public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE797public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
797public.distribution = SELECT798public.distribution = SELECT
798public.distributionjob = SELECT, INSERT799public.distributionjob = SELECT, INSERT
799public.distributionsourcepackage = SELECT, INSERT800public.distributionsourcepackage = SELECT, INSERT
800public.packagediff = SELECT, INSERT, UPDATE801public.packagediff = SELECT, INSERT, UPDATE
801public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE802public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
802type=user803type=user
803804
804[archivepublisher]805[archivepublisher]
805groups=write,script806groups=write,script
806public.answercontact = SELECT807public.answercontact = SELECT
807public.archive = SELECT, UPDATE808public.archive = SELECT, UPDATE
808public.archivearch = SELECT809public.archivearch = SELECT
809public.archiveauthtoken = SELECT, UPDATE810public.archiveauthtoken = SELECT, UPDATE
810public.archivepermission = SELECT, INSERT811public.archivepermission = SELECT, INSERT
811public.archivesubscriber = SELECT, UPDATE812public.archivesubscriber = SELECT, UPDATE
812public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE813public.binarypackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
813public.bug = SELECT, UPDATE814public.bug = SELECT, UPDATE
814public.bugactivity = SELECT, INSERT815public.bugactivity = SELECT, INSERT
815public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE816public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
@@ -830,10 +831,11 @@
830public.bugtrackeralias = SELECT, INSERT831public.bugtrackeralias = SELECT, INSERT
831public.bugwatch = SELECT, INSERT832public.bugwatch = SELECT, INSERT
832public.cve = SELECT, INSERT833public.cve = SELECT, INSERT
833public.distributionjob = SELECT, INSERT, DELETE834public.distributionjob = SELECT, INSERT, DELETE
834public.distributionsourcepackage = SELECT, INSERT, UPDATE835public.distributionsourcepackage = SELECT, INSERT, UPDATE
835public.flatpackagesetinclusion = SELECT, INSERT, UPDATE, DELETE836public.flatpackagesetinclusion = SELECT, INSERT, UPDATE, DELETE
836public.gpgkey = SELECT, INSERT, UPDATE837public.gpgkey = SELECT, INSERT, UPDATE
838public.job = SELECT, INSERT, UPDATE
837public.karma = SELECT, INSERT839public.karma = SELECT, INSERT
838public.karmaaction = SELECT840public.karmaaction = SELECT
839public.language = SELECT841public.language = SELECT
@@ -841,21 +843,22 @@
841public.messagechunk = SELECT, INSERT843public.messagechunk = SELECT, INSERT
842public.milestone = SELECT844public.milestone = SELECT
843public.packagebugsupervisor = SELECT845public.packagebugsupervisor = SELECT
844public.packagecopyrequest = SELECT, INSERT, UPDATE846public.packagecopyrequest = SELECT, INSERT, UPDATE
845public.packagediff = SELECT, INSERT, UPDATE847public.packagediff = SELECT, INSERT, UPDATE
846public.packageset = SELECT, INSERT848public.packageset = SELECT, INSERT
847public.packagesetgroup = SELECT849public.packagesetgroup = SELECT
848public.packagesetinclusion = SELECT, INSERT, UPDATE, DELETE850public.packagesetinclusion = SELECT, INSERT, UPDATE, DELETE
849public.packagesetsources = SELECT, INSERT, UPDATE, DELETE851public.packagesetsources = SELECT, INSERT, UPDATE, DELETE
850public.personlanguage = SELECT852public.personlanguage = SELECT
851public.product = SELECT853public.product = SELECT
852public.productseries = SELECT854public.productseries = SELECT
853public.project = SELECT855public.project = SELECT
854public.publisherconfig = SELECT, INSERT856public.publisherconfig = SELECT, INSERT
855public.question = SELECT857public.question = SELECT
858public.questionjob = SELECT, INSERT
856public.questionbug = SELECT859public.questionbug = SELECT
857public.questionsubscription = SELECT860public.questionsubscription = SELECT
858public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE861public.sourcepackagepublishinghistory = SELECT, INSERT, UPDATE, DELETE
859public.structuralsubscription = SELECT862public.structuralsubscription = SELECT
860public.validpersoncache = SELECT863public.validpersoncache = SELECT
861public.validpersonorteamcache = SELECT864public.validpersonorteamcache = SELECT
@@ -863,58 +866,58 @@
863866
864[fiera]867[fiera]
865groups=script,translations_approval868groups=script,translations_approval
866public.account = SELECT869public.account = SELECT
867public.archive = SELECT, UPDATE870public.archive = SELECT, UPDATE
868public.archivearch = SELECT, UPDATE871public.archivearch = SELECT, UPDATE
869public.archivedependency = SELECT872public.archivedependency = SELECT
870public.binarypackagebuild = SELECT, INSERT, UPDATE873public.binarypackagebuild = SELECT, INSERT, UPDATE
871public.binarypackagefile = SELECT874public.binarypackagefile = SELECT
872public.binarypackagename = SELECT875public.binarypackagename = SELECT
873public.binarypackagepublishinghistory = SELECT876public.binarypackagepublishinghistory = SELECT
874public.binarypackagerelease = SELECT877public.binarypackagerelease = SELECT
875public.branch = SELECT878public.branch = SELECT
876public.branchjob = SELECT, DELETE879public.branchjob = SELECT, DELETE
877public.builder = SELECT, INSERT, UPDATE880public.builder = SELECT, INSERT, UPDATE
878public.buildfarmjob = SELECT, INSERT, UPDATE881public.buildfarmjob = SELECT, INSERT, UPDATE
879public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE882public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE
880public.buildqueue = SELECT, INSERT, UPDATE, DELETE883public.buildqueue = SELECT, INSERT, UPDATE, DELETE
881public.component = SELECT884public.component = SELECT
882public.distribution = SELECT, UPDATE885public.distribution = SELECT, UPDATE
883public.distroarchseries = SELECT, UPDATE886public.distroarchseries = SELECT, UPDATE
884public.distroseries = SELECT, UPDATE887public.distroseries = SELECT, UPDATE
885public.emailaddress = SELECT888public.emailaddress = SELECT
886public.flatpackagesetinclusion = SELECT889public.flatpackagesetinclusion = SELECT
887public.gpgkey = SELECT890public.gpgkey = SELECT
888public.job = SELECT, INSERT, UPDATE, DELETE891public.job = SELECT, INSERT, UPDATE, DELETE
889public.libraryfilealias = SELECT, INSERT892public.libraryfilealias = SELECT, INSERT
890public.libraryfilecontent = SELECT, INSERT893public.libraryfilecontent = SELECT, INSERT
891public.packagebuild = SELECT, INSERT, UPDATE894public.packagebuild = SELECT, INSERT, UPDATE
892public.packageset = SELECT895public.packageset = SELECT
893public.packagesetgroup = SELECT896public.packagesetgroup = SELECT
894public.packagesetinclusion = SELECT897public.packagesetinclusion = SELECT
895public.packagesetsources = SELECT898public.packagesetsources = SELECT
896public.person = SELECT899public.person = SELECT
897public.pocketchroot = SELECT, INSERT, UPDATE900public.pocketchroot = SELECT, INSERT, UPDATE
898public.processor = SELECT901public.processor = SELECT
899public.processorfamily = SELECT902public.processorfamily = SELECT
900public.product = SELECT903public.product = SELECT
901public.productseries = SELECT904public.productseries = SELECT
902public.publisherconfig = SELECT905public.publisherconfig = SELECT
903public.section = SELECT906public.section = SELECT
904public.seriessourcepackagebranch = SELECT907public.seriessourcepackagebranch = SELECT
905public.sourcepackagename = SELECT908public.sourcepackagename = SELECT
906public.sourcepackagepublishinghistory = SELECT909public.sourcepackagepublishinghistory = SELECT
907public.sourcepackagerecipe = SELECT910public.sourcepackagerecipe = SELECT
908public.sourcepackagerecipebuild = SELECT, UPDATE911public.sourcepackagerecipebuild = SELECT, UPDATE
909public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE, DELETE912public.sourcepackagerecipebuildjob = SELECT, INSERT, UPDATE, DELETE
910public.sourcepackagerecipedata = SELECT913public.sourcepackagerecipedata = SELECT
911public.sourcepackagerecipedatainstruction = SELECT914public.sourcepackagerecipedatainstruction = SELECT
912public.sourcepackagerelease = SELECT915public.sourcepackagerelease = SELECT
913public.sourcepackagereleasefile = SELECT916public.sourcepackagereleasefile = SELECT
914public.teammembership = SELECT917public.teammembership = SELECT
915public.teamparticipation = SELECT918public.teamparticipation = SELECT
916public.translationimportqueueentry = SELECT, INSERT, UPDATE919public.translationimportqueueentry = SELECT, INSERT, UPDATE
917public.translationtemplatesbuild = SELECT, INSERT920public.translationtemplatesbuild = SELECT, INSERT
918type=user921type=user
919922
920[ppa-apache-log-parser]923[ppa-apache-log-parser]
@@ -973,45 +976,45 @@
973976
974[sync_packages]977[sync_packages]
975groups=script978groups=script
976public.archive = SELECT979public.archive = SELECT
977public.archivepermission = SELECT, INSERT980public.archivepermission = SELECT, INSERT
978public.binarypackagebuild = SELECT, INSERT981public.binarypackagebuild = SELECT, INSERT
979public.binarypackagefile = SELECT, INSERT982public.binarypackagefile = SELECT, INSERT
980public.binarypackagename = SELECT983public.binarypackagename = SELECT
981public.binarypackagepublishinghistory = SELECT, INSERT984public.binarypackagepublishinghistory = SELECT, INSERT
982public.binarypackagerelease = SELECT985public.binarypackagerelease = SELECT
983public.buildfarmjob = SELECT, INSERT986public.buildfarmjob = SELECT, INSERT
984public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE987public.buildpackagejob = SELECT, INSERT, UPDATE, DELETE
985public.buildqueue = SELECT, INSERT, UPDATE988public.buildqueue = SELECT, INSERT, UPDATE
986public.component = SELECT989public.component = SELECT
987public.componentselection = SELECT, INSERT990public.componentselection = SELECT, INSERT
988public.distribution = SELECT991public.distribution = SELECT
989public.distributionjob = SELECT992public.distributionjob = SELECT
990public.distroarchseries = SELECT, INSERT993public.distroarchseries = SELECT, INSERT
991public.distroseries = SELECT, UPDATE994public.distroseries = SELECT, UPDATE
992public.flatpackagesetinclusion = SELECT, INSERT995public.flatpackagesetinclusion = SELECT, INSERT
993public.gpgkey = SELECT996public.gpgkey = SELECT
994public.job = SELECT, INSERT, UPDATE, DELETE997public.job = SELECT, INSERT, UPDATE, DELETE
995public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE998public.libraryfilealias = SELECT, INSERT, UPDATE, DELETE
996public.libraryfilecontent = SELECT, INSERT999public.libraryfilecontent = SELECT, INSERT
997public.packagebuild = SELECT, INSERT1000public.packagebuild = SELECT, INSERT
998public.packageset = SELECT, INSERT1001public.packageset = SELECT, INSERT
999public.packagesetgroup = SELECT, INSERT1002public.packagesetgroup = SELECT, INSERT
1000public.packagesetinclusion = SELECT, INSERT1003public.packagesetinclusion = SELECT, INSERT
1001public.packagesetsources = SELECT, INSERT1004public.packagesetsources = SELECT, INSERT
1002public.packageupload = SELECT1005public.packageupload = SELECT
1003public.packaging = SELECT, INSERT1006public.packaging = SELECT, INSERT
1004public.person = SELECT1007public.person = SELECT
1005public.pocketchroot = SELECT1008public.pocketchroot = SELECT
1006public.processor = SELECT1009public.processor = SELECT
1007public.processorfamily = SELECT1010public.processorfamily = SELECT
1008public.section = SELECT1011public.section = SELECT
1009public.sectionselection = SELECT, INSERT1012public.sectionselection = SELECT, INSERT
1010public.sourcepackageformatselection = SELECT, INSERT1013public.sourcepackageformatselection = SELECT, INSERT
1011public.sourcepackagename = SELECT1014public.sourcepackagename = SELECT
1012public.sourcepackagepublishinghistory = SELECT, INSERT1015public.sourcepackagepublishinghistory = SELECT, INSERT
1013public.sourcepackagerelease = SELECT1016public.sourcepackagerelease = SELECT
1014public.sourcepackagereleasefile = SELECT, INSERT, UPDATE1017public.sourcepackagereleasefile = SELECT, INSERT, UPDATE
1015type=user1018type=user
10161019
1017[distroseriesdifferencejob]1020[distroseriesdifferencejob]
@@ -1183,7 +1186,7 @@
1183public.distribution = SELECT1186public.distribution = SELECT
1184public.emailaddress = SELECT1187public.emailaddress = SELECT
1185public.faq = SELECT1188public.faq = SELECT
1186public.job = SELECT, UPDATE1189public.job = SELECT, INSERT, UPDATE
1187public.language = SELECT1190public.language = SELECT
1188public.message = SELECT, INSERT1191public.message = SELECT, INSERT
1189public.messagechunk = SELECT, INSERT1192public.messagechunk = SELECT, INSERT
@@ -1192,7 +1195,7 @@
1192public.product = SELECT1195public.product = SELECT
1193public.question = SELECT, UPDATE1196public.question = SELECT, UPDATE
1194public.questionbug = SELECT1197public.questionbug = SELECT
1195public.questionjob = SELECT1198public.questionjob = SELECT, INSERT
1196public.questionmessage = SELECT, INSERT1199public.questionmessage = SELECT, INSERT
1197public.questionsubscription = SELECT1200public.questionsubscription = SELECT
1198public.sourcepackagename = SELECT1201public.sourcepackagename = SELECT
@@ -1285,6 +1288,7 @@
1285public.project = SELECT, UPDATE1288public.project = SELECT, UPDATE
1286public.question = SELECT1289public.question = SELECT
1287public.questionbug = SELECT1290public.questionbug = SELECT
1291public.questionjob = SELECT, INSERT
1288public.questionsubscription = SELECT1292public.questionsubscription = SELECT
1289public.section = SELECT, INSERT1293public.section = SELECT, INSERT
1290public.sectionselection = SELECT1294public.sectionselection = SELECT
@@ -1389,6 +1393,7 @@
1389public.publisherconfig = SELECT1393public.publisherconfig = SELECT
1390public.question = SELECT1394public.question = SELECT
1391public.questionbug = SELECT1395public.questionbug = SELECT
1396public.questionjob = SELECT, INSERT
1392public.questionsubscription = SELECT1397public.questionsubscription = SELECT
1393public.section = SELECT1398public.section = SELECT
1394public.sectionselection = SELECT1399public.sectionselection = SELECT
@@ -1464,6 +1469,7 @@
1464public.project = SELECT, UPDATE1469public.project = SELECT, UPDATE
1465public.question = SELECT1470public.question = SELECT
1466public.questionbug = SELECT1471public.questionbug = SELECT
1472public.questionjob = SELECT, INSERT
1467public.questionsubscription = SELECT1473public.questionsubscription = SELECT
1468public.section = SELECT1474public.section = SELECT
1469public.sourcepackagename = SELECT1475public.sourcepackagename = SELECT
@@ -1667,6 +1673,7 @@
1667public.project = SELECT, UPDATE1673public.project = SELECT, UPDATE
1668public.question = SELECT, UPDATE1674public.question = SELECT, UPDATE
1669public.questionbug = SELECT1675public.questionbug = SELECT
1676public.questionjob = SELECT, INSERT
1670public.questionmessage = SELECT, INSERT1677public.questionmessage = SELECT, INSERT
1671public.questionsubscription = SELECT1678public.questionsubscription = SELECT
1672public.section = SELECT1679public.section = SELECT
@@ -2026,6 +2033,7 @@
2026public.project = SELECT, UPDATE2033public.project = SELECT, UPDATE
2027public.pushmirroraccess = SELECT, UPDATE2034public.pushmirroraccess = SELECT, UPDATE
2028public.question = SELECT, UPDATE2035public.question = SELECT, UPDATE
2036public.questionjob = SELECT, UPDATE
2029public.questionreopening = SELECT, UPDATE2037public.questionreopening = SELECT, UPDATE
2030public.questionsubscription = SELECT, UPDATE, DELETE2038public.questionsubscription = SELECT, UPDATE, DELETE
2031public.revisionauthor = SELECT, UPDATE2039public.revisionauthor = SELECT, UPDATE
20322040
=== modified file 'lib/lp/answers/browser/tests/views.txt'
--- lib/lp/answers/browser/tests/views.txt 2011-04-27 13:59:57 +0000
+++ lib/lp/answers/browser/tests/views.txt 2011-05-04 17:17:01 +0000
@@ -19,22 +19,6 @@
19 >>> firefox_question.subscribe(firefox_question.owner)19 >>> firefox_question.subscribe(firefox_question.owner)
20 <QuestionSubscription...>20 <QuestionSubscription...>
2121
22 # Let's define a helper function which commits the transaction, so
23 # that the notifications are queued in stub.test_emails and pops these
24 # notifications from the queue.
25
26 >>> from lp.services.mail import stub
27 >>> import email
28 >>> import transaction
29 >>> def pop_notifications():
30 ... transaction.commit()
31 ... notifications = [
32 ... email.message_from_string(raw_message)
33 ... for fromaddr, toaddrs, raw_message in sorted(stub.test_emails)
34 ... ]
35 ... stub.test_emails = []
36 ... return notifications
37
3822
39QuestionSubscriptionView23QuestionSubscriptionView
40------------------------24------------------------
@@ -84,11 +68,6 @@
84 >>> view.request.response.getHeader('Location')68 >>> view.request.response.getHeader('Location')
85 '.../+question/3'69 '.../+question/3'
8670
87These two actions didn't generate any notification mails:
88
89 >>> len(pop_notifications())
90 0
91
9271
93QuestionWorkflowView72QuestionWorkflowView
94--------------------73--------------------
@@ -155,13 +134,6 @@
155 >>> workflow_harness.redirectionTarget()134 >>> workflow_harness.redirectionTarget()
156 '.../+question/2'135 '.../+question/2'
157136
158Workflow actions like these will send out notifications to subscribers.
159(Complete notifications testing will be found in answer-tracker-
160notifications.txt)
161
162 >>> len(pop_notifications())
163 1
164
165The available actions for that other user are still comment, give an137The available actions for that other user are still comment, give an
166answer or request more information:138answer or request more information:
167139
@@ -347,10 +319,6 @@
347 >>> workflow_harness.redirectionTarget()319 >>> workflow_harness.redirectionTarget()
348 '.../+question/2'320 '.../+question/2'
349321
350 # Clear all notifications.
351
352 >>> notifications = pop_notifications()
353
354322
355QuestionMakeBugView323QuestionMakeBugView
356-------------------324-------------------
@@ -359,9 +327,6 @@
359question. In addition to creating a bug, this operation will also link327question. In addition to creating a bug, this operation will also link
360the bug to the question.328the bug to the question.
361329
362If the user creates a bug, a "Linked to bug" notification is sent and
363the user is subscribed to the bug.
364
365 >>> login('foo.bar@canonical.com')330 >>> login('foo.bar@canonical.com')
366 >>> request = LaunchpadTestRequest(331 >>> request = LaunchpadTestRequest(
367 ... form={'field.actions.create': 'Create',332 ... form={'field.actions.create': 'Create',
@@ -393,19 +358,6 @@
393 >>> 'Bug #%s created.' % new_bug_id in message[0]358 >>> 'Bug #%s created.' % new_bug_id in message[0]
394 True359 True
395360
396 >>> notifications = pop_notifications()
397 >>> len(notifications)
398 1
399
400 >>> print notifications[0].get_payload(decode=True)
401 Your question #3...
402 ...
403 Linked to bug: #...
404
405 http://bugs.launchpad.dev/bugs/...
406 "Bug title"
407 ...
408
409If the question already has bugs linked to it, no new bug can be361If the question already has bugs linked to it, no new bug can be
410created.362created.
411363
@@ -471,10 +423,6 @@
471 >>> print firefox_question.status.title423 >>> print firefox_question.status.title
472 Solved424 Solved
473425
474 # Clear the notification.
475
476 >>> notifications = pop_notifications()
477
478426
479QuestionEditView427QuestionEditView
480----------------428----------------
@@ -586,10 +534,6 @@
586 >>> print question_three.product.name534 >>> print question_three.product.name
587 firefox535 firefox
588536
589 # Clear out the pending notifications.
590
591 >>> notifications = pop_notifications()
592
593 # Reassign back the question to ubuntu537 # Reassign back the question to ubuntu
594538
595 >>> question_three.target = ubuntu539 >>> question_three.target = ubuntu
596540
=== modified file 'lib/lp/answers/doc/notifications.txt'
--- lib/lp/answers/doc/notifications.txt 2011-04-27 13:59:57 +0000
+++ lib/lp/answers/doc/notifications.txt 2011-05-04 17:17:01 +0000
@@ -7,9 +7,9 @@
7notification looks like:7notification looks like:
88
9 >>> from zope.event import notify9 >>> from zope.event import notify
10 >>> from lazr.lifecycle.event import ObjectCreatedEvent10 >>> from lp.answers.tests.test_question_notifications import (
11 ... pop_questionemailjobs)
11 >>> from lp.registry.interfaces.distribution import IDistributionSet12 >>> from lp.registry.interfaces.distribution import IDistributionSet
12 >>> from lp.testing.mail_helpers import pop_notifications
13 >>> login('test@canonical.com')13 >>> login('test@canonical.com')
14 >>> sample_person = getUtility(ILaunchBag).user14 >>> sample_person = getUtility(ILaunchBag).user
15 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')15 >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
@@ -29,7 +29,7 @@
29 >>> [sub.person.displayname for sub in ubuntu_question.subscriptions]29 >>> [sub.person.displayname for sub in ubuntu_question.subscriptions]
30 [u'Sample Person']30 [u'Sample Person']
3131
32 >>> notifications = pop_notifications()32 >>> notifications = pop_questionemailjobs()
33 >>> len(notifications)33 >>> len(notifications)
34 134 1
3535
@@ -41,44 +41,26 @@
41Danilo have a story worth telling.41Danilo have a story worth telling.
4242
43 >>> add_notification = notifications[0]43 >>> add_notification = notifications[0]
44 >>> add_notification['From']44
45 'Sample Person <question...@answers.launchpad.net>'45 >>> print add_notification.subject
4646 [Question #...]: Can't install Ubuntu
47 >>> add_notification['Reply-To']
48 'question...@answers.launchpad.net'
49
50 >>> add_notification['To']
51 'test@canonical.com'
52
53 >>> add_notification['Subject']
54 "[Question #...]: Can't install Ubuntu"
5547
56Like all Launchpad notifications should, the message contain in the48Like all Launchpad notifications should, the message contain in the
57footer the reason why the user is receiving the notification.49footer the reason why the user is receiving the notification.
5850
59 >>> notification_body = add_notification.get_payload(decode=True)51 >>> print add_notification.body
60 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
61 New question #... on Ubuntu:52 New question #... on Ubuntu:
62 http://.../ubuntu/+question/...53 http://.../ubuntu/+question/...
63 <BLANKLINE>54 <BLANKLINE>
64 I insert the install CD in the CD-ROM drive, but it won't boot.55 I insert the install CD in the CD-ROM drive, but it won't boot.
65 <BLANKLINE>
66 --...
67 You received this question notification because you asked the question.
6856
69The notification also includes a 'X-Launchpad-Question' header that57The notification also includes a 'X-Launchpad-Question' header that
70contains information about the question.58contains information about the question.
7159
72 >>> print add_notification['X-Launchpad-Question']60 >>> print add_notification.headers['X-Launchpad-Question']
73 distribution=ubuntu; sourcepackage=None; status=Open;61 distribution=ubuntu; sourcepackage=None; status=Open;
74 assignee=None; priority=Normal; language=en62 assignee=None; priority=Normal; language=en
7563
76As well as the standard 'X-Launchpad-Message-Rationale' header that
77contains in short format the reason for the user to be contacted.
78
79 >>> print add_notification['X-Launchpad-Message-Rationale']
80 Asker
81
82Register the Ubuntu Team as Ubuntu's answer contact, so that they get64Register the Ubuntu Team as Ubuntu's answer contact, so that they get
83notified about the changes as well:65notified about the changes as well:
8466
@@ -124,17 +106,12 @@
124Three copies of the notification got sent, one to Sample Person, one to106Three copies of the notification got sent, one to Sample Person, one to
125Foo Bar, and one to Ubuntu Team:107Foo Bar, and one to Ubuntu Team:
126108
127 >>> from operator import itemgetter109 >>> notifications = pop_questionemailjobs()
128 >>> notifications = sorted(pop_notifications(), key=itemgetter('To'))110 >>> edit_notification = notifications[1]
129 >>> [notification['To'] for notification in notifications]111 >>> print edit_notification.subject
130 ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
131
132 >>> edit_notification = notifications[0]
133 >>> notification_body = edit_notification.get_payload(decode=True)
134 >>> print edit_notification['Subject']
135 Re: [Question #...]: Installer doesn't work on a Mac112 Re: [Question #...]: Installer doesn't work on a Mac
136113
137 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE114 >>> print edit_notification.body
138 Question #... libstdc++ in Ubuntu changed:115 Question #... libstdc++ in Ubuntu changed:
139 http://.../ubuntu/+source/libstdc++/+question/...116 http://.../ubuntu/+source/libstdc++/+question/...
140 <BLANKLINE>117 <BLANKLINE>
@@ -148,10 +125,6 @@
148 drive, but it won't boot.125 drive, but it won't boot.
149 <BLANKLINE>126 <BLANKLINE>
150 It boots straight into MacOS 9.127 It boots straight into MacOS 9.
151 <BLANKLINE>
152 --...
153 You received this question notification because you are the assignee for
154 this question.
155128
156# XXX flacoste 2006-09-19: Add checks for notification of change to #129# XXX flacoste 2006-09-19: Add checks for notification of change to #
157status whiteboard, priority. For example, if a question is # transferred130status whiteboard, priority. For example, if a question is # transferred
@@ -163,21 +136,13 @@
163 >>> ubuntu_question.target = ubuntu136 >>> ubuntu_question.target = ubuntu
164 >>> notify(ObjectModifiedEvent(137 >>> notify(ObjectModifiedEvent(
165 ... ubuntu_question, unmodified_question, ['target']))138 ... ubuntu_question, unmodified_question, ['target']))
166 >>> notifications = sorted(pop_notifications(), key=itemgetter('To'))139 >>> notifications = pop_questionemailjobs()
167 >>> [notification['To'] for notification in notifications]140 >>> edit_notification = notifications[1]
168 ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']141 >>> print edit_notification.body
169
170 >>> edit_notification = notifications[0]
171 >>> notification_body = edit_notification.get_payload(decode=True)
172 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
173 Question #... Ubuntu changed:142 Question #... Ubuntu changed:
174 http://.../ubuntu/+question/...143 http://.../ubuntu/+question/...
175 <BLANKLINE>144 <BLANKLINE>
176 Project: libstdc++ in Ubuntu => Ubuntu145 Project: libstdc++ in Ubuntu => Ubuntu
177 <BLANKLINE>
178 --...
179 You received this question notification because you are the assignee for
180 this question.
181146
182Changing the assignee will trigger a notification.147Changing the assignee will trigger a notification.
183148
@@ -187,21 +152,13 @@
187 >>> ubuntu_question.assignee = no_priv152 >>> ubuntu_question.assignee = no_priv
188 >>> notify(ObjectModifiedEvent(153 >>> notify(ObjectModifiedEvent(
189 ... ubuntu_question, unmodified_question, ['assignee']))154 ... ubuntu_question, unmodified_question, ['assignee']))
190 >>> notifications = sorted(pop_notifications(), key=itemgetter('To'))155 >>> notifications = pop_questionemailjobs()
191 >>> [notification['To'] for notification in notifications]156 >>> edit_notification = notifications[1]
192 ['no-priv@canonical.com', 'support@ubuntu.com', 'test@canonical.com']157 >>> print edit_notification.body
193
194 >>> edit_notification = notifications[0]
195 >>> notification_body = edit_notification.get_payload(decode=True)
196 >>> print notification_body
197 Question #... Ubuntu changed:158 Question #... Ubuntu changed:
198 http://.../ubuntu/+question/...159 http://.../ubuntu/+question/...
199 <BLANKLINE>160 <BLANKLINE>
200 Assignee: Foo Bar => No Privileges Person161 Assignee: Foo Bar => No Privileges Person
201 <BLANKLINE>
202 --...
203 You received this question notification because you are the assignee for
204 this question.
205162
206If we trigger a modification event when no changes worth notifying about163If we trigger a modification event when no changes worth notifying about
207was made, no notification is sent:164was made, no notification is sent:
@@ -211,7 +168,7 @@
211 >>> notify(ObjectModifiedEvent(168 >>> notify(ObjectModifiedEvent(
212 ... ubuntu_question, unmodified_question, ['status']))169 ... ubuntu_question, unmodified_question, ['status']))
213170
214 >>> notifications = pop_notifications()171 >>> notifications = pop_questionemailjobs()
215 >>> len(notifications)172 >>> len(notifications)
216 0173 0
217174
@@ -245,23 +202,18 @@
245 >>> notify(ObjectModifiedEvent(202 >>> notify(ObjectModifiedEvent(
246 ... ubuntu_question, unmodified_question, ['bugs']))203 ... ubuntu_question, unmodified_question, ['bugs']))
247204
248 >>> notifications = pop_notifications()205 >>> notifications = pop_questionemailjobs()
249 >>> len(notifications)206 >>> len(notifications)
250 2207 2
251208
252 >>> edit_notification = notifications[0]209 >>> edit_notification = notifications[1]
253 >>> notification_body = edit_notification.get_payload(decode=True)210 >>> print edit_notification.body
254 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
255 Question #... on Ubuntu changed:211 Question #... on Ubuntu changed:
256 http://.../ubuntu/+question/...212 http://.../ubuntu/+question/...
257 <BLANKLINE>213 <BLANKLINE>
258 Linked to bug: #...214 Linked to bug: #...
259 http://.../bugs/...215 http://.../bugs/...
260 "Installer fails on a Mac PPC"216 "Installer fails on a Mac PPC"
261 <BLANKLINE>
262 --...
263 You received this question notification because you are a member of
264 Ubuntu Team, which is an answer contact for Ubuntu.
265217
266218
267Bug Unlinked Notification219Bug Unlinked Notification
@@ -277,23 +229,18 @@
277 >>> notify(ObjectModifiedEvent(229 >>> notify(ObjectModifiedEvent(
278 ... ubuntu_question, unmodified_question, ['bugs']))230 ... ubuntu_question, unmodified_question, ['bugs']))
279231
280 >>> notifications = pop_notifications()232 >>> notifications = pop_questionemailjobs()
281 >>> len(notifications)233 >>> len(notifications)
282 2234 2
283235
284 >>> edit_notification = notifications[0]236 >>> edit_notification = notifications[1]
285 >>> notification_body = edit_notification.get_payload(decode=True)237 >>> print edit_notification.body
286 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
287 Question #... on Ubuntu changed:238 Question #... on Ubuntu changed:
288 http://.../ubuntu/+question/...239 http://.../ubuntu/+question/...
289 <BLANKLINE>240 <BLANKLINE>
290 Removed link to bug: #...241 Removed link to bug: #...
291 http://.../bugs/...242 http://.../bugs/...
292 "Installer fails on a Mac PPC"243 "Installer fails on a Mac PPC"
293 <BLANKLINE>
294 --...
295 You received this question notification because you are a member of
296 Ubuntu Team, which is an answer contact for Ubuntu.
297244
298245
299Linked Bug Status Changed Notification246Linked Bug Status Changed Notification
@@ -314,13 +261,10 @@
314 >>> request_message = ubuntu_question.requestInfo(261 >>> request_message = ubuntu_question.requestInfo(
315 ... no_priv, "What is your Mac model?")262 ... no_priv, "What is your Mac model?")
316263
317 >>> notifications = pop_notifications()264 >>> notifications = pop_questionemailjobs()
318 >>> [email_msg['To'] for email_msg in notifications]265 >>> support_notification = notifications[1]
319 ['support@ubuntu.com', 'test@canonical.com']266 >>> print support_notification.subject
320267 Re: [Question #...]: Installer doesn't work on a Mac
321 >>> support_notification = notifications[0]
322 >>> support_notification['Subject']
323 "Re: [Question #...]: Installer doesn't work on a Mac"
324268
325For workflow notifications, the content of the notification is slightly269For workflow notifications, the content of the notification is slightly
326different based on whether you are the question owner or somebody else.270different based on whether you are the question owner or somebody else.
@@ -328,8 +272,7 @@
328For example, the notification to the answer contacts and every other272For example, the notification to the answer contacts and every other
329subscribers except the question owner will look like this:273subscribers except the question owner will look like this:
330274
331 >>> notification_body = support_notification.get_payload(decode=True)275 >>> print support_notification.body
332 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
333 Question #... on Ubuntu changed:276 Question #... on Ubuntu changed:
334 http://.../ubuntu/+question/...277 http://.../ubuntu/+question/...
335 <BLANKLINE>278 <BLANKLINE>
@@ -337,16 +280,11 @@
337 <BLANKLINE>280 <BLANKLINE>
338 No Privileges Person requested more information:281 No Privileges Person requested more information:
339 What is your Mac model?282 What is your Mac model?
340 <BLANKLINE>
341 --...
342 You received this question notification because you are a member of
343 Ubuntu Team, which is an answer contact for Ubuntu.
344283
345But the owner notification has a slightly different preamble and has an284But the owner notification has a slightly different preamble and has an
346extra footer.285extra footer.
347286
348 >>> notification_body = notifications[1].get_payload(decode=True)287 >>> print notifications[0].body
349 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
350 Your question #... on Ubuntu changed:288 Your question #... on Ubuntu changed:
351 http://.../ubuntu/+question/...289 http://.../ubuntu/+question/...
352 <BLANKLINE>290 <BLANKLINE>
@@ -359,8 +297,6 @@
359 To answer this request for more information, you can either reply to297 To answer this request for more information, you can either reply to
360 this email or enter your reply at the following page:298 this email or enter your reply at the following page:
361 http://.../ubuntu/+question/...299 http://.../ubuntu/+question/...
362 <BLANKLINE>
363 You received this question notification because you asked the question.
364300
365Of course, if the owner unsubscribe from the question, he won't receives301Of course, if the owner unsubscribe from the question, he won't receives
366a notification.302a notification.
@@ -369,12 +305,8 @@
369 >>> ubuntu_question.unsubscribe(sample_person)305 >>> ubuntu_question.unsubscribe(sample_person)
370 >>> message = ubuntu_question.giveInfo('A PowerMac 7200.')306 >>> message = ubuntu_question.giveInfo('A PowerMac 7200.')
371307
372 >>> notifications = pop_notifications()308 >>> notifications = pop_questionemailjobs()
373 >>> [email_msg['To'] for email_msg in notifications]309 >>> print notifications[1].body
374 ['support@ubuntu.com']
375
376 >>> notification_body = notifications[0].get_payload(decode=True)
377 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
378 Question #... on Ubuntu changed:310 Question #... on Ubuntu changed:
379 http://.../ubuntu/+question/...311 http://.../ubuntu/+question/...
380 <BLANKLINE>312 <BLANKLINE>
@@ -382,15 +314,11 @@
382 <BLANKLINE>314 <BLANKLINE>
383 Sample Person gave more information on the question:315 Sample Person gave more information on the question:
384 A PowerMac 7200.316 A PowerMac 7200.
385 <BLANKLINE>
386 --...
387 You received this question notification because you are a member of
388 Ubuntu Team, which is an answer contact for Ubuntu.
389317
390The notification for new messages on the question contain a 'References'318The notification for new messages on the question contain a 'References'
391header to the previous message for threading purpose.319header to the previous message for threading purpose.
392320
393 >>> references = notifications[0]['References']321 >>> references = notifications[0].headers['References']
394 >>> print references322 >>> print references
395 <...>323 <...>
396324
@@ -413,15 +341,11 @@
413 >>> login('no-priv@canonical.com')341 >>> login('no-priv@canonical.com')
414 >>> message = ubuntu_question.expireQuestion(342 >>> message = ubuntu_question.expireQuestion(
415 ... no_priv, "Expired because of no recent activity.")343 ... no_priv, "Expired because of no recent activity.")
416344 >>> notifications = pop_questionemailjobs()
417 >>> notifications = pop_notifications()
418 >>> [email_msg['To'] for email_msg in notifications]
419 ['support@ubuntu.com', 'test@canonical.com']
420345
421Default notification when the question is expired:346Default notification when the question is expired:
422347
423 >>> notification_body = notifications[0].get_payload(decode=True)348 >>> print notifications[1].body
424 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
425 Question #... on Ubuntu changed:349 Question #... on Ubuntu changed:
426 http://.../ubuntu/+question/...350 http://.../ubuntu/+question/...
427 <BLANKLINE>351 <BLANKLINE>
@@ -430,14 +354,10 @@
430 No Privileges Person expired the question:354 No Privileges Person expired the question:
431 Expired because of no recent activity.355 Expired because of no recent activity.
432 <BLANKLINE>356 <BLANKLINE>
433 --...
434 You received this question notification because you are a member of
435 Ubuntu Team, which is an answer contact for Ubuntu.
436357
437Notification received by the owner:358Notification received by the owner:
438359
439 >>> notification_body = notifications[1].get_payload(decode=True)360 >>> print notifications[0].body
440 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
441 Your question #... on Ubuntu changed:361 Your question #... on Ubuntu changed:
442 http://.../ubuntu/+question/...362 http://.../ubuntu/+question/...
443 <BLANKLINE>363 <BLANKLINE>
@@ -451,8 +371,6 @@
451 by replying to this email or by going to the following page and371 by replying to this email or by going to the following page and
452 entering more information about your problem:372 entering more information about your problem:
453 http://.../ubuntu/+question/...373 http://.../ubuntu/+question/...
454 <BLANKLINE>
455 You received this question notification because you asked the question.
456374
457375
458Notifications for reopen()376Notifications for reopen()
@@ -473,20 +391,16 @@
473 ... "newbie."),391 ... "newbie."),
474 ... owner=sample_person)392 ... owner=sample_person)
475 >>> message = ubuntu_question.reopen(email_msg)393 >>> message = ubuntu_question.reopen(email_msg)
476394 >>> notifications = pop_questionemailjobs()
477 >>> notifications = pop_notifications()
478 >>> [email_msg['To'] for email_msg in notifications]
479 ['support@ubuntu.com', 'test@canonical.com']
480395
481Notice also how the 'Re' handling is handled nicely:396Notice also how the 'Re' handling is handled nicely:
482397
483 >>> print notifications[0]['Subject']398 >>> print notifications[0].subject
484 Re: [Question #...]: Installer doesn't work on a Mac399 Re: [Question #...]: Installer doesn't work on a Mac
485400
486Default notification when the owner reopens the question:401Default notification when the owner reopens the question:
487402
488 >>> notification_body = notifications[0].get_payload(decode=True)403 >>> print notifications[1].body
489 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
490 Question #... on Ubuntu changed:404 Question #... on Ubuntu changed:
491 http://.../ubuntu/+question/...405 http://.../ubuntu/+question/...
492 <BLANKLINE>406 <BLANKLINE>
@@ -497,15 +411,10 @@
497 useful.411 useful.
498 <BLANKLINE>412 <BLANKLINE>
499 Please provide some help to a newbie.413 Please provide some help to a newbie.
500 <BLANKLINE>
501 --...
502 You received this question notification because you are a member of
503 Ubuntu Team, which is an answer contact for Ubuntu.
504414
505Notification received by the owner:415Notification received by the owner:
506416
507 >>> notification_body = notifications[1].get_payload(decode=True)417 >>> print notifications[0].body
508 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
509 Your question #... on Ubuntu changed:418 Your question #... on Ubuntu changed:
510 http://.../ubuntu/+question/...419 http://.../ubuntu/+question/...
511 <BLANKLINE>420 <BLANKLINE>
@@ -516,9 +425,6 @@
516 useful.425 useful.
517 <BLANKLINE>426 <BLANKLINE>
518 Please provide some help to a newbie.427 Please provide some help to a newbie.
519 <BLANKLINE>
520 --...
521 You received this question notification because you asked the question.
522428
523429
524Notifications for giveAnswer()430Notifications for giveAnswer()
@@ -533,14 +439,11 @@
533 ... "https://help.ubuntu.com/community/Installation/OldWorldMacs "439 ... "https://help.ubuntu.com/community/Installation/OldWorldMacs "
534 ... "for all the details.")440 ... "for all the details.")
535441
536 >>> notifications = pop_notifications()442 >>> notifications = pop_questionemailjobs()
537 >>> [email_msg['To'] for email_msg in notifications]
538 ['support@ubuntu.com', 'test@canonical.com']
539443
540Default notification when an answer is proposed:444Default notification when an answer is proposed:
541445
542 >>> notification_body = notifications[0].get_payload(decode=True)446 >>> print notifications[1].body
543 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
544 Question #... on Ubuntu changed:447 Question #... on Ubuntu changed:
545 http://.../ubuntu/+question/...448 http://.../ubuntu/+question/...
546 <BLANKLINE>449 <BLANKLINE>
@@ -553,15 +456,10 @@
553 <BLANKLINE>456 <BLANKLINE>
554 Consult https://help.ubuntu.com/community/Installation/OldWorldMacs for457 Consult https://help.ubuntu.com/community/Installation/OldWorldMacs for
555 all the details.458 all the details.
556 <BLANKLINE>
557 --...
558 You received this question notification because you are a member of
559 Ubuntu Team, which is an answer contact for Ubuntu.
560459
561Notification received by the owner:460Notification received by the owner:
562461
563 >>> notification_body = notifications[1].get_payload(decode=True)462 >>> print notifications[0].body
564 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
565 Your question #... on Ubuntu changed:463 Your question #... on Ubuntu changed:
566 http://.../ubuntu/+question/...464 http://.../ubuntu/+question/...
567 <BLANKLINE>465 <BLANKLINE>
@@ -583,8 +481,6 @@
583 If you still need help, you can reply to this email or go to the481 If you still need help, you can reply to this email or go to the
584 following page to enter your feedback:482 following page to enter your feedback:
585 http://.../ubuntu/+question/...483 http://.../ubuntu/+question/...
586 <BLANKLINE>
587 You received this question notification because you asked the question.
588484
589485
590Notifications for confirm()486Notifications for confirm()
@@ -595,14 +491,11 @@
595 ... "I've installed BootX and the installer CD is now booting. "491 ... "I've installed BootX and the installer CD is now booting. "
596 ... "Thanks!", answer=answer_message)492 ... "Thanks!", answer=answer_message)
597493
598 >>> notifications = pop_notifications()494 >>> notifications = pop_questionemailjobs()
599 >>> [email_msg['To'] for email_msg in notifications]
600 ['support@ubuntu.com', 'test@canonical.com']
601495
602Default notification when the owner confirms an answer:496Default notification when the owner confirms an answer:
603497
604 >>> notification_body = notifications[0].get_payload(decode=True)498 >>> print notifications[1].body
605 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
606 Question #... on Ubuntu changed:499 Question #... on Ubuntu changed:
607 http://.../ubuntu/+question/...500 http://.../ubuntu/+question/...
608 <BLANKLINE>501 <BLANKLINE>
@@ -610,15 +503,10 @@
610 <BLANKLINE>503 <BLANKLINE>
611 Sample Person confirmed that the question is solved:504 Sample Person confirmed that the question is solved:
612 I've installed BootX and the installer CD is now booting. Thanks!505 I've installed BootX and the installer CD is now booting. Thanks!
613 <BLANKLINE>
614 --...
615 You received this question notification because you are a member of
616 Ubuntu Team, which is an answer contact for Ubuntu.
617506
618Notification received by the owner:507Notification received by the owner:
619508
620 >>> notification_body = notifications[1].get_payload(decode=True)509 >>> print notifications[0].body
621 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
622 Your question #... on Ubuntu changed:510 Your question #... on Ubuntu changed:
623 http://.../ubuntu/+question/...511 http://.../ubuntu/+question/...
624 <BLANKLINE>512 <BLANKLINE>
@@ -626,9 +514,6 @@
626 <BLANKLINE>514 <BLANKLINE>
627 You confirmed that the question is solved:515 You confirmed that the question is solved:
628 I've installed BootX and the installer CD is now booting. Thanks!516 I've installed BootX and the installer CD is now booting. Thanks!
629 <BLANKLINE>
630 --...
631 You received this question notification because you asked the question.
632517
633518
634Notifications for addComment()519Notifications for addComment()
@@ -639,38 +524,27 @@
639 ... no_priv, "Unless you have lots of RAM... and even then, the "524 ... no_priv, "Unless you have lots of RAM... and even then, the "
640 ... "system will probably be very slow.")525 ... "system will probably be very slow.")
641526
642 >>> notifications = pop_notifications()527 >>> notifications = pop_questionemailjobs()
643 >>> [email_msg['To'] for email_msg in notifications]
644 ['support@ubuntu.com', 'test@canonical.com']
645528
646Default notification when a comment is posted:529Default notification when a comment is posted:
647530
648 >>> notification_body = notifications[0].get_payload(decode=True)531 >>> print notifications[1].body
649 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
650 Question #... on Ubuntu changed:532 Question #... on Ubuntu changed:
651 http://.../ubuntu/+question/...533 http://.../ubuntu/+question/...
652 <BLANKLINE>534 <BLANKLINE>
653 No Privileges Person posted a new comment:535 No Privileges Person posted a new comment:
654 Unless you have lots of RAM... and even then, the system will probably536 Unless you have lots of RAM... and even then, the system will probably
655 be very slow.537 be very slow.
656 <BLANKLINE>
657 --...
658 You received this question notification because you are a member of
659 Ubuntu Team, which is an answer contact for Ubuntu.
660538
661Notification received by the owner:539Notification received by the owner:
662540
663 >>> notification_body = notifications[1].get_payload(decode=True)541 >>> print notifications[0].body
664 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
665 Your question #... on Ubuntu changed:542 Your question #... on Ubuntu changed:
666 http://.../ubuntu/+question/...543 http://.../ubuntu/+question/...
667 <BLANKLINE>544 <BLANKLINE>
668 No Privileges Person posted a new comment:545 No Privileges Person posted a new comment:
669 Unless you have lots of RAM... and even then, the system will probably546 Unless you have lots of RAM... and even then, the system will probably
670 be very slow.547 be very slow.
671 <BLANKLINE>
672 --...
673 You received this question notification because you asked the question.
674548
675549
676Notifications for reject()550Notifications for reject()
@@ -681,14 +555,11 @@
681 >>> message = ubuntu_question.reject(555 >>> message = ubuntu_question.reject(
682 ... foo_bar, "Yeah! It will be awfully slow.")556 ... foo_bar, "Yeah! It will be awfully slow.")
683557
684 >>> notifications = pop_notifications()558 >>> notifications = pop_questionemailjobs()
685 >>> [email_msg['To'] for email_msg in notifications]
686 ['support@ubuntu.com', 'test@canonical.com']
687559
688Default notification when the question is rejected:560Default notification when the question is rejected:
689561
690 >>> notification_body = notifications[0].get_payload(decode=True)562 >>> print notifications[1].body
691 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
692 Question #... on Ubuntu changed:563 Question #... on Ubuntu changed:
693 http://.../ubuntu/+question/...564 http://.../ubuntu/+question/...
694 <BLANKLINE>565 <BLANKLINE>
@@ -697,14 +568,10 @@
697 Foo Bar rejected the question:568 Foo Bar rejected the question:
698 Yeah! It will be awfully slow.569 Yeah! It will be awfully slow.
699 <BLANKLINE>570 <BLANKLINE>
700 --...
701 You received this question notification because you are a member of
702 Ubuntu Team, which is an answer contact for Ubuntu.
703571
704Notification received by the owner:572Notification received by the owner:
705573
706 >>> notification_body = notifications[1].get_payload(decode=True)574 >>> print notifications[0].body
707 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
708 Your question #... on Ubuntu changed:575 Your question #... on Ubuntu changed:
709 http://.../ubuntu/+question/...576 http://.../ubuntu/+question/...
710 <BLANKLINE>577 <BLANKLINE>
@@ -718,8 +585,6 @@
718 explaining your point of view either by replying to this email or at585 explaining your point of view either by replying to this email or at
719 the following page:586 the following page:
720 http://.../ubuntu/+question/...587 http://.../ubuntu/+question/...
721 <BLANKLINE>
722 You received this question notification because you asked the question.
723588
724589
725Notifications for setStatus()590Notifications for setStatus()
@@ -730,14 +595,11 @@
730 >>> message = ubuntu_question.setStatus(595 >>> message = ubuntu_question.setStatus(
731 ... foo_bar, QuestionStatus.SOLVED, "The rejection was a mistake.")596 ... foo_bar, QuestionStatus.SOLVED, "The rejection was a mistake.")
732597
733 >>> notifications = pop_notifications()598 >>> notifications = pop_questionemailjobs()
734 >>> [email_msg['To'] for email_msg in notifications]
735 ['support@ubuntu.com', 'test@canonical.com']
736599
737Default notification when somebody changes the status:600Default notification when somebody changes the status:
738601
739 >>> notification_body = notifications[0].get_payload(decode=True)602 >>> print notifications[1].body
740 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
741 Question #... on Ubuntu changed:603 Question #... on Ubuntu changed:
742 http://.../ubuntu/+question/...604 http://.../ubuntu/+question/...
743 <BLANKLINE>605 <BLANKLINE>
@@ -745,15 +607,10 @@
745 <BLANKLINE>607 <BLANKLINE>
746 Foo Bar changed the question status:608 Foo Bar changed the question status:
747 The rejection was a mistake.609 The rejection was a mistake.
748 <BLANKLINE>
749 --...
750 You received this question notification because you are a member of
751 Ubuntu Team, which is an answer contact for Ubuntu.
752610
753Notification received by the owner:611Notification received by the owner:
754612
755 >>> notification_body = notifications[1].get_payload(decode=True)613 >>> print notifications[0].body
756 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
757 Your question #... on Ubuntu changed:614 Your question #... on Ubuntu changed:
758 http://.../ubuntu/+question/...615 http://.../ubuntu/+question/...
759 <BLANKLINE>616 <BLANKLINE>
@@ -761,9 +618,6 @@
761 <BLANKLINE>618 <BLANKLINE>
762 Foo Bar changed the question status:619 Foo Bar changed the question status:
763 The rejection was a mistake.620 The rejection was a mistake.
764 <BLANKLINE>
765 --...
766 You received this question notification because you asked the question.
767621
768622
769Notifications for linkFAQ()623Notifications for linkFAQ()
@@ -777,10 +631,7 @@
777 >>> firefox = getUtility(IProductSet).getByName('firefox')631 >>> firefox = getUtility(IProductSet).getByName('firefox')
778 >>> firefox_question = firefox.newQuestion(632 >>> firefox_question = firefox.newQuestion(
779 ... no_priv, 'How can I play Flash?', 'I want Flash!')633 ... no_priv, 'How can I play Flash?', 'I want Flash!')
780634 >>> ignore = pop_questionemailjobs()
781 # Discard notifications.
782
783 >>> notifications = pop_notifications()
784635
785 >>> login('test@canonical.com')636 >>> login('test@canonical.com')
786 >>> firefox_faq = firefox.getFAQ(10)637 >>> firefox_faq = firefox.getFAQ(10)
@@ -789,13 +640,9 @@
789640
790 >>> message = firefox_question.linkFAQ(641 >>> message = firefox_question.linkFAQ(
791 ... sample_person, firefox_faq, "Read the FAQ.")642 ... sample_person, firefox_faq, "Read the FAQ.")
792643 >>> notifications = pop_questionemailjobs()
793 >>> notifications = pop_notifications()644
794 >>> [email_msg['To'] for email_msg in notifications]645 >>> print notifications[0].body
795 ['no-priv@canonical.com']
796
797 >>> notification_body = notifications[0].get_payload(decode=True)
798 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
799 Your question #... on Mozilla Firefox changed:646 Your question #... on Mozilla Firefox changed:
800 http://answers.launchpad.dev/firefox/+question/...647 http://answers.launchpad.dev/firefox/+question/...
801 <BLANKLINE>648 <BLANKLINE>
@@ -814,13 +661,9 @@
814661
815 >>> message = firefox_question.linkFAQ(662 >>> message = firefox_question.linkFAQ(
816 ... sample_person, None, "Sorry, this wasn't so useful.")663 ... sample_person, None, "Sorry, this wasn't so useful.")
817664 >>> notifications = pop_questionemailjobs()
818 >>> notifications = pop_notifications()665
819 >>> [email_msg['To'] for email_msg in notifications]666 >>> print notifications[0].body
820 ['no-priv@canonical.com']
821
822 >>> notification_body = notifications[0].get_payload(decode=True)
823 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
824 Your question #... on Mozilla Firefox changed:667 Your question #... on Mozilla Firefox changed:
825 http://answers.launchpad.dev/firefox/+question/...668 http://answers.launchpad.dev/firefox/+question/...
826 <BLANKLINE>669 <BLANKLINE>
@@ -841,49 +684,9 @@
841from bugs just like when a question is normally created.684from bugs just like when a question is normally created.
842685
843 >>> bug_question = ubuntu.createQuestionFromBug(bug)686 >>> bug_question = ubuntu.createQuestionFromBug(bug)
844 >>> notifications = pop_notifications()687 >>> notifications = pop_questionemailjobs()
845 >>> len(notifications)688 >>> len(notifications)
846 4689 3
847
848 >>> [email_msg['To'] for email_msg in notifications]
849 ['no-priv@canonical.com', 'no-priv@canonical.com',
850 'support@ubuntu.com', 'support@ubuntu.com']
851
852
853Notifications and Teams
854-----------------------
855
856When a team is subscribed to a question, there are two cases two
857consider. The first one is if the team has an email address set, a
858notification will only be sent to that address. (That email address is
859assumed to be a mailing list reaching all the team members.) We already
860saw an example of that case with the Ubuntu Team in the examples above.
861
862The other case is when the team doesn't have an email address set. In
863that case, all the team members will be notified individually.
864
865 >>> launchpad_devs = getUtility(IPersonSet).getByName('launchpad')
866 >>> ubuntu_question.subscribe(launchpad_devs)
867 <QuestionSubscription...>
868
869 >>> login('test@canonical.com')
870 >>> message = ubuntu_question.addComment(sample_person, 'A comment.')
871
872 >>> notifications = pop_notifications()
873 >>> [email_msg['To'] for email_msg in notifications]
874 ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
875
876Of course, if the user is also individually subscribed to the question,
877he will receives only one notification:
878
879 >>> ubuntu_question.subscribe(foo_bar)
880 <QuestionSubscription...>
881
882 >>> message = ubuntu_question.addComment(sample_person, 'A comment.')
883
884 >>> notifications = pop_notifications()
885 >>> [email_msg['To'] for email_msg in notifications]
886 ['foo.bar@canonical.com', 'support@ubuntu.com', 'test@canonical.com']
887690
888691
889Notifications and Localized Questions692Notifications and Localized Questions
@@ -915,13 +718,10 @@
915 ... u'corretamente e mostra a minha versao do java. No entanto, '718 ... u'corretamente e mostra a minha versao do java. No entanto, '
916 ... u'mover o mouse na pagina faz com que o firefox quebre.'),719 ... u'mover o mouse na pagina faz com que o firefox quebre.'),
917 ... language=getUtility(ILanguageSet)['pt_BR'])720 ... language=getUtility(ILanguageSet)['pt_BR'])
918 >>> notifications = pop_notifications()721 >>> notifications = pop_questionemailjobs()
919 >>> [email_msg['To'] for email_msg in notifications]
920 ['guilherme.salgado@canonical.com', 'test@canonical.com']
921722
922 >>> from email.Header import decode_header, make_header723 >>> print notifications[0].subject.encode('ASCII', 'backslashreplace')
923 >>> unicode(make_header(decode_header(notifications[0]['Subject'])))724 [Question #...]: Abrir uma p\xe1gina que requer java quebra o firefox
924 u'[Question #...]: Abrir uma p\xe1gina que requer java quebra o firefox'
925725
926Similarly, when a question in a non-English language is modified or its726Similarly, when a question in a non-English language is modified or its
927status changed, only the subscribers speaking that language will receive727status changed, only the subscribers speaking that language will receive
@@ -931,9 +731,7 @@
931 ... "Veja o screenshot: http://tinyurl.com/y8jq8z")731 ... "Veja o screenshot: http://tinyurl.com/y8jq8z")
932 <QuestionMessage...>732 <QuestionMessage...>
933733
934 >>> notifications = pop_notifications()734 >>> ignore = pop_questionemailjobs()
935 >>> [email_msg['To'] for email_msg in notifications]
936 ['guilherme.salgado@canonical.com', 'test@canonical.com']
937735
938The exception to these general rules is that when a question is created736The exception to these general rules is that when a question is created
939in language spoken by none of the answer contacts, each one will receive737in language spoken by none of the answer contacts, each one will receive
@@ -949,43 +747,35 @@
949 ... sample_person, title="Impossible d'installer Ubuntu",747 ... sample_person, title="Impossible d'installer Ubuntu",
950 ... description=u"Le CD ne semble pas fonctionn\xe9.",748 ... description=u"Le CD ne semble pas fonctionn\xe9.",
951 ... language=french)749 ... language=french)
952 >>> notifications = pop_notifications()750 >>> notifications = pop_questionemailjobs()
953 >>> [email_msg['To'] for email_msg in notifications]
954 ['guilherme.salgado@canonical.com', 'support@ubuntu.com',
955 'test@canonical.com']
956751
957 >>> notifications[0]['Subject']752 >>> print notifications[1].subject
958 "[Question #...]: (French) Impossible d'installer Ubuntu"753 [Question #...]: (French) Impossible d'installer Ubuntu
959754
960 # Define a function that will replace non-ascii character with755 # Define a function that will replace non-ascii character with
961 # its unicoded encoded value.756 # its unicoded encoded value.
962 # Effectively replace u'\xe9' by '\\e9'.757 # Effectively replace u'\xe9' by '\\e9'.
963758
964 >>> def escape_utf8_payload(message):759 >>> def recode_text(notification):
965 ... charset = message.get_content_charset()760 ... return notification.body.encode('ASCII', 'backslashreplace')
966 ... content = unicode(message.get_payload(decode=True), charset)
967 ... return content.encode('us-ascii', 'backslashreplace')
968761
969 >>> notification_body = escape_utf8_payload(notifications[0])762 >>> notification_body = recode_text(notifications[1])
970 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE763 >>> print notification_body
971 A question was asked in a language (French) spoken by764 A question was asked in a language (French) spoken by
972 none of the registered Ubuntu answer contacts.765 none of the registered Ubuntu answer contacts.
973 <BLANKLINE>766 <BLANKLINE>
974 http://.../ubuntu/+question/...767 http://.../ubuntu/+question/...
975 <BLANKLINE>768 <BLANKLINE>
976 Le CD ne semble pas fonctionn\xe9...769 Le CD ne semble pas fonctionn\xe9...
977 --...
978 You received this question notification because you are an answer
979 contact for Ubuntu.
980770
981The notification received by the question owner contain a warning that771The notification received by the question owner contain a warning that
982the question is in a language spoken by none of the answer contacts:772the question is in a language spoken by none of the answer contacts:
983773
984 >>> notifications[-1]['Subject']774 >>> print notifications[0].subject
985 "[Question #...]: Impossible d'installer Ubuntu"775 [Question #...]: Impossible d'installer Ubuntu
986776
987 >>> notification_body = escape_utf8_payload(notifications[-1])777 >>> notification_body = recode_text(notifications[0])
988 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE778 >>> print notification_body
989 New question #... on Ubuntu:779 New question #... on Ubuntu:
990 http://.../ubuntu/+question/...780 http://.../ubuntu/+question/...
991 <BLANKLINE>781 <BLANKLINE>
@@ -993,9 +783,6 @@
993 <BLANKLINE>783 <BLANKLINE>
994 WARNING: This question is asked in a language (French)784 WARNING: This question is asked in a language (French)
995 spoken by none of the registered Ubuntu answer contacts.785 spoken by none of the registered Ubuntu answer contacts.
996 <BLANKLINE>
997 --...
998 You received this question notification because you asked the question.
999786
1000No notification will be sent to the answer contacts when this question787No notification will be sent to the answer contacts when this question
1001is modified. Only the owner will receive a modification notification788is modified. Only the owner will receive a modification notification
@@ -1006,13 +793,10 @@
1006 >>> french_question.title = u"CD d'Ubuntu ne d\xe9marre pas"793 >>> french_question.title = u"CD d'Ubuntu ne d\xe9marre pas"
1007 >>> notify(ObjectModifiedEvent(794 >>> notify(ObjectModifiedEvent(
1008 ... french_question, unmodified_question, ['title']))795 ... french_question, unmodified_question, ['title']))
1009796 >>> notifications = pop_questionemailjobs()
1010 >>> notifications = pop_notifications()797
1011 >>> [email_msg['To'] for email_msg in notifications]798 >>> notification_body = recode_text(notifications[0])
1012 ['test@canonical.com']799 >>> print notification_body
1013
1014 >>> notification_body = escape_utf8_payload(notifications[0])
1015 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE
1016 Your question #... on Ubuntu changed:800 Your question #... on Ubuntu changed:
1017 http://.../ubuntu/+question/...801 http://.../ubuntu/+question/...
1018 <BLANKLINE>802 <BLANKLINE>
@@ -1021,89 +805,3 @@
1021 <BLANKLINE>805 <BLANKLINE>
1022 WARNING: This question is asked in a language (French)806 WARNING: This question is asked in a language (French)
1023 spoken by none of the registered Ubuntu answer contacts.807 spoken by none of the registered Ubuntu answer contacts.
1024 <BLANKLINE>
1025 --...
1026 You received this question notification because you asked the question.
1027
1028
1029Localized Questions and Teams
1030.............................
1031
1032We will notify the team only if the question language is in one of the
1033team's preferred languages. The languages spoken by the team members is
1034unimportant.
1035
1036For example, the rosetta admins team becomes an Answer contact for
1037English questions. Carlos speaks Spanish, and he is an answer contact
1038for Ubuntu. He is also a member of the rosetta admins team. The team
1039wont receive emails because of his membership when they become answer
1040contacts too.
1041
1042 >>> rosetta_admins = getUtility(IPersonSet).getByName('rosetta-admins')
1043 >>> [lang.code for lang in rosetta_admins.languages]
1044 []
1045
1046 >>> rosetta_admins.addLanguage(getUtility(ILanguageSet)['en'])
1047 >>> carlos = getUtility(IPersonSet).getByName('carlos')
1048 >>> carlos.inTeam(rosetta_admins)
1049 True
1050
1051 >>> spanish = getUtility(ILanguageSet)['es']
1052 >>> spanish in carlos.languages
1053 True
1054
1055 >>> ubuntu.addAnswerContact(carlos)
1056 True
1057
1058 >>> ubuntu.addAnswerContact(rosetta_admins)
1059 True
1060
1061 >>> spanish_question = ubuntu.newQuestion(
1062 ... sample_person, title="Necesidad ayuda con Firefox",
1063 ... description="No puedo acceso al Internet en Firefox.",
1064 ... language=spanish)
1065 >>> notifications = pop_notifications()
1066 >>> [email_msg['To'] for email_msg in notifications]
1067 ['carlos@canonical.com', 'test@canonical.com']
1068
1069 >>> ubuntu.removeAnswerContact(carlos)
1070 True
1071
1072But if the team languages attribute is set, this set of languages will
1073be used. So, if the team only officially speaks French, it will only
1074receive notifications about French (and English) questions.
1075
1076 >>> rosetta_admins.addLanguage(french)
1077
1078 # Resend the new message notification
1079
1080 >>> notify(ObjectCreatedEvent(french_question))
1081 >>> notifications = pop_notifications()
1082 >>> [email_msg['To'] for email_msg in notifications]
1083 ['rosetta@launchpad.net', 'test@canonical.com']
1084
1085When the team doesn't use an explicit address. All team members will be
1086contacted if the question language is supported. For example, the
1087Launchpad Developers team doesn't have any preferred email address set.
1088Its only member, Foo Bar will receive a notification if the team
1089supported languages includes the question language:
1090
1091 >>> launchpad_devs = getUtility(IPersonSet).getByName('launchpad')
1092 >>> list(launchpad_devs.languages)
1093 []
1094
1095 >>> [member.name for member in launchpad_devs.activemembers]
1096 [u'name16']
1097
1098 >>> launchpad_devs.addLanguage(spanish)
1099 >>> ubuntu.addAnswerContact(launchpad_devs)
1100 True
1101
1102 # Resend the new message notification
1103
1104 >>> notify(ObjectCreatedEvent(spanish_question))
1105 >>> notifications = pop_notifications()
1106 >>> [email_msg['To'] for email_msg in notifications]
1107 ['foo.bar@canonical.com', 'test@canonical.com']
1108
1109
1110808
=== modified file 'lib/lp/answers/model/questionjob.py'
--- lib/lp/answers/model/questionjob.py 2011-04-28 18:40:45 +0000
+++ lib/lp/answers/model/questionjob.py 2011-05-04 17:17:01 +0000
@@ -51,6 +51,7 @@
51from lp.services.job.runner import BaseRunnableJob51from lp.services.job.runner import BaseRunnableJob
52from lp.services.mail.mailwrapper import MailWrapper52from lp.services.mail.mailwrapper import MailWrapper
53from lp.services.mail.notificationrecipientset import NotificationRecipientSet53from lp.services.mail.notificationrecipientset import NotificationRecipientSet
54from lp.services.mail.sendmail import format_address_for_person
54from lp.services.propertycache import cachedproperty55from lp.services.propertycache import cachedproperty
5556
5657
@@ -170,7 +171,7 @@
170171
171 def getErrorRecipients(self):172 def getErrorRecipients(self):
172 """See `IRunnableJob`."""173 """See `IRunnableJob`."""
173 return self.user174 return [format_address_for_person(self.user)]
174175
175 @property176 @property
176 def from_address(self):177 def from_address(self):
177178
=== modified file 'lib/lp/answers/notification.py'
--- lib/lp/answers/notification.py 2011-04-27 13:59:57 +0000
+++ lib/lp/answers/notification.py 2011-05-04 17:17:01 +0000
@@ -10,16 +10,17 @@
1010
11import os11import os
1212
13from zope.component import getUtility
14
13from canonical.config import config15from canonical.config import config
14from canonical.launchpad.mail import (16from canonical.launchpad.webapp.publisher import canonical_url
15 format_address,17from lp.answers.enums import (
16 simple_sendmail,18 QuestionAction,
19 QuestionRecipientSet,
17 )20 )
18from canonical.launchpad.webapp.publisher import canonical_url21from lp.answers.interfaces.questionjob import IQuestionEmailJobSource
19from lp.answers.enums import QuestionAction
20from lp.registry.interfaces.person import IPerson22from lp.registry.interfaces.person import IPerson
21from lp.services.mail.mailwrapper import MailWrapper23from lp.services.mail.mailwrapper import MailWrapper
22from lp.services.mail.notificationrecipientset import NotificationRecipientSet
23from lp.services.propertycache import cachedproperty24from lp.services.propertycache import cachedproperty
2425
2526
@@ -41,6 +42,8 @@
41 QuestionNotification can be registered as event subscribers.42 QuestionNotification can be registered as event subscribers.
42 """43 """
4344
45 recipient_set = QuestionRecipientSet.ASKER_SUBSCRIBER
46
44 def __init__(self, question, event):47 def __init__(self, question, event):
45 """Base constructor.48 """Base constructor.
4649
@@ -51,25 +54,15 @@
51 self.event = event54 self.event = event
52 self._user = IPerson(self.event.user)55 self._user = IPerson(self.event.user)
53 self.initialize()56 self.initialize()
57 self.job = None
54 if self.shouldNotify():58 if self.shouldNotify():
55 self.send()59 self.job = self.enqueue()
5660
57 @property61 @property
58 def user(self):62 def user(self):
59 """Return the user from the event. """63 """Return the user from the event. """
60 return self._user64 return self._user
6165
62 def getFromAddress(self):
63 """Return a formatted email address suitable for user in the From
64 header of the question notification.
65
66 Default is Event Person Display Name <question#@answertracker_domain>
67 """
68 return format_address(
69 self.user.displayname,
70 'question%s@%s' % (
71 self.question.id, config.answertracker.email_domain))
72
73 def getSubject(self):66 def getSubject(self):
74 """Return the subject of the notification.67 """Return the subject of the notification.
7568
@@ -114,18 +107,6 @@
114107
115 return headers108 return headers
116109
117 def getRecipients(self):
118 """Return the recipient of the notification.
119
120 Default to the question's subscribers that speaks the request
121 languages. If the question owner is subscribed, he's always consider
122 to speak the language.
123
124 :return: A `INotificationRecipientSet` containing the recipients and
125 rationale.
126 """
127 return self.question.getRecipients()
128
129 def initialize(self):110 def initialize(self):
130 """Initialization hook for subclasses.111 """Initialization hook for subclasses.
131112
@@ -144,32 +125,16 @@
144 """125 """
145 return True126 return True
146127
147 def buildBody(self, body, rationale):128 def enqueue(self):
148 """Wrap the body and ensure the rationale is is separated."""129 """Create a job to send email about the event."""
149 wrapper = MailWrapper()
150 body_parts = [body, wrapper.format(rationale)]
151 if '\n-- ' not in body:
152 body_parts.insert(1, '-- ')
153 return '\n'.join(body_parts)
154
155 def send(self):
156 """Sends the notification to all the notification recipients.
157
158 This method takes care of adding the rationale for contacting each
159 recipient and also sets the X-Launchpad-Message-Rationale header on
160 each message.
161 """
162 from_address = self.getFromAddress()
163 subject = self.getSubject()130 subject = self.getSubject()
164 body = self.getBody()131 body = self.getBody()
165 headers = self.getHeaders()132 headers = self.getHeaders()
166 recipients = self.getRecipients()133 job_source = getUtility(IQuestionEmailJobSource)
167 for email in recipients.getEmails():134 job = job_source.create(
168 rationale, header = recipients.getReason(email)135 self.question, self.user, self.recipient_set,
169 headers['X-Launchpad-Message-Rationale'] = header136 subject, body, headers)
170 formatted_body = self.buildBody(body, rationale)137 return job
171 simple_sendmail(
172 from_address, email, subject, formatted_body, headers)
173138
174 @property139 @property
175 def unsupported_language(self):140 def unsupported_language(self):
@@ -215,6 +180,7 @@
215class QuestionModifiedDefaultNotification(QuestionNotification):180class QuestionModifiedDefaultNotification(QuestionNotification):
216 """Base implementation of a notification when a question is modified."""181 """Base implementation of a notification when a question is modified."""
217182
183 recipient_set = QuestionRecipientSet.SUBSCRIBER
218 # Email template used to render the body.184 # Email template used to render the body.
219 body_template = "question-modified-notification.txt"185 body_template = "question-modified-notification.txt"
220186
@@ -347,18 +313,6 @@
347313
348 return get_email_template(self.body_template) % replacements314 return get_email_template(self.body_template) % replacements
349315
350 def getRecipients(self):
351 """The default notification goes to all question subscribers that
352 speak the request language, except the owner.
353 """
354 original_recipients = QuestionNotification.getRecipients(self)
355 recipients = NotificationRecipientSet()
356 for person in original_recipients:
357 if person != self.question.owner:
358 rationale, header = original_recipients.getReason(person)
359 recipients.add(person, rationale, header)
360 return recipients
361
362 # Header template used when a new message is added to the question.316 # Header template used when a new message is added to the question.
363 action_header_template = {317 action_header_template = {
364 QuestionAction.REQUESTINFO:318 QuestionAction.REQUESTINFO:
@@ -397,6 +351,7 @@
397class QuestionModifiedOwnerNotification(QuestionModifiedDefaultNotification):351class QuestionModifiedOwnerNotification(QuestionModifiedDefaultNotification):
398 """Notification sent to the owner when his question is modified."""352 """Notification sent to the owner when his question is modified."""
399353
354 recipient_set = QuestionRecipientSet.ASKER
400 # These actions will be done by the owner, so use the second person.355 # These actions will be done by the owner, so use the second person.
401 action_header_template = dict(356 action_header_template = dict(
402 QuestionModifiedDefaultNotification.action_header_template)357 QuestionModifiedDefaultNotification.action_header_template)
@@ -426,16 +381,6 @@
426 self.body_template = self.body_template_by_action.get(381 self.body_template = self.body_template_by_action.get(
427 self.new_message.action, self.body_template)382 self.new_message.action, self.body_template)
428383
429 def getRecipients(self):
430 """Return the owner of the question if he's still subscribed."""
431 recipients = NotificationRecipientSet()
432 owner = self.question.owner
433 original_recipients = self.question.direct_recipients
434 if owner in self.question.direct_recipients:
435 rationale, header = original_recipients.getReason(owner)
436 recipients.add(owner, rationale, header)
437 return recipients
438
439 def getBody(self):384 def getBody(self):
440 """See QuestionNotification."""385 """See QuestionNotification."""
441 body = QuestionModifiedDefaultNotification.getBody(self)386 body = QuestionModifiedDefaultNotification.getBody(self)
@@ -447,6 +392,8 @@
447class QuestionUnsupportedLanguageNotification(QuestionNotification):392class QuestionUnsupportedLanguageNotification(QuestionNotification):
448 """Notification sent to answer contacts for unsupported languages."""393 """Notification sent to answer contacts for unsupported languages."""
449394
395 recipient_set = QuestionRecipientSet.CONTACT
396
450 def getSubject(self):397 def getSubject(self):
451 """See QuestionNotification."""398 """See QuestionNotification."""
452 return '[Question #%s]: (%s) %s' % (399 return '[Question #%s]: (%s) %s' % (
@@ -457,10 +404,6 @@
457 """Return True when the question is in an unsupported language."""404 """Return True when the question is in an unsupported language."""
458 return self.unsupported_language405 return self.unsupported_language
459406
460 def getRecipients(self):
461 """Notify only the answer contacts."""
462 return self.question.target.getAnswerContactRecipients(None)
463
464 def getBody(self):407 def getBody(self):
465 """See QuestionNotification."""408 """See QuestionNotification."""
466 question = self.question409 question = self.question
467410
=== removed file 'lib/lp/answers/stories/question-confirm-url.txt'
--- lib/lp/answers/stories/question-confirm-url.txt 2009-11-11 22:17:17 +0000
+++ lib/lp/answers/stories/question-confirm-url.txt 1970-01-01 00:00:00 +0000
@@ -1,105 +0,0 @@
1= Confirming an Answer using the Link in the Notification Email =
2
3When an answer is posted on a question, its owner will usually receive a
4notification by email. That email includes a link that can be used by
5the owner to confirm that the answer solved his problem.
6
7 # We will use one browser objects for the owner, and one for the user
8 # providing support, 'No Privileges Person' here.
9
10 >>> owner_browser = setupBrowser(auth='Basic test@canonical.com:test')
11 >>> support_browser = setupBrowser(
12 ... auth='Basic no-priv@canonical.com:test')
13
14When the URL is used when the question isn't in the right state, the user
15will be redirected to the question page and a notification will be
16displayed:
17
18 >>> owner_browser.open(
19 ... 'http://launchpad.dev/firefox/+question/2/+confirm?'
20 ... 'answer_id=1')
21 >>> owner_browser.url
22 'http://.../firefox/+question/2'
23
24 >>> soup = find_main_content(owner_browser.contents)
25 >>> print soup.first('div', 'error message').renderContents()
26 The question is not in a state where you can confirm an
27 answer.
28
29Posting an answer on the question will send an email notification
30containing a link to confirm that answer.
31
32 # First subscribe the owner, so that he receives the notification.
33 >>> owner_browser.open(
34 ... 'http://launchpad.dev/firefox/+question/2/+subscribe')
35 >>> owner_browser.getControl('Subscribe').click()
36
37 # Post the answer...
38 >>> support_browser.open('http://launchpad.dev/firefox/+question/2')
39 >>> support_browser.getControl('Message').value = (
40 ... 'SVG is supported out of the box in recent versions of Firefox. '
41 ... 'I suggest you upgrade your browser.')
42 >>> support_browser.getControl('Add Answer').click()
43
44 # ... and get the confirmation URL from the notification
45 >>> import email
46 >>> import re
47 >>> from lp.services.mail import stub
48 >>> notification = email.message_from_string(stub.test_emails[-1][2])
49 >>> urls = re.findall('(http:[^\s]+)+', notification.get_payload())
50 >>> confirm_url = urls[-2].decode('quoted-printable')
51 >>> print confirm_url
52 http://answers.launchpad.dev/firefox/+question/2/+confirm?answer_id=...
53
54If a cropped URL or forged URL is used, an UnexpectedFormData error will
55be displayed. In the following example, the answer_id parameter refers
56to an answer not part of that question, it mimics a badly handcrafted
57URL:
58
59 >>> owner_browser.open(
60 ... 'http://launchpad.dev/firefox/+question/2/+confirm?'
61 ... 'answer_id=3')
62 Traceback (most recent call last):
63 ...
64 UnexpectedFormData...
65
66 >>> owner_browser.open(
67 ... 'http://launchpad.dev/firefox/+question/2/+confirm')
68 Traceback (most recent call last):
69 ...
70 UnexpectedFormData...
71
72The page is only accessible to the question owner:
73
74 >>> support_browser.open(confirm_url)
75 Traceback (most recent call last):
76 ...
77 Unauthorized...
78
79On the confirmation page, the user can see the answer that he is
80confirming.
81
82 >>> owner_browser.open(confirm_url)
83 >>> soup = find_main_content(owner_browser.contents)
84 >>> for comment in soup.fetch('div', 'boardCommentBody'):
85 ... print comment.renderContents()
86 <p>SVG is supported out of the box in recent versions of Firefox. I
87 suggest you upgrade your browser.</p>
88
89To confirm the answer, he needs to click the 'This Solved My Problem'
90button. He can enter an optional message along his confirmation.
91
92 >>> owner_browser.getControl('Message').value = (
93 ... "Thanks! This indeed solved the problem.")
94 >>> owner_browser.getControl('This Solved My Problem').click()
95
96This adds his comment to the question and mark it as 'Solved.'
97
98 >>> print extract_text(
99 ... find_tag_by_id(owner_browser.contents, 'question-status'))
100 Status: Solved ...
101 >>> print find_tags_by_class(
102 ... owner_browser.contents, 'boardCommentBody')[-1].renderContents()
103 <p>Thanks! This indeed solved the problem.</p>
104
105
1060
=== modified file 'lib/lp/answers/tests/test_question_notifications.py'
--- lib/lp/answers/tests/test_question_notifications.py 2011-04-23 01:31:22 +0000
+++ lib/lp/answers/tests/test_question_notifications.py 2011-05-04 17:17:01 +0000
@@ -5,18 +5,43 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [
9 'pop_questionemailjobs',
10 ]
11
8from unittest import TestCase12from unittest import TestCase
913
14from zope.component import getUtility
10from zope.interface import implements15from zope.interface import implements
16from zope.security.proxy import removeSecurityProxy
1117
18from canonical.testing import DatabaseFunctionalLayer
19from lp.answers.enums import QuestionRecipientSet
20from lp.answers.interfaces.questioncollection import IQuestionSet
21from lp.answers.model.questionjob import QuestionEmailJob
12from lp.answers.notification import (22from lp.answers.notification import (
13 QuestionAddedNotification,23 QuestionAddedNotification,
14 QuestionModifiedDefaultNotification,24 QuestionModifiedDefaultNotification,
25 QuestionModifiedOwnerNotification,
26 QuestionNotification,
27 QuestionUnsupportedLanguageNotification,
15 )28 )
16from lp.registry.interfaces.person import IPerson29from lp.registry.interfaces.person import IPerson
1730from lp.services.worlddata.interfaces.language import ILanguageSet
1831from lp.testing import TestCaseWithFactory
19class TestQuestionModifiedNotification(QuestionModifiedDefaultNotification):32
33
34def pop_questionemailjobs():
35 jobs = sorted(
36 QuestionEmailJob.iterReady(),
37 key=lambda job: job.metadata["recipient_set"])
38 for job in jobs:
39 job.start()
40 job.complete()
41 return jobs
42
43
44class FakeQuestionModifiedNotification(QuestionModifiedDefaultNotification):
20 """Subclass that do not send emails and with simpler initialization.45 """Subclass that do not send emails and with simpler initialization.
2146
22 Since notifications are handlers that accomplish their action on47 Since notifications are handlers that accomplish their action on
@@ -39,6 +64,7 @@
39 self.id = id64 self.id = id
40 self.title = title65 self.title = title
41 self.owner = FakeUser()66 self.owner = FakeUser()
67 self.messages = []
4268
4369
44class StubQuestionMessage:70class StubQuestionMessage:
@@ -56,6 +82,7 @@
56class FakeEvent:82class FakeEvent:
57 """A fake event."""83 """A fake event."""
58 user = FakeUser()84 user = FakeUser()
85 object_before_modification = StubQuestion()
5986
6087
61class QuestionModifiedDefaultNotificationTestCase(TestCase):88class QuestionModifiedDefaultNotificationTestCase(TestCase):
@@ -63,22 +90,13 @@
6390
64 def setUp(self):91 def setUp(self):
65 """Create a notification with a fake question."""92 """Create a notification with a fake question."""
66 self.notification = TestQuestionModifiedNotification(93 self.notification = FakeQuestionModifiedNotification(
67 StubQuestion(), FakeEvent())94 StubQuestion(), FakeEvent())
6895
69 def test_buildBody_with_separator(self):96 def test_recipient_set(self):
70 # A body with a separator is preserved.97 self.assertEqual(
71 formatted_body = self.notification.buildBody(98 QuestionRecipientSet.SUBSCRIBER,
72 "body\n-- ", "rationale")99 self.notification.recipient_set)
73 self.assertEqual(
74 "body\n-- \nrationale", formatted_body)
75
76 def test_buildBody_without_separator(self):
77 # A separator will added to body if one is not present.
78 formatted_body = self.notification.buildBody(
79 "body -- mdash", "rationale")
80 self.assertEqual(
81 "body -- mdash\n-- \nrationale", formatted_body)
82100
83 def test_getSubject(self):101 def test_getSubject(self):
84 """getSubject() when there is no message added to the question."""102 """getSubject() when there is no message added to the question."""
@@ -90,25 +108,120 @@
90 """The notification user is always the event user."""108 """The notification user is always the event user."""
91 question = StubQuestion()109 question = StubQuestion()
92 event = FakeEvent()110 event = FakeEvent()
93 notification = TestQuestionModifiedNotification(question, event)111 notification = FakeQuestionModifiedNotification(question, event)
94 self.assertEqual(event.user, notification.user)112 self.assertEqual(event.user, notification.user)
95 self.assertNotEqual(question.owner, notification.user)113 self.assertNotEqual(question.owner, notification.user)
96114
97115
98class TestQuestionAddedNotification(QuestionAddedNotification):116class FakeQuestionModifiedOwnerNotification(
99 """A subclass that does not send emails."""117 QuestionModifiedOwnerNotification):
100118 """A subclass that does not send emails."""
101 def shouldNotify(self):119
102 return False120 def shouldNotify(self):
103121 return False
104122
105class QuestionCreatedTestCase(TestCase):123
124class QuestionModifiedOwnerNotificationTestCase(TestCase):
125 """Test cases for mail notifications about owner modified questions."""
126
127 def setUp(self):
128 self.question = StubQuestion()
129 self.event = FakeEvent()
130 self.notification = FakeQuestionModifiedOwnerNotification(
131 self.question, self.event)
132
133 def test_recipient_set(self):
134 self.assertEqual(
135 QuestionRecipientSet.ASKER,
136 self.notification.recipient_set)
137
138
139class FakeQuestionAddedNotification(QuestionAddedNotification):
140 """A subclass that does not send emails."""
141
142 def shouldNotify(self):
143 return False
144
145
146class QuestionAddedNotificationTestCase(TestCase):
106 """Test cases for mail notifications about created questions."""147 """Test cases for mail notifications about created questions."""
107148
149 def setUp(self):
150 self.question = StubQuestion()
151 self.event = FakeEvent()
152 self.notification = FakeQuestionAddedNotification(
153 self.question, self.event)
154
155 def test_recipient_set(self):
156 self.assertEqual(
157 QuestionRecipientSet.ASKER_SUBSCRIBER,
158 self.notification.recipient_set)
159
108 def test_user_is_question_owner(self):160 def test_user_is_question_owner(self):
109 """The notification user is always the question owner."""161 """The notification user is always the question owner."""
110 question = StubQuestion()162 self.assertEqual(self.question.owner, self.notification.user)
163 self.assertNotEqual(self.event.user, self.notification.user)
164
165
166class FakeQuestionUnsupportedLanguageNotification(
167 QuestionUnsupportedLanguageNotification):
168 """A subclass that does not send emails."""
169
170 def shouldNotify(self):
171 return False
172
173
174class QuestionUnsupportedLanguageNotificationTestCase(TestCase):
175 """Test notifications about questions with unsupported languages."""
176
177 def setUp(self):
178 self.question = StubQuestion()
179 self.event = FakeEvent()
180 self.notification = FakeQuestionUnsupportedLanguageNotification(
181 self.question, self.event)
182
183 def test_recipient_set(self):
184 self.assertEqual(
185 QuestionRecipientSet.CONTACT,
186 self.notification.recipient_set)
187
188
189class FakeQuestionNotification(QuestionNotification):
190 """A subclass to exercise question notifcations."""
191
192 recipient_set = QuestionRecipientSet.ASKER_SUBSCRIBER
193
194 def getBody(self):
195 return 'body'
196
197
198class QuestionNotificationTestCase(TestCaseWithFactory):
199 """Test common question notification behavior."""
200
201 layer = DatabaseFunctionalLayer
202
203 def makeQuestion(self):
204 """Create question that does not trigger a notification."""
205 asker = self.factory.makePerson()
206 product = self.factory.makeProduct()
207 naked_question_set = removeSecurityProxy(getUtility(IQuestionSet))
208 question = naked_question_set.new(
209 title='title', description='description', owner=asker,
210 language=getUtility(ILanguageSet)['en'],
211 product=product, distribution=None, sourcepackagename=None)
212 return question
213
214 def test_init_enqueue(self):
215 # Creating a question notification creates a queation email job.
216 question = self.makeQuestion()
111 event = FakeEvent()217 event = FakeEvent()
112 notification = TestQuestionAddedNotification(question, event)218 event.user = self.factory.makePerson()
113 self.assertEqual(question.owner, notification.user)219 notification = FakeQuestionNotification(question, event)
114 self.assertNotEqual(event.user, notification.user)220 self.assertEqual(
221 notification.recipient_set.name,
222 notification.job.metadata['recipient_set'])
223 self.assertEqual(notification.question, notification.job.question)
224 self.assertEqual(notification.user, notification.job.user)
225 self.assertEqual(notification.getSubject(), notification.job.subject)
226 self.assertEqual(notification.getBody(), notification.job.body)
227 self.assertEqual(notification.getHeaders(), notification.job.headers)
115228
=== modified file 'lib/lp/answers/tests/test_questionjob.py'
--- lib/lp/answers/tests/test_questionjob.py 2011-04-28 22:25:45 +0000
+++ lib/lp/answers/tests/test_questionjob.py 2011-05-04 17:17:01 +0000
@@ -11,6 +11,7 @@
11from testtools.content_type import UTF8_TEXT11from testtools.content_type import UTF8_TEXT
1212
13from zope.component import getUtility13from zope.component import getUtility
14from zope.security.proxy import removeSecurityProxy
1415
15from canonical.launchpad.interfaces.lpstorm import IStore16from canonical.launchpad.interfaces.lpstorm import IStore
16from canonical.launchpad.mail import format_address17from canonical.launchpad.mail import format_address
@@ -20,6 +21,7 @@
20 QuestionJobType,21 QuestionJobType,
21 QuestionRecipientSet,22 QuestionRecipientSet,
22 )23 )
24from lp.answers.interfaces.questioncollection import IQuestionSet
23from lp.answers.interfaces.questionjob import IQuestionEmailJobSource25from lp.answers.interfaces.questionjob import IQuestionEmailJobSource
24from lp.answers.model.questionjob import (26from lp.answers.model.questionjob import (
25 QuestionJob,27 QuestionJob,
@@ -28,6 +30,7 @@
28from lp.services.job.interfaces.job import JobStatus30from lp.services.job.interfaces.job import JobStatus
29from lp.services.log.logger import BufferLogger31from lp.services.log.logger import BufferLogger
30from lp.services.mail import stub32from lp.services.mail import stub
33from lp.services.mail.sendmail import format_address_for_person
31from lp.services.worlddata.interfaces.language import ILanguageSet34from lp.services.worlddata.interfaces.language import ILanguageSet
32from lp.testing import (35from lp.testing import (
33 run_script,36 run_script,
@@ -107,7 +110,14 @@
107110
108 def test_iterReady(self):111 def test_iterReady(self):
109 # Jobs in the ready state are returned by the iterator.112 # Jobs in the ready state are returned by the iterator.
110 question = self.factory.makeQuestion()113 # Creating a question implicitly created an question email job.
114 asker = self.factory.makePerson()
115 product = self.factory.makeProduct()
116 naked_question_set = removeSecurityProxy(getUtility(IQuestionSet))
117 question = naked_question_set.new(
118 title='title', description='description', owner=asker,
119 language=getUtility(ILanguageSet)['en'],
120 product=product, distribution=None, sourcepackagename=None)
111 user, subject, ignore, headers = self.makeUserSubjectBodyHeaders()121 user, subject, ignore, headers = self.makeUserSubjectBodyHeaders()
112 job_1 = QuestionEmailJob.create(122 job_1 = QuestionEmailJob.create(
113 question, user, QuestionRecipientSet.SUBSCRIBER,123 question, user, QuestionRecipientSet.SUBSCRIBER,
@@ -198,7 +208,8 @@
198 job = QuestionEmailJob.create(208 job = QuestionEmailJob.create(
199 question, user, QuestionRecipientSet.SUBSCRIBER,209 question, user, QuestionRecipientSet.SUBSCRIBER,
200 subject, body, headers)210 subject, body, headers)
201 self.assertEqual(user, job.getErrorRecipients())211 self.assertEqual(
212 [format_address_for_person(job.user)], job.getErrorRecipients())
202213
203 def test_recipients_asker(self):214 def test_recipients_asker(self):
204 # The recipients property contains the question owner.215 # The recipients property contains the question owner.
@@ -323,8 +334,18 @@
323 def test_run_cronscript(self):334 def test_run_cronscript(self):
324 # The cronscript is configured: schema-lazr.conf and security.cfg.335 # The cronscript is configured: schema-lazr.conf and security.cfg.
325 question = self.factory.makeQuestion()336 question = self.factory.makeQuestion()
337 with person_logged_in(question.target.owner):
338 question.linkBug(self.factory.makeBug(product=question.target))
339 question.linkFAQ(
340 question.target.owner,
341 self.factory.makeFAQ(target=question.target),
342 'test FAQ link')
326 self.addAnswerContact(question)343 self.addAnswerContact(question)
327 user, subject, body, headers = self.makeUserSubjectBodyHeaders()344 user, subject, body, headers = self.makeUserSubjectBodyHeaders()
345 with person_logged_in(user):
346 lang_set = getUtility(ILanguageSet)
347 user.addLanguage(lang_set['en'])
348 question.target.addAnswerContact(user)
328 job = QuestionEmailJob.create(349 job = QuestionEmailJob.create(
329 question, user, QuestionRecipientSet.ASKER_SUBSCRIBER,350 question, user, QuestionRecipientSet.ASKER_SUBSCRIBER,
330 subject, body, headers)351 subject, body, headers)
@@ -336,6 +357,8 @@
336 self.addDetail("stdout", Content(UTF8_TEXT, lambda: out))357 self.addDetail("stdout", Content(UTF8_TEXT, lambda: out))
337 self.addDetail("stderr", Content(UTF8_TEXT, lambda: err))358 self.addDetail("stderr", Content(UTF8_TEXT, lambda: err))
338 self.assertEqual(0, exit_code)359 self.assertEqual(0, exit_code)
360 self.assertTrue(
361 'Traceback (most recent call last)' not in err)
339 message = (362 message = (
340 'QuestionEmailJob has sent email for question %s.' % question.id)363 'QuestionEmailJob has sent email for question %s.' % question.id)
341 self.assertTrue(364 self.assertTrue(
342365
=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
--- lib/lp/bugs/tests/test_bugnotification.py 2011-04-05 22:34:35 +0000
+++ lib/lp/bugs/tests/test_bugnotification.py 2011-05-04 17:17:01 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8from itertools import chain8from itertools import chain
9import transaction
9import unittest10import unittest
1011
11from lazr.lifecycle.event import ObjectModifiedEvent12from lazr.lifecycle.event import ObjectModifiedEvent
@@ -24,6 +25,7 @@
24 LaunchpadFunctionalLayer,25 LaunchpadFunctionalLayer,
25 LaunchpadZopelessLayer,26 LaunchpadZopelessLayer,
26 )27 )
28from lp.answers.tests.test_question_notifications import pop_questionemailjobs
27from lp.bugs.interfaces.bugtask import (29from lp.bugs.interfaces.bugtask import (
28 BugTaskStatus,30 BugTaskStatus,
29 IUpstreamBugTask,31 IUpstreamBugTask,
@@ -36,7 +38,6 @@
36from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute38from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute
37from lp.testing import TestCaseWithFactory39from lp.testing import TestCaseWithFactory
38from lp.testing.factory import LaunchpadObjectFactory40from lp.testing.factory import LaunchpadObjectFactory
39from lp.testing.mail_helpers import pop_notifications
40from lp.testing.matchers import Contains41from lp.testing.matchers import Contains
4142
4243
@@ -120,8 +121,9 @@
120 self.subscriber = self.factory.makePerson()121 self.subscriber = self.factory.makePerson()
121 question.subscribe(self.subscriber)122 question.subscribe(self.subscriber)
122 question.linkBug(self.bug)123 question.linkBug(self.bug)
123 # Flush pending notifications for question creation.124 # Flush pending jobs for question creation.
124 pop_notifications()125 pop_questionemailjobs()
126 transaction.commit()
125 self.layer.switchDbUser(config.malone.expiration_dbuser)127 self.layer.switchDbUser(config.malone.expiration_dbuser)
126128
127 def test_notifications_for_question_subscribers(self):129 def test_notifications_for_question_subscribers(self):
@@ -134,10 +136,10 @@
134 bug_modified = ObjectModifiedEvent(136 bug_modified = ObjectModifiedEvent(
135 bugtask, bugtask_before_modification, ["status"])137 bugtask, bugtask_before_modification, ["status"])
136 notify(bug_modified)138 notify(bug_modified)
139 recipients = [
140 job.metadata['recipient_set'] for job in pop_questionemailjobs()]
137 self.assertContentEqual(141 self.assertContentEqual(
138 [self.product.owner.preferredemail.email,142 ['ASKER_SUBSCRIBER'], recipients)
139 self.subscriber.preferredemail.email],
140 [mail['To'] for mail in pop_notifications()])
141143
142144
143class TestNotificationsLinkToFilters(TestCaseWithFactory):145class TestNotificationsLinkToFilters(TestCaseWithFactory):
@@ -254,7 +256,7 @@
254 self.assertEqual(256 self.assertEqual(
255 {self.subscriber: {'sources': sources,257 {self.subscriber: {'sources': sources,
256 'filter descriptions': []},258 'filter descriptions': []},
257 subscriber2: {'sources': sources2, 259 subscriber2: {'sources': sources2,
258 'filter descriptions': [u'Special Filter!']}},260 'filter descriptions': [u'Special Filter!']}},
259 BugNotificationSet().getRecipientFilterData(261 BugNotificationSet().getRecipientFilterData(
260 {self.subscriber: sources, subscriber2: sources2},262 {self.subscriber: sources, subscriber2: sources2},
@@ -278,7 +280,7 @@
278 # Perform the test.280 # Perform the test.
279 sources = list(self.notification.recipients)281 sources = list(self.notification.recipients)
280 sources.extend(self.notification2.recipients)282 sources.extend(self.notification2.recipients)
281 assert(len(sources)==2)283 assert(len(sources) == 2)
282 self.assertEqual(284 self.assertEqual(
283 {self.subscriber: {'sources': sources,285 {self.subscriber: {'sources': sources,
284 'filter descriptions': ['Another Filter!', 'Special Filter!']}},286 'filter descriptions': ['Another Filter!', 'Special Filter!']}},
@@ -316,7 +318,7 @@
316 sources = list(self.notification.recipients)318 sources = list(self.notification.recipients)
317 sources2 = list(notification2.recipients)319 sources2 = list(notification2.recipients)
318 self.assertEqual(320 self.assertEqual(
319 {subscriber2: {'sources': sources2, 321 {subscriber2: {'sources': sources2,
320 'filter descriptions': [u'Special Filter!']}},322 'filter descriptions': [u'Special Filter!']}},
321 BugNotificationSet().getRecipientFilterData(323 BugNotificationSet().getRecipientFilterData(
322 {self.subscriber: sources, subscriber2: sources2},324 {self.subscriber: sources, subscriber2: sources2},
323325
=== modified file 'lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt'
--- lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt 2011-05-04 17:17:01 +0000
@@ -1,4 +1,5 @@
1= Linked Bug Status Changed Notification =1Linked Bug Status Changed Notification
2======================================
23
3While a bug is linked to a question , its subscribers will be notified4While a bug is linked to a question , its subscribers will be notified
4of changes to the bug status:5of changes to the bug status:
@@ -7,6 +8,8 @@
7 >>> from zope.interface import providedBy8 >>> from zope.interface import providedBy
8 >>> from lazr.lifecycle.event import ObjectModifiedEvent9 >>> from lazr.lifecycle.event import ObjectModifiedEvent
9 >>> from lazr.lifecycle.snapshot import Snapshot10 >>> from lazr.lifecycle.snapshot import Snapshot
11 >>> from lp.answers.tests.test_question_notifications import (
12 ... pop_questionemailjobs)
10 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus13 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
11 >>> from lp.registry.interfaces.person import IPersonSet14 >>> from lp.registry.interfaces.person import IPersonSet
1215
@@ -15,20 +18,22 @@
15 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))18 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
16 >>> bugtask.transitionToStatus(BugTaskStatus.CONFIRMED, no_priv)19 >>> bugtask.transitionToStatus(BugTaskStatus.CONFIRMED, no_priv)
17 >>> bugtask.statusexplanation = 'This bug really happened to me.'20 >>> bugtask.statusexplanation = 'This bug really happened to me.'
21 >>> ignore = pop_questionemailjobs()
18 >>> notify(ObjectModifiedEvent(22 >>> notify(ObjectModifiedEvent(
19 ... bugtask, original_bugtask, ['status', 'statusexplanation'],23 ... bugtask, original_bugtask, ['status', 'statusexplanation'],
20 ... user=no_priv))24 ... user=no_priv))
2125
22 >>> from lp.testing.mail_helpers import pop_notifications26 >>> notifications = pop_questionemailjobs()
23 >>> notifications = pop_notifications()
24 >>> len(notifications)27 >>> len(notifications)
25 228 1
26 >>> [notification['To'] for notification in notifications]29
27 ['support@ubuntu.com', 'test@canonical.com']30 >>> print notifications[0].metadata['recipient_set']
28 >>> notification_body = notifications[0].get_payload(decode=True)31 ASKER_SUBSCRIBER
29 >>> print notifications[0]['Subject']32
33 >>> print notifications[0].subject
30 [Question #...]: Status of bug #... changed to 'Confirmed' in Ubuntu34 [Question #...]: Status of bug #... changed to 'Confirmed' in Ubuntu
31 >>> print notification_body #doctest: -NORMALIZE_WHITESPACE35
36 >>> print notifications[0].body
32 Bug #... status changed in Ubuntu:37 Bug #... status changed in Ubuntu:
33 <BLANKLINE>38 <BLANKLINE>
34 New => Confirmed39 New => Confirmed
@@ -43,15 +48,12 @@
43 This bug is linked to #15.48 This bug is linked to #15.
44 Can't install Ubuntu49 Can't install Ubuntu
45 http://.../ubuntu/+question/...50 http://.../ubuntu/+question/...
46 <BLANKLINE>
47 --...
48 You received this question notification because you are a member of
49 Ubuntu Team, which is an answer contact for Ubuntu.
5051
51Only a change in status triggers a notification.52Only a change in status triggers a notification.
5253
53 >>> from lp.testing import login_person54 >>> from lp.testing import login_person
54 >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')55 >>> sample_person = getUtility(IPersonSet).getByEmail(
56 ... 'test@canonical.com')
55 >>> login_person(sample_person)57 >>> login_person(sample_person)
56 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))58 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
57 >>> bugtask.transitionToAssignee(sample_person)59 >>> bugtask.transitionToAssignee(sample_person)
@@ -59,6 +61,5 @@
59 ... bugtask, original_bugtask, ['assignee', 'dateassigned'],61 ... bugtask, original_bugtask, ['assignee', 'dateassigned'],
60 ... user=sample_person))62 ... user=sample_person))
6163
62 >>> len(pop_notifications())64 >>> len(pop_questionemailjobs())
63 065 0
64
6566
=== modified file 'lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt'
--- lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt 2010-10-10 15:39:28 +0000
+++ lib/lp/coop/answersbugs/tests/notifications-linked-private-bug.txt 2011-05-04 17:17:01 +0000
@@ -1,4 +1,5 @@
1= Linked Bug Status Changed Notification (Private) =1Linked Bug Status Changed Notification (Private)
2================================================
23
3See `answer-tracker-notifications-linked-bug.txt` for public bug behavior.4See `answer-tracker-notifications-linked-bug.txt` for public bug behavior.
45
@@ -9,9 +10,10 @@
9 >>> from zope.interface import providedBy10 >>> from zope.interface import providedBy
10 >>> from lazr.lifecycle.event import ObjectModifiedEvent11 >>> from lazr.lifecycle.event import ObjectModifiedEvent
11 >>> from lazr.lifecycle.snapshot import Snapshot12 >>> from lazr.lifecycle.snapshot import Snapshot
13 >>> from lp.answers.tests.test_question_notifications import (
14 ... pop_questionemailjobs)
12 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus15 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
13 >>> from lp.registry.interfaces.person import IPersonSet16 >>> from lp.registry.interfaces.person import IPersonSet
14 >>> from lp.testing.mail_helpers import pop_notifications
1517
16 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')18 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
17 >>> bugtask = get_bugtask_linked_to_question()19 >>> bugtask = get_bugtask_linked_to_question()
@@ -20,8 +22,9 @@
20 True22 True
21 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))23 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
22 >>> bugtask.transitionToStatus(BugTaskStatus.FIXCOMMITTED, no_priv)24 >>> bugtask.transitionToStatus(BugTaskStatus.FIXCOMMITTED, no_priv)
25 >>> ignore = pop_questionemailjobs()
23 >>> notify(ObjectModifiedEvent(26 >>> notify(ObjectModifiedEvent(
24 ... bugtask, original_bugtask, ['status'], user=no_priv))27 ... bugtask, original_bugtask, ['status'], user=no_priv))
25 >>> notifications = pop_notifications()28 >>> notifications = pop_questionemailjobs()
26 >>> len(notifications)29 >>> len(notifications)
27 030 0
2831
=== added directory 'lib/lp/services/mail/doc'
=== renamed file 'lib/canonical/launchpad/doc/notification-recipient-set.txt' => 'lib/lp/services/mail/doc/notification-recipient-set.txt'
--- lib/canonical/launchpad/doc/notification-recipient-set.txt 2010-12-06 22:10:11 +0000
+++ lib/lp/services/mail/doc/notification-recipient-set.txt 2011-05-04 17:17:01 +0000
@@ -1,8 +1,9 @@
1= INotificationRecipientSet =1INotificationRecipientSet
2=========================
23
3It is part of Launchpad policy that all email notifications contain in4It is part of Launchpad policy that all email notifications contain in
4the footer an explanation of why the email was sent. A simpler string5the footer an explanation of why the email was sent. A simpler string is
5is also usually added to a X-Launchpad-Message-Rationale header to allow6also usually added to a X-Launchpad-Message-Rationale header to allow
6easy filtering.7easy filtering.
78
8The easiest way to implement that policy is for methods returning a list9The easiest way to implement that policy is for methods returning a list
@@ -11,11 +12,12 @@
11recipient lists with the rationale for contacting them.12recipient lists with the rationale for contacting them.
1213
13There is a base implementation of the interface available as14There is a base implementation of the interface available as
14canonical.launchpad.mailnotification.NotificationRecipientSet.15canonical.launchpad.mailnotification.NotificationRecipientSet. You can
15You can use it as is or derive from it16use it as is or derive from it (see bugnotificationrecipients.txt for an
16(see bugnotificationrecipients.txt for an example of a derivation).17example of a derivation).
1718
18 >>> from canonical.launchpad.interfaces.launchpad import INotificationRecipientSet19 >>> from canonical.launchpad.interfaces.launchpad import (
20 ... INotificationRecipientSet)
19 >>> from canonical.launchpad.webapp.testing import verifyObject21 >>> from canonical.launchpad.webapp.testing import verifyObject
20 >>> from canonical.launchpad.mailnotification import (22 >>> from canonical.launchpad.mailnotification import (
21 ... NotificationRecipientSet)23 ... NotificationRecipientSet)
@@ -24,10 +26,12 @@
24 >>> verifyObject(INotificationRecipientSet, recipients)26 >>> verifyObject(INotificationRecipientSet, recipients)
25 True27 True
2628
27== Populating the set ==29
2830Populating the set
29You add recipients to the set using the add() method. The method takes the31------------------
30IPerson to add along the notification rationale and header code.32
33You add recipients to the set using the add() method. The method takes
34the IPerson to add along the notification rationale and header code.
3135
32 >>> from lp.registry.interfaces.person import IPersonSet36 >>> from lp.registry.interfaces.person import IPersonSet
33 >>> person_set = getUtility(IPersonSet)37 >>> person_set = getUtility(IPersonSet)
@@ -43,7 +47,8 @@
43value is only used as an example. In practice, you should try to reuse47value is only used as an example. In practice, you should try to reuse
44existing values if they apply to your context.48existing values if they apply to your context.
4549
46The getPersons() method returns the list of recipients sorted by display name.50The getPersons() method returns the list of recipients sorted by display
51name.
4752
48 >>> [person.displayname for person in recipients.getRecipients()]53 >>> [person.displayname for person in recipients.getRecipients()]
49 [u'Celso Providelo', u'Sample Person']54 [u'Celso Providelo', u'Sample Person']
@@ -55,19 +60,21 @@
55 Celso Providelo60 Celso Providelo
56 Sample Person61 Sample Person
5762
58The getEmails() methods return the emails of all the recipients, also sorted63The getEmails() methods return the emails of all the recipients, also
59alphabetically:64sorted alphabetically:
6065
61 >>> recipients.getEmails()66 >>> recipients.getEmails()
62 ['celso.providelo@canonical.com', 'test@canonical.com']67 ['celso.providelo@canonical.com', 'test@canonical.com']
6368
64You can test if an IPerson or an email is part of the recipients using the69You can test if an IPerson or an email is part of the recipients using
65standard `in` operator:70the standard `in` operator:
6671
67 >>> cprov in recipients72 >>> cprov in recipients
68 True73 True
74
69 >>> 'celso.providelo@canonical.com' in recipients75 >>> 'celso.providelo@canonical.com' in recipients
70 True76 True
77
71 >>> u'test@canonical.com' in recipients78 >>> u'test@canonical.com' in recipients
72 True79 True
7380
@@ -85,9 +92,12 @@
85 >>> bool(NotificationRecipientSet())92 >>> bool(NotificationRecipientSet())
86 False93 False
8794
88== Obtaining the rationale ==95
8996Obtaining the rationale
90You can obtain the rationale, header tuple by using the getReason() method:97-----------------------
98
99You can obtain the rationale, header tuple by using the getReason()
100method:
91101
92 >>> recipients.getReason(cprov)102 >>> recipients.getReason(cprov)
93 ('You are notified for no reason.', 'Why not')103 ('You are notified for no reason.', 'Why not')
@@ -117,24 +127,29 @@
117 ...127 ...
118 AssertionError: ...128 AssertionError: ...
119129
120== Team as recipient ==130
121131Team as recipient
122Adding a team with a preferred email address works like adding any132-----------------
123other person:133
134Adding a team with a preferred email address works like adding any other
135person:
124136
125 >>> ubuntu_team = person_set.getByName('ubuntu-team')137 >>> ubuntu_team = person_set.getByName('ubuntu-team')
126 >>> login_person(ubuntu_team.teamowner)138 >>> login_person(ubuntu_team.teamowner)
127 >>> print ubuntu_team.preferredemail.email139 >>> print ubuntu_team.preferredemail.email
128 support@ubuntu.com140 support@ubuntu.com
141
129 >>> recipients.add(ubuntu_team, 'You are notified for fun.', 'Fun')142 >>> recipients.add(ubuntu_team, 'You are notified for fun.', 'Fun')
130143
131 >>> ubuntu_team in recipients144 >>> ubuntu_team in recipients
132 True145 True
146
133 >>> 'support@ubuntu.com' in recipients147 >>> 'support@ubuntu.com' in recipients
134 True148 True
135149
136 >>> [person.displayname for person in recipients]150 >>> [person.displayname for person in recipients]
137 [u'Celso Providelo', u'Sample Person', u'Ubuntu Team']151 [u'Celso Providelo', u'Sample Person', u'Ubuntu Team']
152
138 >>> recipients.getEmails()153 >>> recipients.getEmails()
139 ['celso.providelo@canonical.com', 'support@ubuntu.com',154 ['celso.providelo@canonical.com', 'support@ubuntu.com',
140 'test@canonical.com']155 'test@canonical.com']
@@ -146,11 +161,13 @@
146 >>> ubuntu_gnome_team = person_set.getByName('name18')161 >>> ubuntu_gnome_team = person_set.getByName('name18')
147 >>> print ubuntu_gnome_team.preferredemail162 >>> print ubuntu_gnome_team.preferredemail
148 None163 None
164
149 >>> recipients.add(165 >>> recipients.add(
150 ... ubuntu_gnome_team,166 ... ubuntu_gnome_team,
151 ... 'Notified because a member of the team', 'Team')167 ... 'Notified because a member of the team', 'Team')
152 >>> ubuntu_gnome_team in recipients168 >>> ubuntu_gnome_team in recipients
153 True169 True
170
154 >>> recipients.getEmails()171 >>> recipients.getEmails()
155 ['andrew.bennetts@ubuntulinux.com', 'foo.bar@canonical.com',172 ['andrew.bennetts@ubuntulinux.com', 'foo.bar@canonical.com',
156 'limi@plone.org', 'steve.alexander@ubuntulinux.com',173 'limi@plone.org', 'steve.alexander@ubuntulinux.com',
@@ -162,21 +179,25 @@
162 [u'Ubuntu Gnome Team']179 [u'Ubuntu Gnome Team']
163180
164So Sample Person is not in the recipients list, even if his email will181So Sample Person is not in the recipients list, even if his email will
165be notified for he's a member of Warty Security Team, itself a member182be notified for he's a member of Warty Security Team, itself a member of
166of Ubuntu Gnome Team:183Ubuntu Gnome Team:
167184
168 >>> warty_security_team = person_set.getByName('name20')185 >>> warty_security_team = person_set.getByName('name20')
169 >>> print warty_security_team.displayname186 >>> print warty_security_team.displayname
170 Warty Security Team187 Warty Security Team
188
171 >>> sample_person.inTeam(warty_security_team)189 >>> sample_person.inTeam(warty_security_team)
172 True190 True
191
173 >>> warty_security_team.inTeam(ubuntu_gnome_team)192 >>> warty_security_team.inTeam(ubuntu_gnome_team)
174 True193 True
194
175 >>> sample_person in ubuntu_gnome_team.activemembers195 >>> sample_person in ubuntu_gnome_team.activemembers
176 False196 False
177197
178 >>> sample_person in recipients198 >>> sample_person in recipients
179 False199 False
200
180 >>> 'test@canonical.com' in recipients201 >>> 'test@canonical.com' in recipients
181 True202 True
182203
@@ -184,10 +205,13 @@
184205
185 >>> recipients.getReason(ubuntu_gnome_team)206 >>> recipients.getReason(ubuntu_gnome_team)
186 ('Notified because a member of the team', 'Team')207 ('Notified because a member of the team', 'Team')
208
187 >>> recipients.getReason('test@canonical.com')209 >>> recipients.getReason('test@canonical.com')
188 ('Notified because a member of the team', 'Team')210 ('Notified because a member of the team', 'Team')
189211
190== Adding many persons at the same time ==212
213Adding many persons at the same time
214------------------------------------
191215
192If you pass an iterable sequence to the add() method, all members will216If you pass an iterable sequence to the add() method, all members will
193be added with the same rationale:217be added with the same rationale:
@@ -200,27 +224,37 @@
200224
201 >>> recipients.getReason(no_priv)225 >>> recipients.getReason(no_priv)
202 ('Notified for fun.', 'Fun')226 ('Notified for fun.', 'Fun')
227
203 >>> recipients.getReason(sample_person)228 >>> recipients.getReason(sample_person)
204 ('Notified for fun.', 'Fun')229 ('Notified for fun.', 'Fun')
205230
206== Removing recipients ==231
207232Removing recipients
208It is also possible to remove a person from the NotificationRecipientSet():233-------------------
234
235It is also possible to remove a person from the
236NotificationRecipientSet():
209237
210 >>> recipients = NotificationRecipientSet()238 >>> recipients = NotificationRecipientSet()
211 >>> recipients.add(239 >>> recipients.add(
212 ... [sample_person, no_priv, cprov], 'Notified for fun.', 'Fun')240 ... [sample_person, no_priv, cprov], 'Notified for fun.', 'Fun')
213 >>> [person.displayname for person in recipients.getRecipients()]241 >>> [person.displayname for person in recipients.getRecipients()]
214 [u'Celso Providelo', u'No Privileges Person', u'Sample Person']242 [u'Celso Providelo', u'No Privileges Person', u'Sample Person']
243
215 >>> recipients.remove([sample_person, cprov])244 >>> recipients.remove([sample_person, cprov])
216 >>> [person.displayname for person in recipients.getRecipients()]245 >>> [person.displayname for person in recipients.getRecipients()]
217 [u'No Privileges Person']246 [u'No Privileges Person']
218247
219== A person's first impression sticks ==248 >>> recipients.getEmails()
220249 ['no-priv@canonical.com']
221In general, the most specific rationale is used for a given email.250
222A rationale given for a person is considered more251
223specific than one obtained through team membership.252A person's first impression sticks
253----------------------------------
254
255In general, the most specific rationale is used for a given email. A
256rationale given for a person is considered more specific than one
257obtained through team membership.
224258
225So, if a person is added more than once to the set, the first reason259So, if a person is added more than once to the set, the first reason
226will be the one returned.260will be the one returned.
@@ -265,7 +299,8 @@
265 ('Sample Person', 'Person')299 ('Sample Person', 'Person')
266300
267301
268== Merging recipients set ==302Merging recipients set
303----------------------
269304
270You can merge two recipients set by using the update() method. It will305You can merge two recipients set by using the update() method. It will
271add all the recipients in the second set along their rationale. If the306add all the recipients in the second set along their rationale. If the
@@ -283,3 +318,5 @@
283 Celso Providelo: B (Reason B)318 Celso Providelo: B (Reason B)
284 No Privileges Person: B (Reason B)319 No Privileges Person: B (Reason B)
285 Sample Person: A (Reason A)320 Sample Person: A (Reason A)
321
322
286323
=== modified file 'lib/lp/services/mail/notificationrecipientset.py'
--- lib/lp/services/mail/notificationrecipientset.py 2011-03-30 20:08:42 +0000
+++ lib/lp/services/mail/notificationrecipientset.py 2011-05-04 17:17:01 +0000
@@ -131,6 +131,7 @@
131 removed_person.preferredemail)131 removed_person.preferredemail)
132 email = str(preferred_email.email)132 email = str(preferred_email.email)
133 self._receiving_people.discard((email, removed_person))133 self._receiving_people.discard((email, removed_person))
134 del self._emailToPerson[email]
134135
135 def update(self, recipient_set):136 def update(self, recipient_set):
136 """See `INotificationRecipientSet`."""137 """See `INotificationRecipientSet`."""
137138
=== added file 'lib/lp/services/mail/tests/test_doc.py'
--- lib/lp/services/mail/tests/test_doc.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/mail/tests/test_doc.py 2011-05-04 17:17:01 +0000
@@ -0,0 +1,19 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test mail documentation."""
5
6__metaclass__ = type
7
8import os
9
10from canonical.testing.layers import DatabaseFunctionalLayer
11from lp.services.testing import build_test_suite
12
13
14here = os.path.dirname(os.path.realpath(__file__))
15
16
17def test_suite():
18 suite = build_test_suite(here, {}, layer=DatabaseFunctionalLayer)
19 return suite