← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/person-merge-job-4 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/person-merge-job-4 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #697530 in Launchpad itself: "failing to +adminteammerge two teams "Constraint not satisfied""
  https://bugs.launchpad.net/launchpad/+bug/697530
  Bug #736421 in Launchpad itself: "Person:+delete timeouts : Connect the UI the merge jobs"
  https://bugs.launchpad.net/launchpad/+bug/736421

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/person-merge-job-4/+merge/57037

Enable async person/team merges from the UI.

    Launchpad bug:
        https://bugs.launchpad.net/bugs/736421
        https://bugs.launchpad.net/bugs/697530
    Pre-implementation: allenap
    Test command: ./bin/test -vv \
      -t test_is_merge_pending -t peoplemerge
      -t /xx-adminpeoplemerge -t xx-adminteammerge

- Update call-sites to use mergeAsync() where appropriate.
- UI to show if a user is the subject of a scheduled merge request.

--------------------------------------------------------------------

RULES

    * Add an info notification to the person/team index page that states
      the team is involved in a merge.
    * Ensure a merge cannot be requested is the user/team is involved
      in a queued merge
    * Change the view the call Person.merge_async() and show a message
      explaining that the merge is queued.
    * ADDENDUM. Fix the ambiguous "Constraint not satisfied" message.


QA

    * Using one of the unmergeable users, perform a merge.
    * Verify (quickly) you see the notification that the user is being merged.
    * Verify the user is merged after a few minutes.
    * Using and undeletable teams, perform a delete.
    * Verify (quickly) you see the notification that the team is being merged.
    * Verify the team is merged after a few minutes.


LINT

    lib/canonical/launchpad/browser/logintoken.py
    lib/lp/registry/browser/peoplemerge.py
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/peoplemerge-views.txt
    lib/lp/registry/browser/tests/test_peoplemerge.py
    lib/lp/registry/browser/tests/test_person_view.py
    lib/lp/registry/browser/tests/test_team.py
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/interfaces/persontransferjob.py
    lib/lp/registry/model/person.py
    lib/lp/registry/stories/person/xx-adminpeoplemerge.txt
    lib/lp/registry/stories/team/xx-adminteammerge.txt
    lib/lp/registry/tests/test_person.py



IMPLEMENTATION

Fixed the signature of mergeAsync() and create() methods and updated the
documentation. Added reviewer to Merge() to be compatible with the
aforementioned methods.
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/interfaces/persontransferjob.py

Revised the rule for is_merge_pending because we care about merges in
the duplicate role. It is okay for a person or team to have several
dupes pending to merge. We expect this to be the case for team deletions
which are merges into ~registry.
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_person.py

I deleted the duplicate validate() from the direct subclass. I think it
was left over from a refactoring. I discovered that the common validate()
is used by forms that do not provide a target user since it is assumed to
be the requester. If fixed it to fix the tests I was writing.
Added a check to ensure the duplicate and target do not have a pending merge.
Updated the logintoken and peoplemerge views to use merge_async().
Changed the messages to explain the merge is queued.
I was startled by the "Constraint not satisfied" in a test, since that is
a UI problem. I found a bug was reported about it so I fixed the message.
    lib/canonical/launchpad/browser/logintoken.py
    lib/lp/registry/browser/peoplemerge.py
    lib/lp/registry/browser/tests/peoplemerge-views.txt
    lib/lp/registry/browser/tests/test_peoplemerge.py

I deleted corner cases from these story tests because there are testing in
the view tests. I updated the stories to verify the messages users see.
    lib/lp/registry/stories/person/xx-adminpeoplemerge.txt
    lib/lp/registry/stories/team/xx-adminteammerge.txt

Added a notification to explain that the team or person is being merged.
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_person_view.py
    lib/lp/registry/browser/tests/test_team.py
-- 
https://code.launchpad.net/~sinzui/launchpad/person-merge-job-4/+merge/57037
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/person-merge-job-4 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/browser/logintoken.py'
--- lib/canonical/launchpad/browser/logintoken.py	2011-02-16 17:31:11 +0000
+++ lib/canonical/launchpad/browser/logintoken.py	2011-04-09 02:58:29 +0000
@@ -605,10 +605,11 @@
         if emailset.getByPerson(self.dupe):
             self.mergeCompleted = False
             return
-
-        # Call Stuart's magic function which will reassign all of the dupe
-        # account's stuff to the user account.
-        getUtility(IPersonSet).merge(self.dupe, requester)
+        getUtility(IPersonSet).mergeAsync(
+            self.dupe, requester, reviewer=requester)
+        merge_message = _(
+            'A merge is queued and is expected to complete in a few minutes.')
+        self.request.response.addInfoNotification(merge_message)
         self.mergeCompleted = True
 
 

=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py	2011-03-25 23:42:33 +0000
+++ lib/lp/registry/browser/peoplemerge.py	2011-04-09 02:58:29 +0000
@@ -56,14 +56,14 @@
     def validate(self, data):
         """Check that user is not attempting to merge a person into itself."""
         dupe_person = data.get('dupe_person')
-        target_person = data.get('target_person')
-
-        if dupe_person == target_person and dupe_person is not None:
-            self.addError(_("You can't merge ${name} into itself.",
-                  mapping=dict(name=dupe_person.name)))
-        # We cannot merge if there is a PPA with published
-        # packages on the duplicate, unless that PPA is removed.
-        if dupe_person is not None:
+        target_person = data.get('target_person') or self.user
+        if dupe_person is None:
+            self.setFieldError(
+                'dupe_person', 'The duplicate is not a valid person or team.')
+        else:
+            if dupe_person == target_person:
+                self.addError(_("You can't merge ${name} into itself.",
+                      mapping=dict(name=dupe_person.name)))
             dupe_person_ppas = getUtility(IArchiveSet).getPPAOwnedByPerson(
                 dupe_person, statuses=[ArchiveStatus.ACTIVE,
                                        ArchiveStatus.DELETING])
@@ -73,6 +73,12 @@
                     "can be merged. It may take ten minutes to remove the "
                     "deleted PPA's files.",
                     mapping=dict(name=dupe_person.name)))
+            if dupe_person.is_merge_pending:
+                self.addError(_("${name} is already queued for merging.",
+                      mapping=dict(name=dupe_person.name)))
+        if target_person is not None and target_person.is_merge_pending:
+            self.addError(_("${name} is already queued for merging.",
+                  mapping=dict(name=target_person.name)))
 
 
 class AdminMergeBaseView(ValidatingMergeView):
@@ -84,7 +90,8 @@
     # subclasses.
     should_confirm_email_reassignment = False
     should_confirm_member_deactivation = False
-    merge_message = _('Merge completed successfully.')
+    merge_message = _(
+        'A merge is queued and is expected to complete in a few minutes.')
 
     dupe_person_emails = ()
     dupe_person = None
@@ -98,25 +105,6 @@
     def success_url(self):
         return canonical_url(self.target_person)
 
-    def validate(self, data):
-        """Check that user is not attempting to merge a person into itself."""
-        dupe_person = data.get('dupe_person')
-        target_person = data.get('target_person')
-        if dupe_person == target_person and dupe_person is not None:
-            self.addError(_("You can't merge ${name} into itself.",
-                  mapping=dict(name=dupe_person.name)))
-        # We cannot merge the teams if there is a PPA with published
-        # packages on the duplicate person, unless that PPA is removed.
-        dupe_person_ppas = getUtility(IArchiveSet).getPPAOwnedByPerson(
-            dupe_person, statuses=[ArchiveStatus.ACTIVE,
-                                   ArchiveStatus.DELETING])
-        if dupe_person_ppas is not None:
-            self.addError(_(
-                "${name} has a PPA that must be deleted before it "
-                "can be merged. It may take ten minutes to remove the "
-                "deleted PPA's files.",
-                mapping=dict(name=dupe_person.name)))
-
     def render(self):
         # Subclasses may define other actions that they will render manually
         # only in certain circunstances, so don't include them in the list of
@@ -151,7 +139,7 @@
                 naked_email.personID = self.target_person.id
                 naked_email.accountID = self.target_person.accountID
                 naked_email.status = EmailAddressStatus.NEW
-        getUtility(IPersonSet).merge(
+        job = getUtility(IPersonSet).mergeAsync(
             self.dupe_person, self.target_person, reviewer=self.user)
         self.request.response.addInfoNotification(self.merge_message)
         self.next_url = self.success_url
@@ -264,7 +252,7 @@
 
     page_title = 'Delete'
     field_names = ['dupe_person', 'target_person']
-    merge_message = _('Team deleted.')
+    merge_message = _('The team is queued to be deleted.')
 
     @property
     def label(self):

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2011-03-11 00:06:42 +0000
+++ lib/lp/registry/browser/person.py	2011-04-09 02:58:29 +0000
@@ -3391,6 +3391,14 @@
 
     def initialize(self):
         super(PersonIndexView, self).initialize()
+        if self.context.is_merge_pending:
+            if self.context.is_team:
+                merge_action = 'merged or deleted'
+            else:
+                merge_action = 'merged'
+            self.request.response.addInfoNotification(
+                "%s is queued to be be %s in a few minutes." % (
+                self.context.displayname, merge_action))
         if self.request.method == "POST":
             self.processForm()
 

=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
--- lib/lp/registry/browser/tests/peoplemerge-views.txt	2011-03-25 13:44:19 +0000
+++ lib/lp/registry/browser/tests/peoplemerge-views.txt	2011-04-09 02:58:29 +0000
@@ -31,13 +31,10 @@
     ...     person_set, '+adminteammerge', form=form)
     >>> len(view.errors)
     0
-
-The old team is now renamed but the target team remains.
-
-    >>> print person_set.getByName('name21')
-    None
-    >>> print person_set.getByName('ubuntu-team').displayname
-    Ubuntu Team
+    >>> for notification in view.request.response.notifications:
+    ...     print notification.message
+    A merge is queued and is expected to complete in a few minutes.
+
 
 Attempting to merge a non-existent team results in an error.
 
@@ -76,12 +73,9 @@
     >>> view = create_initialized_view(
     ...     person_set, '+requestmerge', form=form)
     >>> len(view.errors)
-    1
-
-    # XXX Salgado, 2009-07-08, bug=396410: There should be a meaningful
-    # error message here instead of this one.
+    2
     >>> print view.getFieldError('dupe_person')
-    Constraint not satisfied
+    The duplicate is not a valid person or team.
 
 Admins, on the other hand, are allowed to merge people without a single email
 address.
@@ -181,18 +175,15 @@
 
     >>> for notification in view.request.response.notifications:
     ...     print notification.message
-    Team deleted.
+    The team is queued to be deleted.
 
     >>> print view.next_url
     http://launchpad.dev/people
 
-    >>> print person_set.getByName('deletable')
-    None
-
 The delete team view cannot be hacked to delete another team, or change
 the team that the merge operation is performed with.
 
-    >>> deletable_team = factory.makeTeam(owner=team_owner, name='deletable')
+    >>> deletable_team = factory.makeTeam(owner=team_owner, name='delete-me')
     >>> transaction.commit()
     >>> form = {
     ...     'field.target_person': 'rosetta-admins',
@@ -206,9 +197,6 @@
     >>> view.request.response.notifications
     []
 
-    >>> print deletable_team.name
-    deletable
-
 A team with a mailing list cannot be deleted.
 
     >>> team, mailing_list = factory.makeTeamAndMailingList(
@@ -246,4 +234,4 @@
     []
     >>> for notification in view.request.response.notifications:
     ...     print notification.message
-    Team deleted.
+    The team is queued to be deleted.

=== modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
--- lib/lp/registry/browser/tests/test_peoplemerge.py	2011-03-25 13:44:19 +0000
+++ lib/lp/registry/browser/tests/test_peoplemerge.py	2011-04-09 02:58:29 +0000
@@ -6,15 +6,12 @@
 
 from zope.component import getUtility
 
-from canonical.launchpad.interfaces.emailaddress import (
-    EmailAddressStatus,
-    IEmailAddressSet,
-    )
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.registry.interfaces.person import (
     IPersonSet,
     TeamSubscriptionPolicy,
     )
+from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
 from lp.testing import (
     login_celebrity,
     login_person,
@@ -27,33 +24,70 @@
     )
 
 
-class TestRequestPeopleMergeView(TestCaseWithFactory):
+class TestValidatingMergeView(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        super(TestRequestPeopleMergeView, self).setUp()
+        super(TestValidatingMergeView, self).setUp()
         self.person_set = getUtility(IPersonSet)
         self.dupe = self.factory.makePerson(name='dupe')
-        self.target = self.factory.makeTeam(name='target')
+        self.target = self.factory.makePerson(name='target')
+
+    def getForm(self, dupe_name=None):
+        if dupe_name is None:
+            dupe_name = self.dupe.name
+        return {
+            'field.dupe_person': dupe_name,
+            'field.target_person': self.target.name,
+            'field.actions.continue': 'Continue',
+            }
 
     def test_cannot_merge_person_with_ppas(self):
         # A team with a PPA cannot be merged.
         login_celebrity('admin')
         archive = self.dupe.createPPA()
         login_celebrity('registry_experts')
-        form = {
-            'field.dupe_person': self.dupe.name,
-            'field.target_person': self.target.name,
-            'field.actions.continue': 'Continue',
-            }
+        view = create_initialized_view(
+            self.person_set, '+requestmerge', form=self.getForm())
+        self.assertEqual(
+            [u"dupe has a PPA that must be deleted before it can be "
+              "merged. It may take ten minutes to remove the deleted PPA's "
+              "files."],
+            view.errors)
+
+    def test_cannot_merge_person_with_itself(self):
+        # A IPerson cannot be merged with itself.
+        login_person(self.target)
+        form = self.getForm(dupe_name=self.target.name)
         view = create_initialized_view(
             self.person_set, '+requestmerge', form=form)
         self.assertEqual(
-            [u"dupe has a PPA that must be deleted before it can be "
-              "merged. It may take ten minutes to remove the deleted PPA's "
-              "files."],
-            view.errors)
+            ["You can't merge target into itself."], view.errors)
+
+    def test_cannot_merge_dupe_person_with_an_existing_merge_job(self):
+        # A merge cannot be requested for an IPerson if it there is a job
+        # queued to merge it into another IPerson.
+        job_source = getUtility(IPersonMergeJobSource)
+        duplicate_job = job_source.create(
+            from_person=self.dupe, to_person=self.target)
+        login_person(self.target)
+        view = create_initialized_view(
+            self.person_set, '+requestmerge', form=self.getForm())
+        self.assertEqual(
+            ["dupe is already queued for merging."], view.errors)
+
+    def test_cannot_merge_target_person_with_an_existing_merge_job(self):
+        # A merge cannot be requested for an IPerson if it there is a job
+        # queued to merge it into another IPerson.
+        job_source = getUtility(IPersonMergeJobSource)
+        duplicate_job = job_source.create(
+            from_person=self.target, to_person=self.dupe)
+        login_person(self.target)
+        view = create_initialized_view(
+            self.person_set, '+requestmerge', form=self.getForm())
+        self.assertEqual(
+            ["target is already queued for merging."], view.errors)
 
 
 class TestRequestPeopleMergeMultipleEmailsView(TestCaseWithFactory):
@@ -135,16 +169,6 @@
         return create_initialized_view(
             self.person_set, '+adminteammerge', form=form)
 
-    def test_merge_team_with_email_address(self):
-        # Team email addresses are not transferred.
-        self.factory.makeEmail(
-            "del@xxxxxx", self.dupe_team, email_status=EmailAddressStatus.NEW)
-        view = self.getView()
-        self.assertEqual([], view.errors)
-        self.assertEqual(self.target_team, self.dupe_team.merged)
-        emails = getUtility(IEmailAddressSet).getByPerson(self.target_team)
-        self.assertEqual(0, emails.count())
-
     def test_cannot_merge_team_with_ppa(self):
         # A team with a PPA cannot be merged.
         login_celebrity('admin')

=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py	2011-03-08 23:06:21 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py	2011-04-09 02:58:29 +0000
@@ -6,10 +6,7 @@
 import transaction
 from storm.expr import LeftJoin
 from storm.store import Store
-from testtools.matchers import (
-    Equals,
-    LessThan,
-    )
+from testtools.matchers import LessThan
 from zope.component import getUtility
 
 from canonical.config import config
@@ -43,6 +40,7 @@
     PersonVisibility,
     IPersonSet,
     )
+from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
 from lp.registry.interfaces.teammembership import (
     ITeamMembershipSet,
     TeamMembershipStatus,
@@ -69,6 +67,22 @@
     )
 
 
+class TestPersonIndexView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_is_merge_pending(self):
+        dupe_person = self.factory.makePerson(name='finch')
+        target_person = self.factory.makePerson()
+        job_source = getUtility(IPersonMergeJobSource)
+        job_source.create(from_person=dupe_person, to_person=target_person)
+        view = create_initialized_view(dupe_person, name="+index")
+        notifications = view.request.response.notifications
+        message = 'Finch is queued to be be merged in a few minutes.'
+        self.assertEqual(1, len(notifications))
+        self.assertEqual(message, notifications[0].message)
+
+
 class TestPersonViewKarma(TestCaseWithFactory):
 
     layer = LaunchpadZopelessLayer

=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py	2010-12-03 00:03:15 +0000
+++ lib/lp/registry/browser/tests/test_team.py	2011-04-09 02:58:29 +0000
@@ -3,9 +3,12 @@
 
 __metaclass__ = type
 
+from zope.component import getUtility
+
 from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.registry.browser.person import TeamOverviewMenu
+from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
 from lp.testing import (
     login_person,
     TestCaseWithFactory,
@@ -149,3 +152,17 @@
     def test_add_member_step_title(self):
         view = create_initialized_view(self.team, '+index')
         self.assertEqual('Search', view.add_member_step_title)
+
+    def test_is_merge_pending(self):
+        target_team = self.factory.makeTeam()
+        job_source = getUtility(IPersonMergeJobSource)
+        job_source.create(
+            from_person=self.team, to_person=target_team,
+            reviewer=target_team.teamowner)
+        view = create_initialized_view(self.team, name="+index")
+        notifications = view.request.response.notifications
+        message = (
+            'Test Team is queued to be be merged or deleted '
+            'in a few minutes.')
+        self.assertEqual(1, len(notifications))
+        self.assertEqual(message, notifications[0].message)

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2011-03-23 16:28:51 +0000
+++ lib/lp/registry/interfaces/person.py	2011-04-09 02:58:29 +0000
@@ -1428,12 +1428,14 @@
         "The number of real people who are members of this team.")
     # activemembers.value_type.schema will be set to IPerson once
     # IPerson is defined.
-    activemembers = Attribute('List of direct members with ADMIN or APPROVED status')
+    activemembers = Attribute(
+        'List of direct members with ADMIN or APPROVED status')
     # For the API we need eager loading
     api_activemembers = exported(
         doNotSnapshot(
             CollectionField(
-                title=_("List of direct members with ADMIN or APPROVED status"),
+                title=_(
+                    "List of direct members with ADMIN or APPROVED status"),
                 value_type=Reference(schema=Interface))),
         exported_as='members')
     adminmembers = exported(
@@ -2170,7 +2172,7 @@
     def latest_teams(limit=5):
         """Return the latest teams registered, up to the limit specified."""
 
-    def mergeAsync(from_person, to_person):
+    def mergeAsync(from_person, to_person, reviewer=None):
         """Merge a person/team into another asynchronously.
 
         This schedules a call to `merge()` to happen outside of the current
@@ -2179,10 +2181,13 @@
         guaranteed to succeed. If either user is in a pending person merge
         job, None is returned.
 
+        :param from_person: An IPerson or ITeam that is a duplicate.
+        :param to_person: An IPerson or ITeam that is a master.
+        :param reviewer: An IPerson who approved the ITeam merger.
         :return: A `PersonMergeJob` or None.
         """
 
-    def merge(from_person, to_person):
+    def merge(from_person, to_person, reviewer=None):
         """Merge a person/team into another.
 
         The old person/team (from_person) will be left as an atavism.
@@ -2196,9 +2201,9 @@
         passing deactivate_members=True. In that case the user who's
         performing the merge must be provided as well.
 
-        We are not yet game to delete the `from_person` entry from the
-        database yet. We will let it roll for a while and see what cruft
-        develops. -- StuartBishop 20050812
+        :param from_person: An IPerson or ITeam that is a duplicate.
+        :param to_person: An IPerson or ITeam that is a master.
+        :param reviewer: An IPerson who approved the ITeam merger.
         """
 
     def getValidPersons(persons):

=== modified file 'lib/lp/registry/interfaces/persontransferjob.py'
--- lib/lp/registry/interfaces/persontransferjob.py	2011-03-26 21:50:45 +0000
+++ lib/lp/registry/interfaces/persontransferjob.py	2011-04-09 02:58:29 +0000
@@ -102,7 +102,7 @@
 class IPersonMergeJobSource(IJobSource):
     """An interface for acquiring IPersonMergeJobs."""
 
-    def create(from_person, to_person, review=None):
+    def create(from_person, to_person, reviewer=None):
         """Create a new IPersonMergeJob.
 
         None is returned if either the from_person or to_person are already
@@ -111,7 +111,7 @@
 
         :param from_person: An IPerson or ITeam that is a duplicate.
         :param to_person: An IPerson or ITeam that is a master.
-        :param review: An IPerson who approved ITeam merger.
+        :param reviewer: An IPerson who approved ITeam merger.
         """
 
     def find(from_person=None, to_person=None, any_person=False):

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2011-04-06 17:08:45 +0000
+++ lib/lp/registry/model/person.py	2011-04-09 02:58:29 +0000
@@ -2076,8 +2076,7 @@
     def is_merge_pending(self):
         """See `IPublicPerson`."""
         return not getUtility(
-            IPersonMergeJobSource).find(
-                from_person=self, to_person=self, any_person=True).is_empty()
+            IPersonMergeJobSource).find(from_person=self).is_empty()
 
     def visibilityConsistencyWarning(self, new_value):
         """Warning used when changing the team's visibility.
@@ -3836,10 +3835,10 @@
             naked_from_team.retractTeamMembership(team, reviewer)
         IStore(from_team).flush()
 
-    def mergeAsync(self, from_person, to_person):
+    def mergeAsync(self, from_person, to_person, reviewer=None):
         """See `IPersonSet`."""
         return getUtility(IPersonMergeJobSource).create(
-            from_person=from_person, to_person=to_person)
+            from_person=from_person, to_person=to_person, reviewer=reviewer)
 
     def merge(self, from_person, to_person, reviewer=None):
         """See `IPersonSet`."""

=== modified file 'lib/lp/registry/stories/person/xx-adminpeoplemerge.txt'
--- lib/lp/registry/stories/person/xx-adminpeoplemerge.txt	2010-10-10 15:39:28 +0000
+++ lib/lp/registry/stories/person/xx-adminpeoplemerge.txt	2011-04-09 02:58:29 +0000
@@ -1,4 +1,5 @@
-= Merging people or teams =
+Merging people or teams
+=======================
 
 Launchpad admins can merge any two people or teams, without the need of
 any email address confirmation or something like that.  There's one page
@@ -35,17 +36,6 @@
     >>> admin_browser.url
     'http://launchpad.dev/~salgado'
 
-    >>> from zope.component import getUtility
-    >>> from canonical.launchpad.ftests import ANONYMOUS, login, logout
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> login(ANONYMOUS)
-    >>> person_set = getUtility(IPersonSet)
-    >>> person_set.getByEmail('andrew.bennetts@xxxxxxxxxxxxxxx').name
-    u'salgado'
-
-    >>> spiv = person_set.getByName('spiv-merged', ignore_merged=False)
-    >>> spiv.merged.name
-    u'salgado'
-
-    >>> logout()
+    >>> print get_feedback_messages(admin_browser.contents)[0]
+    A merge is queued and is expected to complete in a few minutes.
 

=== modified file 'lib/lp/registry/stories/team/xx-adminteammerge.txt'
--- lib/lp/registry/stories/team/xx-adminteammerge.txt	2010-10-18 22:24:59 +0000
+++ lib/lp/registry/stories/team/xx-adminteammerge.txt	2011-04-09 02:58:29 +0000
@@ -1,4 +1,5 @@
-= Merging Teams =
+Merging Teams
+=============
 
 There's a separate page for merging teams.  We can't merge teams with
 active members, so the user will first have to confirm that the
@@ -32,70 +33,5 @@
     >>> registry_browser.url
     'http://launchpad.dev/~guadamen'
 
-    >>> from canonical.launchpad.ftests import ANONYMOUS, login, logout
-    >>> from zope.component import getUtility
-    >>> from lp.registry.interfaces.mailinglist import IMailingListSet
-    >>> login(ANONYMOUS)
-    >>> new_team.merged.name
-    u'guadamen'
-    >>> logout()
-
-
-==  Non-display of merged teams ==
-
-Merged teams are not visible anymore.
-
-    >>> browser.open("http://launchpad.dev/~new-team-merged";)
-    Traceback (most recent call last):
-    ...
-    NotFound:...
-
-
-== Corner cases ==
-
-If the duplicated team is associated with a mailing list, it can't be
-merged, though.
-
-    >>> login(ANONYMOUS)
-    >>> guadamen = getUtility(IPersonSet).getByName('guadamen')
-    >>> mailing_list = getUtility(IMailingListSet).new(guadamen)
-    >>> logout()
-
-    >>> registry_browser.open('http://launchpad.dev/people/+adminteammerge')
-    >>> registry_browser.getControl('Duplicated Team').value = 'guadamen'
-    >>> registry_browser.getControl('Target Team').value = 'ubuntu-team'
-    >>> registry_browser.getControl('Merge').click()
-
-    >>> registry_browser.url
-    'http://launchpad.dev/people/+adminteammerge'
-
-    >>> print get_feedback_messages(registry_browser.contents)
-    [u'There is 1 error.',
-     u"guadamen is associated with a Launchpad mailing list;
-       we can't merge it."]
-
-We also can't (for obvious reasons) merge any person/team into itself.
-
-    >>> registry_browser.getControl('Duplicated Team').value = 'shipit-admins'
-    >>> registry_browser.getControl('Target Team').value = 'shipit-admins'
-    >>> registry_browser.getControl('Merge').click()
-
-    >>> registry_browser.url
-    'http://launchpad.dev/people/+adminteammerge'
-
-    >>> print get_feedback_messages(registry_browser.contents)
-    [u'There is 1 error.', u"You can't merge shipit-admins into itself."]
-
-Nor can we merge a person into a team or a team into a person.
-
-    >>> registry_browser.getControl('Duplicated Team').value = 'ubuntu-team'
-    >>> registry_browser.getControl('Target Team').value = 'salgado'
-    >>> registry_browser.getControl('Merge').click()
-
-    >>> registry_browser.url
-    'http://launchpad.dev/people/+adminteammerge'
-
-    # Yes, the error message is not of much help, but this UI is only for
-    # admins and they're supposed to know what they're doing.
-    >>> print get_feedback_messages(registry_browser.contents)
-    [u'There is 1 error.', u'Constraint not satisfied']
+    >>> print get_feedback_messages(registry_browser.contents)[0]
+    A merge is queued and is expected to complete in a few minutes.

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2011-04-05 12:10:01 +0000
+++ lib/lp/registry/tests/test_person.py	2011-04-09 02:58:29 +0000
@@ -289,14 +289,14 @@
         person = self.factory.makePerson()
         self.assertFalse(person.is_merge_pending)
 
-    def test_merge_pending(self):
+    def test_is_merge_pending(self):
         # is_merge_pending returns True when this person is being merged with
         # another person in an active merge job.
         from_person = self.factory.makePerson()
         to_person = self.factory.makePerson()
         getUtility(IPersonSet).mergeAsync(from_person, to_person)
         self.assertTrue(from_person.is_merge_pending)
-        self.assertTrue(to_person.is_merge_pending)
+        self.assertFalse(to_person.is_merge_pending)
 
     def test_mergeAsync_success(self):
         # mergeAsync returns a job with the from and to persons.