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
=== modified file 'lib/canonical/launchpad/interfaces/emailaddress.py'
--- lib/canonical/launchpad/interfaces/emailaddress.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/interfaces/emailaddress.py 2010-09-15 13:37:49 +0000
@@ -115,7 +115,7 @@
115115
116 def destroySelf():116 def destroySelf():
117 """Destroy this email address and any associated subscriptions.117 """Destroy this email address and any associated subscriptions.
118 118
119 :raises UndeletableEmailAddress: When the email address is a person's119 :raises UndeletableEmailAddress: When the email address is a person's
120 preferred one or a hosted mailing list's address.120 preferred one or a hosted mailing list's address.
121 """121 """
@@ -153,4 +153,3 @@
153153
154 Return None if there is no such email address.154 Return None if there is no such email address.
155 """155 """
156
157156
=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
--- lib/canonical/launchpad/interfaces/validation.py 2010-08-22 18:31:30 +0000
+++ lib/canonical/launchpad/interfaces/validation.py 2010-09-15 13:37:49 +0000
@@ -19,7 +19,7 @@
19 'validate_new_distrotask',19 'validate_new_distrotask',
20 'valid_upstreamtask',20 'valid_upstreamtask',
21 'valid_password',21 'valid_password',
22 'validate_date_interval'22 'validate_date_interval',
23 ]23 ]
2424
25from cgi import escape25from cgi import escape
@@ -31,10 +31,16 @@
3131
32from canonical.launchpad import _32from canonical.launchpad import _
33from canonical.launchpad.interfaces.launchpad import ILaunchBag33from canonical.launchpad.interfaces.launchpad import ILaunchBag
34from canonical.launchpad.interfaces.account import IAccount
35from canonical.launchpad.interfaces.emailaddress import (
36 IEmailAddress,
37 IEmailAddressSet,
38 )
34from canonical.launchpad.validators import LaunchpadValidationError39from canonical.launchpad.validators import LaunchpadValidationError
35from canonical.launchpad.validators.cve import valid_cve40from canonical.launchpad.validators.cve import valid_cve
36from canonical.launchpad.validators.email import valid_email41from canonical.launchpad.validators.email import valid_email
37from canonical.launchpad.validators.url import valid_absolute_url42from canonical.launchpad.validators.url import valid_absolute_url
43from canonical.launchpad.webapp import canonical_url
38from canonical.launchpad.webapp.menu import structured44from canonical.launchpad.webapp.menu import structured
39from lp.app.errors import NotFoundError45from lp.app.errors import NotFoundError
4046
@@ -124,6 +130,7 @@
124 scheme (for instance, http:// for a web URL), and ensure the130 scheme (for instance, http:// for a web URL), and ensure the
125 URL uses either http, https or ftp.""")))131 URL uses either http, https or ftp.""")))
126132
133
127def valid_branch_url(branch_url):134def valid_branch_url(branch_url):
128 """Returns True if web_ref is a valid download URL, or raises a135 """Returns True if web_ref is a valid download URL, or raises a
129 LaunchpadValidationError.136 LaunchpadValidationError.
@@ -188,6 +195,28 @@
188 "${email} isn't a valid email address.",195 "${email} isn't a valid email address.",
189 mapping={'email': email}))196 mapping={'email': email}))
190197
198def _check_email_availability(email):
199 email_address = getUtility(IEmailAddressSet).getByEmail(email)
200 if email_address is not None:
201 # The email already exists; determine what has it.
202 if email_address.person is not None:
203 person = email_address.person
204 message = _('${email} is already registered in Launchpad and is '
205 'associated with <a href="${url}">${person}</a>.',
206 mapping={'email': escape(email),
207 'url': canonical_url(person),
208 'person': escape(person.displayname)})
209 elif email_address.account is not None:
210 account = email_address.account
211 message = _('${email} is already registered in Launchpad and is '
212 'associated with the ${account} account.',
213 mapping={'email': escape(email),
214 'account': escape(account.displayname)})
215 else:
216 message = _('${email} is already registered in Launchpad.',
217 mapping={'email': escape(email)})
218 raise LaunchpadValidationError(structured(message))
219
191220
192def validate_new_team_email(email):221def validate_new_team_email(email):
193 """Check that the given email is valid and not registered to222 """Check that the given email is valid and not registered to
@@ -195,16 +224,9 @@
195 """224 """
196 from canonical.launchpad.webapp import canonical_url225 from canonical.launchpad.webapp import canonical_url
197 from canonical.launchpad.interfaces import IEmailAddressSet226 from canonical.launchpad.interfaces import IEmailAddressSet
227
198 _validate_email(email)228 _validate_email(email)
199 email_address = getUtility(IEmailAddressSet).getByEmail(email)229 _check_email_availability(email)
200 if email_address is not None:
201 person = email_address.person
202 message = _('${email} is already registered in Launchpad and is '
203 'associated with <a href="${url}">${team}</a>.',
204 mapping={'email': escape(email),
205 'url': canonical_url(person),
206 'team': escape(person.displayname)})
207 raise LaunchpadValidationError(structured(message))
208 return True230 return True
209231
210232
211233
=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py 2010-09-02 19:28:35 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py 2010-09-15 13:37:49 +0000
@@ -5,6 +5,7 @@
55
6import transaction6import transaction
7from zope.component import getUtility7from zope.component import getUtility
8from zope.security.proxy import removeSecurityProxy
89
9from canonical.config import config10from canonical.config import config
10from canonical.launchpad.ftests import (11from canonical.launchpad.ftests import (
@@ -29,7 +30,10 @@
29 )30 )
3031
31from lp.registry.interfaces.karma import IKarmaCacheManager32from lp.registry.interfaces.karma import IKarmaCacheManager
32from lp.registry.interfaces.person import PersonVisibility33from lp.registry.interfaces.person import (
34 PersonVisibility,
35 IPersonSet,
36 )
33from lp.registry.interfaces.teammembership import (37from lp.registry.interfaces.teammembership import (
34 ITeamMembershipSet,38 ITeamMembershipSet,
35 TeamMembershipStatus,39 TeamMembershipStatus,
@@ -257,6 +261,58 @@
257 self.assertFalse(self.view.form_fields['name'].for_display)261 self.assertFalse(self.view.form_fields['name'].for_display)
258262
259263
264class TestTeamCreationView(TestCaseWithFactory):
265
266 layer = DatabaseFunctionalLayer
267
268 def setUp(self):
269 super(TestTeamCreationView, self).setUp()
270 person = self.factory.makePerson()
271 login_person(person)
272
273 def test_team_creation_good_data(self):
274 form = {
275 'field.actions.create': 'Create Team',
276 'field.contactemail': 'contactemail@example.com',
277 'field.displayname': 'liberty-land',
278 'field.name': 'libertyland',
279 'field.renewal_policy': 'NONE',
280 'field.renewal_policy-empty-marker': 1,
281 'field.subscriptionpolicy': 'RESTRICTED',
282 'field.subscriptionpolicy-empty-marker': 1,
283 }
284 person_set = getUtility(IPersonSet)
285 view = create_initialized_view(
286 person_set, '+newteam', form=form)
287 team = person_set.getByName('libertyland')
288 self.assertTrue(team is not None)
289 self.assertEqual('libertyland', team.name)
290
291 def test_validate_email_catches_taken_emails(self):
292 email_address = self.factory.getUniqueEmailAddress()
293 account = self.factory.makeAccount(
294 displayname='libertylandaccount',
295 email=email_address,
296 status=AccountStatus.NOACCOUNT)
297 form = {
298 'field.actions.create': 'Create Team',
299 'field.contactemail': email_address,
300 'field.displayname': 'liberty-land',
301 'field.name': 'libertyland',
302 'field.renewal_policy': 'NONE',
303 'field.renewal_policy-empty-marker': 1,
304 'field.subscriptionpolicy': 'RESTRICTED',
305 'field.subscriptionpolicy-empty-marker': 1,
306 }
307 person_set = getUtility(IPersonSet)
308 view = create_initialized_view(person_set, '+newteam', form=form)
309 expected_msg = ('%s is already registered in Launchpad and is '
310 'associated with the libertylandaccount '
311 'account.' % email_address)
312 error_msg = view.errors[0].errors[0]
313 self.assertEqual(expected_msg, error_msg)
314
315
260class TestPersonParticipationView(TestCaseWithFactory):316class TestPersonParticipationView(TestCaseWithFactory):
261317
262 layer = DatabaseFunctionalLayer318 layer = DatabaseFunctionalLayer
@@ -602,6 +658,7 @@
602 self.assertEqual(658 self.assertEqual(
603 'This account is already deactivated.', view.errors[0])659 'This account is already deactivated.', view.errors[0])
604660
661
605class TestTeamInvitationView(TestCaseWithFactory):662class TestTeamInvitationView(TestCaseWithFactory):
606 """Tests for TeamInvitationView."""663 """Tests for TeamInvitationView."""
607664