Merge lp:~edwin-grubbs/launchpad/bug-554153-claim-profile-bug into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Edwin Grubbs
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~edwin-grubbs/launchpad/bug-554153-claim-profile-bug
Merge into: lp:launchpad
Diff against target: 654 lines (+56/-377)
10 files modified
lib/canonical/launchpad/browser/logintoken.py (+4/-56)
lib/canonical/launchpad/doc/logintoken-pages.txt (+9/-77)
lib/canonical/launchpad/interfaces/authtoken.py (+0/-7)
lib/canonical/launchpad/zcml/logintoken.zcml (+0/-7)
lib/lp/registry/browser/configure.zcml (+0/-6)
lib/lp/registry/browser/person.py (+22/-62)
lib/lp/registry/stories/person/xx-person-claim-merge.txt (+17/-2)
lib/lp/registry/stories/person/xx-person-claim.txt (+0/-148)
lib/lp/registry/stories/team/xx-team-claim.txt (+4/-6)
lib/lp/registry/templates/person-index.pt (+0/-6)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-554153-claim-profile-bug
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+23550@code.launchpad.net

Description of the change

Summary
-------

Fixes bug 554153.
Remove $person/+claim page and point links at /people/+requestmerge instead.

Implementation details
----------------------

The +claim page emails the user a token that lets them access the
+claimprofile page. We can rid of them both.
    lib/canonical/launchpad/browser/logintoken.py
    lib/canonical/launchpad/zcml/logintoken.zcml
    lib/lp/registry/browser/configure.zcml

The +claimteam page is still used. It was subclassed from
PersonClaimView, but now those two views can be combined.
    lib/lp/registry/browser/person.py

There is no real reason to display a different set of links for an
anonymous user looking at an inactive person page. The anonymous
user will be forced to log in if the +requestmerge or +claimteam
links is clicked.
    lib/lp/registry/templates/person-index.pt

Tests:
    lib/lp/registry/stories/person/xx-person-claim-merge.txt
    lib/lp/registry/stories/person/xx-person-claim.txt
    lib/lp/registry/stories/team/xx-team-claim.txt

Tests
-----

./bin/test -vv -t claim

Demo and Q/A
------------

* Open http://launchpad.dev/~matsubara/+requestmerge
  * The "Are you Diogo Matsubara?" link should now point to +requestmerge.
* Open http://launchpad.dev/~doc/+requestmerge
  * The "Is this a team you run?" link should still point to +claimteam.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/browser/logintoken.py'
--- lib/canonical/launchpad/browser/logintoken.py 2010-04-01 19:22:36 +0000
+++ lib/canonical/launchpad/browser/logintoken.py 2010-04-17 16:08:28 +0000
@@ -6,7 +6,6 @@
6__all__ = [6__all__ = [
7 'BaseTokenView',7 'BaseTokenView',
8 'BugTrackerHandshakeView',8 'BugTrackerHandshakeView',
9 'ClaimProfileView',
10 'ClaimTeamView',9 'ClaimTeamView',
11 'LoginTokenSetNavigation',10 'LoginTokenSetNavigation',
12 'LoginTokenView',11 'LoginTokenView',
@@ -36,7 +35,6 @@
36 IGPGHandler)35 IGPGHandler)
37from canonical.launchpad.interfaces.logintoken import (36from canonical.launchpad.interfaces.logintoken import (
38 IGPGKeyValidationForm, ILoginTokenSet)37 IGPGKeyValidationForm, ILoginTokenSet)
39from canonical.launchpad.interfaces.lpstorm import IMasterObject
40from canonical.launchpad.webapp.interfaces import (38from canonical.launchpad.webapp.interfaces import (
41 IAlwaysSubmittedWidget, IPlacelessLoginSource)39 IAlwaysSubmittedWidget, IPlacelessLoginSource)
42from canonical.launchpad.webapp.login import logInPrincipal40from canonical.launchpad.webapp.login import logInPrincipal
@@ -45,10 +43,10 @@
45from canonical.launchpad.webapp import (43from canonical.launchpad.webapp import (
46 action, canonical_url, custom_widget, GetitemNavigation,44 action, canonical_url, custom_widget, GetitemNavigation,
47 LaunchpadEditFormView, LaunchpadFormView, LaunchpadView)45 LaunchpadEditFormView, LaunchpadFormView, LaunchpadView)
48from canonical.widgets import LaunchpadRadioWidget, PasswordChangeWidget46from canonical.widgets import LaunchpadRadioWidget
4947
50from lp.registry.browser.team import HasRenewalPolicyMixin48from lp.registry.browser.team import HasRenewalPolicyMixin
51from lp.registry.interfaces.person import IPerson, IPersonSet, ITeam49from lp.registry.interfaces.person import IPersonSet, ITeam
5250
5351
54UTC = pytz.UTC52UTC = pytz.UTC
@@ -77,7 +75,6 @@
77 LoginTokenType.VALIDATETEAMEMAIL: '+validateteamemail',75 LoginTokenType.VALIDATETEAMEMAIL: '+validateteamemail',
78 LoginTokenType.VALIDATEGPG: '+validategpg',76 LoginTokenType.VALIDATEGPG: '+validategpg',
79 LoginTokenType.VALIDATESIGNONLYGPG: '+validatesignonlygpg',77 LoginTokenType.VALIDATESIGNONLYGPG: '+validatesignonlygpg',
80 LoginTokenType.PROFILECLAIM: '+claimprofile',
81 LoginTokenType.TEAMCLAIM: '+claimteam',78 LoginTokenType.TEAMCLAIM: '+claimteam',
82 LoginTokenType.BUGTRACKER: '+bugtracker-handshake',79 LoginTokenType.BUGTRACKER: '+bugtracker-handshake',
83 }80 }
@@ -184,56 +181,6 @@
184 return True181 return True
185182
186183
187class ClaimProfileView(BaseTokenView, LaunchpadFormView):
188 schema = IPerson
189 field_names = ['displayname', 'hide_email_addresses', 'password']
190 custom_widget('password', PasswordChangeWidget)
191 label = 'Claim Launchpad profile'
192
193 expected_token_types = (LoginTokenType.PROFILECLAIM,)
194
195 def initialize(self):
196 if not self.redirectIfInvalidOrConsumedToken():
197 self.claimed_profile = getUtility(IEmailAddressSet).getByEmail(
198 self.context.email).person
199 super(ClaimProfileView, self).initialize()
200
201 @property
202 def initial_values(self):
203 return {'displayname': self.claimed_profile.displayname}
204
205 @property
206 def default_next_url(self):
207 return canonical_url(self.claimed_profile)
208
209 @action(_('Continue'), name='confirm')
210 def confirm_action(self, action, data):
211 email = getUtility(IEmailAddressSet).getByEmail(self.context.email)
212 person = IMasterObject(email.person)
213
214 # The user is not yet logged in, but we need to set some
215 # things on his new account, so we need to remove the security
216 # proxy from it.
217 # XXX: Guilherme Salgado 2006-09-27 bug=62674:
218 # We should be able to login with this person and set the
219 # password, to avoid removing the security proxy, but it didn't
220 # work, so I'm leaving this hack for now.
221 naked_person = removeSecurityProxy(person)
222 naked_person.displayname = data['displayname']
223 naked_person.hide_email_addresses = data['hide_email_addresses']
224
225 naked_email = removeSecurityProxy(email)
226
227 removeSecurityProxy(IMasterObject(email.account)).activate(
228 comment="Activated by claim profile.",
229 password=data['password'],
230 preferred_email=naked_email)
231 self.context.consume()
232 self.logInPrincipalByEmail(naked_email.email)
233 self.request.response.addInfoNotification(_(
234 "Profile claimed successfully"))
235
236
237class ClaimTeamView(184class ClaimTeamView(
238 BaseTokenView, HasRenewalPolicyMixin, LaunchpadEditFormView):185 BaseTokenView, HasRenewalPolicyMixin, LaunchpadEditFormView):
239186
@@ -356,7 +303,8 @@
356303
357 # We compare the word-splitted content to avoid failures due304 # We compare the word-splitted content to avoid failures due
358 # to whitepace differences.305 # to whitepace differences.
359 if signature.plain_data.split() != self.context.validation_phrase.split():306 if (signature.plain_data.split()
307 != self.context.validation_phrase.split()):
360 self.addError(_(308 self.addError(_(
361 'The signed content does not match the message found '309 'The signed content does not match the message found '
362 'in the email.'))310 'in the email.'))
363311
=== modified file 'lib/canonical/launchpad/doc/logintoken-pages.txt'
--- lib/canonical/launchpad/doc/logintoken-pages.txt 2010-04-01 18:48:04 +0000
+++ lib/canonical/launchpad/doc/logintoken-pages.txt 2010-04-17 16:08:28 +0000
@@ -6,11 +6,16 @@
6token sent to him in an email.6token sent to him in an email.
77
88
9== Claiming a profile ==9== Validating GPG keys ==
1010
11The ClaimProfileView allows users to claim accounts created by Launchpad11In order to add a GPG key to Launchpad we require that the person
12processes. The user profile for Matsubara was created during an import.12registering it decrypt an email and follow the instructions in it.
13The user profile does not have a preferred email address or a password.13The last step in that workflow is the +validategpg page, which can
14be accessed by non-logged-in users. That page will attempt to access
15all email addresses of the requester but that may fail when the user
16is not logged in and the requester has chosen to hide his email
17addresses, so here we'll make sure that even non-logged-in users can
18perform the last step of the workflow for adding a GPG key.
1419
15 >>> from zope.component import getMultiAdapter20 >>> from zope.component import getMultiAdapter
16 >>> from canonical.launchpad.interfaces.authtoken import LoginTokenType21 >>> from canonical.launchpad.interfaces.authtoken import LoginTokenType
@@ -25,79 +30,6 @@
25 >>> person_set = PersonSet()30 >>> person_set = PersonSet()
26 >>> login_token_set = getUtility(ILoginTokenSet)31 >>> login_token_set = getUtility(ILoginTokenSet)
2732
28 >>> matsubara = person_set.getByEmail('matsubara@async.com.br')
29 >>> matsubara.name
30 u'matsubara'
31 >>> matsubara.displayname
32 u'Diogo Matsubara'
33 >>> print matsubara.preferredemail
34 None
35
36 >>> matsubara.account.status
37 <DBItem AccountStatus.NOACCOUNT, ...>
38 >>> print matsubara.account.password
39 None
40 >>> matsubara.is_valid_person
41 False
42
43The user can claim his profile by providing the email address that
44is associated with the profile. If the correct address is provided,
45the user is sent an email with a link to a LoginToken.
46
47 >>> login_token = login_token_set.new(
48 ... matsubara, 'matsubara@async.com.br',
49 ... 'matsubara@async.com.br', LoginTokenType.PROFILECLAIM)
50
51The user is asked to provide a display name and password, and to
52indicate if his email address can be shown to other Launchpad users.
53
54 >>> request = LaunchpadTestRequest(
55 ... SERVER_URL='http://launchpad.dev',
56 ... PATH_INFO='/token/%s/+claimprofile' % login_token.token,
57 ... method='POST',
58 ... form={
59 ... 'field.displayname': 'D. Matsubara',
60 ... 'field.hide_email_addresses': 'on',
61 ... 'field.password': 'test4',
62 ... 'field.password_dupe': 'test4',
63 ... 'field.actions.confirm': 'Continue',
64 ... })
65 >>> login(ANONYMOUS, request)
66 >>> claimprofile_view = getMultiAdapter(
67 ... (login_token, request), name="+claimprofile")
68 >>> claimprofile_view.initialize()
69 >>> claimprofile_view.errors
70 []
71 >>> transaction.commit()
72
73The user's Person, EmailAddress, and Account objects are updated.
74
75 >>> matsubara.displayname
76 u'D. Matsubara'
77 >>> matsubara.hide_email_addresses
78 True
79 >>> matsubara.account.preferredemail.email
80 u'matsubara@async.com.br'
81
82 >>> matsubara.account.status
83 <DBItem AccountStatus.ACTIVE, ...>
84 >>> matsubara.account.status_comment
85 u'Activated by claim profile.'
86 >>> matsubara.is_valid_person
87 True
88
89
90== Validating GPG keys ==
91
92In order to add a GPG key to Launchpad we require that the person
93registering it decrypt an email and follow the instructions in it.
94The last step in that workflow is the +validategpg page, which can
95be accessed by non-logged-in users. That page will attempt to access
96all email addresses of the requester but that may fail when the user
97is not logged in and the requester has chosen to hide his email
98addresses, so here we'll make sure that even non-logged-in users can
99perform the last step of the workflow for adding a GPG key.
100
101 >>> sample_person = person_set.getByEmail('test@canonical.com')33 >>> sample_person = person_set.getByEmail('test@canonical.com')
102 >>> sample_person.hide_email_addresses34 >>> sample_person.hide_email_addresses
103 True35 True
10436
=== modified file 'lib/canonical/launchpad/interfaces/authtoken.py'
--- lib/canonical/launchpad/interfaces/authtoken.py 2010-03-30 20:02:53 +0000
+++ lib/canonical/launchpad/interfaces/authtoken.py 2010-04-17 16:08:28 +0000
@@ -78,13 +78,6 @@
78 needs to be validated.78 needs to be validated.
79 """)79 """)
8080
81 PROFILECLAIM = DBItem(8, """
82 Claim an unvalidated Launchpad profile
83
84 A user has found an unvalidated profile in Launchpad and is trying
85 to claim it.
86 """)
87
88 NEWPROFILE = DBItem(9, """81 NEWPROFILE = DBItem(9, """
89 A user created a new Launchpad profile for another person.82 A user created a new Launchpad profile for another person.
9083
9184
=== modified file 'lib/canonical/launchpad/zcml/logintoken.zcml'
--- lib/canonical/launchpad/zcml/logintoken.zcml 2010-03-30 20:02:53 +0000
+++ lib/canonical/launchpad/zcml/logintoken.zcml 2010-04-17 16:08:28 +0000
@@ -77,13 +77,6 @@
7777
78 <browser:page78 <browser:page
79 for="canonical.launchpad.interfaces.ILoginToken"79 for="canonical.launchpad.interfaces.ILoginToken"
80 name="+claimprofile"
81 permission="zope.Public"
82 class="canonical.launchpad.browser.ClaimProfileView"
83 template="../templates/logintoken-claimprofile.pt" />
84
85 <browser:page
86 for="canonical.launchpad.interfaces.ILoginToken"
87 name="+claimteam"80 name="+claimteam"
88 permission="launchpad.AnyPerson"81 permission="launchpad.AnyPerson"
89 class="canonical.launchpad.browser.ClaimTeamView"82 class="canonical.launchpad.browser.ClaimTeamView"
9083
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-04-12 15:33:19 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-04-17 16:08:28 +0000
@@ -790,12 +790,6 @@
790 permission="launchpad.AnyPerson"790 permission="launchpad.AnyPerson"
791 template="../templates/person-claim-team.pt"/>791 template="../templates/person-claim-team.pt"/>
792 <browser:page792 <browser:page
793 name="+claim"
794 for="lp.registry.interfaces.person.IPerson"
795 class="lp.registry.browser.person.PersonClaimView"
796 permission="zope.Public"
797 template="../templates/person-claim.pt"/>
798 <browser:page
799 name="+editlanguages"793 name="+editlanguages"
800 for="lp.registry.interfaces.person.IPerson"794 for="lp.registry.interfaces.person.IPerson"
801 class="lp.registry.browser.person.PersonLanguagesView"795 class="lp.registry.browser.person.PersonLanguagesView"
802796
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-04-14 18:31:51 +0000
+++ lib/lp/registry/browser/person.py 2010-04-17 16:08:28 +0000
@@ -18,7 +18,6 @@
18 'PersonAssignedBugTaskSearchListingView',18 'PersonAssignedBugTaskSearchListingView',
19 'PersonBrandingView',19 'PersonBrandingView',
20 'PersonBugsMenu',20 'PersonBugsMenu',
21 'PersonClaimView',
22 'PersonCodeOfConductEditView',21 'PersonCodeOfConductEditView',
23 'PersonCommentedBugTaskSearchListingView',22 'PersonCommentedBugTaskSearchListingView',
24 'PersonDeactivateAccountView',23 'PersonDeactivateAccountView',
@@ -218,8 +217,7 @@
218from canonical.launchpad.webapp.authorization import check_permission217from canonical.launchpad.webapp.authorization import check_permission
219from canonical.launchpad.webapp.batching import BatchNavigator218from canonical.launchpad.webapp.batching import BatchNavigator
220from canonical.launchpad.webapp.breadcrumb import Breadcrumb219from canonical.launchpad.webapp.breadcrumb import Breadcrumb
221from canonical.launchpad.webapp.login import (220from canonical.launchpad.webapp.login import logoutPerson
222 logoutPerson, allowUnauthenticatedSession)
223from canonical.launchpad.webapp.menu import get_current_view221from canonical.launchpad.webapp.menu import get_current_view
224from canonical.launchpad.webapp.publisher import LaunchpadView222from canonical.launchpad.webapp.publisher import LaunchpadView
225from canonical.launchpad.webapp.tales import (223from canonical.launchpad.webapp.tales import (
@@ -1397,11 +1395,14 @@
1397 self.next_url = self.request.getApplicationURL()1395 self.next_url = self.request.getApplicationURL()
13981396
13991397
1400class PersonClaimView(LaunchpadFormView):1398class BeginTeamClaimView(LaunchpadFormView):
1401 """The page where a user can claim an unvalidated profile."""1399 """Where you can claim an unvalidated profile turning it into a team.
14021400
1401 This is actually just the first step, where you enter the email address
1402 of the team and we email further instructions to that address.
1403 """
1404 label = 'Claim team'
1403 schema = IPersonClaim1405 schema = IPersonClaim
1404 label = 'Claim account'
14051406
1406 def initialize(self):1407 def initialize(self):
1407 if self.context.is_valid_person_or_team:1408 if self.context.is_valid_person_or_team:
@@ -1431,28 +1432,17 @@
1431 "associated with."1432 "associated with."
1432 % self.context.name)1433 % self.context.name)
1433 elif email.personID != self.context.id:1434 elif email.personID != self.context.id:
1434 if email.person.is_valid_person:1435 error = structured(
1435 error = structured(1436 "This email address is associated with yet another "
1436 "This email address is associated with yet another "1437 "Launchpad profile, which you seem to have used at "
1437 "Launchpad profile, which you seem to have used at "1438 "some point. If that's the case, you can "
1438 "some point. If that's the case, you can "1439 '<a href="/people/+requestmerge'
1439 '<a href="/people/+requestmerge'1440 '?field.dupe_person=%s">combine '
1440 '?field.dupe_person=%s">combine '1441 "this profile with the other one</a> (you'll "
1441 "this profile with the other one</a> (you'll "1442 "have to log in with the other profile first, "
1442 "have to log in with the other profile first, "1443 "though). If that's not the case, please try with a "
1443 "though). If that's not the case, please try with a "1444 "different email address.",
1444 "different email address.",1445 self.context.name)
1445 self.context.name)
1446 else:
1447 # There seems to be another unvalidated profile for you!
1448 error = structured(
1449 "Although this email address is not associated with "
1450 "this profile, it's associated with yet another "
1451 'one. You can <a href="%s/+claim">claim that other '
1452 'profile</a> and then later '
1453 '<a href="/people/+requestmerge">combine</a> both '
1454 'of them into a single one.',
1455 canonical_url(email.person))
1456 else:1446 else:
1457 # Yay! You got the right email this time.1447 # Yay! You got the right email this time.
1458 pass1448 pass
@@ -1463,34 +1453,6 @@
1463 def next_url(self):1453 def next_url(self):
1464 return canonical_url(self.context)1454 return canonical_url(self.context)
14651455
1466 @action(_("E-mail Me"), name="confirm")
1467 def confirm_action(self, action, data):
1468 email = data['emailaddress']
1469 token = getUtility(ILoginTokenSet).new(
1470 requester=None, requesteremail=None, email=email,
1471 tokentype=LoginTokenType.PROFILECLAIM)
1472 token.sendClaimProfileEmail()
1473 # A dance to assert that we want to break the rules about no
1474 # unauthenticated sessions. Only after this next line is it safe
1475 # to use the ``addNoticeNotification`` method.
1476 allowUnauthenticatedSession(self.request)
1477 self.request.response.addInfoNotification(_(
1478 "A confirmation message has been sent to '${email}'. "
1479 "Follow the instructions in that message to finish claiming this "
1480 "profile. "
1481 "(If the message doesn't arrive in a few minutes, your mail "
1482 "provider might use 'greylisting', which could delay the message "
1483 "for up to an hour or two.)", mapping=dict(email=email)))
1484
1485
1486class BeginTeamClaimView(PersonClaimView):
1487 """Where you can claim an unvalidated profile turning it into a team.
1488
1489 This is actually just the first step, where you enter the email address
1490 of the team and we email further instructions to that address.
1491 """
1492 label = 'Claim team'
1493
1494 @action(_("Continue"), name="confirm")1456 @action(_("Continue"), name="confirm")
1495 def confirm_action(self, action, data):1457 def confirm_action(self, action, data):
1496 email = data['emailaddress']1458 email = data['emailaddress']
@@ -2398,9 +2360,9 @@
2398 # The call to redeemVoucher returns True if it succeeds or it2360 # The call to redeemVoucher returns True if it succeeds or it
2399 # raises an exception. Therefore the return value does not need2361 # raises an exception. Therefore the return value does not need
2400 # to be checked.2362 # to be checked.
2401 result = salesforce_proxy.redeemVoucher(voucher.voucher_id,2363 salesforce_proxy.redeemVoucher(voucher.voucher_id,
2402 self.context,2364 self.context,
2403 project)2365 project)
2404 project.redeemSubscriptionVoucher(2366 project.redeemSubscriptionVoucher(
2405 voucher=voucher.voucher_id,2367 voucher=voucher.voucher_id,
2406 registrant=self.context,2368 registrant=self.context,
@@ -3391,7 +3353,7 @@
3391 'The URL scheme "%(scheme)s" is not allowed. '3353 'The URL scheme "%(scheme)s" is not allowed. '
3392 'Only http or https URLs may be used.', scheme=uri.scheme)3354 'Only http or https URLs may be used.', scheme=uri.scheme)
3393 return False3355 return False
3394 except InvalidURIError, e:3356 except InvalidURIError:
3395 self.error_message = structured(3357 self.error_message = structured(
3396 '"%(url)s" is not a valid URL.', url=url)3358 '"%(url)s" is not a valid URL.', url=url)
3397 return False3359 return False
@@ -4226,8 +4188,6 @@
42264188
4227 @action(_("Leave"), name="leave")4189 @action(_("Leave"), name="leave")
4228 def action_save(self, action, data):4190 def action_save(self, action, data):
4229 response = self.request.response
4230
4231 if self.user_can_request_to_leave:4191 if self.user_can_request_to_leave:
4232 self.user.leave(self.context)4192 self.user.leave(self.context)
42334193
42344194
=== modified file 'lib/lp/registry/stories/person/xx-person-claim-merge.txt'
--- lib/lp/registry/stories/person/xx-person-claim-merge.txt 2009-07-20 15:27:26 +0000
+++ lib/lp/registry/stories/person/xx-person-claim-merge.txt 2010-04-17 16:08:28 +0000
@@ -1,3 +1,20 @@
1Claiming a person while not logged in
2=====================================
3
4An anonymous user visiting an unclaimed account will be given the option
5to request a merge, but the +requestmerge page will give an Unauthorized
6exception that will redirect the user to the login page.
7
8 >>> anon_browser.open('http://launchpad.dev/~matsubara')
9 >>> link = anon_browser.getLink("Are you Diogo Matsubara?")
10 >>> print link.url
11 http://launchpad.dev/people/+requestmerge?field.dupe_person=matsubara
12 >>> link.click()
13 Traceback (most recent call last):
14 ...
15 Unauthorized: ...
16
17
1Claiming a person while logged in18Claiming a person while logged in
2=================================19=================================
320
@@ -5,8 +22,6 @@
5option to request a merge.22option to request a merge.
623
7 >>> user_browser.open('http://launchpad.dev/~matsubara')24 >>> user_browser.open('http://launchpad.dev/~matsubara')
8 >>> user_browser.url
9 'http://launchpad.dev/~matsubara'
10 >>> user_browser.getLink("Are you Diogo Matsubara?").click()25 >>> user_browser.getLink("Are you Diogo Matsubara?").click()
1126
12Clicking on that link brought him to a page where he can request to27Clicking on that link brought him to a page where he can request to
1328
=== removed file 'lib/lp/registry/stories/person/xx-person-claim.txt'
--- lib/lp/registry/stories/person/xx-person-claim.txt 2009-09-16 17:56:17 +0000
+++ lib/lp/registry/stories/person/xx-person-claim.txt 1970-01-01 00:00:00 +0000
@@ -1,148 +0,0 @@
1= Claiming a person =
2
3Matsubara was browsing through Launchpad anonymously and found a page
4for himself, even though he has never used Launchpad (what a lie!).
5
6 >>> browser.open('http://launchpad.dev/~matsubara')
7 >>> browser.url
8 'http://launchpad.dev/~matsubara'
9
10He was wondering who created that when he saw a very prominent link,
11saying 'Are you Diogo Matsubara?'. He promptly clicked on it.
12
13 >>> print extract_text(
14 ... find_tag_by_id(browser.contents, 'not-lp-user-or-team'))
15 Diogo Matsubara does not use Launchpad. This page was created...
16 ...2006-12-13...when importing the Portuguese (Brazil) (pt_BR)...
17
18 >>> browser.getLink("Are you Diogo Matsubara?").click()
19
20Clicking on that link brought him to a page where he can claim that
21Launchpad profile.
22
23 >>> browser.url
24 'http://launchpad.dev/%7Ematsubara/+claim'
25
26He first tries it with an old email address he had, but we tell him that
27the given email address is not associated with that profile.
28
29 >>> browser.getControl('Email address').value = 'non@existent.org'
30 >>> browser.getControl('E-mail Me').click()
31 >>> soup = find_main_content(browser.contents)
32 >>> for tag in soup('div', 'message'):
33 ... print tag.renderContents()
34 We couldn't find this email address. Please try another one that could
35 possibly be associated with this profile. Note that this profile's name
36 (matsubara) was generated based on the email address it's associated with.
37
38Then he tries with Sample Person's email address.
39
40 >>> browser.getControl('Email address').value = 'test@canonical.com'
41 >>> browser.getControl('E-mail Me').click()
42 >>> soup = find_main_content(browser.contents)
43 >>> for tag in soup('div', 'message'):
44 ... print tag.renderContents()
45 This email address is associated with yet another Launchpad profile, which
46 you seem to have used at some point. If that's the case, you can <a
47 href="/people/+requestmerge?field.dupe_person=matsubara">combine this
48 profile with the other one</a> (you'll have to log in with the other
49 profile first, though). If that's not the case, please try with a
50 different email address.
51
52And once again he tries with an email address he found in Launchpad, but
53this one is associated with an unvalidated profile.
54
55 >>> browser.getControl('Email address').value = 'morten@wtf.dk'
56 >>> browser.getControl('E-mail Me').click()
57 >>> soup = find_main_content(browser.contents)
58 >>> for tag in soup('div', 'message'):
59 ... print tag.renderContents()
60 Although this email address is not associated with this profile, it's
61 associated with yet another one. You can <a
62 href="http://launchpad.dev/~morten/+claim">claim that other
63 profile</a> and then later <a href="/people/+requestmerge">combine</a>
64 both of them into a single one.
65
66Now he finally remembered his email address.
67
68 >>> browser.getControl('Email address').value = 'matsubara@async.com.br'
69 >>> browser.getControl('E-mail Me').click()
70
71 >>> from lp.services.mail import stub
72 >>> from canonical.launchpad.ftests.logintoken import (
73 ... get_token_url_from_email)
74 >>> from_addr, to_addr, msg = stub.test_emails.pop()
75 >>> to_addr
76 ['matsubara@async.com.br']
77 >>> token_url = get_token_url_from_email(msg)
78 >>> token_url
79 'http://launchpad.dev/token/...'
80
81 >>> browser.open(token_url)
82 >>> browser.url == "%s/+claimprofile" % token_url
83 True
84
85 >>> browser.getControl(name='field.password').value = 'foo'
86 >>> browser.getControl(name='field.password_dupe').value = 'foo'
87 >>> browser.getControl('Continue').click()
88
89 >>> browser.url
90 'http://launchpad.dev/~matsubara'
91 >>> soup = find_main_content(browser.contents)
92 >>> for tag in soup('div', 'informational message'):
93 ... print tag.renderContents()
94 Profile claimed successfully
95
96It's also possible that a person tries to claim an account that has a
97preferred email but no password, like some accounts imported from
98Ubuntu's bugzilla. Although these accounts are not considered valid
99(that's why they can be claimed) they /do/ have a preferred email and we
100need to handle this.
101
102 # To see kiko's email we must be logged in, so we'll use user_browser.
103 >>> user_browser.open('http://launchpad.dev/~kiko')
104 >>> unregistered_user_info = find_tag_by_id(
105 ... user_browser.contents, 'not-lp-user-or-team')
106 >>> unregistered_user_info.li.renderContents()
107 '...christian.reis@ubuntulinux.com...'
108 >>> unregistered_user_info.a.renderContents()
109 'Are you Christian Reis?'
110
111 >>> anon_browser.open('http://launchpad.dev/~kiko')
112 >>> anon_browser.getLink("Are you Christian Reis?").click()
113 >>> anon_browser.url
114 'http://launchpad.dev/%7Ekiko/+claim'
115
116 >>> anon_browser.getControl('Email address').value = (
117 ... 'christian.reis@ubuntulinux.com')
118 >>> anon_browser.getControl('E-mail Me').click()
119
120 >>> from_addr, to_addr, msg = stub.test_emails.pop()
121 >>> to_addr
122 ['christian.reis@ubuntulinux.com']
123 >>> token_url = get_token_url_from_email(msg)
124 >>> token_url
125 'http://launchpad.dev/token/...'
126
127 >>> anon_browser.open(token_url)
128 >>> anon_browser.url == "%s/+claimprofile" % token_url
129 True
130
131 >>> anon_browser.getControl(name='field.password').value = 'foo'
132 >>> anon_browser.getControl(name='field.password_dupe').value = 'foo'
133 >>> anon_browser.getControl('Continue').click()
134
135 >>> anon_browser.url
136 'http://launchpad.dev/~kiko'
137 >>> soup = find_main_content(anon_browser.contents)
138 >>> for tag in soup('div', 'informational message'):
139 ... print tag.renderContents()
140 Profile claimed successfully
141
142Trying to access +claim for an already active team 404s as you would
143expect it to (we shouldn't be linking to from anywhere):
144
145 >>> user_browser.open("http://launchpad.dev/~ubuntu-team/+claim")
146 Traceback (most recent call last):
147 ...
148 NotFound:...
1490
=== modified file 'lib/lp/registry/stories/team/xx-team-claim.txt'
--- lib/lp/registry/stories/team/xx-team-claim.txt 2009-11-22 15:43:16 +0000
+++ lib/lp/registry/stories/team/xx-team-claim.txt 2010-04-17 16:08:28 +0000
@@ -14,7 +14,7 @@
14definitely doesn't seem to be a team.14definitely doesn't seem to be a team.
1515
16 >>> browser.open('http://launchpad.dev/~matsubara')16 >>> browser.open('http://launchpad.dev/~matsubara')
17 >>> browser.getLink(url='+claim')17 >>> browser.getLink(url='+requestmerge')
18 <Link text='Are you Diogo Matsubara?'...18 <Link text='Are you Diogo Matsubara?'...
1919
20If a profile does look like a team and there is a logged in user, the link20If a profile does look like a team and there is a logged in user, the link
@@ -25,15 +25,13 @@
25 >>> [email.email for email in doc_team.guessedemails]25 >>> [email.email for email in doc_team.guessedemails]
26 [u'doc@lists.ubuntu.com']26 [u'doc@lists.ubuntu.com']
2727
28Non-logged-in users see only a link to claim the profile..28Non-logged-in users must log in to claim a team.
2929
30 >>> browser.open('http://launchpad.dev/~doc')30 >>> browser.open('http://launchpad.dev/~doc')
31 >>> browser.getLink(url='+claimteam')31 >>> browser.getLink(url='+claimteam').click()
32 Traceback (most recent call last):32 Traceback (most recent call last):
33 ...33 ...
34 LinkNotFoundError34 Unauthorized:...
35 >>> browser.getLink(url='+claim')
36 <Link text='Are you Ubuntu Doc Team?'...
3735
38While logged-in users get a link to claim that profile and turn it into a36While logged-in users get a link to claim that profile and turn it into a
39team. This works even if the profile's email addresses would normally be37team. This works even if the profile's email addresses would normally be
4038
=== modified file 'lib/lp/registry/templates/person-index.pt'
--- lib/lp/registry/templates/person-index.pt 2009-12-12 15:21:34 +0000
+++ lib/lp/registry/templates/person-index.pt 2010-04-17 16:08:28 +0000
@@ -157,7 +157,6 @@
157 </tal:block>157 </tal:block>
158 </p>158 </p>
159159
160 <tal:logged-in condition="request/lp:person">
161 <tal:person condition="not: view/context_is_probably_a_team">160 <tal:person condition="not: view/context_is_probably_a_team">
162 <ul tal:condition="context/preferredemail">161 <ul tal:condition="context/preferredemail">
163 <li>162 <li>
@@ -183,11 +182,6 @@
183 <tal:team condition="view/context_is_probably_a_team">182 <tal:team condition="view/context_is_probably_a_team">
184 <a href="+claimteam">Is this a team you run?</a>183 <a href="+claimteam">Is this a team you run?</a>
185 </tal:team>184 </tal:team>
186 </tal:logged-in>
187
188 <tal:not-logged-in condition="not: request/lp:person">
189 <a href="+claim">Are you <span tal:replace="context/displayname" />?</a>
190 </tal:not-logged-in>
191 </tal:noaccount>185 </tal:noaccount>
192186
193 <tal:deactivated-account187 <tal:deactivated-account