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
=== added file 'database/schema/pending/delete-unused-team-addresses.sql'
--- database/schema/pending/delete-unused-team-addresses.sql 1970-01-01 00:00:00 +0000
+++ database/schema/pending/delete-unused-team-addresses.sql 2010-04-20 20:05:34 +0000
@@ -0,0 +1,30 @@
1-- Copyright 2010 Canonical Ltd. This software is licensed under the
2-- GNU Affero General Public License version 3 (see the file LICENSE).
3
4SET client_min_messages=ERROR;
5
6--SELECT person.name, email, status
7--FROM EmailAddress
8-- JOIN Person ON EmailAddress.person = Person.id
9-- AND Person.teamowner IS NOT NULL
10--WHERE
11-- EmailAddress.status in (1, 2)
12-- AND EmailAddress.email not in (
13-- person.name || '@lists.launchpad.net',
14-- person.name || '@lists.staging.launchpad.net')
15--;
16
17-- Delete all the unused team email addresses that Launchpad claims to have
18-- removed. Status 1 (NEW, meaning merged) or 2 (VALID, meaning not used)
19-- and email not the team's mailing list address.
20DELETE
21FROM EmailAddress
22USING Person
23WHERE
24 EmailAddress.person = Person.id
25 AND Person.teamowner IS NOT NULL
26 AND EmailAddress.status in (1, 2)
27 AND EmailAddress.email not in (
28 person.name || '@lists.launchpad.net',
29 person.name || '@lists.staging.launchpad.net')
30;
031
=== 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-20 20:05:34 +0000
@@ -251,21 +251,23 @@
251 >>> print find_tag_by_id(content, 'field.actions.delete')251 >>> print find_tag_by_id(content, 'field.actions.delete')
252 None252 None
253253
254The registry experts should be able to delete a team with an254The registry experts can delete a team with a new email address (from
255validated email address, which will be invisible, since only255an import), which will be invisible, since only preferred email addresses are
256preferred email addresses are shown for teams.256shown for teams.
257257
258 >>> from canonical.launchpad.interfaces.emailaddress import (258 >>> from canonical.launchpad.interfaces.emailaddress import (
259 ... IEmailAddressSet)259 ... IEmailAddressSet, EmailAddressStatus)
260 >>> login_person(registry_expert)260 >>> login_person(registry_expert)
261 >>> admin = getUtility(ILaunchpadCelebrities).admin261 >>> admin = getUtility(ILaunchpadCelebrities).admin
262 >>> registry_expert.inTeam(admin)262 >>> registry_expert.inTeam(admin)
263 False263 False
264 >>> deletable_team = factory.makeTeam(email="del@example.com")264 >>> deletable_team = factory.makeTeam()
265 >>> deletable_team.setContactAddress(None)265 >>> email = factory.makeEmail(
266 ... "del@example.com", deletable_team,
267 ... email_status=EmailAddressStatus.NEW)
266 >>> for email in getUtility(IEmailAddressSet).getByPerson(deletable_team):268 >>> for email in getUtility(IEmailAddressSet).getByPerson(deletable_team):
267 ... print email.email, email.status.title269 ... print email.email, email.status.title
268 del@example.com Validated Email Address270 del@example.com New Email Address
269 >>> form = {'field.actions.delete': 'Delete'}271 >>> form = {'field.actions.delete': 'Delete'}
270 >>> view = create_initialized_view(deletable_team, '+delete', form=form)272 >>> view = create_initialized_view(deletable_team, '+delete', form=form)
271 >>> view.errors273 >>> view.errors
272274
=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt 2010-03-11 20:54:36 +0000
+++ lib/lp/registry/browser/tests/person-views.txt 2010-04-20 20:05:34 +0000
@@ -207,18 +207,15 @@
207 >>> view.visible_email_addresses207 >>> view.visible_email_addresses
208 [u'support@ubuntu.com']208 [u'support@ubuntu.com']
209209
210It is possible for a team to have more than one valid address, but only210It is possible for a team to have more than two addresses (from a mailing
211the preferred address is listed in the visible_email_addresses property.211list), but only the preferred address is listed in the visible_email_addresses
212212property.
213 >>> from canonical.launchpad.interfaces.emailaddress import (213
214 ... EmailAddressStatus, IEmailAddressSet)214 >>> email_address = factory.makeEmail(
215215 ... 'ubuntu_team@canonical.com', ubuntu_team)
216 >>> email_address_set = getUtility(IEmailAddressSet)
217 >>> email_address = email_address_set.new(
218 ... 'ubuntu_team@canonical.com',
219 ... person=ubuntu_team,
220 ... status=EmailAddressStatus.VALIDATED)
221 >>> ubuntu_team.setContactAddress(email_address)216 >>> ubuntu_team.setContactAddress(email_address)
217 >>> mailing_list = factory.makeMailingList(
218 ... ubuntu_team, ubuntu_team.teamowner)
222 >>> view = create_initialized_view(ubuntu_team, '+index')219 >>> view = create_initialized_view(ubuntu_team, '+index')
223 >>> view.visible_email_addresses220 >>> view.visible_email_addresses
224 [u'ubuntu_team@canonical.com']221 [u'ubuntu_team@canonical.com']
@@ -739,10 +736,8 @@
739736
740EmailToPersonView will use the contact address when the team has one.737EmailToPersonView will use the contact address when the team has one.
741738
742 >>> email_address = email_address_set.new(739 >>> email_address = factory.makeEmail(
743 ... 'landscapers@canonical.com',740 ... 'landscapers@canonical.com', landscape_developers)
744 ... person=landscape_developers,
745 ... status=EmailAddressStatus.VALIDATED)
746 >>> landscape_developers.setContactAddress(email_address)741 >>> landscape_developers.setContactAddress(email_address)
747742
748 >>> view = create_initialized_view(landscape_developers, '+contactuser')743 >>> view = create_initialized_view(landscape_developers, '+contactuser')
749744
=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt 2010-02-17 14:42:16 +0000
+++ lib/lp/registry/doc/person.txt 2010-04-20 20:05:34 +0000
@@ -188,9 +188,6 @@
188to setContactAddress() to leave a team without a contact address.188to setContactAddress() to leave a team without a contact address.
189189
190 >>> shipit_admins.setContactAddress(None)190 >>> shipit_admins.setContactAddress(None)
191 >>> email.status
192 <DBItem EmailAddressStatus.VALIDATED...
193
194 >>> print shipit_admins.preferredemail191 >>> print shipit_admins.preferredemail
195 None192 None
196193
197194
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-04-06 21:42:28 +0000
+++ lib/lp/registry/model/person.py 2010-04-20 20:05:34 +0000
@@ -123,8 +123,7 @@
123from lp.registry.interfaces.teammembership import (123from lp.registry.interfaces.teammembership import (
124 TeamMembershipStatus)124 TeamMembershipStatus)
125from lp.registry.interfaces.wikiname import IWikiName, IWikiNameSet125from lp.registry.interfaces.wikiname import IWikiName, IWikiNameSet
126from canonical.launchpad.webapp.interfaces import (126from canonical.launchpad.webapp.interfaces import ILaunchBag
127 ILaunchBag, IStoreSelector, MASTER_FLAVOR)
128127
129from lp.soyuz.model.archive import Archive128from lp.soyuz.model.archive import Archive
130from lp.registry.model.codeofconduct import SignedCodeOfConduct129from lp.registry.model.codeofconduct import SignedCodeOfConduct
@@ -1162,11 +1161,8 @@
1162 "You need to specify a reviewer when a team joins another.")1161 "You need to specify a reviewer when a team joins another.")
1163 requester = self1162 requester = self
11641163
1165 expired = TeamMembershipStatus.EXPIRED
1166 proposed = TeamMembershipStatus.PROPOSED1164 proposed = TeamMembershipStatus.PROPOSED
1167 approved = TeamMembershipStatus.APPROVED1165 approved = TeamMembershipStatus.APPROVED
1168 declined = TeamMembershipStatus.DECLINED
1169 deactivated = TeamMembershipStatus.DEACTIVATED
11701166
1171 if team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:1167 if team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:
1172 raise JoinNotAllowed("This is a restricted team")1168 raise JoinNotAllowed("This is a restricted team")
@@ -1261,7 +1257,7 @@
1261 comment=comment)1257 comment=comment)
1262 # Accessing the id attribute ensures that the team1258 # Accessing the id attribute ensures that the team
1263 # creation has been flushed to the database.1259 # creation has been flushed to the database.
1264 tm_id = tm.id1260 tm.id
1265 notify(event(person, self))1261 notify(event(person, self))
1266 else:1262 else:
1267 # We can't use tm.setExpirationDate() here because the reviewer1263 # We can't use tm.setExpirationDate() here because the reviewer
@@ -2058,6 +2054,19 @@
2058 self._unsetPreferredEmail()2054 self._unsetPreferredEmail()
2059 else:2055 else:
2060 self._setPreferredEmail(email)2056 self._setPreferredEmail(email)
2057 # A team can have up to two addresses, the preferred one and one used
2058 # by the team mailing list.
2059 if self.mailing_list is not None:
2060 mailing_list_email = getUtility(IEmailAddressSet).getByEmail(
2061 self.mailing_list.address)
2062 mailing_list_email = IMasterObject(mailing_list_email)
2063 else:
2064 mailing_list_email = None
2065 all_addresses = IMasterStore(self).find(
2066 EmailAddress, EmailAddress.personID == self.id)
2067 for address in all_addresses :
2068 if address not in (email, mailing_list_email):
2069 address.destroySelf()
20612070
2062 def _unsetPreferredEmail(self):2071 def _unsetPreferredEmail(self):
2063 """Change the preferred email address to VALIDATED."""2072 """Change the preferred email address to VALIDATED."""
@@ -2112,6 +2121,7 @@
21122121
2113 email = removeSecurityProxy(email)2122 email = removeSecurityProxy(email)
2114 IMasterObject(email).status = EmailAddressStatus.PREFERRED2123 IMasterObject(email).status = EmailAddressStatus.PREFERRED
2124 IMasterObject(email).syncUpdate()
21152125
2116 # Now we update our cache of the preferredemail.2126 # Now we update our cache of the preferredemail.
2117 self._preferredemail_cached = email2127 self._preferredemail_cached = email
21182128
=== added file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_team.py 2010-04-20 20:05:34 +0000
@@ -0,0 +1,86 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for PersonSet."""
5
6__metaclass__ = type
7
8import transaction
9from unittest import TestLoader
10
11from zope.component import getUtility
12
13from canonical.launchpad.database.emailaddress import EmailAddress
14from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
15from canonical.launchpad.interfaces.lpstorm import IMasterStore
16from canonical.testing import DatabaseFunctionalLayer
17
18from lp.testing import TestCaseWithFactory
19
20
21class TestTeamContactAddress(TestCaseWithFactory):
22
23 layer = DatabaseFunctionalLayer
24
25 def getAllEmailAddresses(self):
26 transaction.commit()
27 all_addresses = self.store.find(
28 EmailAddress, EmailAddress.personID == self.team.id)
29 return [address for address in all_addresses.order_by('email')]
30
31 def createMailingListAndGetAddress(self):
32 mailing_list = self.factory.makeMailingList(
33 self.team, self.team.teamowner)
34 return getUtility(IEmailAddressSet).getByEmail(
35 mailing_list.address)
36
37 def setUp(self):
38 super(TestTeamContactAddress, self).setUp()
39 self.team = self.factory.makeTeam(name='alpha')
40 self.address = self.factory.makeEmail('team@noplace.org', self.team)
41 self.store = IMasterStore(self.address)
42
43 def test_setContactAddress_from_none(self):
44 self.team.setContactAddress(self.address)
45 self.assertEqual(self.address, self.team.preferredemail)
46 self.assertEqual([self.address], self.getAllEmailAddresses())
47
48 def test_setContactAddress_to_none(self):
49 self.team.setContactAddress(self.address)
50 self.team.setContactAddress(None)
51 self.assertEqual(None, self.team.preferredemail)
52 self.assertEqual([], self.getAllEmailAddresses())
53
54 def test_setContactAddress_to_new_address(self):
55 self.team.setContactAddress(self.address)
56 new_address = self.factory.makeEmail('new@noplace.org', self.team)
57 self.team.setContactAddress(new_address)
58 self.assertEqual(new_address, self.team.preferredemail)
59 self.assertEqual([new_address], self.getAllEmailAddresses())
60
61 def test_setContactAddress_to_mailing_list(self):
62 self.team.setContactAddress(self.address)
63 list_address = self.createMailingListAndGetAddress()
64 self.team.setContactAddress(list_address)
65 self.assertEqual(list_address, self.team.preferredemail)
66 self.assertEqual([list_address], self.getAllEmailAddresses())
67
68 def test_setContactAddress_from_mailing_list(self):
69 list_address = self.createMailingListAndGetAddress()
70 self.team.setContactAddress(list_address)
71 new_address = self.factory.makeEmail('new@noplace.org', self.team)
72 self.team.setContactAddress(new_address)
73 self.assertEqual(new_address, self.team.preferredemail)
74 self.assertEqual(
75 [list_address, new_address], self.getAllEmailAddresses())
76
77 def test_setContactAddress_from_mailing_list_to_none(self):
78 list_address = self.createMailingListAndGetAddress()
79 self.team.setContactAddress(list_address)
80 self.team.setContactAddress(None)
81 self.assertEqual(None, self.team.preferredemail)
82 self.assertEqual([list_address], self.getAllEmailAddresses())
83
84
85def test_suite():
86 return TestLoader().loadTestsFromName(__name__)