Merge lp:~sinzui/launchpad/team-delete-email into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~sinzui/launchpad/team-delete-email
Merge into: lp:launchpad
Diff against target: 281 lines (+151/-31)
6 files modified
database/schema/pending/delete-unused-team-addresses.sql (+30/-0)
lib/lp/registry/browser/tests/peoplemerge-views.txt (+9/-7)
lib/lp/registry/browser/tests/person-views.txt (+10/-15)
lib/lp/registry/doc/person.txt (+0/-3)
lib/lp/registry/model/person.py (+16/-6)
lib/lp/registry/tests/test_team.py (+86/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/team-delete-email
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+23713@code.launchpad.net

Description of the change

This is my branch to allow teams to delete old email addresses.

    lp:~sinzui/launchpad/team-delete-email
    Diff size: 282
    Launchpad bug: https://bugs.launchpad.net/bugs/250103
    Test command: ./bin/test -vv \
        -t TestTeamContactAddress
        -t doc/person.txt \
        -t person-views \
        -t peoplemerge-views
    Pre-implementation: no one
    Target release: 10.04

Allow teams to delete old email addresses
-----------------------------------------

Launchpad never deletes email addresses. When a team changes its email
address, the old one is hidden and marked valid. When a team is merged, the
old email address is marked new and hidden so that it cannot be reclaimed.
This causes lots of problems because email addresses are really owned/managed
by users and they want to reuse the addresses. LOSAs delete 1 or more email
addresses every week after a user discovers that the email address they know
they removed are still associated with a team.

Launchpad should delete the old address as the UI implies was done.

Rules
-----

    * Delete the old email address when the team sets a new contact address.
      If users want to reuse the address they must reconfirm it.
    * Provide a SQL script to remove all the historic old team email
      addresses.

QA
--

On staging where the db can be examined:
    * Add an email address to a team, then remove it.
    * Verify the team has no email address in the DB.
    * Create a mailing list for a team and set it as the contact addresses,
      then remove it.
    * Verify the team has one email address for the mailing list.
    * Add another email address.
    * Verify the team has two email addresses.

Lint
----

Linting changed files:
  database/schema/pending/delete-unused-team-addresses.sql
  lib/lp/registry/browser/tests/peoplemerge-views.txt
  lib/lp/registry/browser/tests/person-views.txt
  lib/lp/registry/doc/person.txt
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_team.py

Test
----

    * lib/lp/registry/browser/tests/peoplemerge-views.txt
      * Updated the delete a team test to explain the only case a team can
        have a NEW email address. Updated the test to use the factory.
    * lib/lp/registry/browser/tests/person-views.txt
      * Updated the visible_email_addresses test to explain that teams may
        have two possible email addresses. Updated the test to use the
        factory.
    * lib/lp/registry/doc/person.txt
      * Updated the setContactAddress() doc not verify the email address
        that ceased to exist.
    * lib/lp/registry/tests/test_team.py
      * Added a new test to verify the conditions that setContactAddress()
        supports.

Implementation
--------------

    * database/schema/pending/delete-unused-team-addresses.sql
      * Added a script that can be run by admins to delete the historic
        data.
    * lib/lp/registry/model/person.py
      * Removed some unused variables that new pyflakes reports are lint.
      * Revised the setContactAddress() method to purge the unused team
        email addresses when the team email address is set.

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

Hi Curtis,

This change is very nice. I found one typo but everything else looks great.

> === modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
> --- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-04-12 08:11:18 +0000
> +++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-04-19 21:35:31 +0000
> @@ -251,21 +251,23 @@
> >>> print find_tag_by_id(content, 'field.actions.delete')
> None
>
> -The registry experts should be able to delete a team with an
> -validated email address, which will be invisible, since only
> -preferred email addresses are shown for teams.
> +The registry experts can delete a team with an new email address (from

s/an new/a new

> +an import), which will be invisible, since only preferred email addresses are
> +shown for teams.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'database/schema/pending/delete-unused-team-addresses.sql'
2--- database/schema/pending/delete-unused-team-addresses.sql 1970-01-01 00:00:00 +0000
3+++ database/schema/pending/delete-unused-team-addresses.sql 2010-04-20 20:05:34 +0000
4@@ -0,0 +1,30 @@
5+-- Copyright 2010 Canonical Ltd. This software is licensed under the
6+-- GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+SET client_min_messages=ERROR;
9+
10+--SELECT person.name, email, status
11+--FROM EmailAddress
12+-- JOIN Person ON EmailAddress.person = Person.id
13+-- AND Person.teamowner IS NOT NULL
14+--WHERE
15+-- EmailAddress.status in (1, 2)
16+-- AND EmailAddress.email not in (
17+-- person.name || '@lists.launchpad.net',
18+-- person.name || '@lists.staging.launchpad.net')
19+--;
20+
21+-- Delete all the unused team email addresses that Launchpad claims to have
22+-- removed. Status 1 (NEW, meaning merged) or 2 (VALID, meaning not used)
23+-- and email not the team's mailing list address.
24+DELETE
25+FROM EmailAddress
26+USING Person
27+WHERE
28+ EmailAddress.person = Person.id
29+ AND Person.teamowner IS NOT NULL
30+ AND EmailAddress.status in (1, 2)
31+ AND EmailAddress.email not in (
32+ person.name || '@lists.launchpad.net',
33+ person.name || '@lists.staging.launchpad.net')
34+;
35
36=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
37--- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-04-12 08:11:18 +0000
38+++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-04-20 20:05:34 +0000
39@@ -251,21 +251,23 @@
40 >>> print find_tag_by_id(content, 'field.actions.delete')
41 None
42
43-The registry experts should be able to delete a team with an
44-validated email address, which will be invisible, since only
45-preferred email addresses are shown for teams.
46+The registry experts can delete a team with a new email address (from
47+an import), which will be invisible, since only preferred email addresses are
48+shown for teams.
49
50 >>> from canonical.launchpad.interfaces.emailaddress import (
51- ... IEmailAddressSet)
52+ ... IEmailAddressSet, EmailAddressStatus)
53 >>> login_person(registry_expert)
54 >>> admin = getUtility(ILaunchpadCelebrities).admin
55 >>> registry_expert.inTeam(admin)
56 False
57- >>> deletable_team = factory.makeTeam(email="del@example.com")
58- >>> deletable_team.setContactAddress(None)
59+ >>> deletable_team = factory.makeTeam()
60+ >>> email = factory.makeEmail(
61+ ... "del@example.com", deletable_team,
62+ ... email_status=EmailAddressStatus.NEW)
63 >>> for email in getUtility(IEmailAddressSet).getByPerson(deletable_team):
64 ... print email.email, email.status.title
65- del@example.com Validated Email Address
66+ del@example.com New Email Address
67 >>> form = {'field.actions.delete': 'Delete'}
68 >>> view = create_initialized_view(deletable_team, '+delete', form=form)
69 >>> view.errors
70
71=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
72--- lib/lp/registry/browser/tests/person-views.txt 2010-03-11 20:54:36 +0000
73+++ lib/lp/registry/browser/tests/person-views.txt 2010-04-20 20:05:34 +0000
74@@ -207,18 +207,15 @@
75 >>> view.visible_email_addresses
76 [u'support@ubuntu.com']
77
78-It is possible for a team to have more than one valid address, but only
79-the preferred address is listed in the visible_email_addresses property.
80-
81- >>> from canonical.launchpad.interfaces.emailaddress import (
82- ... EmailAddressStatus, IEmailAddressSet)
83-
84- >>> email_address_set = getUtility(IEmailAddressSet)
85- >>> email_address = email_address_set.new(
86- ... 'ubuntu_team@canonical.com',
87- ... person=ubuntu_team,
88- ... status=EmailAddressStatus.VALIDATED)
89+It is possible for a team to have more than two addresses (from a mailing
90+list), but only the preferred address is listed in the visible_email_addresses
91+property.
92+
93+ >>> email_address = factory.makeEmail(
94+ ... 'ubuntu_team@canonical.com', ubuntu_team)
95 >>> ubuntu_team.setContactAddress(email_address)
96+ >>> mailing_list = factory.makeMailingList(
97+ ... ubuntu_team, ubuntu_team.teamowner)
98 >>> view = create_initialized_view(ubuntu_team, '+index')
99 >>> view.visible_email_addresses
100 [u'ubuntu_team@canonical.com']
101@@ -739,10 +736,8 @@
102
103 EmailToPersonView will use the contact address when the team has one.
104
105- >>> email_address = email_address_set.new(
106- ... 'landscapers@canonical.com',
107- ... person=landscape_developers,
108- ... status=EmailAddressStatus.VALIDATED)
109+ >>> email_address = factory.makeEmail(
110+ ... 'landscapers@canonical.com', landscape_developers)
111 >>> landscape_developers.setContactAddress(email_address)
112
113 >>> view = create_initialized_view(landscape_developers, '+contactuser')
114
115=== modified file 'lib/lp/registry/doc/person.txt'
116--- lib/lp/registry/doc/person.txt 2010-02-17 14:42:16 +0000
117+++ lib/lp/registry/doc/person.txt 2010-04-20 20:05:34 +0000
118@@ -188,9 +188,6 @@
119 to setContactAddress() to leave a team without a contact address.
120
121 >>> shipit_admins.setContactAddress(None)
122- >>> email.status
123- <DBItem EmailAddressStatus.VALIDATED...
124-
125 >>> print shipit_admins.preferredemail
126 None
127
128
129=== modified file 'lib/lp/registry/model/person.py'
130--- lib/lp/registry/model/person.py 2010-04-06 21:42:28 +0000
131+++ lib/lp/registry/model/person.py 2010-04-20 20:05:34 +0000
132@@ -123,8 +123,7 @@
133 from lp.registry.interfaces.teammembership import (
134 TeamMembershipStatus)
135 from lp.registry.interfaces.wikiname import IWikiName, IWikiNameSet
136-from canonical.launchpad.webapp.interfaces import (
137- ILaunchBag, IStoreSelector, MASTER_FLAVOR)
138+from canonical.launchpad.webapp.interfaces import ILaunchBag
139
140 from lp.soyuz.model.archive import Archive
141 from lp.registry.model.codeofconduct import SignedCodeOfConduct
142@@ -1162,11 +1161,8 @@
143 "You need to specify a reviewer when a team joins another.")
144 requester = self
145
146- expired = TeamMembershipStatus.EXPIRED
147 proposed = TeamMembershipStatus.PROPOSED
148 approved = TeamMembershipStatus.APPROVED
149- declined = TeamMembershipStatus.DECLINED
150- deactivated = TeamMembershipStatus.DEACTIVATED
151
152 if team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:
153 raise JoinNotAllowed("This is a restricted team")
154@@ -1261,7 +1257,7 @@
155 comment=comment)
156 # Accessing the id attribute ensures that the team
157 # creation has been flushed to the database.
158- tm_id = tm.id
159+ tm.id
160 notify(event(person, self))
161 else:
162 # We can't use tm.setExpirationDate() here because the reviewer
163@@ -2058,6 +2054,19 @@
164 self._unsetPreferredEmail()
165 else:
166 self._setPreferredEmail(email)
167+ # A team can have up to two addresses, the preferred one and one used
168+ # by the team mailing list.
169+ if self.mailing_list is not None:
170+ mailing_list_email = getUtility(IEmailAddressSet).getByEmail(
171+ self.mailing_list.address)
172+ mailing_list_email = IMasterObject(mailing_list_email)
173+ else:
174+ mailing_list_email = None
175+ all_addresses = IMasterStore(self).find(
176+ EmailAddress, EmailAddress.personID == self.id)
177+ for address in all_addresses :
178+ if address not in (email, mailing_list_email):
179+ address.destroySelf()
180
181 def _unsetPreferredEmail(self):
182 """Change the preferred email address to VALIDATED."""
183@@ -2112,6 +2121,7 @@
184
185 email = removeSecurityProxy(email)
186 IMasterObject(email).status = EmailAddressStatus.PREFERRED
187+ IMasterObject(email).syncUpdate()
188
189 # Now we update our cache of the preferredemail.
190 self._preferredemail_cached = email
191
192=== added file 'lib/lp/registry/tests/test_team.py'
193--- lib/lp/registry/tests/test_team.py 1970-01-01 00:00:00 +0000
194+++ lib/lp/registry/tests/test_team.py 2010-04-20 20:05:34 +0000
195@@ -0,0 +1,86 @@
196+# Copyright 2010 Canonical Ltd. This software is licensed under the
197+# GNU Affero General Public License version 3 (see the file LICENSE).
198+
199+"""Tests for PersonSet."""
200+
201+__metaclass__ = type
202+
203+import transaction
204+from unittest import TestLoader
205+
206+from zope.component import getUtility
207+
208+from canonical.launchpad.database.emailaddress import EmailAddress
209+from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
210+from canonical.launchpad.interfaces.lpstorm import IMasterStore
211+from canonical.testing import DatabaseFunctionalLayer
212+
213+from lp.testing import TestCaseWithFactory
214+
215+
216+class TestTeamContactAddress(TestCaseWithFactory):
217+
218+ layer = DatabaseFunctionalLayer
219+
220+ def getAllEmailAddresses(self):
221+ transaction.commit()
222+ all_addresses = self.store.find(
223+ EmailAddress, EmailAddress.personID == self.team.id)
224+ return [address for address in all_addresses.order_by('email')]
225+
226+ def createMailingListAndGetAddress(self):
227+ mailing_list = self.factory.makeMailingList(
228+ self.team, self.team.teamowner)
229+ return getUtility(IEmailAddressSet).getByEmail(
230+ mailing_list.address)
231+
232+ def setUp(self):
233+ super(TestTeamContactAddress, self).setUp()
234+ self.team = self.factory.makeTeam(name='alpha')
235+ self.address = self.factory.makeEmail('team@noplace.org', self.team)
236+ self.store = IMasterStore(self.address)
237+
238+ def test_setContactAddress_from_none(self):
239+ self.team.setContactAddress(self.address)
240+ self.assertEqual(self.address, self.team.preferredemail)
241+ self.assertEqual([self.address], self.getAllEmailAddresses())
242+
243+ def test_setContactAddress_to_none(self):
244+ self.team.setContactAddress(self.address)
245+ self.team.setContactAddress(None)
246+ self.assertEqual(None, self.team.preferredemail)
247+ self.assertEqual([], self.getAllEmailAddresses())
248+
249+ def test_setContactAddress_to_new_address(self):
250+ self.team.setContactAddress(self.address)
251+ new_address = self.factory.makeEmail('new@noplace.org', self.team)
252+ self.team.setContactAddress(new_address)
253+ self.assertEqual(new_address, self.team.preferredemail)
254+ self.assertEqual([new_address], self.getAllEmailAddresses())
255+
256+ def test_setContactAddress_to_mailing_list(self):
257+ self.team.setContactAddress(self.address)
258+ list_address = self.createMailingListAndGetAddress()
259+ self.team.setContactAddress(list_address)
260+ self.assertEqual(list_address, self.team.preferredemail)
261+ self.assertEqual([list_address], self.getAllEmailAddresses())
262+
263+ def test_setContactAddress_from_mailing_list(self):
264+ list_address = self.createMailingListAndGetAddress()
265+ self.team.setContactAddress(list_address)
266+ new_address = self.factory.makeEmail('new@noplace.org', self.team)
267+ self.team.setContactAddress(new_address)
268+ self.assertEqual(new_address, self.team.preferredemail)
269+ self.assertEqual(
270+ [list_address, new_address], self.getAllEmailAddresses())
271+
272+ def test_setContactAddress_from_mailing_list_to_none(self):
273+ list_address = self.createMailingListAndGetAddress()
274+ self.team.setContactAddress(list_address)
275+ self.team.setContactAddress(None)
276+ self.assertEqual(None, self.team.preferredemail)
277+ self.assertEqual([list_address], self.getAllEmailAddresses())
278+
279+
280+def test_suite():
281+ return TestLoader().loadTestsFromName(__name__)