launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #15335
[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