Merge lp:~sinzui/launchpad/merge-mailing-list-bug-471770 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~sinzui/launchpad/merge-mailing-list-bug-471770
Merge into: lp:launchpad
Diff against target: 272 lines (+109/-32)
5 files modified
lib/canonical/launchpad/emailtemplates/person-merged.txt (+15/-0)
lib/lp/registry/doc/person-merge.txt (+26/-2)
lib/lp/registry/model/person.py (+14/-15)
lib/lp/registry/tests/test_personset.py (+52/-11)
lib/lp/testing/menu.py (+2/-4)
To merge this branch: bzr merge lp:~sinzui/launchpad/merge-mailing-list-bug-471770
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+14806@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

This is my branch to remove mailing list subscriptions from merged accounts.

    lp:~sinzui/launchpad/merge-mailing-list-bug-471770
    Diff size: 247
    Launchpad bug: https://bugs.launchpad.net/bugs/471770
    Test command: ./bin/test -vvt "registry.*test_pesonset"
    Pre-implementation: bac, salgado
    Target release: 3.1.11

= Remove mailing list subscriptions from merged accounts =

OOPS-1402EB708 shows an oops that occurs when ~lhavelund tries to edit his
email addresses. This failure is SimpleVocabulary(terms). lhavelund is
subscribed to a mailing list under a NEW address. The email address came
from a merged account. Merge sets the email addresses to NEW so that they
can be reconfirmed--but that is not possible in this case because the user
cannot request a validation email from +editemail.

== Rules ==

    * Update PersonSet._mergeMailingListSubscriptions() to delete the
      subscriptions
    * Send an email to the user explaining that he should confirm the
      merged email addresses and update his mailing list subscriptions
      * This ensure that users are notified regardless of if the merge was
        admin-assisted or the user did it himself.
      * This is a step towards making all merges happen offline to avoid
        time outs.

== QA ==

    * On staging, merge an account that has a mailing list.
    * Verify that there is a notification email in the staging inbox.
    * Verify that you can access your +editemails
    * Update your mailing list subscriptions.

== Lint ==

Linting changed files:
  lib/canonical/launchpad/emailtemplates/person-merged.txt
  lib/lp/registry/doc/person-merge.txt
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_personset.py

== Test ==

    * lib/lp/registry/doc/person-merge.txt
      * Added a test to verify the user is sent an email after the merge.
    * lib/lp/registry/tests/test_personset.py
      * Moved the tests to the DatabaseFunctionalLayer
      * Removed the duplicate ProductSet lines
      * Added a test to verify that that mailing list subscriptions are
        deleted.

== Implementation ==

    * lib/canonical/launchpad/emailtemplates/person-merged.txt
      * Added an email to inform the user to review his email settings.
    * lib/lp/registry/model/person.py
      * Removed the mailing list transfer SQL. The delete SQL was there to
        clean up duplicate subscriptions, but now deletes them all
      * Add a PersonNotification to the merged user so that he knows to
        review his email settings.

Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Curtis,

nice branch. I have only two spelling questions/suggestions.

Abel

> === added file 'lib/canonical/launchpad/emailtemplates/person-merged.txt'
> --- lib/canonical/launchpad/emailtemplates/person-merged.txt 1970-01-01 00:00:00 +0000
> +++ lib/canonical/launchpad/emailtemplates/person-merged.txt 2009-11-12 21:15:25 +0000
> @@ -0,0 +1,15 @@
> +
> +The Launchpad account named '%(dupename)s' was merged into the account
> +named '%(person)s'. You can confirm the merged email addresses to
> +associate them with your account. Merged email address are unsubscribed

I think that should be "email addresses are".

> +from all Launchpad mailing lists during the merge. All team memberships
> +for the merged account were also transfered to your account, and

I am not sure, but shouldn't this be "transferred"?

> +those team may have mailing lists.
> +
> +You can review and update your email and subscription settings at:
> +
> + https://launchpad.net/~%(person)s/+editemails
> +
> +Thank you,
> +
> +The Launchpad team

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/canonical/launchpad/emailtemplates/person-merged.txt'
--- lib/canonical/launchpad/emailtemplates/person-merged.txt 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/emailtemplates/person-merged.txt 2009-11-13 13:12:15 +0000
@@ -0,0 +1,15 @@
1
2The Launchpad account named '%(dupename)s' was merged into the account
3named '%(person)s'. You can confirm the merged email addresses to
4associate them with your account. Merged email addresses are unsubscribed
5from all Launchpad mailing lists during the merge. All team memberships
6for the merged account were also transferred to your account, and
7those team may have mailing lists.
8
9You can review and update your email and subscription settings at:
10
11 https://launchpad.net/~%(person)s/+editemails
12
13Thank you,
14
15The Launchpad team
016
=== renamed file 'lib/lp/registry/doc/xx-coc-views.txt' => 'lib/lp/registry/browser/tests/coc-views.txt'
=== renamed file 'lib/lp/registry/doc/person-edit-views.txt' => 'lib/lp/registry/browser/tests/person-edit-views.txt'
=== renamed file 'lib/lp/registry/doc/person-karma-views.txt' => 'lib/lp/registry/browser/tests/person-karma-views.txt'
=== renamed file 'lib/lp/registry/doc/team-hierarchy-views.txt' => 'lib/lp/registry/browser/tests/team-hierarchy-views.txt'
=== renamed file 'lib/lp/registry/doc/teammembership-views.txt' => 'lib/lp/registry/browser/tests/teammembership-views.txt'
=== modified file 'lib/lp/registry/doc/person-merge.txt'
--- lib/lp/registry/doc/person-merge.txt 2009-08-13 19:03:36 +0000
+++ lib/lp/registry/doc/person-merge.txt 2009-11-13 13:12:15 +0000
@@ -199,8 +199,32 @@
199 >>> results.get_one()[0] is None199 >>> results.get_one()[0] is None
200 True200 True
201201
202202An email is sent to the user informing him that he should review his email
203== Person decoration ==203and mailing list subscription settings.
204
205 >>> from canonical.launchpad.interfaces.personnotification import (
206 ... IPersonNotificationSet)
207
208 >>> notification_set = getUtility(IPersonNotificationSet)
209 >>> notifications = notification_set.getNotificationsToSend()
210 >>> notifications.count()
211 1
212 >>> notification = notifications[0]
213 >>> print notification.person.name
214 name12
215 >>> print notification.subject
216 Launchpad accounts merged
217 >>> print notification.body
218 The Launchpad account named 'marilize-merged' was merged into the account
219 named 'name12'. ...
220
221 You can review and update your email and subscription settings at:
222
223 https://launchpad.net/name12/+editemails ...
224
225
226Person decoration
227-----------------
204228
205Several tables "extend" the Person table by having additional information229Several tables "extend" the Person table by having additional information
206that is UNIQUEly keyed to Person.id. We have a utility function that merges230that is UNIQUEly keyed to Person.id. We have a utility function that merges
207231
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2009-10-16 09:25:45 +0000
+++ lib/lp/registry/model/person.py 2009-11-13 13:12:15 +0000
@@ -2861,21 +2861,9 @@
2861 name=name, new_name=new_name, product=product))2861 name=name, new_name=new_name, product=product))
28622862
2863 def _mergeMailingListSubscriptions(self, cur, from_id, to_id):2863 def _mergeMailingListSubscriptions(self, cur, from_id, to_id):
2864 # Update MailingListSubscription. Note that no remaining records2864 # Update MailingListSubscription. Note that since all the from_id
2865 # will have email_address set, as we assert earlier that the2865 # email addresses are set to NEW, all the subscriptions must be
2866 # from_person has no email addresses.2866 # removed because the user must confirm them.
2867 # Update records that don't conflict.
2868 cur.execute('''
2869 UPDATE MailingListSubscription
2870 SET person=%(to_id)d
2871 WHERE person=%(from_id)d
2872 AND mailing_list NOT IN (
2873 SELECT mailing_list
2874 FROM MailingListSubscription
2875 WHERE person=%(to_id)d
2876 )
2877 ''' % vars())
2878 # Then trash the remainders.
2879 cur.execute('''2867 cur.execute('''
2880 DELETE FROM MailingListSubscription WHERE person=%(from_id)d2868 DELETE FROM MailingListSubscription WHERE person=%(from_id)d
2881 ''' % vars())2869 ''' % vars())
@@ -3472,6 +3460,17 @@
3472 # flush its caches.3460 # flush its caches.
3473 store.invalidate()3461 store.invalidate()
34743462
3463 # Inform the user of the merge changes.
3464 if not to_person.isTeam():
3465 mail_text = get_email_template('person-merged.txt')
3466 mail_text = mail_text % {
3467 'dupename': from_person.name,
3468 'person': to_person.name,
3469 }
3470 subject = 'Launchpad accounts merged'
3471 getUtility(IPersonNotificationSet).addNotification(
3472 to_person, subject, mail_text)
3473
3475 def getValidPersons(self, persons):3474 def getValidPersons(self, persons):
3476 """See `IPersonSet.`"""3475 """See `IPersonSet.`"""
3477 person_ids = [person.id for person in persons]3476 person_ids = [person.id for person in persons]
34783477
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2009-09-29 19:00:28 +0000
+++ lib/lp/registry/tests/test_personset.py 2009-11-13 13:12:15 +0000
@@ -7,46 +7,56 @@
77
8from unittest import TestLoader8from unittest import TestLoader
99
10import transaction
10from zope.component import getUtility11from zope.component import getUtility
11from zope.security.proxy import removeSecurityProxy12from zope.security.proxy import removeSecurityProxy
1213
13from lp.registry.model.person import PersonSet14from lp.registry.model.person import PersonSet
15from lp.registry.interfaces.mailinglistsubscription import (
16 MailingListAutoSubscribePolicy)
14from lp.registry.interfaces.person import (17from lp.registry.interfaces.person import (
15 PersonCreationRationale, IPersonSet)18 PersonCreationRationale, IPersonSet)
16from lp.testing import TestCaseWithFactory19from lp.testing import TestCaseWithFactory, login_person, logout
20
21from canonical.database.sqlbase import cursor
17from canonical.launchpad.testing.databasehelpers import (22from canonical.launchpad.testing.databasehelpers import (
18 remove_all_sample_data_branches)23 remove_all_sample_data_branches)
19from canonical.testing import LaunchpadFunctionalLayer24from canonical.testing import DatabaseFunctionalLayer
2025
2126
22class TestPersonSetBranchCounts(TestCaseWithFactory):27class TestPersonSetBranchCounts(TestCaseWithFactory):
2328
24 layer = LaunchpadFunctionalLayer29 layer = DatabaseFunctionalLayer
2530
26 def setUp(self):31 def setUp(self):
27 TestCaseWithFactory.setUp(self)32 TestCaseWithFactory.setUp(self)
28 remove_all_sample_data_branches()33 remove_all_sample_data_branches()
34 self.person_set = getUtility(IPersonSet)
2935
30 def test_no_branches(self):36 def test_no_branches(self):
31 """Initially there should be no branches."""37 """Initially there should be no branches."""
32 self.assertEqual(0, PersonSet().getPeopleWithBranches().count())38 self.assertEqual(0, self.person_set.getPeopleWithBranches().count())
3339
34 def test_five_branches(self):40 def test_five_branches(self):
35 branches = [self.factory.makeAnyBranch() for x in range(5)]41 branches = [self.factory.makeAnyBranch() for x in range(5)]
36 # Each branch has a different product, so any individual product42 # Each branch has a different product, so any individual product
37 # will return one branch.43 # will return one branch.
38 self.assertEqual(5, PersonSet().getPeopleWithBranches().count())44 self.assertEqual(5, self.person_set.getPeopleWithBranches().count())
39 self.assertEqual(1, PersonSet().getPeopleWithBranches(45 self.assertEqual(1, self.person_set.getPeopleWithBranches(
40 branches[0].product).count())46 branches[0].product).count())
4147
4248
43class TestPersonSetEnsurePerson(TestCaseWithFactory):49class TestPersonSetEnsurePerson(TestCaseWithFactory):
4450
45 layer = LaunchpadFunctionalLayer51 layer = DatabaseFunctionalLayer
46 email_address = 'testing.ensure.person@example.com'52 email_address = 'testing.ensure.person@example.com'
47 displayname = 'Testing ensurePerson'53 displayname = 'Testing ensurePerson'
48 rationale = PersonCreationRationale.SOURCEPACKAGEUPLOAD54 rationale = PersonCreationRationale.SOURCEPACKAGEUPLOAD
4955
56 def setUp(self):
57 TestCaseWithFactory.setUp(self)
58 self.person_set = getUtility(IPersonSet)
59
50 def test_ensurePerson_returns_existing_person(self):60 def test_ensurePerson_returns_existing_person(self):
51 # IPerson.ensurePerson returns existing person and does not61 # IPerson.ensurePerson returns existing person and does not
52 # override its details.62 # override its details.
@@ -54,7 +64,7 @@
54 testing_person = self.factory.makePerson(64 testing_person = self.factory.makePerson(
55 email=self.email_address, displayname=testing_displayname)65 email=self.email_address, displayname=testing_displayname)
5666
57 ensured_person = getUtility(IPersonSet).ensurePerson(67 ensured_person = self.person_set.ensurePerson(
58 self.email_address, self.displayname, self.rationale)68 self.email_address, self.displayname, self.rationale)
59 self.assertEquals(testing_person.id, ensured_person.id)69 self.assertEquals(testing_person.id, ensured_person.id)
60 self.assertIsNot(70 self.assertIsNot(
@@ -67,7 +77,7 @@
67 def test_ensurePerson_hides_new_person_email(self):77 def test_ensurePerson_hides_new_person_email(self):
68 # IPersonSet.ensurePerson creates new person with78 # IPersonSet.ensurePerson creates new person with
69 # 'hide_email_addresses' set.79 # 'hide_email_addresses' set.
70 ensured_person = getUtility(IPersonSet).ensurePerson(80 ensured_person = self.person_set.ensurePerson(
71 self.email_address, self.displayname, self.rationale)81 self.email_address, self.displayname, self.rationale)
72 self.assertTrue(ensured_person.hide_email_addresses)82 self.assertTrue(ensured_person.hide_email_addresses)
7383
@@ -78,7 +88,7 @@
78 self.displayname, email=self.email_address)88 self.displayname, email=self.email_address)
79 self.assertIs(None, test_account.preferredemail.person)89 self.assertIs(None, test_account.preferredemail.person)
8090
81 ensured_person = getUtility(IPersonSet).ensurePerson(91 ensured_person = self.person_set.ensurePerson(
82 self.email_address, self.displayname, self.rationale)92 self.email_address, self.displayname, self.rationale)
83 self.assertEquals(test_account.id, ensured_person.account.id)93 self.assertEquals(test_account.id, ensured_person.account.id)
84 self.assertTrue(ensured_person.hide_email_addresses)94 self.assertTrue(ensured_person.hide_email_addresses)
@@ -98,7 +108,7 @@
98 self.assertIs(None, testing_account.preferredemail.person)108 self.assertIs(None, testing_account.preferredemail.person)
99 self.assertIs(None, testing_person.preferredemail)109 self.assertIs(None, testing_person.preferredemail)
100110
101 ensured_person = getUtility(IPersonSet).ensurePerson(111 ensured_person = self.person_set.ensurePerson(
102 self.email_address, self.displayname, self.rationale)112 self.email_address, self.displayname, self.rationale)
103113
104 # The existing Person was retrieved and the Account114 # The existing Person was retrieved and the Account
@@ -108,5 +118,36 @@
108 ensured_person.preferredemail.id)118 ensured_person.preferredemail.id)
109119
110120
121class TestPersonSetMerge(TestCaseWithFactory):
122
123 layer = DatabaseFunctionalLayer
124
125 def setUp(self):
126 TestCaseWithFactory.setUp(self)
127 # Use the unsecured PersonSet so that private methods can be tested.
128 self.person_set = PersonSet()
129 self.from_person = self.factory.makePerson()
130 self.to_person = self.factory.makePerson()
131 self.cur = cursor()
132
133 def test__mergeMailingListSubscriptions_no_subscriptions(self):
134 self.person_set._mergeMailingListSubscriptions(
135 self.cur, self.from_person.id, self.to_person.id)
136 self.assertEqual(0, self.cur.rowcount)
137
138 def test__mergeMailingListSubscriptions_with_subscriptions(self):
139 self.from_person.mailing_list_auto_subscribe_policy = (
140 MailingListAutoSubscribePolicy.ALWAYS)
141 self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
142 'test-mailinglist', 'team-owner')
143 login_person(self.team.teamowner)
144 self.team.addMember(self.from_person, reviewer=self.team.teamowner)
145 logout()
146 transaction.commit()
147 self.person_set._mergeMailingListSubscriptions(
148 self.cur, self.from_person.id, self.to_person.id)
149 self.assertEqual(1, self.cur.rowcount)
150
151
111def test_suite():152def test_suite():
112 return TestLoader().loadTestsFromName(__name__)153 return TestLoader().loadTestsFromName(__name__)
113154
=== modified file 'lib/lp/testing/menu.py'
--- lib/lp/testing/menu.py 2009-09-10 20:35:24 +0000
+++ lib/lp/testing/menu.py 2009-11-13 13:12:15 +0000
@@ -13,7 +13,6 @@
1313
14def check_menu_links(menu):14def check_menu_links(menu):
15 context = menu.context15 context = menu.context
16 is_sane_menu = True
17 for link in menu.iterlinks():16 for link in menu.iterlinks():
18 if '?' in link.target:17 if '?' in link.target:
19 view_name, _args = link.target.split('?')18 view_name, _args = link.target.split('?')
@@ -24,6 +23,5 @@
24 try:23 try:
25 view = getMultiAdapter((context, request), name=view_name)24 view = getMultiAdapter((context, request), name=view_name)
26 except:25 except:
27 is_sane_menu = False26 return 'Bad link %s: %s' % (link.name, url)
28 print 'Bad link %s: %s' % (link.name, url)27 return True
29 print is_sane_menu