← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/person-deactivate-job into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/person-deactivate-job into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #988954 in Launchpad itself: "Cannot deactivate account"
  https://bugs.launchpad.net/launchpad/+bug/988954

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/person-deactivate-job/+merge/152809

Create a new job, PersonDeactivateJob that will perform the unbounded heavy lifting of a deactivating a person, rather than attempting to have it all done in the webapp and constantly failing.

I have renamed IPerson.deactivateAccount() to just IPerson.deactivate(), as well as collapsing IPerson.canDeactivateAccount() and IPerson.canDeactivateAccountWithErrors() to just IPerson.canDeactivate(), and also cleaned them both up.

Person:+deactive will now just create a PDJ instead, except the deactivation will not occur until process-job-source runs. I'm not certain we wish to bother rejecting logins with a pending deactivate job for a person, but I suspect not.

Since minor_person for PersonTransferJob is mandatory, I have set it to the janitor, but it will not be referenced by the job when run, and have noted in a comment during job creation.

I've also cleaned up some lint and some whitespace.

I have more than enough LoC spare for this branch.
-- 
https://code.launchpad.net/~stevenk/launchpad/person-deactivate-job/+merge/152809
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/person-deactivate-job into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2013-02-06 11:52:10 +0000
+++ database/schema/security.cfg	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 #
 # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -2093,7 +2093,7 @@
 public.bugnomination                    = SELECT, UPDATE
 public.bugnotificationrecipient         = SELECT, UPDATE, DELETE
 public.bugnotificationrecipientarchive  = SELECT, UPDATE
-public.bugsubscription                  = SELECT, UPDATE
+public.bugsubscription                  = SELECT, UPDATE, DELETE
 public.bugsubscriptionfilter            = SELECT, UPDATE, DELETE
 public.bugsummary                       = SELECT
 public.bugtask                          = SELECT, UPDATE
@@ -2125,7 +2125,7 @@
 public.karmacache                       = SELECT, DELETE
 public.karmacategory                    = SELECT, DELETE
 public.karmatotalcache                  = SELECT, UPDATE, DELETE
-public.latestpersonsourcepackagereleasecache = SELECT
+public.latestpersonsourcepackagereleasecache = SELECT, DELETE
 public.logintoken                       = SELECT, UPDATE, DELETE
 public.mailinglist                      = SELECT, UPDATE
 public.mailinglistsubscription          = SELECT, DELETE
@@ -2194,6 +2194,7 @@
 public.translationrelicensingagreement  = SELECT, UPDATE
 public.translator                       = SELECT, UPDATE
 public.usertouseremail                  = SELECT, UPDATE
+public.validpersoncache                 = SELECT
 public.vote                             = SELECT, UPDATE
 public.votecast                         = SELECT, UPDATE
 public.wikiname                         = SELECT, UPDATE

=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
--- lib/lp/code/model/tests/test_recipebuilder.py	2013-01-24 05:50:23 +0000
+++ lib/lp/code/model/tests/test_recipebuilder.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test RecipeBuildBehavior."""
@@ -240,7 +240,7 @@
         self._setBuilderConfig()
         owner = self.factory.makePerson()
         with person_logged_in(owner):
-            owner.deactivateAccount('deactivating')
+            owner.deactivate('deactivating')
         job = self.makeJob(owner)
         distroarchseries = job.build.distroseries.architectures[0]
         extra_args = job._extraBuildArgs(distroarchseries)

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2013-01-16 05:10:38 +0000
+++ lib/lp/registry/browser/person.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Person-related view classes."""
@@ -171,6 +171,7 @@
     IPersonSet,
     )
 from lp.registry.interfaces.personproduct import IPersonProductFactory
+from lp.registry.interfaces.persontransferjob import IPersonDeactivateJobSource
 from lp.registry.interfaces.pillar import IPillarNameSet
 from lp.registry.interfaces.poll import IPollSubset
 from lp.registry.interfaces.product import IProduct
@@ -942,15 +943,14 @@
 
     def validate(self, data):
         """See `LaunchpadFormView`."""
-        can_deactivate, errors = self.context.canDeactivateAccountWithErrors()
-        if not can_deactivate:
-            [self.addError(message) for message in errors]
+        [self.addError(message) for message in self.context.canDeactivate()]
 
     @action(_("Deactivate My Account"), name="deactivate")
     def deactivate_action(self, action, data):
         # We override the can_deactivate since validation already processed
         # this information.
-        self.context.deactivateAccount(data['comment'], can_deactivate=True)
+        getUtility(IPersonDeactivateJobSource).create(
+            person=self.context, comment=data['comment'])
         logoutPerson(self.request)
         self.request.response.addInfoNotification(
             _(u'Your account has been deactivated.'))

=== modified file 'lib/lp/registry/browser/tests/test_person.py'
--- lib/lp/registry/browser/tests/test_person.py	2013-01-24 05:50:23 +0000
+++ lib/lp/registry/browser/tests/test_person.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -1163,8 +1163,7 @@
     layer = DatabaseFunctionalLayer
     form = {
         'field.comment': 'Gotta go.',
-        'field.actions.deactivate': 'Deactivate My Account',
-        }
+        'field.actions.deactivate': 'Deactivate My Account'}
 
     def test_deactivate_user_active(self):
         user = self.factory.makePerson()
@@ -1176,12 +1175,11 @@
         self.assertEqual(1, len(notifications))
         self.assertEqual(
             'Your account has been deactivated.', notifications[0].message)
-        self.assertEqual(AccountStatus.DEACTIVATED, user.account_status)
 
     def test_deactivate_user_already_deactivated(self):
         deactivated_user = self.factory.makePerson()
         login_person(deactivated_user)
-        deactivated_user.deactivateAccount('going.')
+        deactivated_user.deactivate('going.')
         view = create_initialized_view(
             deactivated_user, '+deactivate-account', form=self.form)
         self.assertEqual(1, len(view.errors))

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2013-01-25 04:58:47 +0000
+++ lib/lp/registry/configure.zcml	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -97,6 +97,12 @@
       <allow interface=".interfaces.persontransferjob.IPersonMergeJobSource"/>
     </securedutility>
 
+    <securedutility
+        component=".model.persontransferjob.PersonDeactivateJob"
+        provides=".interfaces.persontransferjob.IPersonDeactivateJobSource">
+      <allow interface=".interfaces.persontransferjob.IPersonDeactivateJobSource"/>
+    </securedutility>
+
     <class class=".model.persontransferjob.PersonTransferJob">
         <allow interface=".interfaces.persontransferjob.IPersonTransferJob"/>
     </class>
@@ -109,6 +115,10 @@
       <allow interface=".interfaces.persontransferjob.IPersonMergeJob"/>
     </class>
 
+    <class class=".model.persontransferjob.PersonDeactivateJob">
+      <allow interface=".interfaces.persontransferjob.IPersonDeactivateJob"/>
+    </class>
+
     <!-- IProductNotificationJob -->
     <securedutility
         component=".model.productjob.CommercialExpiredJob"

=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt	2013-01-16 06:42:14 +0000
+++ lib/lp/registry/doc/person-account.txt	2013-03-12 03:23:25 +0000
@@ -123,17 +123,17 @@
     >>> comment = ("I'm a person who doesn't want to be listed "
     ...            "as a Launchpad user.")
 
-The deactivateAccount method is restricted to the user himself --not
+The deactivate method is restricted to the user himself --not
 even launchpad admins can use it.
 
     >>> login('mark@xxxxxxxxxxx')
-    >>> foobar.deactivateAccount(comment)
+    >>> foobar.deactivate(comment)
     Traceback (most recent call last):
     ...
     Unauthorized: ...'launchpad.Special')
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> foobar.deactivateAccount(comment)
+    >>> foobar.deactivate(comment)
     >>> transaction.commit()
 
 Deactivating an account changes many of the person's attributes.  It

=== modified file 'lib/lp/registry/enums.py'
--- lib/lp/registry/enums.py	2012-11-26 16:09:05 +0000
+++ lib/lp/registry/enums.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Enums for the Registry app."""
@@ -343,6 +343,13 @@
         Merge one person or team into another person or team.
         """)
 
+    DEACTIVATE = DBItem(2, """
+        Deactivate person
+
+        Do the work to deactivate a person, like reassigning bugs and removing
+        the user from teams.
+        """)
+
 
 class ProductJobType(DBEnumeratedType):
     """Values that IProductJob.job_type can take."""

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2013-02-21 01:10:08 +0000
+++ lib/lp/registry/interfaces/person.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Person interfaces."""
@@ -1762,33 +1762,25 @@
 class IPersonSpecialRestricted(Interface):
     """IPerson methods that require launchpad.Special permission to use."""
 
-    def canDeactivateAccount():
+    def canDeactivate():
         """Verify we safely deactivate this user account.
 
-        :return: True if the person can be deactivated, False otherwise.
-        """
-
-    def canDeactivateAccountWithErrors():
-        """See canDeactivateAccount with the addition of error messages for
-        why the account cannot be deactivated.
-
-        :return tuple: boolean, list of error messages.
-        """
-
-    def deactivateAccount(comment, can_deactivate=None):
+        :return: A possibly empty list which contains error messages.
+        """
+
+    def deactivate(comment, validate=True):
         """Deactivate this person's Launchpad account.
 
         Deactivating an account means:
-            - Removing the user from all teams he's a member of;
-            - Changing all his email addresses' status to NEW;
+            - Removing the user from all teams they are a member of;
+            - Changing all of their email addresses' status to NEW;
             - Revoking Code of Conduct signatures of that user;
-            - Reassigning bugs/specs assigned to him;
-            - Changing the ownership of products/projects/teams owned by him.
+            - Reassigning bugs/specs assigned to that user;
+            - Changing the ownership of products/projects/teams owned by that
+              user.
 
         :param comment: An explanation of why the account status changed.
-        :param can_deactivate: Override the check if we can deactivate by
-            supplying a known value. If None, then the method will run the
-            checks.
+        :param validate: Run validation checks.
         """
 
     def reactivate(comment, preferred_email):
@@ -1798,7 +1790,7 @@
         address.
 
         If the person's name contains a -deactivatedaccount suffix (usually
-        added by `IPerson`.deactivateAccount(), it is removed.
+        added by `IPerson`.deactivate(), it is removed.
 
         :param comment: An explanation of why the account status changed.
         :param preferred_email: The `EmailAddress` to set as the account's

=== modified file 'lib/lp/registry/interfaces/persontransferjob.py'
--- lib/lp/registry/interfaces/persontransferjob.py	2011-12-24 16:54:44 +0000
+++ lib/lp/registry/interfaces/persontransferjob.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interface for the Jobs system to change memberships or merge persons."""
@@ -7,6 +7,8 @@
 __all__ = [
     'IMembershipNotificationJob',
     'IMembershipNotificationJobSource',
+    'IPersonDeactivateJob',
+    'IPersonDeactivateJobSource',
     'IPersonMergeJob',
     'IPersonMergeJobSource',
     'IPersonTransferJob',
@@ -131,3 +133,32 @@
         `from_person` and `to_person` are supplied, jobs with either person
         specified are returned.
         """
+
+
+class IPersonDeactivateJob(IPersonTransferJob):
+    """A Job that deactivates a person."""
+
+    person = PublicPersonChoice(
+        title=_('Alias for person attribute'), vocabulary='ValidPersonOrTeam',
+        required=True)
+
+    def getErrorRecipients(self):
+        """See `BaseRunnableJob`."""
+
+
+class IPersonDeactivateJobSource(IJobSource):
+    """An interface for acquiring IPersonDeactivateJobs."""
+
+    def create(person, comment):
+        """Create a new IPersonMergeJob.
+
+        :param person: A `IPerson` to deactivate.
+        :param comment: The comment why the user is deactivating.
+        """
+
+    def find(person=None):
+        """Finds pending merge jobs.
+
+        :param person: Match jobs on `person`, or `None` to ignore.
+        :return: A `ResultSet` yielding `IPersonDeactivateJob`.
+        """

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2013-02-21 01:10:08 +0000
+++ lib/lp/registry/model/person.py	2013-03-12 03:23:25 +0000
@@ -2172,12 +2172,7 @@
             clauseTables=['Person'],
             orderBy=Person.sortingColumns)
 
-    def canDeactivateAccount(self):
-        """See `IPerson`."""
-        can_deactivate, errors = self.canDeactivateAccountWithErrors()
-        return can_deactivate
-
-    def canDeactivateAccountWithErrors(self):
+    def canDeactivate(self):
         """See `IPerson`."""
         # Users that own non-public products cannot be deactivated until the
         # products are reassigned.
@@ -2192,26 +2187,18 @@
         if self.account_status != AccountStatus.ACTIVE:
             errors.append('This account is already deactivated.')
 
-        return (not errors), errors
+        return errors
 
-    # XXX: salgado, 2009-04-16: This should be called just deactivate(),
-    # because it not only deactivates this person's account but also the
-    # person.
-    def deactivateAccount(self, comment, can_deactivate=None):
+    def deactivate(self, comment, validate=True):
         """See `IPersonSpecialRestricted`."""
-        if not self.is_valid_person:
-            raise AssertionError(
-                "You can only deactivate an account of a valid person.")
+        assert self.is_valid_person, (
+            "You can only deactivate an account of a valid person.")
 
-        if can_deactivate is None:
+        if validate:
             # The person can only be deactivated if they do not own any
             # non-public products.
-            can_deactivate = self.canDeactivateAccount()
-
-        if not can_deactivate:
-            message = ("You cannot deactivate an account that owns a "
-                       "non-public product.")
-            raise AssertionError(message)
+            errors = self.canDeactivate()
+            assert not errors, ' & '.join(errors)
 
         for membership in self.team_memberships:
             self.leave(membership.team)
@@ -2618,9 +2605,8 @@
         self.account.reactivate(comment)
         self.setPreferredEmail(preferred_email)
         if '-deactivatedaccount' in self.name:
-            # The name was changed by deactivateAccount(). Restore the
-            # name, but we must ensure it does not conflict with a current
-            # user.
+            # The name was changed by deactivate(). Restore the name, but we
+            # must ensure it does not conflict with a current user.
             name_parts = self.name.split('-deactivatedaccount')
             base_new_name = name_parts[0]
             self.name = self._ensureNewName(base_new_name)

=== modified file 'lib/lp/registry/model/persontransferjob.py'
--- lib/lp/registry/model/persontransferjob.py	2012-08-13 21:04:17 +0000
+++ lib/lp/registry/model/persontransferjob.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Job classes related to PersonTransferJob."""
@@ -39,6 +39,8 @@
 from lp.registry.interfaces.persontransferjob import (
     IMembershipNotificationJob,
     IMembershipNotificationJobSource,
+    IPersonDeactivateJob,
+    IPersonDeactivateJobSource,
     IPersonMergeJob,
     IPersonMergeJobSource,
     IPersonTransferJob,
@@ -462,3 +464,71 @@
     def getOperationDescription(self):
         return ('merging ~%s into ~%s' %
                 (self.from_person.name, self.to_person.name))
+
+
+class PersonDeactivateJob(PersonTransferJobDerived):
+    """A Job that deactivates a person."""
+
+    implements(IPersonDeactivateJob)
+    classProvides(IPersonDeactivateJobSource)
+
+    class_job_type = PersonTransferJobType.DEACTIVATE
+
+    config = config.IPersonMergeJobSource
+
+    @classmethod
+    def create(cls, person, comment):
+        """See `IPersonMergeJobSource`."""
+        metadata = {'comment': comment}
+        # Minor person has to be not null, so use the janitor.
+        janitor = getUtility(ILaunchpadCelebrities).janitor
+        return super(PersonDeactivateJob, cls).create(
+            minor_person=janitor, major_person=person, metadata=metadata)
+
+    @classmethod
+    def find(cls, person=None):
+        """See `IPersonMergeJobSource`."""
+        conditions = [
+            PersonTransferJob.job_type == cls.class_job_type,
+            PersonTransferJob.job_id == Job.id,
+            Job._status.is_in(Job.PENDING_STATUSES)]
+        arg_conditions = []
+        if person:
+            arg_conditions.append(PersonTransferJob.major_person == person)
+        conditions.extend(arg_conditions)
+        return DecoratedResultSet(
+            IStore(PersonTransferJob).find(
+                PersonTransferJob, *conditions), cls)
+
+    @property
+    def person(self):
+        """See `IPersonMergeJob`."""
+        return self.major_person
+
+    @property
+    def comment(self):
+        return self.metadata['comment']
+
+    @property
+    def log_name(self):
+        return self.__class__.__name__
+
+    def getErrorRecipients(self):
+        """See `IPersonMergeJob`."""
+        return [format_address_for_person(self.person)]
+
+    def run(self):
+        """Perform the merge."""
+        from lp.services.scripts import log
+        person_name = self.person.name
+        log.debug('about to deactivate ~%s', person_name)
+        self.person.deactivate(self.comment, validate=False)
+        log.debug('done deactivating ~%s', person_name)
+
+    def __repr__(self):
+        return (
+            "<{self.__class__.__name__} to deactivate "
+            "~{self.person.name}").format(self=self)
+
+    def getOperationDescription(self):
+        return 'deactivating ~%s' % self.person.name

=== modified file 'lib/lp/registry/tests/test_distributionmirror.py'
--- lib/lp/registry/tests/test_distributionmirror.py	2012-04-16 23:02:44 +0000
+++ lib/lp/registry/tests/test_distributionmirror.py	2013-03-12 03:23:25 +0000
@@ -1,10 +1,8 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
-import unittest
-
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -24,26 +22,26 @@
 from lp.testing import (
     login,
     login_as,
+    TestCase,
+    TestCaseWithFactory,
     )
-from lp.testing.factory import LaunchpadObjectFactory
 from lp.testing.layers import LaunchpadFunctionalLayer
 
-# XXX Jan 20, 2010, jcsackett: This test case really needs to be updated to
-# TestCaseWithFactory.
-class TestDistributionMirror(unittest.TestCase):
+
+class TestDistributionMirror(TestCaseWithFactory):
     layer = LaunchpadFunctionalLayer
 
     def setUp(self):
+        super(TestDistributionMirror, self).setUp()
         login('test@xxxxxxxxxxxxx')
-        self.factory = LaunchpadObjectFactory()
         mirrorset = getUtility(IDistributionMirrorSet)
         self.cdimage_mirror = mirrorset.getByName('releases-mirror')
         self.archive_mirror = mirrorset.getByName('archive-mirror')
         self.hoary = getUtility(IDistributionSet)['ubuntu']['hoary']
         self.hoary_i386 = self.hoary['i386']
 
-    def _create_source_mirror(
-            self, distroseries, pocket, component, freshness):
+    def _create_source_mirror(self, distroseries, pocket, component,
+                              freshness):
         source_mirror1 = self.archive_mirror.ensureMirrorDistroSeriesSource(
             distroseries, pocket, component)
         removeSecurityProxy(source_mirror1).freshness = freshness
@@ -65,7 +63,7 @@
 
     def test_cdimage_mirror_not_missing_content_should_not_be_disabled(self):
         expected_file_count = 1
-        mirror = self.cdimage_mirror.ensureMirrorCDImageSeries(
+        self.cdimage_mirror.ensureMirrorCDImageSeries(
             self.hoary, flavour='ubuntu')
         self.failIf(self.cdimage_mirror.shouldDisable(expected_file_count))
 
@@ -75,9 +73,9 @@
             self.cdimage_mirror.shouldDisable(expected_file_count))
 
     def test_delete_all_mirror_cdimage_series(self):
-        mirror = self.cdimage_mirror.ensureMirrorCDImageSeries(
+        self.cdimage_mirror.ensureMirrorCDImageSeries(
             self.hoary, flavour='ubuntu')
-        mirror = self.cdimage_mirror.ensureMirrorCDImageSeries(
+        self.cdimage_mirror.ensureMirrorCDImageSeries(
             self.hoary, flavour='edubuntu')
         self.failUnlessEqual(
             self.cdimage_mirror.cdimage_series.count(), 2)
@@ -214,7 +212,7 @@
         mirror = self.cdimage_mirror
         login_as(mirror.owner)
         # Deactivate the mirror owner to remove the contact address.
-        mirror.owner.deactivateAccount("I hate mirror spam.")
+        mirror.owner.deactivate("I hate mirror spam.")
         login_as(mirror.distribution.mirror_admin)
         # Clear out notifications about the new team member.
         transaction.commit()
@@ -228,7 +226,7 @@
         self.failUnlessEqual(len(stub.test_emails), 1)
 
 
-class TestDistributionMirrorSet(unittest.TestCase):
+class TestDistributionMirrorSet(TestCase):
     layer = LaunchpadFunctionalLayer
 
     def test_getBestMirrorsForCountry_randomizes_results(self):

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2012-12-10 20:49:43 +0000
+++ lib/lp/registry/tests/test_person.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -789,27 +789,22 @@
         self.bzr = product_set.getByName('bzr')
         self.now = datetime.now(pytz.UTC)
 
-    def test_canDeactivateAccount_private_projects(self):
+    def test_canDeactivate_private_projects(self):
         """A user owning non-public products cannot be deactivated."""
         user = self.factory.makePerson()
         self.factory.makeProduct(
             information_type=InformationType.PUBLIC,
-            name="public",
-            owner=user)
+            name="public", owner=user)
         self.factory.makeProduct(
             information_type=InformationType.PROPRIETARY,
-            name="private",
-            owner=user)
+            name="private", owner=user)
 
         login(user.preferredemail.email)
-        can_deactivate, errors = user.canDeactivateAccountWithErrors()
-
-        self.assertFalse(can_deactivate)
         expected_error = ('This account cannot be deactivated because it owns '
                         'the following non-public products: private')
-        self.assertIn(expected_error, errors)
+        self.assertEquals([expected_error], user.canDeactivate())
 
-    def test_deactivateAccount_copes_with_names_already_in_use(self):
+    def test_deactivate_copes_with_names_already_in_use(self):
         """When a user deactivates his account, its name is changed.
 
         We do that so that other users can use that name, which the original
@@ -821,7 +816,7 @@
         """
         sample_person = Person.byName('name12')
         login(sample_person.preferredemail.email)
-        sample_person.deactivateAccount("blah!")
+        sample_person.deactivate("blah!")
         self.failUnlessEqual(sample_person.name, 'name12-deactivatedaccount')
         # Now that name12 is free Foo Bar can use it.
         foo_bar = Person.byName('name16')
@@ -829,10 +824,10 @@
         # If Foo Bar deactivates his account, though, we'll have to use a name
         # other than name12-deactivatedaccount because that is already in use.
         login(foo_bar.preferredemail.email)
-        foo_bar.deactivateAccount("blah!")
+        foo_bar.deactivate("blah!")
         self.failUnlessEqual(foo_bar.name, 'name12-deactivatedaccount1')
 
-    def test_deactivateAccountReassignsOwnerAndDriver(self):
+    def test_deactivate_reassigns_owner_and_driver(self):
         """Product owner and driver are reassigned.
 
         If a user is a product owner and/or driver, when the user is
@@ -845,14 +840,11 @@
         with person_logged_in(user):
             product.driver = user
             product.bug_supervisor = user
-            user.deactivateAccount("Going off the grid.")
+            user.deactivate("Going off the grid.")
         registry_team = getUtility(ILaunchpadCelebrities).registry_experts
-        self.assertEqual(registry_team, product.owner,
-                         "Owner is not registry team.")
-        self.assertEqual(None, product.driver,
-                         "Driver is not emptied.")
-        self.assertEqual(None, product.bug_supervisor,
-                         "Driver is not emptied.")
+        self.assertEqual(registry_team, product.owner)
+        self.assertIs(None, product.driver)
+        self.assertIs(None, product.bug_supervisor)
 
     def test_getDirectMemberIParticipateIn(self):
         sample_person = Person.byName('name12')

=== added file 'lib/lp/registry/tests/test_person_deactivate_job.py'
--- lib/lp/registry/tests/test_person_deactivate_job.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_person_deactivate_job.py	2013-03-12 03:23:25 +0000
@@ -0,0 +1,38 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `PersonDeactivateJob`."""
+
+__metaclass__ = type
+
+from zope.component import getUtility
+from zope.interface.verify import verifyObject
+
+from lp.registry.interfaces.persontransferjob import (
+    IPersonDeactivateJob,
+    IPersonDeactivateJobSource,
+    )
+from lp.services.identity.interfaces.account import AccountStatus
+from lp.testing import TestCaseWithFactory
+from lp.testing.dbuser import dbuser
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestPersonDeactivateJob(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def makeJob(self):
+        return getUtility(IPersonDeactivateJobSource).create(
+            person=self.factory.makePerson(), comment='Because I Can')
+
+    def test_interface(self):
+        verifyObject(IPersonDeactivateJob, self.makeJob())
+
+    def test_deactivate(self):
+        job = self.makeJob()
+        with dbuser('person-merge-job'):
+            job.run()
+        self.assertEquals(
+            AccountStatus.DEACTIVATED, job.person.account_status)
+        self.assertEquals('Because I Can', job.person.account_status_comment)

=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py	2012-09-18 19:41:02 +0000
+++ lib/lp/registry/tests/test_teammembership.py	2013-03-12 03:23:25 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -1099,7 +1099,7 @@
     def test_no_message_sent_for_non_active_users(self):
         # Non-active users do not get an expiration message.
         with person_logged_in(self.member):
-            self.member.deactivateAccount('Goodbye.')
+            self.member.deactivate('Goodbye.')
         IStore(self.member).flush()
         now = datetime.now(pytz.UTC)
         removeSecurityProxy(self.tm).dateexpires = now + timedelta(days=1)

=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2012-10-22 02:30:44 +0000
+++ lib/lp/services/config/schema-lazr.conf	2013-03-12 03:23:25 +0000
@@ -1745,6 +1745,7 @@
 job_sources:
     ICommercialExpiredJobSource,
     IMembershipNotificationJobSource,
+    IPersonDeactivateJobSource,
     IPersonMergeJobSource,
     IPlainPackageCopyJobSource,
     IProcessAcceptedBugsJobSource,
@@ -1770,6 +1771,12 @@
 dbuser: person-merge-job
 crontab_group: MAIN
 
+[IPersonDeactivateJobSource]
+# This section is used by cronscripts/process-job-source.py.
+module: lp.registry.interfaces.persontransferjob
+dbuser: person-merge-job
+crontab_group: MAIN
+
 [IPlainPackageCopyJobSource]
 # This section is used by cronscripts/process-job-source.py.
 module: lp.soyuz.interfaces.packagecopyjob


Follow ups