Merge lp:~bac/launchpad/bug-419930 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Merged at revision: not available
Proposed branch: lp:~bac/launchpad/bug-419930
Merge into: lp:launchpad
Diff against target: 500 lines (+219/-105)
9 files modified
lib/canonical/launchpad/doc/tales.txt (+1/-2)
lib/canonical/launchpad/interfaces/launchpad.py (+1/-1)
lib/canonical/launchpad/pagetitles.py (+0/-4)
lib/lp/bugs/doc/externalbugtracker-bug-imports.txt (+2/-2)
lib/lp/registry/browser/peoplemerge.py (+48/-30)
lib/lp/registry/interfaces/person.py (+8/-7)
lib/lp/registry/stories/person/merge-people.txt (+63/-6)
lib/lp/registry/templates/people-requestmerge-multiple.pt (+95/-52)
lib/lp/soyuz/browser/sourcepackagerelease.py (+1/-1)
To merge this branch: bzr merge lp:~bac/launchpad/bug-419930
Reviewer Review Type Date Requested Status
Martin Albisetti (community) ui Approve
Guilherme Salgado (community) Approve
Review via email: mp+17015@code.launchpad.net

Commit message

Rework merge request pages

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

= Summary =

Users can choose to hide their email addresses and we should always honor that
request. Currently anyone who attempts to merge a person with hidden email addresses
is shown all addresses for that user.

Also did a drive-by fix to the person picker vocabulary that was not catching an
Unauthorized exception which prevented people with hidden emails from being selected
in the picker.

== Proposed fix ==

Rather than displaying the addresses and letting the user choose which ones to
contact we should just show a count of addresses and send notification to them all.

== Pre-implementation notes ==

Quick call with Curtis as a sanity check of the approach.

== Implementation details ==

As above.

== Tests ==

bin/test -vvm lp.registry -t merge-people.txt

== Demo and Q/A ==

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/browser/peoplemerge.py
  lib/lp/registry/stories/person/merge-people.txt
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/templates/people-requestmerge-multiple.pt
  lib/canonical/launchpad/browser/vocabulary.py

== Pylint notices ==

Non-issues:

lib/lp/registry/browser/peoplemerge.py
    250: [C0322, AdminTeamMergeView.deactivate_members_and_merge_action] Operator not
preceded by a space
    name='deactivate_members_and_merge')
    ^
    def deactivate_members_and_merge_action(self, action, data):

lib/lp/registry/interfaces/person.py
    52: [F0401] Unable to import 'lazr.enum' (No module named enum)
    53: [F0401] Unable to import 'lazr.lifecycle.snapshot' (No module named lifecycle)
    54: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    55: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    62: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    407: [E1002, PersonNameField._validate] Use super on an old style class
    1405: [C0322, IPersonEditRestricted.addMember] Operator not preceded by a space
    status=copy_field(ITeamMembership['status']),
    ^
    comment=Text(required=False))
    @export_write_operation()
    def addMember(person, reviewer, status=TeamMembershipStatus.APPROVED,
    comment=None, force_team_add=False,
    may_subscribe_to_list=True):
    1444: [C0322, IPersonEditRestricted.acceptInvitationToBeMemberOf] Operator not
preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def acceptInvitationToBeMemberOf(team, comment):
    1456: [C0322, IPersonEditRestricted.declineInvitationToBeMemberOf] Operator not
preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def declineInvitationToBeMemberOf(team, comment):
    1754: [C0322, IPersonSet.newTeam] Operator not preceded by a space
    defaultmembershipperiod='default_membership_period',
    ^
    defaultrenewalperiod='default_renewal_period')
    @operation_parameters(
    subscriptionpolicy=Choice(
    title=_('Subscription policy'), vocabulary=TeamSubscriptionPolicy,
    required=False, default=TeamSubscriptionPolicy.MODERATED))
    @export_factory_oper...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (12.4 KiB)

Hi Brad,

This looks pretty good. I have only a few comments/suggestions and one
request for a missing test.

 review needsfixing

On Fri, 2010-01-08 at 13:36 +0000, Brad Crittenden wrote:
> Brad Crittenden has proposed merging lp:~bac/launchpad/bug-419930 into
> lp:launchpad/devel.
>
> Requested reviews:
> Canonical Launchpad Engineering (launchpad)
> Related bugs:
> #419930 non-public email address shown after merge request
> https://bugs.launchpad.net/bugs/419930
>
>
> = Summary =
>
> Users can choose to hide their email addresses and we should always
> honor that
> request. Currently anyone who attempts to merge a person with hidden
> email addresses
> is shown all addresses for that user.
>
> Also did a drive-by fix to the person picker vocabulary that was not
> catching an
> Unauthorized exception which prevented people with hidden emails from
> being selected
> in the picker.

Does that mean we can't merge people with hidden email addresses unless
we know their Launchpad ID or email address?

>
> == Proposed fix ==
>
> Rather than displaying the addresses and letting the user choose which
> ones to
> contact we should just show a count of addresses and send notification
> to them all.
>
> == Pre-implementation notes ==
>
> Quick call with Curtis as a sanity check of the approach.
>
> == Implementation details ==
>
> As above.
>
> == Tests ==
>
> bin/test -vvm lp.registry -t merge-people.txt
>

> === modified file 'lib/canonical/launchpad/browser/vocabulary.py'
> --- lib/canonical/launchpad/browser/vocabulary.py 2009-07-31 19:38:18 +0000
> +++ lib/canonical/launchpad/browser/vocabulary.py 2010-01-08 13:36:23 +0000
> @@ -82,7 +82,7 @@
> def person_to_pickerentry(person):
> """Adapts IPerson to IPickerEntry."""
> extra = default_pickerentry_adapter(person)
> - if person.preferredemail is not None:
> + if person.preferredemail is not None and not person.hide_email_addresses:

I didn't see a test for this; care to add one?

> extra.description = person.preferredemail.email
> return extra
>
>
> === modified file 'lib/lp/registry/browser/peoplemerge.py'
> --- lib/lp/registry/browser/peoplemerge.py 2010-01-05 15:44:09 +0000
> +++ lib/lp/registry/browser/peoplemerge.py 2010-01-08 13:36:23 +0000
> @@ -359,8 +359,7 @@
>
>
> class RequestPeopleMergeMultipleEmailsView:
> - """A view for the page where the user asks a merge and the dupe account
> - have more than one email address."""
> + """Merge request view when dupe account has multiple email addresses."""
>
> def __init__(self, context, request):
> self.context = context
> @@ -390,29 +389,38 @@
> login = getUtility(ILaunchBag).login
> logintokenset = getUtility(ILoginTokenSet)
>
> - emails = self.request.form.get("selected")
> - if emails is not None:
> - # We can have multiple email adressess selected, and in this case
> - # emails will be a list. Otherwise it will be a string and we need
> - # to make a list with that value to use in the for loop.
> - if not isinstance(emails, list):
> - em...

review: Needs Fixing
Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (6.9 KiB)

> Hi Brad,
>
> This looks pretty good. I have only a few comments/suggestions and one
> request for a missing test.

Thanks for the review.

> > Also did a drive-by fix to the person picker vocabulary that was not
> > catching an
> > Unauthorized exception which prevented people with hidden emails from
> > being selected
> > in the picker.
>
> Does that mean we can't merge people with hidden email addresses unless
> we know their Launchpad ID or email address?

Currently we cannot use the picker to select people with hidden email addresses. But you can merge them by typing in the ID into the field on the form.

> > === modified file 'lib/canonical/launchpad/browser/vocabulary.py'
> > --- lib/canonical/launchpad/browser/vocabulary.py 2009-07-31 19:38:18
> +0000
> > +++ lib/canonical/launchpad/browser/vocabulary.py 2010-01-08 13:36:23
> +0000
> > @@ -82,7 +82,7 @@
> > def person_to_pickerentry(person):
> > """Adapts IPerson to IPickerEntry."""
> > extra = default_pickerentry_adapter(person)
> > - if person.preferredemail is not None:
> > + if person.preferredemail is not None and not
> person.hide_email_addresses:
>
> I didn't see a test for this; care to add one?

I have discovered Edwin is working to redo the way these pickers are working and my drive-by change would soon be replaced by his work. Therefore I'm going to back out this portion of the change and just wait for his more thorough fix.

> > === modified file 'lib/lp/registry/browser/peoplemerge.py'

> > + # We can have multiple email adressess selected, and in
> this
>
> Typo: adressess

This typo was pre-existing. I looked to see if it existed elsewhere and found and fixed six other occurrences.

> > === modified file 'lib/lp/registry/stories/person/merge-people.txt'
> > --- lib/lp/registry/stories/person/merge-people.txt 2009-07-31 22:34:24
> +0000
> > +++ lib/lp/registry/stories/person/merge-people.txt 2010-01-08 13:36:23
> +0000
> > @@ -172,3 +172,56 @@
> > #>>> browser.open('http://launchpad.dev/people/+mergerequest-
> sent?dupe=55')
> > #>>> browser.url
> > #'http://launchpad.dev/~no-priv'
> > +
> > +If the duplicate account has multiple email addresses and has chosen
> > +to hide them the process is slightly different. We cannot display the
> > +hidden addresses so instead we just inform the user to check all of
> > +them (and hope they know which ones) and we send merge request
> > +messages to them all.
> > +
> > + >>> login('<email address hidden>')
> > + >>> dupe = factory.makePerson(name="duplicate-person",
> <email address hidden>",
> > + ... hide_email_addresses=True)
> > + >>> email = factory.makeEmail(<email address hidden>", person=dupe)
> > + >>> logout()
> > +
> > +Ensure there are no leftover emails.
> > +
> > + >>> len(stub.test_emails)
> > + 0
> > +
> > +Open the merge page and merge our duplicate account with hidden email
> > +addresses.
> > +
> > + >>> browser.open('http://launchpad.dev/people/+requestmerge')
> > + >>> browser.getControl(
> > + ... 'Duplicated Account').value = 'duplicate-person'
> > + >>> browser.getC...

Read more...

Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Brad Crittenden (bac) wrote :

Added screenshots to the bug 419930 for a UI review.

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Fri, 2010-01-08 at 21:37 +0000, Brad Crittenden wrote:
> > > === modified file 'lib/canonical/launchpad/browser/vocabulary.py'
> > > --- lib/canonical/launchpad/browser/vocabulary.py 2009-07-31 19:38:18
> > +0000
> > > +++ lib/canonical/launchpad/browser/vocabulary.py 2010-01-08 13:36:23
> > +0000
> > > @@ -82,7 +82,7 @@
> > > def person_to_pickerentry(person):
> > > """Adapts IPerson to IPickerEntry."""
> > > extra = default_pickerentry_adapter(person)
> > > - if person.preferredemail is not None:
> > > + if person.preferredemail is not None and not
> > person.hide_email_addresses:
> >
> > I didn't see a test for this; care to add one?
>
> I have discovered Edwin is working to redo the way these pickers are
> working and my drive-by change would soon be replaced by his work.
> Therefore I'm going to back out this portion of the change and just
> wait for his more thorough fix.

Ok, do you mind filing a bug and assigning it to him, just to make sure
it's not forgotten?

>
>
> > > === modified file 'lib/lp/registry/browser/peoplemerge.py'
>
> > > + # We can have multiple email adressess selected, and in
> > this
> >
> > Typo: adressess
>
> This typo was pre-existing. I looked to see if it existed elsewhere and found and fixed six other occurrences.
>

Nice, thanks!

 review approve

--
Guilherme Salgado <email address hidden>

review: Approve
Revision history for this message
Martin Albisetti (beuno) wrote :

Thanks for the changes.

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/doc/tales.txt'
--- lib/canonical/launchpad/doc/tales.txt 2010-01-12 09:12:59 +0000
+++ lib/canonical/launchpad/doc/tales.txt 2010-01-15 01:40:24 +0000
@@ -1111,7 +1111,7 @@
1111 >>> test_tales('foo/fmt:linkify-email', foo='nobody@example.com')1111 >>> test_tales('foo/fmt:linkify-email', foo='nobody@example.com')
1112 'nobody@example.com'1112 'nobody@example.com'
11131113
1114Users who specifiy that their email adresses must be hidden also do not1114Users who specify that their email addresses must be hidden also do not
1115get linkified. test@canonical.com is hidden:1115get linkified. test@canonical.com is hidden:
11161116
1117 >>> person_set = getUtility(IPersonSet)1117 >>> person_set = getUtility(IPersonSet)
@@ -1583,4 +1583,3 @@
15831583
1584 >>> test_tales("team/fmt:unique_displayname", team=myteam)1584 >>> test_tales("team/fmt:unique_displayname", team=myteam)
1585 u'<hidden>'1585 u'<hidden>'
1586
15871586
=== modified file 'lib/canonical/launchpad/interfaces/launchpad.py'
--- lib/canonical/launchpad/interfaces/launchpad.py 2010-01-08 16:16:50 +0000
+++ lib/canonical/launchpad/interfaces/launchpad.py 2010-01-15 01:40:24 +0000
@@ -612,7 +612,7 @@
612 should be a short code that will appear in an612 should be a short code that will appear in an
613 X-Launchpad-Message-Rationale header for automatic filtering.613 X-Launchpad-Message-Rationale header for automatic filtering.
614614
615 :param person_or_email: An `IPerson` or email adress that is in the615 :param person_or_email: An `IPerson` or email address that is in the
616 recipients list.616 recipients list.
617617
618 :raises UnknownRecipientError: if the person or email isn't in the618 :raises UnknownRecipientError: if the person or email isn't in the
619619
=== modified file 'lib/canonical/launchpad/pagetitles.py'
--- lib/canonical/launchpad/pagetitles.py 2009-11-23 03:10:04 +0000
+++ lib/canonical/launchpad/pagetitles.py 2010-01-15 01:40:24 +0000
@@ -526,10 +526,6 @@
526526
527people_mergerequest_sent = 'Merge request sent'527people_mergerequest_sent = 'Merge request sent'
528528
529people_requestmerge = 'Merge Launchpad accounts'
530
531people_requestmerge_multiple = 'Merge Launchpad accounts'
532
533person_answer_contact_for = ContextDisplayName(529person_answer_contact_for = ContextDisplayName(
534 'Projects for which %s is an answer contact')530 'Projects for which %s is an answer contact')
535531
536532
=== modified file 'lib/lp/bugs/doc/externalbugtracker-bug-imports.txt'
--- lib/lp/bugs/doc/externalbugtracker-bug-imports.txt 2009-06-15 11:10:49 +0000
+++ lib/lp/bugs/doc/externalbugtracker-bug-imports.txt 2010-01-15 01:40:24 +0000
@@ -76,9 +76,9 @@
76account is marked as NEW, since we don't know whether it's valid.76account is marked as NEW, since we don't know whether it's valid.
7777
78 >>> from canonical.launchpad.interfaces import IEmailAddressSet78 >>> from canonical.launchpad.interfaces import IEmailAddressSet
79 >>> reporter_email_adresses = getUtility(IEmailAddressSet).getByPerson(79 >>> reporter_email_addresses = getUtility(IEmailAddressSet).getByPerson(
80 ... bug.owner)80 ... bug.owner)
81 >>> for email_address in reporter_email_adresses:81 >>> for email_address in reporter_email_addresses:
82 ... print "%s: %s" % (email_address.email, email_address.status.name)82 ... print "%s: %s" % (email_address.email, email_address.status.name)
83 joe.bloggs@example.com: NEW83 joe.bloggs@example.com: NEW
84 >>> print bug.owner.preferredemail84 >>> print bug.owner.preferredemail
8585
=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py 2010-01-05 15:44:09 +0000
+++ lib/lp/registry/browser/peoplemerge.py 2010-01-15 01:40:24 +0000
@@ -11,7 +11,8 @@
11 'DeleteTeamView',11 'DeleteTeamView',
12 'FinishedPeopleMergeRequestView',12 'FinishedPeopleMergeRequestView',
13 'RequestPeopleMergeMultipleEmailsView',13 'RequestPeopleMergeMultipleEmailsView',
14 'RequestPeopleMergeView']14 'RequestPeopleMergeView',
15 ]
1516
1617
17from zope.component import getUtility18from zope.component import getUtility
@@ -44,6 +45,7 @@
44 """45 """
4546
46 label = 'Merge Launchpad accounts'47 label = 'Merge Launchpad accounts'
48 page_title = label
47 schema = IRequestPeopleMerge49 schema = IRequestPeopleMerge
4850
49 @property51 @property
@@ -359,8 +361,10 @@
359361
360362
361class RequestPeopleMergeMultipleEmailsView:363class RequestPeopleMergeMultipleEmailsView:
362 """A view for the page where the user asks a merge and the dupe account364 """Merge request view when dupe account has multiple email addresses."""
363 have more than one email address."""365
366 label = 'Merge Launchpad accounts'
367 page_title = label
364368
365 def __init__(self, context, request):369 def __init__(self, context, request):
366 self.context = context370 self.context = context
@@ -368,6 +372,7 @@
368 self.form_processed = False372 self.form_processed = False
369 self.dupe = None373 self.dupe = None
370 self.notified_addresses = []374 self.notified_addresses = []
375 self.user = getUtility(ILaunchBag).user
371376
372 def processForm(self):377 def processForm(self):
373 dupe = self.request.form.get('dupe')378 dupe = self.request.form.get('dupe')
@@ -386,33 +391,46 @@
386 return391 return
387392
388 self.form_processed = True393 self.form_processed = True
389 user = getUtility(ILaunchBag).user
390 login = getUtility(ILaunchBag).login394 login = getUtility(ILaunchBag).login
391 logintokenset = getUtility(ILoginTokenSet)395 logintokenset = getUtility(ILoginTokenSet)
392396
393 emails = self.request.form.get("selected")397 email_addresses = []
394 if emails is not None:398 if self.email_hidden:
395 # We can have multiple email adressess selected, and in this case399 # If the email addresses are hidden we must send a merge request
396 # emails will be a list. Otherwise it will be a string and we need400 # to each of them. But first we've got to remove the security
397 # to make a list with that value to use in the for loop.401 # proxy so we can get to them.
398 if not isinstance(emails, list):402 from zope.security.proxy import removeSecurityProxy
399 emails = [emails]403 email_addresses = [removeSecurityProxy(email).email
400404 for email in self.dupeemails]
401 for email in emails:405 else:
402 emailaddress = emailaddrset.getByEmail(email)406 # Otherwise we send a merge request only to the ones the user
403 assert emailaddress in self.dupeemails407 # selected.
404 token = logintokenset.new(408 emails = self.request.form.get("selected")
405 user, login, email, LoginTokenType.ACCOUNTMERGE)409 if emails is not None:
406 token.sendMergeRequestEmail()410 # We can have multiple email addresses selected, and in this
407 self.notified_addresses.append(email)411 # case emails will be a list. Otherwise it will be a string
408412 # and we need to make a list with that value to use in the for
409 # XXX: salgado, 2008-07-02: We need to somehow disclose the dupe person's413 # loop.
410 # email addresses so that the logged in user knows where to look for the414 if not isinstance(emails, list):
411 # message with instructions to finish the merge. Since people can choose415 emails = [emails]
412 # to have their email addresses hidden, we need to remove the security416
413 # proxy here to ensure they can be shown in this page.417 for emailaddress in emails:
414 @property418 email = emailaddrset.getByEmail(emailaddress)
415 def naked_dupeemails(self):419 assert email in self.dupeemails
416 """Non-security-proxied email addresses of the dupe person."""420 email_addresses.append(emailaddress)
417 from zope.security.proxy import removeSecurityProxy421
418 return [removeSecurityProxy(email) for email in self.dupeemails]422 for emailaddress in email_addresses:
423 token = logintokenset.new(
424 self.user, login, emailaddress, LoginTokenType.ACCOUNTMERGE)
425 token.sendMergeRequestEmail()
426 self.notified_addresses.append(emailaddress)
427
428 @property
429 def cancel_url(self):
430 """Cancel URL."""
431 return canonical_url(self.user)
432
433 @property
434 def email_hidden(self):
435 """Does the duplicate account hide email addresses?"""
436 return self.dupe.hide_email_addresses
419437
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2010-01-13 23:18:29 +0000
+++ lib/lp/registry/interfaces/person.py 2010-01-15 01:40:24 +0000
@@ -96,6 +96,7 @@
96from canonical.launchpad.webapp.interfaces import NameLookupFailed96from canonical.launchpad.webapp.interfaces import NameLookupFailed
97from canonical.launchpad.webapp.authorization import check_permission97from canonical.launchpad.webapp.authorization import check_permission
9898
99
99PRIVATE_TEAM_PREFIX = 'private-'100PRIVATE_TEAM_PREFIX = 'private-'
100101
101102
@@ -333,7 +334,7 @@
333 MODERATED = DBItem(1, """334 MODERATED = DBItem(1, """
334 Moderated Team335 Moderated Team
335336
336 All subscriptions for this team are subject to approval by one of 337 All subscriptions for this team are subject to approval by one of
337 the team's administrators.338 the team's administrators.
338 """)339 """)
339340
@@ -366,17 +367,17 @@
366 PRIVATE_MEMBERSHIP = DBItem(20, """367 PRIVATE_MEMBERSHIP = DBItem(20, """
367 Private Membership368 Private Membership
368369
369 Only Launchpad admins and team members can view the 370 Only Launchpad admins and team members can view the
370 membership list for this team. The team is severely restricted in the 371 membership list for this team. The team is severely restricted in the
371 roles it can assume.372 roles it can assume.
372 """)373 """)
373374
374 PRIVATE = DBItem(30, """375 PRIVATE = DBItem(30, """
375 Private376 Private
376377
377 Only Launchpad admins and team members can view the membership list 378 Only Launchpad admins and team members can view the membership list
378 for this team or its name. The team roles are restricted to 379 for this team or its name. The team roles are restricted to
379 subscribing to bugs, being bug supervisor, owning code branches, and 380 subscribing to bugs, being bug supervisor, owning code branches, and
380 having a PPA.381 having a PPA.
381 """)382 """)
382383
@@ -1850,7 +1851,7 @@
1850 While we don't have Full Text Indexes in the emailaddress table, we'll1851 While we don't have Full Text Indexes in the emailaddress table, we'll
1851 be trying to match the text only against the beginning of an email1852 be trying to match the text only against the beginning of an email
1852 address.1853 address.
1853 1854
1854 If created_before or created_after are not None, they are used to1855 If created_before or created_after are not None, they are used to
1855 restrict the search to the dates provided.1856 restrict the search to the dates provided.
1856 """1857 """
18571858
=== modified file 'lib/lp/registry/stories/person/merge-people.txt'
--- lib/lp/registry/stories/person/merge-people.txt 2009-07-31 22:34:24 +0000
+++ lib/lp/registry/stories/person/merge-people.txt 2010-01-15 01:40:24 +0000
@@ -63,8 +63,10 @@
63 >>> browser.getControl(63 >>> browser.getControl(
64 ... 'Duplicated Account').value = 'foo'64 ... 'Duplicated Account').value = 'foo'
65 >>> browser.getControl('Continue').click()65 >>> browser.getControl('Continue').click()
66 >>> 'has more than one registered e-mail address' in browser.contents66 >>> explanation = find_tag_by_id(browser.contents, 'explanation')
67 True67 >>> print extract_text(explanation)
68 The account...has more than one registered e-mail address...
69
6870
69Make sure we haven't got leftovers from a previous test71Make sure we haven't got leftovers from a previous test
7072
@@ -76,9 +78,10 @@
76 >>> email_select_control = browser.getControl(name='selected')78 >>> email_select_control = browser.getControl(name='selected')
77 >>> for ctrl in email_select_control.controls:79 >>> for ctrl in email_select_control.controls:
78 ... ctrl.selected = True80 ... ctrl.selected = True
79 >>> browser.getControl('Submit').click()81 >>> browser.getControl('Merge Accounts').click()
80 >>> 'Individual email messages were sent to' in browser.contents82 >>> confirmation = find_tag_by_id(browser.contents, 'confirmation')
81 True83 >>> print extract_text(confirmation)
84 Confirmation email messages were sent to:...
82 >>> 'foo@baz.com' in browser.contents85 >>> 'foo@baz.com' in browser.contents
83 True86 True
84 >>> 'bar.foo@canonical.com' in browser.contents87 >>> 'bar.foo@canonical.com' in browser.contents
@@ -107,7 +110,7 @@
107 True110 True
108111
109User confirms the merge request submitting the form, but the merge wasn't112User confirms the merge request submitting the form, but the merge wasn't
110finished because the duplicate account still have a registered email adresses.113finished because the duplicate account still have a registered email addresses.
111114
112 >>> browser.getControl('Confirm').click()115 >>> browser.getControl('Confirm').click()
113 >>> 'has other registered e-mail addresses too' in browser.contents116 >>> 'has other registered e-mail addresses too' in browser.contents
@@ -172,3 +175,57 @@
172 #>>> browser.open('http://launchpad.dev/people/+mergerequest-sent?dupe=55')175 #>>> browser.open('http://launchpad.dev/people/+mergerequest-sent?dupe=55')
173 #>>> browser.url176 #>>> browser.url
174 #'http://launchpad.dev/~no-priv'177 #'http://launchpad.dev/~no-priv'
178
179If the duplicate account has multiple email addresses and has chosen
180to hide them the process is slightly different. We cannot display the
181hidden addresses so instead we just inform the user to check all of
182them (and hope they know which ones) and we send merge request
183messages to them all.
184
185 >>> login('foo.bar@canonical.com')
186 >>> dupe = factory.makePerson(name="duplicate-person", email="dupe1@example.com",
187 ... hide_email_addresses=True)
188 >>> email = factory.makeEmail(address="dupe2@example.com", person=dupe)
189 >>> logout()
190
191Ensure there are no leftover emails.
192
193 >>> len(stub.test_emails)
194 0
195
196Open the merge page and merge our duplicate account with hidden email
197addresses.
198
199 >>> browser.open('http://launchpad.dev/people/+requestmerge')
200 >>> browser.getControl(
201 ... 'Duplicated Account').value = 'duplicate-person'
202 >>> browser.getControl('Continue').click()
203
204 >>> explanation = find_tag_by_id(browser.contents, 'explanation')
205 >>> print extract_text(explanation)
206 The account...has 2 registered e-mail addresses...
207
208
209Since the addresses are hidden we cannot display them therefore there
210are no controls to select.
211
212 >>> email_select_control = browser.getControl(name='selected')
213 Traceback (most recent call last):
214 ...
215 LookupError: name 'selected'
216
217 >>> browser.getControl('Merge Accounts').click()
218
219 >>> len(stub.test_emails)
220 2
221
222 >>> confirmation = find_tag_by_id(browser.contents, 'confirmation')
223 >>> print extract_text(confirmation)
224 Confirmation email messages were sent to the 2 registered e-mail addresses...
225
226 >>> maincontent = extract_text(find_main_content(browser.contents))
227
228 >>> 'dupe1@example.com' in maincontent
229 False
230 >>> 'dupe2@example.com' in maincontent
231 False
175232
=== modified file 'lib/lp/registry/templates/people-requestmerge-multiple.pt'
--- lib/lp/registry/templates/people-requestmerge-multiple.pt 2009-08-31 19:53:54 +0000
+++ lib/lp/registry/templates/people-requestmerge-multiple.pt 2010-01-15 01:40:24 +0000
@@ -14,59 +14,102 @@
14>14>
15 <body>15 <body>
16 <div metal:fill-slot="main">16 <div metal:fill-slot="main">
17 <h1 tal:condition="not: view/dupe">No account found to merge</h1>17 <p tal:condition="not: view/dupe">No account found to merge</p>
18 <tal:duplicate condition="view/dupe">18 <tal:duplicate condition="view/dupe">
19 <tal:block tal:condition="not: view/form_processed">19 <tal:block tal:condition="not: view/form_processed">
20 <h1>Merge accounts</h1>20
2121 <div style="margin-top: 1em;">
22 <p>22
23 The account <code tal:content="view/dupe/name">foo</code>,23 <tal:email_visible condition="not: view/email_hidden">
24 which you&#8217;re trying to merge,24 <p id="explanation">
25 has more than one registered e-mail address.25 The account <code tal:content="view/dupe/name">foo</code>
26 To complete the merge, you need to prove that you have access to26 has more than one registered e-mail address.
27 all e-mail addresses registered for this account.27 You need to prove that you have access to
28 To do this, select all of them and click <samp>Submit</samp>.28 all e-mail addresses registered for this account.
29 If you don&#8217;t have access to one or more of these addresses,29 Unselect any you cannot access.
30 Launchpad will not be able to merge the account into your existing one,30 If you don&#8217;t have access to one or more of these addresses,
31 but any other addresses will be transferred to your account.31 Launchpad will not be able to merge the account but all confirmed
32 </p>32 addresses will be transferred to your account.
3333 </p>
34 <form name="mergerequest" action="" method="POST">34
35 <input type="hidden" name="dupe" tal:attributes="value request/dupe" />35 <form name="mergerequest" action="" method="POST">
36 <table>36 <input type="hidden" name="dupe" tal:attributes="value request/dupe" />
37 <tr tal:repeat="email view/naked_dupeemails">37 <table>
38 <td>38 <tr tal:repeat="email view/dupeemails">
39 <input type="checkbox" name="selected" checked="checked"39 <td>
40 tal:attributes="value email/email" />40 <input type="checkbox" name="selected" checked="checked"
41 </td>41 tal:attributes="value email/email" />
42 <td tal:content="email/email">42 <span tal:content="email/email" />
43 </td>43 </td>
44 </tr>44 </tr>
45 <tr>45 <tr>
46 <td colspan="2" align="right">46 <td>
47 <input type="submit" value="Submit" />47 <input type="submit" value="Merge Accounts" />
48 </td>48 or <a href tal:attributes="href view/cancel_url">Cancel</a>
49 </tr>49 </td>
50 </table>50 </tr>
51 </form>51 </table>
52 </tal:block>52 </form>
5353 </tal:email_visible>
54 <tal:block tal:condition="view/form_processed">54
55 <h1>Confirmation mail sent</h1>55 <tal:email_hidden condition="view/email_hidden">
5656 <p id="explanation">
57 <p>Individual email messages were sent to57 The account <code tal:content="view/dupe/name">foo</code>
58 <tal:block repeat="email view/notified_addresses">58 has <span tal:replace="view/dupeemails/count" /> registered
59 <strong tal:content="email"59 e-mail addresses but they are hidden.
60 /><span tal:condition="not: repeat/email/end">, </span>60 You need to prove that you have access to
61 </tal:block>.61 all e-mail addresses registered for this account.
62 Please follow the instructions on each of those messages to complete62 </p>
63 the merge.63 <p>
64 </p>64 To do so, click the button. An email will be sent to each
65 </tal:block>65 address on file for the duplicate account. Follow the
6666 instructions for each email.
67 </tal:duplicate>67 If you don&#8217;t have access to some of these addresses,
68</div> 68 Launchpad will not be able to merge the account.
6969 The addresses you verify will be transferred to your account.
70</body>70 </p>
71
72 <form name="mergerequest" action="" method="POST">
73 <input type="hidden" name="dupe" tal:attributes="value request/dupe" />
74 <input type="submit" value="Merge Accounts" />
75 or <a href tal:attributes="href view/cancel_url">Cancel</a>
76 </form>
77 </tal:email_hidden>
78 </div>
79 </tal:block>
80
81 <tal:block tal:condition="view/form_processed">
82
83 <tal:email_visible condition="not: view/email_hidden">
84 <p id="confirmation">
85 Confirmation email messages were sent to:
86 </p>
87 <tal:block repeat="email view/notified_addresses">
88 <p><strong tal:content="email"/></p>
89 </tal:block>
90 </tal:email_visible>
91
92 <tal:email_hidden condition="view/email_hidden">
93 <p id="confirmation">
94 Confirmation email messages were sent to the
95 <span tal:content="view/dupeemails/count" /> registered
96 e-mail addresses for
97 <code tal:content="view/dupe/name">foo</code>.
98 </p>
99 </tal:email_hidden>
100
101 <p>
102 Please follow the instructions on each of those messages to complete
103 the merge.
104 </p>
105 <p>
106 <a href tal:attributes="href view/cancel_url">Back to my account</a>
107 </p>
108
109 </tal:block>
110
111 </tal:duplicate>
112 </div>
113 </body>
71</html>114</html>
72</tal:root>115</tal:root>
73116
=== modified file 'lib/lp/soyuz/browser/sourcepackagerelease.py'
--- lib/lp/soyuz/browser/sourcepackagerelease.py 2010-01-06 11:59:21 +0000
+++ lib/lp/soyuz/browser/sourcepackagerelease.py 2010-01-15 01:40:24 +0000
@@ -84,7 +84,7 @@
8484
8585
86def extract_email_addresses(text):86def extract_email_addresses(text):
87 '''Unique email adresses in the text.'''87 '''Unique email addresses in the text.'''
88 matches = re.finditer(FormattersAPI._re_email, text)88 matches = re.finditer(FormattersAPI._re_email, text)
89 return list(set([match.group() for match in matches]))89 return list(set([match.group() for match in matches]))
9090