Merge lp:~jcsackett/launchpad/no-canonical-urls-628247 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11561
Proposed branch: lp:~jcsackett/launchpad/no-canonical-urls-628247
Merge into: lp:launchpad
Diff against target: 193 lines (+91/-13)
3 files modified
lib/canonical/launchpad/interfaces/emailaddress.py (+1/-2)
lib/canonical/launchpad/interfaces/validation.py (+32/-10)
lib/lp/registry/browser/tests/test_person_view.py (+58/-1)
To merge this branch: bzr merge lp:~jcsackett/launchpad/no-canonical-urls-628247
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+35301@code.launchpad.net

Commit message

Fixes a bad condition in canonical validation of team email addresses; now instead of assuming that if an email is taken in LP there must be a team, it finds all associations with that email before returning errors.

Description of the change

Summary
=======

Fixes a condition which raised an OOPS on team creation when emails were taken.

Proposed Fix
============

Create or improve a checker for email addresses so that the condition can't happen.

Pre-implementation notes
========================

Spoke with Curtis Hovey (sinzui) who proposed that the error wasn't actually where the OOPS was raised, but earlier in checking the availability of the email address entered.

Implementation details
======================

Adds a function to get the highest order association of an email (Person/Team, then Account, then the Email model instance itself, then None). This is used to craft an appropriate error message. Previously the checker assumed that if an email was already in the LP db, then a person must exist--it was in crafting the error message with this assumed person that NoCanonicalURL would be raised.

Tests
=====

bin/test -t TestTeamCreation

Demo and Q/A
============

If you create an Account without a Person in the database for launchpad.dev, then try to add a team via the web interface with that email address, it should inform you in an error on contactemail that an account uses that email address. Previously it would OOPS.

Lint
====

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/database/emailaddress.py
  lib/canonical/launchpad/interfaces/emailaddress.py
  lib/canonical/launchpad/interfaces/validation.py
  lib/lp/registry/browser/tests/test_person_view.py

./lib/canonical/launchpad/interfaces/validation.py
      65: E302 expected 2 blank lines, found 1

Error in validation.py is from the linter getting confused about whitespace around comments.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (7.3 KiB)

Diff from the last change:

=== modified file 'lib/canonical/launchpad/database/emailaddress.py'
--- lib/canonical/launchpad/database/emailaddress.py 2010-09-13 16:17:51 +0000
+++ lib/canonical/launchpad/database/emailaddress.py 2010-09-13 19:29:21 +0000
@@ -145,22 +145,6 @@
             personID=personID,
             account=account)

- def getEmailAssociation(self, email):
- """See IEmailAddressSet`."""
- # First check if the email address is in use.
- email_address = self.getByEmail(email)
- if email_address is None:
- # If the email address doesn't exist in the system, it is
- # available.
- return
-
- # The email already exists; determine what has it.
- if email_address.person is not None:
- return email_address.person
- if email_address.account is not None:
- return email_address.account
- return email_address
-

 class UndeletableEmailAddress(Exception):
     """User attempted to delete an email address which can't be deleted."""

=== modified file 'lib/canonical/launchpad/interfaces/emailaddress.py'
--- lib/canonical/launchpad/interfaces/emailaddress.py 2010-09-13 16:17:51 +0000
+++ lib/canonical/launchpad/interfaces/emailaddress.py 2010-09-13 19:29:21 +0000
@@ -153,12 +153,3 @@

         Return None if there is no such email address.
         """
-
- def getEmailAssociation(email):
- """Return the entity associated with the email.
-
- Returns the person or team with the email, if one exists. If not,
- returns the account with the email, if it exists. If it doesn't,
- return the email model instance, if it exists. If it doesn't, return
- None.
- """

=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
--- lib/canonical/launchpad/interfaces/validation.py 2010-09-13 16:17:51 +0000
+++ lib/canonical/launchpad/interfaces/validation.py 2010-09-13 19:29:21 +0000
@@ -32,13 +32,15 @@
 from canonical.launchpad import _
 from canonical.launchpad.interfaces.launchpad import ILaunchBag
 from canonical.launchpad.interfaces.account import IAccount
-from canonical.launchpad.interfaces.emailaddress import IEmailAddress
+from canonical.launchpad.interfaces.emailaddress import (
+ IEmailAddress,
+ IEmailAddressSet,
+ )
 from canonical.launchpad.validators import LaunchpadValidationError
 from canonical.launchpad.validators.cve import valid_cve
 from canonical.launchpad.validators.email import valid_email
 from canonical.launchpad.validators.url import valid_absolute_url
 from canonical.launchpad.webapp.menu import structured
-
 from lp.app.errors import NotFoundError

@@ -192,6 +194,28 @@
             "${email} isn't a valid email address.",
             mapping={'email': email}))

+def _check_email_availability(email):
+ email_address = getUtility(IEmailAddressSet).getByEmail(email)
+ if email_address is not None:
+ # The email already exists; determine what has it.
+ if email_address.person is not None:
+ person = email_address.person
+ message = _('${email} is already registered in Launchpad and is '
+ ...

Read more...

Revision history for this message
Aaron Bentley (abentley) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/emailaddress.py'
2--- lib/canonical/launchpad/interfaces/emailaddress.py 2010-08-20 20:31:18 +0000
3+++ lib/canonical/launchpad/interfaces/emailaddress.py 2010-09-15 13:37:49 +0000
4@@ -115,7 +115,7 @@
5
6 def destroySelf():
7 """Destroy this email address and any associated subscriptions.
8-
9+
10 :raises UndeletableEmailAddress: When the email address is a person's
11 preferred one or a hosted mailing list's address.
12 """
13@@ -153,4 +153,3 @@
14
15 Return None if there is no such email address.
16 """
17-
18
19=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
20--- lib/canonical/launchpad/interfaces/validation.py 2010-08-22 18:31:30 +0000
21+++ lib/canonical/launchpad/interfaces/validation.py 2010-09-15 13:37:49 +0000
22@@ -19,7 +19,7 @@
23 'validate_new_distrotask',
24 'valid_upstreamtask',
25 'valid_password',
26- 'validate_date_interval'
27+ 'validate_date_interval',
28 ]
29
30 from cgi import escape
31@@ -31,10 +31,16 @@
32
33 from canonical.launchpad import _
34 from canonical.launchpad.interfaces.launchpad import ILaunchBag
35+from canonical.launchpad.interfaces.account import IAccount
36+from canonical.launchpad.interfaces.emailaddress import (
37+ IEmailAddress,
38+ IEmailAddressSet,
39+ )
40 from canonical.launchpad.validators import LaunchpadValidationError
41 from canonical.launchpad.validators.cve import valid_cve
42 from canonical.launchpad.validators.email import valid_email
43 from canonical.launchpad.validators.url import valid_absolute_url
44+from canonical.launchpad.webapp import canonical_url
45 from canonical.launchpad.webapp.menu import structured
46 from lp.app.errors import NotFoundError
47
48@@ -124,6 +130,7 @@
49 scheme (for instance, http:// for a web URL), and ensure the
50 URL uses either http, https or ftp.""")))
51
52+
53 def valid_branch_url(branch_url):
54 """Returns True if web_ref is a valid download URL, or raises a
55 LaunchpadValidationError.
56@@ -188,6 +195,28 @@
57 "${email} isn't a valid email address.",
58 mapping={'email': email}))
59
60+def _check_email_availability(email):
61+ email_address = getUtility(IEmailAddressSet).getByEmail(email)
62+ if email_address is not None:
63+ # The email already exists; determine what has it.
64+ if email_address.person is not None:
65+ person = email_address.person
66+ message = _('${email} is already registered in Launchpad and is '
67+ 'associated with <a href="${url}">${person}</a>.',
68+ mapping={'email': escape(email),
69+ 'url': canonical_url(person),
70+ 'person': escape(person.displayname)})
71+ elif email_address.account is not None:
72+ account = email_address.account
73+ message = _('${email} is already registered in Launchpad and is '
74+ 'associated with the ${account} account.',
75+ mapping={'email': escape(email),
76+ 'account': escape(account.displayname)})
77+ else:
78+ message = _('${email} is already registered in Launchpad.',
79+ mapping={'email': escape(email)})
80+ raise LaunchpadValidationError(structured(message))
81+
82
83 def validate_new_team_email(email):
84 """Check that the given email is valid and not registered to
85@@ -195,16 +224,9 @@
86 """
87 from canonical.launchpad.webapp import canonical_url
88 from canonical.launchpad.interfaces import IEmailAddressSet
89+
90 _validate_email(email)
91- email_address = getUtility(IEmailAddressSet).getByEmail(email)
92- if email_address is not None:
93- person = email_address.person
94- message = _('${email} is already registered in Launchpad and is '
95- 'associated with <a href="${url}">${team}</a>.',
96- mapping={'email': escape(email),
97- 'url': canonical_url(person),
98- 'team': escape(person.displayname)})
99- raise LaunchpadValidationError(structured(message))
100+ _check_email_availability(email)
101 return True
102
103
104
105=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
106--- lib/lp/registry/browser/tests/test_person_view.py 2010-09-02 19:28:35 +0000
107+++ lib/lp/registry/browser/tests/test_person_view.py 2010-09-15 13:37:49 +0000
108@@ -5,6 +5,7 @@
109
110 import transaction
111 from zope.component import getUtility
112+from zope.security.proxy import removeSecurityProxy
113
114 from canonical.config import config
115 from canonical.launchpad.ftests import (
116@@ -29,7 +30,10 @@
117 )
118
119 from lp.registry.interfaces.karma import IKarmaCacheManager
120-from lp.registry.interfaces.person import PersonVisibility
121+from lp.registry.interfaces.person import (
122+ PersonVisibility,
123+ IPersonSet,
124+ )
125 from lp.registry.interfaces.teammembership import (
126 ITeamMembershipSet,
127 TeamMembershipStatus,
128@@ -257,6 +261,58 @@
129 self.assertFalse(self.view.form_fields['name'].for_display)
130
131
132+class TestTeamCreationView(TestCaseWithFactory):
133+
134+ layer = DatabaseFunctionalLayer
135+
136+ def setUp(self):
137+ super(TestTeamCreationView, self).setUp()
138+ person = self.factory.makePerson()
139+ login_person(person)
140+
141+ def test_team_creation_good_data(self):
142+ form = {
143+ 'field.actions.create': 'Create Team',
144+ 'field.contactemail': 'contactemail@example.com',
145+ 'field.displayname': 'liberty-land',
146+ 'field.name': 'libertyland',
147+ 'field.renewal_policy': 'NONE',
148+ 'field.renewal_policy-empty-marker': 1,
149+ 'field.subscriptionpolicy': 'RESTRICTED',
150+ 'field.subscriptionpolicy-empty-marker': 1,
151+ }
152+ person_set = getUtility(IPersonSet)
153+ view = create_initialized_view(
154+ person_set, '+newteam', form=form)
155+ team = person_set.getByName('libertyland')
156+ self.assertTrue(team is not None)
157+ self.assertEqual('libertyland', team.name)
158+
159+ def test_validate_email_catches_taken_emails(self):
160+ email_address = self.factory.getUniqueEmailAddress()
161+ account = self.factory.makeAccount(
162+ displayname='libertylandaccount',
163+ email=email_address,
164+ status=AccountStatus.NOACCOUNT)
165+ form = {
166+ 'field.actions.create': 'Create Team',
167+ 'field.contactemail': email_address,
168+ 'field.displayname': 'liberty-land',
169+ 'field.name': 'libertyland',
170+ 'field.renewal_policy': 'NONE',
171+ 'field.renewal_policy-empty-marker': 1,
172+ 'field.subscriptionpolicy': 'RESTRICTED',
173+ 'field.subscriptionpolicy-empty-marker': 1,
174+ }
175+ person_set = getUtility(IPersonSet)
176+ view = create_initialized_view(person_set, '+newteam', form=form)
177+ expected_msg = ('%s is already registered in Launchpad and is '
178+ 'associated with the libertylandaccount '
179+ 'account.' % email_address)
180+ error_msg = view.errors[0].errors[0]
181+ self.assertEqual(expected_msg, error_msg)
182+
183+
184 class TestPersonParticipationView(TestCaseWithFactory):
185
186 layer = DatabaseFunctionalLayer
187@@ -602,6 +658,7 @@
188 self.assertEqual(
189 'This account is already deactivated.', view.errors[0])
190
191+
192 class TestTeamInvitationView(TestCaseWithFactory):
193 """Tests for TeamInvitationView."""
194