launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02739
[Merge] lp:~allenap/launchpad/async-person-merge-162510 into lp:launchpad
Gavin Panella has proposed merging lp:~allenap/launchpad/async-person-merge-162510 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#162510 Person:+delete timeouts : Person merging needs to be done asynchronously
https://bugs.launchpad.net/bugs/162510
For more details, see:
https://code.launchpad.net/~allenap/launchpad/async-person-merge-162510/+merge/50919
This branch adds a new PersonMergeJob, so that person (and team)
merges can be done asynchronously, and adds a convenient API for them,
IPersonSet.mergeAsync(), that parallels the existing synchronous API,
IPersonSet.merge().
It also:
- Adds a context manager to _LogWrapper, which makes it easier to test
the output of code that logs with canonical.launchpad.scripts.log.
- Documents several of the IPersonSet.merge() call-sites (or, rather,
the methods containing those call-sites) to explain what pre-merge
checks they perform.
These checks should probably be consolidated into some sort of
"pre-flight" checks that can be done before calling merge() or
mergeAsync().
- Adds a new property, IPerson.is_merge_pending, which indicates if
the person or team is due to be merged into another.
- Fixes the __repr__() for MembershipNotificationJob.
The __repr__() for PersonTransferJobDerived, its superclass, was
part nonsense, and was misleading (as I experienced when looking at
the output of failing tests). I removed the one-size-fits-none
__repr__() from the superclass and just provided a decent one for
MembershipNotificationJob.
--
https://code.launchpad.net/~allenap/launchpad/async-person-merge-162510/+merge/50919
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/async-person-merge-162510 into lp:launchpad.
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2011-02-21 18:54:53 +0000
+++ lib/canonical/config/schema-lazr.conf 2011-02-23 13:46:27 +0000
@@ -2122,10 +2122,16 @@
# Each job source class also needs its own config section to specify the
# dbuser, the crontab_group, and the module that the job source class
# can be loaded from.
-job_sources: IMembershipNotificationJobSource
+job_sources: IMembershipNotificationJobSource, IPersonMergeJobSource
[IMembershipNotificationJobSource]
# This section is used by cronscripts/process-job-source.py.
module: lp.registry.interfaces.persontransferjob
dbuser: person-transfer-job
crontab_group: MAIN
+
+[IPersonMergeJobSource]
+# This section is used by cronscripts/process-job-source.py.
+module: lp.registry.interfaces.persontransferjob
+dbuser: person-transfer-job
+crontab_group: MAIN
=== modified file 'lib/canonical/launchpad/browser/logintoken.py'
--- lib/canonical/launchpad/browser/logintoken.py 2011-02-02 15:43:31 +0000
+++ lib/canonical/launchpad/browser/logintoken.py 2011-02-23 13:46:27 +0000
@@ -572,6 +572,17 @@
self.context.consume()
def _doMerge(self):
+ """Merges a duplicate person into a target person.
+
+ - Reassigns the duplicate user's primary email address to the
+ requesting user.
+
+ - Ensures that the requesting user has a preferred email address, and
+ uses the newly acquired one if not.
+
+ - If the duplicate user has no other email addresses, does the merge.
+
+ """
# The user proved that he has access to this email address of the
# dupe account, so we can assign it to him.
requester = self.context.requester
=== modified file 'lib/canonical/launchpad/scripts/logger.py'
--- lib/canonical/launchpad/scripts/logger.py 2011-02-17 16:15:50 +0000
+++ lib/canonical/launchpad/scripts/logger.py 2011-02-23 13:46:27 +0000
@@ -31,6 +31,7 @@
]
+from contextlib import contextmanager
from cStringIO import StringIO
from datetime import (
datetime,
@@ -410,6 +411,15 @@
else:
return setattr(self._log, key, value)
+ @contextmanager
+ def use(self, log):
+ """Temporarily use a different `log`."""
+ self._log, log = log, self._log
+ try:
+ yield
+ finally:
+ self._log = log
+
def shortException(self, msg, *args):
"""Like Logger.exception, but does not print a traceback."""
exctype, value = sys.exc_info()[:2]
=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py 2011-01-07 15:16:52 +0000
+++ lib/lp/registry/browser/peoplemerge.py 2011-02-23 13:46:27 +0000
@@ -137,7 +137,11 @@
self.dupe_person_emails = emailset.getByPerson(self.dupe_person)
def doMerge(self, data):
- """Merge the two person/team entries specified in the form."""
+ """Merge the two person/team entries specified in the form.
+
+ Before merging this moves each email address of the duplicate person
+ to the target person, and resets them to `NEW`.
+ """
for email in self.dupe_person_emails:
email = IMasterObject(email)
# EmailAddress.person and EmailAddress.account are readonly
@@ -215,7 +219,21 @@
return getUtility(ILaunchpadCelebrities).registry_experts
def doMerge(self, data):
- """Purge the non-transferable team data and merge."""
+ """Purge the non-transferable team data and merge.
+
+ For the duplicate team:
+
+ - If a mailing list exists, and is REGISTERED, DECLINED, FAILED or
+ INACTIVE, it is purged.
+
+ - Unsets the contact address.
+
+ If the target team is the Registry Experts:
+
+ - The duplicate team is withdrawn from all teams that it is itself a
+ member of.
+
+ """
# A team cannot have more than one mailing list. The old list will
# remain in the archive.
purge_list = (self.dupe_person.mailing_list is not None
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2011-01-21 08:12:29 +0000
+++ lib/lp/registry/configure.zcml 2011-02-23 13:46:27 +0000
@@ -87,19 +87,27 @@
<!-- IPersonTransferJob -->
<securedutility
- component="lp.registry.model.persontransferjob.MembershipNotificationJob"
- provides="lp.registry.interfaces.persontransferjob.IMembershipNotificationJobSource">
- <allow
- interface="lp.registry.interfaces.persontransferjob.IMembershipNotificationJobSource"/>
- </securedutility>
-
- <class class="lp.registry.model.persontransferjob.PersonTransferJob">
- <allow interface="lp.registry.interfaces.persontransferjob.IPersonTransferJob"/>
- </class>
-
- <class class="lp.registry.model.persontransferjob.MembershipNotificationJob">
- <allow
- interface="lp.registry.interfaces.persontransferjob.IMembershipNotificationJob"/>
+ component=".model.persontransferjob.MembershipNotificationJob"
+ provides=".interfaces.persontransferjob.IMembershipNotificationJobSource">
+ <allow interface=".interfaces.persontransferjob.IMembershipNotificationJobSource"/>
+ </securedutility>
+
+ <securedutility
+ component=".model.persontransferjob.PersonMergeJob"
+ provides=".interfaces.persontransferjob.IPersonMergeJobSource">
+ <allow interface=".interfaces.persontransferjob.IPersonMergeJobSource"/>
+ </securedutility>
+
+ <class class=".model.persontransferjob.PersonTransferJob">
+ <allow interface=".interfaces.persontransferjob.IPersonTransferJob"/>
+ </class>
+
+ <class class=".model.persontransferjob.MembershipNotificationJob">
+ <allow interface=".interfaces.persontransferjob.IMembershipNotificationJob"/>
+ </class>
+
+ <class class=".model.persontransferjob.PersonMergeJob">
+ <allow interface=".interfaces.persontransferjob.IPersonMergeJob"/>
</class>
<!-- INameBlacklist -->
=== modified file 'lib/lp/registry/enum.py'
--- lib/lp/registry/enum.py 2011-02-01 04:57:42 +0000
+++ lib/lp/registry/enum.py 2011-02-23 13:46:27 +0000
@@ -80,3 +80,9 @@
Notify affected users of new team membership.
""")
+
+ MERGE = DBItem(1, """
+ Person merge
+
+ Merge one person or team into another person or team.
+ """)
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2011-02-16 11:03:04 +0000
+++ lib/lp/registry/interfaces/person.py 2011-02-23 13:46:27 +0000
@@ -974,6 +974,10 @@
description=_("Private teams are visible only to "
"their members.")))
+ is_merge_pending = Bool(
+ title=_("Is this person due to be merged into another?"),
+ required=False, default=False)
+
@invariant
def personCannotHaveIcon(person):
"""Only Persons can have icons."""
@@ -2150,6 +2154,17 @@
def latest_teams(limit=5):
"""Return the latest teams registered, up to the limit specified."""
+ def mergeAsync(from_person, to_person):
+ """Merge a person/team into another asynchronously.
+
+ This schedules a call to `merge()` to happen outside of the current
+ context/request. The intention is that it is called soon after this
+ method is called but there is no guarantee, nor is it not guaranteed
+ to succeed.
+
+ :return: A `PersonMergeJob`.
+ """
+
def merge(from_person, to_person):
"""Merge a person/team into another.
=== modified file 'lib/lp/registry/interfaces/persontransferjob.py'
--- lib/lp/registry/interfaces/persontransferjob.py 2010-09-30 20:39:02 +0000
+++ lib/lp/registry/interfaces/persontransferjob.py 2011-02-23 13:46:27 +0000
@@ -7,6 +7,8 @@
__all__ = [
'IMembershipNotificationJob',
'IMembershipNotificationJobSource',
+ 'IPersonMergeJob',
+ 'IPersonMergeJobSource',
'IPersonTransferJob',
'IPersonTransferJobSource',
]
@@ -78,3 +80,37 @@
def create(member, team, reviewer, old_status, new_status,
last_change_comment=None):
"""Create a new IMembershipNotificationJob."""
+
+
+class IPersonMergeJob(IPersonTransferJob):
+ """A Job that merges one person or team into another."""
+
+ from_person = PublicPersonChoice(
+ title=_('Alias for minor_person attribute'),
+ vocabulary='ValidPersonOrTeam',
+ required=True)
+
+ to_person = PublicPersonChoice(
+ title=_('Alias for major_person attribute'),
+ vocabulary='ValidPersonOrTeam',
+ required=True)
+
+
+class IPersonMergeJobSource(IJobSource):
+ """An interface for acquiring IMembershipNotificationJobs."""
+
+ def create(from_person, to_person):
+ """Create a new IMembershipNotificationJob."""
+
+ def find(from_person=None, to_person=None):
+ """Finds pending or running merge jobs.
+
+ :param from_person: Match jobs on `from_person`, or `None` to ignore
+ `from_person`.
+ :param to_person: Match jobs on `to_person`, or `None` to ignore
+ `from_person`.
+ :return: A `ResultSet` yielding `IPersonMergeJob`.
+
+ If both `from_person` and `to_person` is supplied, only jobs where
+ both match are returned.
+ """
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-02-16 11:03:04 +0000
+++ lib/lp/registry/model/person.py 2011-02-23 13:46:27 +0000
@@ -235,6 +235,7 @@
validate_public_person,
)
from lp.registry.interfaces.personnotification import IPersonNotificationSet
+from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
from lp.registry.interfaces.pillar import IPillarNameSet
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup
@@ -2133,6 +2134,12 @@
else:
return True
+ @property
+ def is_merge_pending(self):
+ """See `IPublicPerson`."""
+ return not getUtility(
+ IPersonMergeJobSource).find(from_person=self).is_empty()
+
def visibilityConsistencyWarning(self, new_value):
"""Warning used when changing the team's visibility.
@@ -3859,6 +3866,11 @@
WHERE id = %(to_id)d
''' % vars())
+ def mergeAsync(self, from_person, to_person):
+ """See `IPersonSet`."""
+ return getUtility(IPersonMergeJobSource).create(
+ from_person=from_person, to_person=to_person)
+
def merge(self, from_person, to_person):
"""See `IPersonSet`."""
# Sanity checks
=== modified file 'lib/lp/registry/model/persontransferjob.py'
--- lib/lp/registry/model/persontransferjob.py 2011-01-24 20:10:41 +0000
+++ lib/lp/registry/model/persontransferjob.py 2011-02-23 13:46:27 +0000
@@ -25,11 +25,17 @@
from canonical.config import config
from canonical.database.enumcol import EnumCol
+from canonical.launchpad.components.decoratedresultset import (
+ DecoratedResultSet,
+ )
from canonical.launchpad.helpers import (
get_contact_email_addresses,
get_email_template,
)
-from canonical.launchpad.interfaces.lpstorm import IMasterStore
+from canonical.launchpad.interfaces.lpstorm import (
+ IMasterStore,
+ IStore,
+ )
from canonical.launchpad.mail import (
format_address,
simple_sendmail,
@@ -45,14 +51,17 @@
from lp.registry.interfaces.persontransferjob import (
IMembershipNotificationJob,
IMembershipNotificationJobSource,
+ IPersonMergeJob,
+ IPersonMergeJobSource,
IPersonTransferJob,
IPersonTransferJobSource,
)
from lp.registry.interfaces.teammembership import TeamMembershipStatus
from lp.registry.model.person import Person
+from lp.services.database.stormbase import StormBase
+from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
from lp.services.job.runner import BaseRunnableJob
-from lp.services.database.stormbase import StormBase
class PersonTransferJob(StormBase):
@@ -120,17 +129,6 @@
def __init__(self, job):
self.context = job
- def __repr__(self):
- return (
- '<%(job_type)s branch job (%(id)s) for %(minor_person)s '
- 'as part of %(major_person)s. status=%(status)s>' % {
- 'job_type': self.context.job_type.name,
- 'id': self.context.id,
- 'minor_person': self.minor_person.name,
- 'major_person': self.major_person.name,
- 'status': self.job.status,
- })
-
@classmethod
def create(cls, minor_person, major_person, metadata):
"""See `IPersonTransferJob`."""
@@ -321,3 +319,79 @@
self.member_template % replacements, force_wrap=True)
simple_sendmail(from_addr, address, subject, msg)
log.debug('MembershipNotificationJob sent email')
+
+ def __repr__(self):
+ return (
+ "<{self.__class__.__name__} about "
+ "~{self.minor_person.name} in ~{self.major_person.name}; "
+ "status={self.job.status}>").format(self=self)
+
+
+class PersonMergeJob(PersonTransferJobDerived):
+ """A Job that merges one person or team into another."""
+
+ implements(IPersonMergeJob)
+ classProvides(IPersonMergeJobSource)
+
+ class_job_type = PersonTransferJobType.MERGE
+
+ @classmethod
+ def create(cls, from_person, to_person):
+ """See `IPersonMergeJobSource`."""
+ return super(PersonMergeJob, cls).create(
+ minor_person=from_person, major_person=to_person, metadata={})
+
+ @classmethod
+ def find(cls, from_person=None, to_person=None):
+ """See `IPersonMergeJobSource`."""
+ conditions = [
+ PersonTransferJob.job_type == cls.class_job_type,
+ PersonTransferJob.job_id == Job.id,
+ Job._status.is_in(
+ (JobStatus.WAITING, JobStatus.RUNNING))]
+ if from_person is not None:
+ conditions.append(
+ PersonTransferJob.minor_person == from_person)
+ if to_person is not None:
+ conditions.append(
+ PersonTransferJob.major_person == to_person)
+ return DecoratedResultSet(
+ IStore(PersonTransferJob).find(
+ PersonTransferJob, *conditions), cls)
+
+ @property
+ def from_person(self):
+ """See `IPersonMergeJob`."""
+ return self.minor_person
+
+ @property
+ def to_person(self):
+ """See `IPersonMergeJob`."""
+ return self.major_person
+
+ @property
+ def log_name(self):
+ return self.__class__.__name__
+
+ def run(self):
+ """Perform the merge."""
+ from_person_name = self.from_person.name
+ to_person_name = self.to_person.name
+
+ from canonical.launchpad.scripts import log
+ log.debug(
+ "%s is about to merge ~%s into ~%s", self.log_name,
+ from_person_name, to_person_name)
+
+ getUtility(IPersonSet).merge(
+ from_person=self.from_person, to_person=self.to_person)
+
+ log.debug(
+ "%s has merged ~%s into ~%s", self.log_name,
+ from_person_name, to_person_name)
+
+ def __repr__(self):
+ return (
+ "<{self.__class__.__name__} to merge "
+ "~{self.from_person.name} into ~{self.to_person.name}; "
+ "status={self.job.status}>").format(self=self)
=== renamed file 'lib/lp/registry/tests/test_membership_notification_job_creation.py' => 'lib/lp/registry/tests/test_membership_notification_job.py'
--- lib/lp/registry/tests/test_membership_notification_job_creation.py 2010-09-30 21:30:50 +0000
+++ lib/lp/registry/tests/test_membership_notification_job.py 2011-02-23 13:46:27 +0000
@@ -1,7 +1,7 @@
# Copyright 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test that MembershipNotificationJobs are created appropriately."""
+"""Tests of `MembershipNotificationJob`."""
__metaclass__ = type
@@ -20,17 +20,18 @@
from lp.services.job.interfaces.job import JobStatus
from lp.testing import (
login_person,
+ person_logged_in,
TestCaseWithFactory,
)
from lp.testing.sampledata import ADMIN_EMAIL
-class CreateMembershipNotificationJobTest(TestCaseWithFactory):
- """Test that MembershipNotificationJobs are created appropriately."""
+class MembershipNotificationJobTest(TestCaseWithFactory):
+
layer = LaunchpadFunctionalLayer
def setUp(self):
- super(CreateMembershipNotificationJobTest, self).setUp()
+ super(MembershipNotificationJobTest, self).setUp()
self.person = self.factory.makePerson(name='murdock')
self.team = self.factory.makeTeam(name='a-team')
self.job_source = getUtility(IMembershipNotificationJobSource)
@@ -63,3 +64,18 @@
tm.setStatus(
TeamMembershipStatus.ADMIN, admin, silent=True)
self.assertEqual([], list(self.job_source.iterReady()))
+
+ def test_repr(self):
+ # A useful representation is available for MembershipNotificationJob
+ # instances.
+ with person_logged_in(self.team.teamowner):
+ self.team.addMember(self.person, self.team.teamowner)
+ membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
+ self.person, self.team)
+ membership.setStatus(
+ TeamMembershipStatus.ADMIN, self.team.teamowner)
+ [job] = self.job_source.iterReady()
+ self.assertEqual(
+ ("<MembershipNotificationJob about "
+ "~murdock in ~a-team; status=Waiting>"),
+ repr(job))
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2011-02-18 13:26:58 +0000
+++ lib/lp/registry/tests/test_person.py 2011-02-23 13:46:27 +0000
@@ -41,6 +41,7 @@
from lp.bugs.interfaces.bugtask import IllegalRelatedBugTasksParams
from lp.bugs.model.bug import Bug
from lp.bugs.model.bugtask import get_related_bugtasks_search_params
+from lp.bugs.model.structuralsubscription import StructuralSubscription
from lp.registry.errors import (
NameAlreadyTaken,
PrivatePersonLinkageError,
@@ -277,12 +278,30 @@
user.getOwnedOrDrivenPillars()]
self.assertEqual(expected_pillars, received_pillars)
+<<<<<<< TREE
def test_selfgenerated_bugnotifications_none_by_default(self):
# Default for new accounts is to not get any
# self-generated bug notifications by default.
user = self.factory.makePerson()
self.assertFalse(user.selfgenerated_bugnotifications)
+=======
+ def test_no_merge_pending(self):
+ # is_merge_pending returns False when this person is not the "from"
+ # person of an active merge job.
+ person = self.factory.makePerson()
+ self.assertFalse(person.is_merge_pending)
+
+ def test_merge_pending(self):
+ # is_merge_pending returns True when this person is the "from" person
+ # of 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.assertFalse(to_person.is_merge_pending)
+
+>>>>>>> MERGE-SOURCE
class TestPersonStates(TestCaseWithFactory):
@@ -756,6 +775,15 @@
self.assertEqual([u'TO', u'FROM'], descriptions)
self.assertEqual(u'foo-1', recipes[1].name)
+ def test_mergeAsync(self):
+ # mergeAsync() creates a new `PersonMergeJob`.
+ from_person = self.factory.makePerson()
+ to_person = self.factory.makePerson()
+ login_person(from_person)
+ job = self.person_set.mergeAsync(from_person, to_person)
+ self.assertEqual(from_person, job.from_person)
+ self.assertEqual(to_person, job.to_person)
+
class TestPersonSetCreateByOpenId(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
=== added file 'lib/lp/registry/tests/test_person_merge_job.py'
--- lib/lp/registry/tests/test_person_merge_job.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_person_merge_job.py 2011-02-23 13:46:27 +0000
@@ -0,0 +1,99 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests of `PersonMergeJob`."""
+
+__metaclass__ = type
+
+from zope.component import getUtility
+from zope.interface.verify import verifyObject
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.launchpad.scripts import log
+from canonical.testing import DatabaseFunctionalLayer
+from lp.registry.interfaces.persontransferjob import (
+ IPersonMergeJob,
+ IPersonMergeJobSource,
+ )
+from lp.services.job.interfaces.job import JobStatus
+from lp.services.log.logger import BufferLogger
+from lp.testing import TestCaseWithFactory
+
+
+class TestPersonMergeJob(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestPersonMergeJob, self).setUp()
+ self.from_person = self.factory.makePerson(name='void')
+ self.to_person = self.factory.makeTeam(name='gestalt')
+ self.job_source = getUtility(IPersonMergeJobSource)
+ self.job = self.job_source.create(
+ from_person=self.from_person, to_person=self.to_person)
+
+ def test_interface(self):
+ # PersonMergeJob implements IPersonMergeJob.
+ verifyObject(IPersonMergeJob, self.job)
+
+ def test_properties(self):
+ # PersonMergeJobs have a few interesting properties.
+ self.assertEqual(self.from_person, self.job.from_person)
+ self.assertEqual(self.from_person, self.job.minor_person)
+ self.assertEqual(self.to_person, self.job.to_person)
+ self.assertEqual(self.to_person, self.job.major_person)
+ self.assertEqual({}, self.job.metadata)
+
+ def test_enqueue(self):
+ # Newly created jobs are enqueued.
+ self.assertEqual([self.job], list(self.job_source.iterReady()))
+
+ def test_run(self):
+ # When run it merges from_person into to_person. First we need to
+ # reassign from_person's email address over to to_person because
+ # IPersonSet.merge() does not (yet) promise to do that.
+ from_email = self.from_person.preferredemail
+ removeSecurityProxy(from_email).personID = self.to_person.id
+ removeSecurityProxy(from_email).accountID = self.to_person.accountID
+
+ logger = BufferLogger()
+ with log.use(logger):
+ self.job.run()
+
+ self.assertEqual(self.to_person, self.from_person.merged)
+ self.assertEqual(
+ ["DEBUG PersonMergeJob is about to merge ~void into ~gestalt",
+ "DEBUG PersonMergeJob has merged ~void into ~gestalt"],
+ logger.getLogBuffer().splitlines())
+
+ def test_repr(self):
+ # A useful representation is available for PersonMergeJob instances.
+ self.assertEqual(
+ "<PersonMergeJob to merge ~void into ~gestalt; status=Waiting>",
+ repr(self.job))
+
+ def find(self, **kwargs):
+ return list(self.job_source.find(**kwargs))
+
+ def test_find(self):
+ # find() looks for merge jobs.
+ self.assertEqual([self.job], self.find())
+ self.assertEqual(
+ [self.job], self.find(from_person=self.from_person))
+ self.assertEqual(
+ [self.job], self.find(to_person=self.to_person))
+ self.assertEqual(
+ [self.job], self.find(
+ from_person=self.from_person,
+ to_person=self.to_person))
+ self.assertEqual(
+ [], self.find(from_person=self.to_person))
+
+ def test_find_only_pending_or_running(self):
+ # find() only returns jobs that are pending or running.
+ for status in JobStatus.items:
+ removeSecurityProxy(self.job.job)._status = status
+ if status in (JobStatus.WAITING, JobStatus.RUNNING):
+ self.assertEqual([self.job], self.find())
+ else:
+ self.assertEqual([], self.find())