Merge lp:~adeuring/launchpad/bug-675595 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 11973
Proposed branch: lp:~adeuring/launchpad/bug-675595
Merge into: lp:launchpad
Diff against target: 434 lines (+183/-22)
7 files modified
lib/lp/bugs/interfaces/bugtarget.py (+6/-3)
lib/lp/bugs/interfaces/bugtask.py (+11/-7)
lib/lp/bugs/model/bugtarget.py (+4/-4)
lib/lp/bugs/model/bugtask.py (+10/-2)
lib/lp/bugs/tests/test_bugtarget.py (+108/-1)
lib/lp/bugs/tests/test_bugtask_search.py (+37/-2)
lib/lp/registry/model/person.py (+7/-3)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-675595
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+41595@code.launchpad.net

Description of the change

This branch is a first step to fix bug 675595:
BugTaskSet.getAssignedMilestonesFromSearch() can call BugTaskSet._search()
directly and retrieve the milestones with one SQL query instead of two.

It adds an optional parameter "prejoins" to BugTaskSet.search()
and to HasBugs.searchTasks().

BugTaskSet.search() already prejoined a number of tables in order to
reduce the total query count; in certain cases it makes sense to
prejoin other tables too.

tests:

./bin/test -vvt test_bugtarget -t test_bugtask_search

no lint

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py 2010-10-21 16:40:10 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py 2010-11-23 13:40:38 +0000
@@ -82,7 +82,8 @@
82 "omit_duplicates": copy_field(IBugTaskSearch['omit_dupes']),82 "omit_duplicates": copy_field(IBugTaskSearch['omit_dupes']),
83 "omit_targeted": copy_field(IBugTaskSearch['omit_targeted']),83 "omit_targeted": copy_field(IBugTaskSearch['omit_targeted']),
84 "status_upstream": copy_field(IBugTaskSearch['status_upstream']),84 "status_upstream": copy_field(IBugTaskSearch['status_upstream']),
85 "milestone_assignment": copy_field(IBugTaskSearch['milestone_assignment']),85 "milestone_assignment": copy_field(
86 IBugTaskSearch['milestone_assignment']),
86 "milestone": copy_field(IBugTaskSearch['milestone']),87 "milestone": copy_field(IBugTaskSearch['milestone']),
87 "component": copy_field(IBugTaskSearch['component']),88 "component": copy_field(IBugTaskSearch['component']),
88 "nominated_for": Reference(schema=Interface),89 "nominated_for": Reference(schema=Interface),
@@ -232,7 +233,7 @@
232 hardware_owner_is_subscribed_to_bug=False,233 hardware_owner_is_subscribed_to_bug=False,
233 hardware_is_linked_to_bug=False, linked_branches=None,234 hardware_is_linked_to_bug=False, linked_branches=None,
234 structural_subscriber=None, modified_since=None,235 structural_subscriber=None, modified_since=None,
235 created_since=None):236 created_since=None, prejoins=[]):
236 """Search the IBugTasks reported on this entity.237 """Search the IBugTasks reported on this entity.
237238
238 :search_params: a BugTaskSearchParams object239 :search_params: a BugTaskSearchParams object
@@ -337,8 +338,10 @@
337 :istargeted: Is there a fix targeted to this series?338 :istargeted: Is there a fix targeted to this series?
338 :sourcepackage: The sourcepackage to which the fix would be targeted.339 :sourcepackage: The sourcepackage to which the fix would be targeted.
339 :assignee: An IPerson, or None if no assignee.340 :assignee: An IPerson, or None if no assignee.
340 :status: A BugTaskStatus dbschema item, or None, if series is not targeted.341 :status: A BugTaskStatus dbschema item, or None, if series is not
342 targeted.
341 """343 """
344
342 def __init__(self, series, istargeted=False, sourcepackage=None,345 def __init__(self, series, istargeted=False, sourcepackage=None,
343 assignee=None, status=None):346 assignee=None, status=None):
344 self.series = series347 self.series = series
345348
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-11-03 16:50:05 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-11-23 13:40:38 +0000
@@ -667,10 +667,11 @@
667 underlying bug for this bugtask.667 underlying bug for this bugtask.
668668
669 This method was documented as being required here so that669 This method was documented as being required here so that
670 MentorshipOffers could happen on IBugTask. If that was the sole reason670 MentorshipOffers could happen on IBugTask. If that was the sole
671 then this method should be deletable. When we move to context-less bug671 reason then this method should be deletable. When we move to
672 presentation (where the bug is at /bugs/n?task=ubuntu) then we can672 context-less bug presentation (where the bug is at
673 eliminate this if it is no longer useful.673 /bugs/n?task=ubuntu) then we can eliminate this if it is no
674 longer useful.
674 """675 """
675676
676 @mutator_for(milestone)677 @mutator_for(milestone)
@@ -1387,15 +1388,18 @@
1387 Only BugTasks that the user has access to will be returned.1388 Only BugTasks that the user has access to will be returned.
1388 """1389 """
13891390
1390 def search(params, *args):1391 def search(params, *args, **kwargs):
1391 """Search IBugTasks with the given search parameters.1392 """Search IBugTasks with the given search parameters.
13921393
1393 Note: only use this method of BugTaskSet if you want to query1394 Note: only use this method of BugTaskSet if you want to query
1394 tasks across multiple IBugTargets; otherwise, use the1395 tasks across multiple IBugTargets; otherwise, use the
1395 IBugTarget's searchTasks() method.1396 IBugTarget's searchTasks() method.
13961397
1397 :search_params: a BugTaskSearchParams object1398 :param search_params: a BugTaskSearchParams object
1398 :args: any number of BugTaskSearchParams objects1399 :param args: any number of BugTaskSearchParams objects
1400 :param prejoins: (keyword) A sequence of tuples
1401 (table, table_join) which should be pre-joined in addition
1402 to the default prejoins.
13991403
1400 If more than one BugTaskSearchParams is given, return the union of1404 If more than one BugTaskSearchParams is given, return the union of
1401 IBugTasks which match any of them, with the results ordered by the1405 IBugTasks which match any of them, with the results ordered by the
14021406
=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py 2010-10-21 16:40:10 +0000
+++ lib/lp/bugs/model/bugtarget.py 2010-11-23 13:40:38 +0000
@@ -72,6 +72,7 @@
72 All `IHasBugs` implementations should inherit from this class72 All `IHasBugs` implementations should inherit from this class
73 or from `BugTargetBase`.73 or from `BugTargetBase`.
74 """74 """
75
75 def searchTasks(self, search_params, user=None,76 def searchTasks(self, search_params, user=None,
76 order_by=None, search_text=None,77 order_by=None, search_text=None,
77 status=None,78 status=None,
@@ -93,7 +94,7 @@
93 hardware_owner_is_affected_by_bug=False,94 hardware_owner_is_affected_by_bug=False,
94 hardware_owner_is_subscribed_to_bug=False,95 hardware_owner_is_subscribed_to_bug=False,
95 hardware_is_linked_to_bug=False, linked_branches=None,96 hardware_is_linked_to_bug=False, linked_branches=None,
96 modified_since=None, created_since=None):97 modified_since=None, created_since=None, prejoins=[]):
97 """See `IHasBugs`."""98 """See `IHasBugs`."""
98 if status is None:99 if status is None:
99 # If no statuses are supplied, default to the100 # If no statuses are supplied, default to the
@@ -109,9 +110,10 @@
109 del kwargs['self']110 del kwargs['self']
110 del kwargs['user']111 del kwargs['user']
111 del kwargs['search_params']112 del kwargs['search_params']
113 del kwargs['prejoins']
112 search_params = BugTaskSearchParams.fromSearchForm(user, **kwargs)114 search_params = BugTaskSearchParams.fromSearchForm(user, **kwargs)
113 self._customizeSearchParams(search_params)115 self._customizeSearchParams(search_params)
114 return BugTaskSet().search(search_params)116 return BugTaskSet().search(search_params, prejoins=prejoins)
115117
116 def _customizeSearchParams(self, search_params):118 def _customizeSearchParams(self, search_params):
117 """Customize `search_params` for a specific target."""119 """Customize `search_params` for a specific target."""
@@ -328,7 +330,6 @@
328 self.project.recalculateBugHeatCache()330 self.project.recalculateBugHeatCache()
329331
330332
331
332class OfficialBugTagTargetMixin:333class OfficialBugTagTargetMixin:
333 """See `IOfficialBugTagTarget`.334 """See `IOfficialBugTagTarget`.
334335
@@ -441,4 +442,3 @@
441 'IDistribution instance or an IProduct instance.')442 'IDistribution instance or an IProduct instance.')
442443
443 target = property(target, _settarget, doc=target.__doc__)444 target = property(target, _settarget, doc=target.__doc__)
444
445445
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2010-11-18 13:09:22 +0000
+++ lib/lp/bugs/model/bugtask.py 2010-11-23 13:40:38 +0000
@@ -2277,6 +2277,9 @@
2277 :param _noprejoins: Private internal parameter to BugTaskSet which2277 :param _noprejoins: Private internal parameter to BugTaskSet which
2278 disables all use of prejoins : consolidated from code paths that2278 disables all use of prejoins : consolidated from code paths that
2279 claim they were inefficient and unwanted.2279 claim they were inefficient and unwanted.
2280 :param prejoins: A sequence of tuples (table, table_join) which
2281 which should be pre-joined in addition to the default prejoins.
2282 This parameter has no effect if _noprejoins is True.
2280 """2283 """
2281 # Prevent circular import problems.2284 # Prevent circular import problems.
2282 from lp.registry.model.product import Product2285 from lp.registry.model.product import Product
@@ -2286,6 +2289,7 @@
2286 prejoins = []2289 prejoins = []
2287 resultrow = BugTask2290 resultrow = BugTask
2288 else:2291 else:
2292 requested_joins = kwargs.get('prejoins', [])
2289 prejoins = [2293 prejoins = [
2290 (Bug, LeftJoin(Bug, BugTask.bug == Bug.id)),2294 (Bug, LeftJoin(Bug, BugTask.bug == Bug.id)),
2291 (Product, LeftJoin(Product, BugTask.product == Product.id)),2295 (Product, LeftJoin(Product, BugTask.product == Product.id)),
@@ -2293,8 +2297,12 @@
2293 LeftJoin(2297 LeftJoin(
2294 SourcePackageName,2298 SourcePackageName,
2295 BugTask.sourcepackagename == SourcePackageName.id)),2299 BugTask.sourcepackagename == SourcePackageName.id)),
2296 ]2300 ] + requested_joins
2297 resultrow = (BugTask, Bug, Product, SourcePackageName, )2301 resultrow = (BugTask, Bug, Product, SourcePackageName)
2302 additional_result_objects = [
2303 table for table, join in requested_joins
2304 if table not in resultrow]
2305 resultrow = resultrow + tuple(additional_result_objects)
2298 return self._search(resultrow, prejoins, params, *args)2306 return self._search(resultrow, prejoins, params, *args)
22992307
2300 def searchBugIds(self, params):2308 def searchBugIds(self, params):
23012309
=== modified file 'lib/lp/bugs/tests/test_bugtarget.py'
--- lib/lp/bugs/tests/test_bugtarget.py 2010-10-04 19:50:45 +0000
+++ lib/lp/bugs/tests/test_bugtarget.py 2010-11-23 13:40:38 +0000
@@ -15,8 +15,11 @@
15__all__ = []15__all__ = []
1616
17import random17import random
18from testtools.matchers import Equals
18import unittest19import unittest
1920
21from storm.expr import LeftJoin
22from storm.store import Store
20from zope.component import getUtility23from zope.component import getUtility
2124
22from canonical.launchpad.testing.systemdocs import (25from canonical.launchpad.testing.systemdocs import (
@@ -28,15 +31,24 @@
28from canonical.testing.layers import LaunchpadFunctionalLayer31from canonical.testing.layers import LaunchpadFunctionalLayer
29from lp.bugs.interfaces.bug import CreateBugParams32from lp.bugs.interfaces.bug import CreateBugParams
30from lp.bugs.interfaces.bugtask import (33from lp.bugs.interfaces.bugtask import (
34 BugTaskSearchParams,
31 BugTaskStatus,35 BugTaskStatus,
32 IBugTaskSet,36 IBugTaskSet,
33 )37 )
38from lp.bugs.model.bugtask import BugTask
34from lp.registry.interfaces.distribution import (39from lp.registry.interfaces.distribution import (
35 IDistribution,40 IDistribution,
36 IDistributionSet,41 IDistributionSet,
37 )42 )
38from lp.registry.interfaces.product import IProductSet43from lp.registry.interfaces.product import IProductSet
39from lp.registry.interfaces.projectgroup import IProjectGroupSet44from lp.registry.interfaces.projectgroup import IProjectGroupSet
45from lp.registry.model.milestone import Milestone
46from lp.testing import (
47 person_logged_in,
48 StormStatementRecorder,
49 TestCaseWithFactory,
50 )
51from lp.testing.matchers import HasQueryCount
4052
4153
42def bugtarget_filebug(bugtarget, summary, status=None):54def bugtarget_filebug(bugtarget, summary, status=None):
@@ -176,6 +188,101 @@
176 test.globs['question_target'] = ubuntu.getSourcePackage('mozilla-firefox')188 test.globs['question_target'] = ubuntu.getSourcePackage('mozilla-firefox')
177189
178190
191class TestBugTargetSearchTasks(TestCaseWithFactory):
192 """Tests of IHasBugs.searchTasks()."""
193
194 layer = LaunchpadFunctionalLayer
195
196 def setUp(self):
197 super(TestBugTargetSearchTasks, self).setUp()
198 self.bug = self.factory.makeBug()
199 self.target = self.bug.default_bugtask.target
200 self.milestone = self.factory.makeMilestone(product=self.target)
201 with person_logged_in(self.target.owner):
202 self.bug.default_bugtask.transitionToMilestone(
203 self.milestone, self.target.owner)
204 self.store = Store.of(self.bug)
205 self.store.flush()
206 self.store.invalidate()
207
208 def test_preload_other_objects(self):
209 # We can prejoin objects in calls of searchTasks().
210
211 # Without prejoining the table Milestone, accessing the
212 # BugTask property milestone requires an extra query.
213 with StormStatementRecorder() as recorder:
214 params = BugTaskSearchParams(user=None)
215 found_tasks = self.target.searchTasks(params)
216 found_tasks[0].milestone
217 self.assertThat(recorder, HasQueryCount(Equals(2)))
218
219 # When we prejoin Milestone, the milestone of our bugtask is
220 # already loaded during the main search query.
221 self.store.invalidate()
222 with StormStatementRecorder() as recorder:
223 params = BugTaskSearchParams(user=None)
224 prejoins = [(Milestone,
225 LeftJoin(Milestone,
226 BugTask.milestone == Milestone.id))]
227 found_tasks = self.target.searchTasks(params, prejoins=prejoins)
228 found_tasks[0].milestone
229 self.assertThat(recorder, HasQueryCount(Equals(1)))
230
231 def test_preload_other_objects_for_person_search_no_params_passed(self):
232 # We can prejoin objects in calls of Person.searchTasks().
233 owner = self.bug.owner
234 with StormStatementRecorder() as recorder:
235 found_tasks = owner.searchTasks(None, user=None)
236 found_tasks[0].milestone
237 self.assertThat(recorder, HasQueryCount(Equals(2)))
238
239 self.store.invalidate()
240 with StormStatementRecorder() as recorder:
241 prejoins = [(Milestone,
242 LeftJoin(Milestone,
243 BugTask.milestone == Milestone.id))]
244 found_tasks = owner.searchTasks(
245 None, user=None, prejoins=prejoins)
246 found_tasks[0].milestone
247 self.assertThat(recorder, HasQueryCount(Equals(1)))
248
249 def test_preload_other_objects_for_person_search_no_keywords_passed(self):
250 # We can prejoin objects in calls of Person.searchTasks().
251 owner = self.bug.owner
252 params = BugTaskSearchParams(user=None, owner=owner)
253 with StormStatementRecorder() as recorder:
254 found_tasks = owner.searchTasks(params)
255 found_tasks[0].milestone
256 self.assertThat(recorder, HasQueryCount(Equals(2)))
257
258 self.store.invalidate()
259 with StormStatementRecorder() as recorder:
260 prejoins = [(Milestone,
261 LeftJoin(Milestone,
262 BugTask.milestone == Milestone.id))]
263 found_tasks = owner.searchTasks(params, prejoins=prejoins)
264 found_tasks[0].milestone
265 self.assertThat(recorder, HasQueryCount(Equals(1)))
266
267 def test_preload_other_objects_for_person_search_keywords_passed(self):
268 # We can prejoin objects in calls of Person.searchTasks().
269 owner = self.bug.owner
270 params = BugTaskSearchParams(user=None, owner=owner)
271 with StormStatementRecorder() as recorder:
272 found_tasks = owner.searchTasks(params, order_by=BugTask.id)
273 found_tasks[0].milestone
274 self.assertThat(recorder, HasQueryCount(Equals(2)))
275
276 self.store.invalidate()
277 with StormStatementRecorder() as recorder:
278 prejoins = [(Milestone,
279 LeftJoin(Milestone,
280 BugTask.milestone == Milestone.id))]
281 found_tasks = owner.searchTasks(params, prejoins=prejoins)
282 found_tasks[0].milestone
283 self.assertThat(recorder, HasQueryCount(Equals(1)))
284
285
179def test_suite():286def test_suite():
180 """Return the `IBugTarget` TestSuite."""287 """Return the `IBugTarget` TestSuite."""
181 suite = unittest.TestSuite()288 suite = unittest.TestSuite()
@@ -195,7 +302,6 @@
195 layer=LaunchpadFunctionalLayer)302 layer=LaunchpadFunctionalLayer)
196 suite.addTest(test)303 suite.addTest(test)
197304
198
199 setUpMethods.remove(sourcePackageForQuestionSetUp)305 setUpMethods.remove(sourcePackageForQuestionSetUp)
200 setUpMethods.append(sourcePackageSetUp)306 setUpMethods.append(sourcePackageSetUp)
201 setUpMethods.append(projectSetUp)307 setUpMethods.append(projectSetUp)
@@ -206,4 +312,5 @@
206 layer=LaunchpadFunctionalLayer)312 layer=LaunchpadFunctionalLayer)
207 suite.addTest(test)313 suite.addTest(test)
208314
315 suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
209 return suite316 return suite
210317
=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
--- lib/lp/bugs/tests/test_bugtask_search.py 2010-11-12 17:42:43 +0000
+++ lib/lp/bugs/tests/test_bugtask_search.py 2010-11-23 13:40:38 +0000
@@ -10,10 +10,14 @@
10from new import classobj10from new import classobj
11import pytz11import pytz
12import sys12import sys
13from testtools.matchers import Equals
13import unittest14import unittest
1415
15from zope.component import getUtility16from zope.component import getUtility
1617
18from storm.expr import Join
19from storm.store import Store
20
17from canonical.launchpad.searchbuilder import (21from canonical.launchpad.searchbuilder import (
18 all,22 all,
19 any,23 any,
@@ -31,6 +35,7 @@
31 BugTaskStatus,35 BugTaskStatus,
32 IBugTaskSet,36 IBugTaskSet,
33 )37 )
38from lp.bugs.model.bugtask import BugTask
34from lp.registry.interfaces.distribution import IDistribution39from lp.registry.interfaces.distribution import IDistribution
35from lp.registry.interfaces.distributionsourcepackage import (40from lp.registry.interfaces.distributionsourcepackage import (
36 IDistributionSourcePackage,41 IDistributionSourcePackage,
@@ -39,10 +44,13 @@
39from lp.registry.interfaces.person import IPersonSet44from lp.registry.interfaces.person import IPersonSet
40from lp.registry.interfaces.product import IProduct45from lp.registry.interfaces.product import IProduct
41from lp.registry.interfaces.sourcepackage import ISourcePackage46from lp.registry.interfaces.sourcepackage import ISourcePackage
47from lp.registry.model.person import Person
42from lp.testing import (48from lp.testing import (
43 person_logged_in,49 person_logged_in,
50 StormStatementRecorder,
44 TestCaseWithFactory,51 TestCaseWithFactory,
45 )52 )
53from lp.testing.matchers import HasQueryCount
4654
4755
48class SearchTestBase:56class SearchTestBase:
@@ -907,13 +915,40 @@
907 def setUp(self):915 def setUp(self):
908 super(PreloadBugtaskTargets, self).setUp()916 super(PreloadBugtaskTargets, self).setUp()
909917
910 def runSearch(self, params, *args):918 def runSearch(self, params, *args, **kw):
911 """Run BugTaskSet.search() and preload bugtask target objects."""919 """Run BugTaskSet.search() and preload bugtask target objects."""
912 return list(self.bugtask_set.search(params, *args, _noprejoins=False))920 return list(self.bugtask_set.search(
921 params, *args, _noprejoins=False, **kw))
913922
914 def resultValuesForBugtasks(self, expected_bugtasks):923 def resultValuesForBugtasks(self, expected_bugtasks):
915 return expected_bugtasks924 return expected_bugtasks
916925
926 def test_preload_additional_objects(self):
927 # It is possible to join additional tables in the search query
928 # in order to load related Storm objects during the query.
929 store = Store.of(self.bugtasks[0])
930 store.invalidate()
931
932 # If we do not prejoin the owner, two queries a run
933 # in order to retrieve the owner of the bugtask.
934 with StormStatementRecorder() as recorder:
935 params = self.getBugTaskSearchParams(user=None)
936 found_tasks = self.runSearch(params)
937 found_tasks[0].owner
938 self.assertTrue(len(recorder.statements) > 1)
939
940 # If we join the table person on bugtask.owner == person.id
941 # the owner object is loaded in the query that retrieves the
942 # bugtasks.
943 store.invalidate()
944 with StormStatementRecorder() as recorder:
945 params = self.getBugTaskSearchParams(user=None)
946 found_tasks = self.runSearch(
947 params,
948 prejoins=[(Person, Join(Person, BugTask.owner == Person.id))])
949 found_tasks[0].owner
950 self.assertThat(recorder, HasQueryCount(Equals(1)))
951
917952
918class NoPreloadBugtaskTargets(MultipleParams):953class NoPreloadBugtaskTargets(MultipleParams):
919 """Do not preload bug targets during a BugTaskSet.search() query."""954 """Do not preload bug targets during a BugTaskSet.search() query."""
920955
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-11-18 12:05:34 +0000
+++ lib/lp/registry/model/person.py 2010-11-23 13:40:38 +0000
@@ -882,6 +882,7 @@
882882
883 def searchTasks(self, search_params, *args, **kwargs):883 def searchTasks(self, search_params, *args, **kwargs):
884 """See `IHasBugs`."""884 """See `IHasBugs`."""
885 prejoins = kwargs.pop('prejoins', [])
885 if search_params is None and len(args) == 0:886 if search_params is None and len(args) == 0:
886 # this method is called via webapi directly887 # this method is called via webapi directly
887 # calling this method on a Person object directly via the888 # calling this method on a Person object directly via the
@@ -896,15 +897,18 @@
896 # method, see docstring of897 # method, see docstring of
897 # `lazr.restful.declarations.webservice_error()`898 # `lazr.restful.declarations.webservice_error()`
898 raise e899 raise e
899 return getUtility(IBugTaskSet).search(*search_params)900 return getUtility(IBugTaskSet).search(
901 *search_params, prejoins=prejoins)
900 if len(kwargs) > 0:902 if len(kwargs) > 0:
901 # if keyword arguments are supplied, use the deault903 # if keyword arguments are supplied, use the deault
902 # implementation in HasBugsBase.904 # implementation in HasBugsBase.
903 return HasBugsBase.searchTasks(self, search_params, **kwargs)905 return HasBugsBase.searchTasks(
906 self, search_params, prejoins=prejoins, **kwargs)
904 else:907 else:
905 # Otherwise pass all positional arguments to the908 # Otherwise pass all positional arguments to the
906 # implementation in BugTaskSet.909 # implementation in BugTaskSet.
907 return getUtility(IBugTaskSet).search(search_params, *args)910 return getUtility(IBugTaskSet).search(
911 search_params, *args, prejoins=prejoins)
908912
909 def getProjectsAndCategoriesContributedTo(self, limit=5):913 def getProjectsAndCategoriesContributedTo(self, limit=5):
910 """See `IPerson`."""914 """See `IPerson`."""