Merge lp:~sinzui/launchpad/merge-mailing-list-bug-471770 into lp:launchpad
- merge-mailing-list-bug-471770
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Curtis Hovey (sinzui) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Abel Deuring (adeuring) wrote : | # |
Hi Curtis,
nice branch. I have only two spelling questions/
Abel
> === added file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -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:/
> +
> +Thank you,
> +
> +The Launchpad team
Preview Diff
1 | === added file 'lib/canonical/launchpad/emailtemplates/person-merged.txt' | |||
2 | --- lib/canonical/launchpad/emailtemplates/person-merged.txt 1970-01-01 00:00:00 +0000 | |||
3 | +++ lib/canonical/launchpad/emailtemplates/person-merged.txt 2009-11-13 13:12:15 +0000 | |||
4 | @@ -0,0 +1,15 @@ | |||
5 | 1 | |||
6 | 2 | The Launchpad account named '%(dupename)s' was merged into the account | ||
7 | 3 | named '%(person)s'. You can confirm the merged email addresses to | ||
8 | 4 | associate them with your account. Merged email addresses are unsubscribed | ||
9 | 5 | from all Launchpad mailing lists during the merge. All team memberships | ||
10 | 6 | for the merged account were also transferred to your account, and | ||
11 | 7 | those team may have mailing lists. | ||
12 | 8 | |||
13 | 9 | You can review and update your email and subscription settings at: | ||
14 | 10 | |||
15 | 11 | https://launchpad.net/~%(person)s/+editemails | ||
16 | 12 | |||
17 | 13 | Thank you, | ||
18 | 14 | |||
19 | 15 | The Launchpad team | ||
20 | 0 | 16 | ||
21 | === renamed file 'lib/lp/registry/doc/xx-coc-views.txt' => 'lib/lp/registry/browser/tests/coc-views.txt' | |||
22 | === renamed file 'lib/lp/registry/doc/person-edit-views.txt' => 'lib/lp/registry/browser/tests/person-edit-views.txt' | |||
23 | === renamed file 'lib/lp/registry/doc/person-karma-views.txt' => 'lib/lp/registry/browser/tests/person-karma-views.txt' | |||
24 | === renamed file 'lib/lp/registry/doc/team-hierarchy-views.txt' => 'lib/lp/registry/browser/tests/team-hierarchy-views.txt' | |||
25 | === renamed file 'lib/lp/registry/doc/teammembership-views.txt' => 'lib/lp/registry/browser/tests/teammembership-views.txt' | |||
26 | === modified file 'lib/lp/registry/doc/person-merge.txt' | |||
27 | --- lib/lp/registry/doc/person-merge.txt 2009-08-13 19:03:36 +0000 | |||
28 | +++ lib/lp/registry/doc/person-merge.txt 2009-11-13 13:12:15 +0000 | |||
29 | @@ -199,8 +199,32 @@ | |||
30 | 199 | >>> results.get_one()[0] is None | 199 | >>> results.get_one()[0] is None |
31 | 200 | True | 200 | True |
32 | 201 | 201 | ||
35 | 202 | 202 | An email is sent to the user informing him that he should review his email | |
36 | 203 | == Person decoration == | 203 | and mailing list subscription settings. |
37 | 204 | |||
38 | 205 | >>> from canonical.launchpad.interfaces.personnotification import ( | ||
39 | 206 | ... IPersonNotificationSet) | ||
40 | 207 | |||
41 | 208 | >>> notification_set = getUtility(IPersonNotificationSet) | ||
42 | 209 | >>> notifications = notification_set.getNotificationsToSend() | ||
43 | 210 | >>> notifications.count() | ||
44 | 211 | 1 | ||
45 | 212 | >>> notification = notifications[0] | ||
46 | 213 | >>> print notification.person.name | ||
47 | 214 | name12 | ||
48 | 215 | >>> print notification.subject | ||
49 | 216 | Launchpad accounts merged | ||
50 | 217 | >>> print notification.body | ||
51 | 218 | The Launchpad account named 'marilize-merged' was merged into the account | ||
52 | 219 | named 'name12'. ... | ||
53 | 220 | |||
54 | 221 | You can review and update your email and subscription settings at: | ||
55 | 222 | |||
56 | 223 | https://launchpad.net/name12/+editemails ... | ||
57 | 224 | |||
58 | 225 | |||
59 | 226 | Person decoration | ||
60 | 227 | ----------------- | ||
61 | 204 | 228 | ||
62 | 205 | Several tables "extend" the Person table by having additional information | 229 | Several tables "extend" the Person table by having additional information |
63 | 206 | that is UNIQUEly keyed to Person.id. We have a utility function that merges | 230 | that is UNIQUEly keyed to Person.id. We have a utility function that merges |
64 | 207 | 231 | ||
65 | === modified file 'lib/lp/registry/model/person.py' | |||
66 | --- lib/lp/registry/model/person.py 2009-10-16 09:25:45 +0000 | |||
67 | +++ lib/lp/registry/model/person.py 2009-11-13 13:12:15 +0000 | |||
68 | @@ -2861,21 +2861,9 @@ | |||
69 | 2861 | name=name, new_name=new_name, product=product)) | 2861 | name=name, new_name=new_name, product=product)) |
70 | 2862 | 2862 | ||
71 | 2863 | def _mergeMailingListSubscriptions(self, cur, from_id, to_id): | 2863 | def _mergeMailingListSubscriptions(self, cur, from_id, to_id): |
87 | 2864 | # Update MailingListSubscription. Note that no remaining records | 2864 | # Update MailingListSubscription. Note that since all the from_id |
88 | 2865 | # will have email_address set, as we assert earlier that the | 2865 | # email addresses are set to NEW, all the subscriptions must be |
89 | 2866 | # from_person has no email addresses. | 2866 | # removed because the user must confirm them. |
75 | 2867 | # Update records that don't conflict. | ||
76 | 2868 | cur.execute(''' | ||
77 | 2869 | UPDATE MailingListSubscription | ||
78 | 2870 | SET person=%(to_id)d | ||
79 | 2871 | WHERE person=%(from_id)d | ||
80 | 2872 | AND mailing_list NOT IN ( | ||
81 | 2873 | SELECT mailing_list | ||
82 | 2874 | FROM MailingListSubscription | ||
83 | 2875 | WHERE person=%(to_id)d | ||
84 | 2876 | ) | ||
85 | 2877 | ''' % vars()) | ||
86 | 2878 | # Then trash the remainders. | ||
90 | 2879 | cur.execute(''' | 2867 | cur.execute(''' |
91 | 2880 | DELETE FROM MailingListSubscription WHERE person=%(from_id)d | 2868 | DELETE FROM MailingListSubscription WHERE person=%(from_id)d |
92 | 2881 | ''' % vars()) | 2869 | ''' % vars()) |
93 | @@ -3472,6 +3460,17 @@ | |||
94 | 3472 | # flush its caches. | 3460 | # flush its caches. |
95 | 3473 | store.invalidate() | 3461 | store.invalidate() |
96 | 3474 | 3462 | ||
97 | 3463 | # Inform the user of the merge changes. | ||
98 | 3464 | if not to_person.isTeam(): | ||
99 | 3465 | mail_text = get_email_template('person-merged.txt') | ||
100 | 3466 | mail_text = mail_text % { | ||
101 | 3467 | 'dupename': from_person.name, | ||
102 | 3468 | 'person': to_person.name, | ||
103 | 3469 | } | ||
104 | 3470 | subject = 'Launchpad accounts merged' | ||
105 | 3471 | getUtility(IPersonNotificationSet).addNotification( | ||
106 | 3472 | to_person, subject, mail_text) | ||
107 | 3473 | |||
108 | 3475 | def getValidPersons(self, persons): | 3474 | def getValidPersons(self, persons): |
109 | 3476 | """See `IPersonSet.`""" | 3475 | """See `IPersonSet.`""" |
110 | 3477 | person_ids = [person.id for person in persons] | 3476 | person_ids = [person.id for person in persons] |
111 | 3478 | 3477 | ||
112 | === modified file 'lib/lp/registry/tests/test_personset.py' | |||
113 | --- lib/lp/registry/tests/test_personset.py 2009-09-29 19:00:28 +0000 | |||
114 | +++ lib/lp/registry/tests/test_personset.py 2009-11-13 13:12:15 +0000 | |||
115 | @@ -7,46 +7,56 @@ | |||
116 | 7 | 7 | ||
117 | 8 | from unittest import TestLoader | 8 | from unittest import TestLoader |
118 | 9 | 9 | ||
119 | 10 | import transaction | ||
120 | 10 | from zope.component import getUtility | 11 | from zope.component import getUtility |
121 | 11 | from zope.security.proxy import removeSecurityProxy | 12 | from zope.security.proxy import removeSecurityProxy |
122 | 12 | 13 | ||
123 | 13 | from lp.registry.model.person import PersonSet | 14 | from lp.registry.model.person import PersonSet |
124 | 15 | from lp.registry.interfaces.mailinglistsubscription import ( | ||
125 | 16 | MailingListAutoSubscribePolicy) | ||
126 | 14 | from lp.registry.interfaces.person import ( | 17 | from lp.registry.interfaces.person import ( |
127 | 15 | PersonCreationRationale, IPersonSet) | 18 | PersonCreationRationale, IPersonSet) |
129 | 16 | from lp.testing import TestCaseWithFactory | 19 | from lp.testing import TestCaseWithFactory, login_person, logout |
130 | 20 | |||
131 | 21 | from canonical.database.sqlbase import cursor | ||
132 | 17 | from canonical.launchpad.testing.databasehelpers import ( | 22 | from canonical.launchpad.testing.databasehelpers import ( |
133 | 18 | remove_all_sample_data_branches) | 23 | remove_all_sample_data_branches) |
135 | 19 | from canonical.testing import LaunchpadFunctionalLayer | 24 | from canonical.testing import DatabaseFunctionalLayer |
136 | 20 | 25 | ||
137 | 21 | 26 | ||
138 | 22 | class TestPersonSetBranchCounts(TestCaseWithFactory): | 27 | class TestPersonSetBranchCounts(TestCaseWithFactory): |
139 | 23 | 28 | ||
141 | 24 | layer = LaunchpadFunctionalLayer | 29 | layer = DatabaseFunctionalLayer |
142 | 25 | 30 | ||
143 | 26 | def setUp(self): | 31 | def setUp(self): |
144 | 27 | TestCaseWithFactory.setUp(self) | 32 | TestCaseWithFactory.setUp(self) |
145 | 28 | remove_all_sample_data_branches() | 33 | remove_all_sample_data_branches() |
146 | 34 | self.person_set = getUtility(IPersonSet) | ||
147 | 29 | 35 | ||
148 | 30 | def test_no_branches(self): | 36 | def test_no_branches(self): |
149 | 31 | """Initially there should be no branches.""" | 37 | """Initially there should be no branches.""" |
151 | 32 | self.assertEqual(0, PersonSet().getPeopleWithBranches().count()) | 38 | self.assertEqual(0, self.person_set.getPeopleWithBranches().count()) |
152 | 33 | 39 | ||
153 | 34 | def test_five_branches(self): | 40 | def test_five_branches(self): |
154 | 35 | branches = [self.factory.makeAnyBranch() for x in range(5)] | 41 | branches = [self.factory.makeAnyBranch() for x in range(5)] |
155 | 36 | # Each branch has a different product, so any individual product | 42 | # Each branch has a different product, so any individual product |
156 | 37 | # will return one branch. | 43 | # will return one branch. |
159 | 38 | self.assertEqual(5, PersonSet().getPeopleWithBranches().count()) | 44 | self.assertEqual(5, self.person_set.getPeopleWithBranches().count()) |
160 | 39 | self.assertEqual(1, PersonSet().getPeopleWithBranches( | 45 | self.assertEqual(1, self.person_set.getPeopleWithBranches( |
161 | 40 | branches[0].product).count()) | 46 | branches[0].product).count()) |
162 | 41 | 47 | ||
163 | 42 | 48 | ||
164 | 43 | class TestPersonSetEnsurePerson(TestCaseWithFactory): | 49 | class TestPersonSetEnsurePerson(TestCaseWithFactory): |
165 | 44 | 50 | ||
167 | 45 | layer = LaunchpadFunctionalLayer | 51 | layer = DatabaseFunctionalLayer |
168 | 46 | email_address = 'testing.ensure.person@example.com' | 52 | email_address = 'testing.ensure.person@example.com' |
169 | 47 | displayname = 'Testing ensurePerson' | 53 | displayname = 'Testing ensurePerson' |
170 | 48 | rationale = PersonCreationRationale.SOURCEPACKAGEUPLOAD | 54 | rationale = PersonCreationRationale.SOURCEPACKAGEUPLOAD |
171 | 49 | 55 | ||
172 | 56 | def setUp(self): | ||
173 | 57 | TestCaseWithFactory.setUp(self) | ||
174 | 58 | self.person_set = getUtility(IPersonSet) | ||
175 | 59 | |||
176 | 50 | def test_ensurePerson_returns_existing_person(self): | 60 | def test_ensurePerson_returns_existing_person(self): |
177 | 51 | # IPerson.ensurePerson returns existing person and does not | 61 | # IPerson.ensurePerson returns existing person and does not |
178 | 52 | # override its details. | 62 | # override its details. |
179 | @@ -54,7 +64,7 @@ | |||
180 | 54 | testing_person = self.factory.makePerson( | 64 | testing_person = self.factory.makePerson( |
181 | 55 | email=self.email_address, displayname=testing_displayname) | 65 | email=self.email_address, displayname=testing_displayname) |
182 | 56 | 66 | ||
184 | 57 | ensured_person = getUtility(IPersonSet).ensurePerson( | 67 | ensured_person = self.person_set.ensurePerson( |
185 | 58 | self.email_address, self.displayname, self.rationale) | 68 | self.email_address, self.displayname, self.rationale) |
186 | 59 | self.assertEquals(testing_person.id, ensured_person.id) | 69 | self.assertEquals(testing_person.id, ensured_person.id) |
187 | 60 | self.assertIsNot( | 70 | self.assertIsNot( |
188 | @@ -67,7 +77,7 @@ | |||
189 | 67 | def test_ensurePerson_hides_new_person_email(self): | 77 | def test_ensurePerson_hides_new_person_email(self): |
190 | 68 | # IPersonSet.ensurePerson creates new person with | 78 | # IPersonSet.ensurePerson creates new person with |
191 | 69 | # 'hide_email_addresses' set. | 79 | # 'hide_email_addresses' set. |
193 | 70 | ensured_person = getUtility(IPersonSet).ensurePerson( | 80 | ensured_person = self.person_set.ensurePerson( |
194 | 71 | self.email_address, self.displayname, self.rationale) | 81 | self.email_address, self.displayname, self.rationale) |
195 | 72 | self.assertTrue(ensured_person.hide_email_addresses) | 82 | self.assertTrue(ensured_person.hide_email_addresses) |
196 | 73 | 83 | ||
197 | @@ -78,7 +88,7 @@ | |||
198 | 78 | self.displayname, email=self.email_address) | 88 | self.displayname, email=self.email_address) |
199 | 79 | self.assertIs(None, test_account.preferredemail.person) | 89 | self.assertIs(None, test_account.preferredemail.person) |
200 | 80 | 90 | ||
202 | 81 | ensured_person = getUtility(IPersonSet).ensurePerson( | 91 | ensured_person = self.person_set.ensurePerson( |
203 | 82 | self.email_address, self.displayname, self.rationale) | 92 | self.email_address, self.displayname, self.rationale) |
204 | 83 | self.assertEquals(test_account.id, ensured_person.account.id) | 93 | self.assertEquals(test_account.id, ensured_person.account.id) |
205 | 84 | self.assertTrue(ensured_person.hide_email_addresses) | 94 | self.assertTrue(ensured_person.hide_email_addresses) |
206 | @@ -98,7 +108,7 @@ | |||
207 | 98 | self.assertIs(None, testing_account.preferredemail.person) | 108 | self.assertIs(None, testing_account.preferredemail.person) |
208 | 99 | self.assertIs(None, testing_person.preferredemail) | 109 | self.assertIs(None, testing_person.preferredemail) |
209 | 100 | 110 | ||
211 | 101 | ensured_person = getUtility(IPersonSet).ensurePerson( | 111 | ensured_person = self.person_set.ensurePerson( |
212 | 102 | self.email_address, self.displayname, self.rationale) | 112 | self.email_address, self.displayname, self.rationale) |
213 | 103 | 113 | ||
214 | 104 | # The existing Person was retrieved and the Account | 114 | # The existing Person was retrieved and the Account |
215 | @@ -108,5 +118,36 @@ | |||
216 | 108 | ensured_person.preferredemail.id) | 118 | ensured_person.preferredemail.id) |
217 | 109 | 119 | ||
218 | 110 | 120 | ||
219 | 121 | class TestPersonSetMerge(TestCaseWithFactory): | ||
220 | 122 | |||
221 | 123 | layer = DatabaseFunctionalLayer | ||
222 | 124 | |||
223 | 125 | def setUp(self): | ||
224 | 126 | TestCaseWithFactory.setUp(self) | ||
225 | 127 | # Use the unsecured PersonSet so that private methods can be tested. | ||
226 | 128 | self.person_set = PersonSet() | ||
227 | 129 | self.from_person = self.factory.makePerson() | ||
228 | 130 | self.to_person = self.factory.makePerson() | ||
229 | 131 | self.cur = cursor() | ||
230 | 132 | |||
231 | 133 | def test__mergeMailingListSubscriptions_no_subscriptions(self): | ||
232 | 134 | self.person_set._mergeMailingListSubscriptions( | ||
233 | 135 | self.cur, self.from_person.id, self.to_person.id) | ||
234 | 136 | self.assertEqual(0, self.cur.rowcount) | ||
235 | 137 | |||
236 | 138 | def test__mergeMailingListSubscriptions_with_subscriptions(self): | ||
237 | 139 | self.from_person.mailing_list_auto_subscribe_policy = ( | ||
238 | 140 | MailingListAutoSubscribePolicy.ALWAYS) | ||
239 | 141 | self.team, self.mailing_list = self.factory.makeTeamAndMailingList( | ||
240 | 142 | 'test-mailinglist', 'team-owner') | ||
241 | 143 | login_person(self.team.teamowner) | ||
242 | 144 | self.team.addMember(self.from_person, reviewer=self.team.teamowner) | ||
243 | 145 | logout() | ||
244 | 146 | transaction.commit() | ||
245 | 147 | self.person_set._mergeMailingListSubscriptions( | ||
246 | 148 | self.cur, self.from_person.id, self.to_person.id) | ||
247 | 149 | self.assertEqual(1, self.cur.rowcount) | ||
248 | 150 | |||
249 | 151 | |||
250 | 111 | def test_suite(): | 152 | def test_suite(): |
251 | 112 | return TestLoader().loadTestsFromName(__name__) | 153 | return TestLoader().loadTestsFromName(__name__) |
252 | 113 | 154 | ||
253 | === modified file 'lib/lp/testing/menu.py' | |||
254 | --- lib/lp/testing/menu.py 2009-09-10 20:35:24 +0000 | |||
255 | +++ lib/lp/testing/menu.py 2009-11-13 13:12:15 +0000 | |||
256 | @@ -13,7 +13,6 @@ | |||
257 | 13 | 13 | ||
258 | 14 | def check_menu_links(menu): | 14 | def check_menu_links(menu): |
259 | 15 | context = menu.context | 15 | context = menu.context |
260 | 16 | is_sane_menu = True | ||
261 | 17 | for link in menu.iterlinks(): | 16 | for link in menu.iterlinks(): |
262 | 18 | if '?' in link.target: | 17 | if '?' in link.target: |
263 | 19 | view_name, _args = link.target.split('?') | 18 | view_name, _args = link.target.split('?') |
264 | @@ -24,6 +23,5 @@ | |||
265 | 24 | try: | 23 | try: |
266 | 25 | view = getMultiAdapter((context, request), name=view_name) | 24 | view = getMultiAdapter((context, request), name=view_name) |
267 | 26 | except: | 25 | except: |
271 | 27 | is_sane_menu = False | 26 | return 'Bad link %s: %s' % (link.name, url) |
272 | 28 | print 'Bad link %s: %s' % (link.name, url) | 27 | return True |
270 | 29 | print is_sane_menu |
This is my branch to remove mailing list subscriptions from merged accounts.
lp:~sinzui/launchpad/merge-mailing-list-bug-471770 /bugs.launchpad .net/bugs/ 471770 *test_pesonset" implementation: bac, salgado
Diff size: 247
Launchpad bug: https:/
Test command: ./bin/test -vvt "registry.
Pre-
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 y(terms) . lhavelund is
email addresses. This failure is SimpleVocabular
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. _mergeMailingLi stSubscriptions () to delete the
admin- assisted or the user did it himself.
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
* 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: /launchpad/ emailtemplates/ person- merged. txt registry/ doc/person- merge.txt registry/ model/person. py registry/ tests/test_ personset. py
lib/canonical
lib/lp/
lib/lp/
lib/lp/
== Test ==
* lib/lp/ registry/ doc/person- merge.txt registry/ tests/test_ personset. py nalLayer
* Added a test to verify the user is sent an email after the merge.
* lib/lp/
* Moved the tests to the DatabaseFunctio
* 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 registry/ model/person. py
* Added an email to inform the user to review his email settings.
* lib/lp/
* 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.