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

Proposed by Abel Deuring
Status: Merged
Approved by: Māris Fogels
Approved revision: no longer in the source branch.
Merged at revision: 11973
Proposed branch: lp:~adeuring/launchpad/bug-675595-2
Merge into: lp:launchpad
Prerequisite: lp:~adeuring/launchpad/bug-675595
Diff against target: 565 lines (+200/-119)
9 files modified
lib/canonical/launchpad/systemhomes.py (+7/-4)
lib/lp/bugs/browser/bugtask.py (+2/-2)
lib/lp/bugs/browser/tests/test_buglisting.py (+26/-0)
lib/lp/bugs/doc/milestones-from-bugtask-search.txt (+0/-54)
lib/lp/bugs/interfaces/bugtask.py (+0/-7)
lib/lp/bugs/interfaces/malone.py (+1/-1)
lib/lp/bugs/model/bugtask.py (+0/-38)
lib/lp/registry/browser/person.py (+26/-12)
lib/lp/registry/browser/tests/test_person_view.py (+138/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-675595-2
Reviewer Review Type Date Requested Status
Māris Fogels (community) Approve
Review via email: mp+41604@code.launchpad.net

Commit message

[r=mars][ui=none] [r=mars, gmb][ui=none][bug=675595] new optional parameter prejoins for BugTaskSearchListingView.searchUnbatched() and for searchUnbatched() of several person related view classes; BugTaskSet.getAssignedMilestonesFromSearch() removed; RelevantMilestonesMixin.getMilestoneWidgetValues() retrieves Milestone instances directly via searchUnbatched(); allow to prejoin arbitrary tables in the SQL query in BugTaskSet.search()

Description of the change

This branch fixes bug 675595: "BugTaskSet.getAssignedMilestonesFromSearch()
can call BugTaskSet._search() directly and retrieve the milestones with
one SQL query instead of two"

Working on the branch, I realized that the method
BugTaskSet.getAssignedMilestonesFromSearch() is in fact unnecessary:

The only callsite was RelevantMilestonesMixin.getMilestoneWidgetValues();
since we can now retrieve milestones by pre-joining the table in
BugTaskSet.searchTasks(), I simply removed getAssignedMilestonesFromSearch().

getMilestoneWidgetValues() calls searchTasks() via the method
BugTaskSearchListingView.searchUnbatched(), so I added the parameter
"prejoins" to this method.

tests:

./bin/test -vvt test_buglisting -t test_person_view

To post a comment you must log in.
Revision history for this message
Māris Fogels (mars) wrote :

Hi Abel,

This code looks good! r=mars, with two comments:

First, on line 72 of the diff, I think you should include a comment that says "If the table prejoin failed, then this will issue two additional SQL queries".

Second, the SQL prejoins in lp.registry.browser.person look odd - it looks like ORM code that should live in the model has been placed in the view. Is there a better place to put these operations?

I recognize that a refactoring of the TaskSearchListingView is not trivial, so please consider the refactoring as optional. If you do want to try the refactoring, it should be in a follow-up branch.

With the above taken into consideration, I think you can land this.

Maris

review: Approve
Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Māris,

thanks for the review!

On 23.11.2010 16:16, Māris Fogels wrote:
> Review: Approve
> Hi Abel,
>
> This code looks good! r=mars, with two comments:
>
> First, on line 72 of the diff, I think you should include a comment that says "If the table prejoin failed, then this will issue two additional SQL queries".

Good idea, added.

>
> Second, the SQL prejoins in lp.registry.browser.person look odd - it looks like ORM code that should live in the model has been placed in the view. Is there a better place to put these operations?

I am not if it is worth the effort: Basically, we can now pre-join more
or less arbitrary tables in calls of BugTaskSet.searchTasks(). This
method is used quite frequently, and for each callsite we might need to
pre-join other tables. There might even be conflicts: We can at present
join the same table only once, and for one callsite it might be
reasonable to pre-join for exmaple Person in order to retrieve a bug
reporter, while another callsite would benefit from pre-joining a
bugtask assignee.

So I think that the callsites of searchTasks() should decide what to
pre-join -- and these callsite live quite often in browser code. But
perhaps I am too much focused on the way how I implemented the branch
and I'm completely missing some better way to tell searchTasks() what
pre-join...

Abel

>
> I recognize that a refactoring of the TaskSearchListingView is not trivial, so please consider the refactoring as optional. If you do want to try the refactoring, it should be in a follow-up branch.
>
> With the above taken into consideration, I think you can land this.
>
> Maris

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/systemhomes.py'
2--- lib/canonical/launchpad/systemhomes.py 2010-11-08 13:11:30 +0000
3+++ lib/canonical/launchpad/systemhomes.py 2010-11-24 10:13:32 +0000
4@@ -127,9 +127,10 @@
5 def __init__(self):
6 self.title = 'Malone: the Launchpad bug tracker'
7
8- def searchTasks(self, search_params):
9+ def searchTasks(self, search_params, prejoins=[]):
10 """See `IMaloneApplication`."""
11- return getUtility(IBugTaskSet).search(search_params)
12+ return getUtility(IBugTaskSet).search(
13+ search_params, prejoins=prejoins)
14
15 def createBug(self, owner, title, description, target,
16 security_related=False, private=False, tags=None):
17@@ -235,8 +236,10 @@
18 """See `IRosettaApplication`."""
19 projects = getUtility(ITranslationsOverview)
20 for project in projects.getMostTranslatedPillars():
21- yield { 'pillar' : project['pillar'],
22- 'font_size' : project['weight'] * 10 }
23+ yield {
24+ 'pillar': project['pillar'],
25+ 'font_size': project['weight'] * 10,
26+ }
27
28 def translatable_distroseriess(self):
29 """See `IRosettaApplication`."""
30
31=== modified file 'lib/lp/bugs/browser/bugtask.py'
32--- lib/lp/bugs/browser/bugtask.py 2010-11-24 08:05:42 +0000
33+++ lib/lp/bugs/browser/bugtask.py 2010-11-24 10:13:32 +0000
34@@ -2679,7 +2679,7 @@
35 return self._getBatchNavigator(unbatchedTasks)
36
37 def searchUnbatched(self, searchtext=None, context=None,
38- extra_params=None):
39+ extra_params=None, prejoins=[]):
40 """Return a `SelectResults` object for the GET search criteria.
41
42 :param searchtext: Text that must occur in the bug report. If
43@@ -2697,7 +2697,7 @@
44 search_params = self.buildSearchParams(
45 searchtext=searchtext, extra_params=extra_params)
46 search_params.user = self.user
47- tasks = context.searchTasks(search_params)
48+ tasks = context.searchTasks(search_params, prejoins=prejoins)
49 return tasks
50
51 def getWidgetValues(
52
53=== modified file 'lib/lp/bugs/browser/tests/test_buglisting.py'
54--- lib/lp/bugs/browser/tests/test_buglisting.py 2010-11-21 17:36:32 +0000
55+++ lib/lp/bugs/browser/tests/test_buglisting.py 2010-11-24 10:13:32 +0000
56@@ -3,6 +3,9 @@
57
58 __metaclass__ = type
59
60+from storm.expr import LeftJoin
61+from storm.store import Store
62+from testtools.matchers import Equals
63 from zope.component import getUtility
64
65 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
66@@ -13,12 +16,17 @@
67 )
68 from canonical.launchpad.webapp.publisher import canonical_url
69 from canonical.testing.layers import DatabaseFunctionalLayer
70+from lp.bugs.model.bugtask import BugTask
71+from lp.registry.model.person import Person
72 from lp.testing import (
73 BrowserTestCase,
74 login_person,
75 person_logged_in,
76+ StormStatementRecorder,
77 TestCaseWithFactory,
78 )
79+
80+from lp.testing.matchers import HasQueryCount
81 from lp.testing.views import create_initialized_view
82
83
84@@ -130,6 +138,24 @@
85 find_tag_by_id(browser.contents, 'portlet-tags'),
86 "portlet-tags should not be shown.")
87
88+ def test_searchUnbatched_can_preload_objects(self):
89+ # BugTaskSearchListingView.searchUnbatched() can optionally
90+ # preload objects while retureving the bugtasks.
91+ product = self.factory.makeProduct()
92+ bugtask_1 = self.factory.makeBug(product=product).default_bugtask
93+ bugtask_2 = self.factory.makeBug(product=product).default_bugtask
94+ view = create_initialized_view(product, '+bugs')
95+ Store.of(product).invalidate()
96+ with StormStatementRecorder() as recorder:
97+ prejoins=[(Person, LeftJoin(Person, BugTask.owner==Person.id))]
98+ bugtasks = list(view.searchUnbatched(prejoins=prejoins))
99+ self.assertEqual(
100+ [bugtask_1, bugtask_2], bugtasks)
101+ # If the table prejoin failed, then this will issue two
102+ # additional SQL queries
103+ [bugtask.owner for bugtask in bugtasks]
104+ self.assertThat(recorder, HasQueryCount(Equals(1)))
105+
106
107 class BugTargetTestCase(TestCaseWithFactory):
108 """Test helpers for setting up `IBugTarget` tests."""
109
110=== removed file 'lib/lp/bugs/doc/milestones-from-bugtask-search.txt'
111--- lib/lp/bugs/doc/milestones-from-bugtask-search.txt 2010-09-14 15:32:53 +0000
112+++ lib/lp/bugs/doc/milestones-from-bugtask-search.txt 1970-01-01 00:00:00 +0000
113@@ -1,54 +0,0 @@
114-= Discover the assigned milestones from a BugTask search =
115-
116-The IBugTaskSet method getAssignedMilestonesFromSearch() accepts a
117-result set that yields BugTasks. It attempt to efficiently find all
118-the distinct milestones assigned to those BugTasks. Typically this
119-should be called with the results from calling IHasBugs.searchTasks()
120-or IBugTaskSet.search().
121-
122- >>> from lp.bugs.interfaces.bugtask import (
123- ... BugTaskSearchParams, IBugTaskSet)
124-
125- >>> person = factory.makePerson()
126- >>> login(person.preferredemail.email)
127-
128- >>> milestone = factory.makeMilestone()
129-
130- >>> bugtask = factory.makeBugTask(target=milestone.product)
131- >>> bugtask.milestone = milestone
132- >>> bugtask.subscribe(person, person)
133- <lp.bugs.model.bugsubscription.BugSubscription ...>
134- >>> transaction.commit()
135-
136-The results of a search for all bug tasks related to a person can be
137-passed to getAssignedMilestonesFromSearch() to discover all the
138-milestones used.
139-
140- >>> bugtask_set = getUtility(IBugTaskSet)
141-
142- >>> bugtasks = bugtask_set.search(
143- ... BugTaskSearchParams(person, assignee=person),
144- ... BugTaskSearchParams(person, subscriber=person),
145- ... BugTaskSearchParams(person, owner=person, bug_reporter=person),
146- ... BugTaskSearchParams(person, bug_commenter=person))
147-
148- >>> milestones = bugtask_set.getAssignedMilestonesFromSearch(
149- ... bugtasks)
150-
151- >>> milestones
152- [<Milestone ...>]
153- >>> len(milestones)
154- 1
155- >>> milestone in milestones
156- True
157-
158-Because get_assigned_milestones_from_bugtasks() attempts to be
159-efficient, by customising the result set passed in, it rejects
160-attempts to pass in lists of bug tasks.
161-
162- >>> milestones = bugtask_set.getAssignedMilestonesFromSearch(
163- ... list(bugtasks))
164- Traceback (most recent call last):
165- ...
166- AssertionError: search_results must provide
167- IResultSet or ISQLObjectResultSet
168
169=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
170--- lib/lp/bugs/interfaces/bugtask.py 2010-11-24 10:13:31 +0000
171+++ lib/lp/bugs/interfaces/bugtask.py 2010-11-24 10:13:32 +0000
172@@ -1414,13 +1414,6 @@
173 :param params: the BugTaskSearchParams to search on.
174 """
175
176- def getAssignedMilestonesFromSearch(search_results):
177- """Returns distinct milestones for the given tasks.
178-
179- :param search_results: A result set yielding BugTask objects,
180- typically the result of calling `BugTaskSet.search()`.
181- """
182-
183 def getStatusCountsForProductSeries(user, product_series):
184 """Returns status counts for a product series' bugs.
185
186
187=== modified file 'lib/lp/bugs/interfaces/malone.py'
188--- lib/lp/bugs/interfaces/malone.py 2010-08-20 20:31:18 +0000
189+++ lib/lp/bugs/interfaces/malone.py 2010-11-24 10:13:32 +0000
190@@ -32,7 +32,7 @@
191 """Application root for malone."""
192 export_as_webservice_collection(IBug)
193
194- def searchTasks(search_params):
195+ def searchTasks(search_params, prejoins=[]):
196 """Search IBugTasks with the given search parameters."""
197
198 bug_count = Attribute("The number of bugs recorded in Launchpad")
199
200=== modified file 'lib/lp/bugs/model/bugtask.py'
201--- lib/lp/bugs/model/bugtask.py 2010-11-24 10:13:31 +0000
202+++ lib/lp/bugs/model/bugtask.py 2010-11-24 10:13:32 +0000
203@@ -2309,44 +2309,6 @@
204 """See `IBugTaskSet`."""
205 return self._search(BugTask.bugID, [], params).result_set
206
207- def getAssignedMilestonesFromSearch(self, search_results):
208- """See `IBugTaskSet`."""
209- # XXX: Gavin Panella 2009-03-05 bug=338184: There is currently
210- # no clean way to get the underlying Storm ResultSet from an
211- # SQLObjectResultSet, so we must remove the security proxy for
212- # a moment.
213- if ISQLObjectResultSet.providedBy(search_results):
214- search_results = removeSecurityProxy(search_results)._result_set
215- # Check that we have a Storm result set before we start doing
216- # things with it.
217- assert IResultSet.providedBy(search_results), (
218- "search_results must provide IResultSet or ISQLObjectResultSet")
219- # Remove ordering and make distinct.
220- search_results = search_results.order_by().config(distinct=True)
221- # Get milestone IDs.
222- milestone_ids = [
223- milestone_id for milestone_id in (
224- search_results.values(BugTask.milestoneID))
225- if milestone_id is not None]
226- # Query for milestones.
227- if len(milestone_ids) == 0:
228- return []
229- else:
230- # Import here because of cyclic references.
231- from lp.registry.model.milestone import (
232- Milestone, milestone_sort_key)
233- # We need the store that was used, we have no objects to key off
234- # of other than the search result, and Store.of() doesn't
235- # currently work on result sets. Additionally it may be a
236- # DecoratedResultSet.
237- if zope_isinstance(search_results, DecoratedResultSet):
238- store = removeSecurityProxy(search_results).result_set._store
239- else:
240- store = search_results._store
241- milestones = store.find(
242- Milestone, Milestone.id.is_in(milestone_ids))
243- return sorted(milestones, key=milestone_sort_key, reverse=True)
244-
245 def createTask(self, bug, owner, product=None, productseries=None,
246 distribution=None, distroseries=None,
247 sourcepackagename=None,
248
249=== modified file 'lib/lp/registry/browser/person.py'
250--- lib/lp/registry/browser/person.py 2010-11-24 08:05:42 +0000
251+++ lib/lp/registry/browser/person.py 2010-11-24 10:13:32 +0000
252@@ -105,6 +105,7 @@
253 URI,
254 )
255 import pytz
256+from storm.expr import Join
257 from storm.zope.interfaces import IResultSet
258 from z3c.ptcompat import ViewPageTemplateFile
259 from zope.app.form.browser import (
260@@ -242,6 +243,7 @@
261 IBugTaskSet,
262 UNRESOLVED_BUGTASK_STATUSES,
263 )
264+from lp.bugs.model.bugtask import BugTask
265 from lp.buildmaster.enums import BuildStatus
266 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
267 from lp.code.errors import InvalidNamespace
268@@ -304,6 +306,10 @@
269 IWikiName,
270 IWikiNameSet,
271 )
272+from lp.registry.model.milestone import (
273+ Milestone,
274+ milestone_sort_key,
275+ )
276 from lp.services.fields import LocationField
277 from lp.services.geoip.interfaces import IRequestPreferredLanguages
278 from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
279@@ -2133,8 +2139,12 @@
280
281 def getMilestoneWidgetValues(self):
282 """Return data used to render the milestone checkboxes."""
283- milestones = getUtility(IBugTaskSet).getAssignedMilestonesFromSearch(
284- self.searchUnbatched())
285+ prejoins = [
286+ (Milestone, Join(Milestone, BugTask.milestone == Milestone.id))]
287+ milestones = [
288+ bugtask.milestone
289+ for bugtask in self.searchUnbatched(prejoins=prejoins)]
290+ milestones = sorted(milestones, key=milestone_sort_key, reverse=True)
291 return [
292 dict(title=milestone.title, value=milestone.id, checked=False)
293 for milestone in milestones]
294@@ -2150,7 +2160,7 @@
295 page_title = 'Related bugs'
296
297 def searchUnbatched(self, searchtext=None, context=None,
298- extra_params=None):
299+ extra_params=None, prejoins=[]):
300 """Return the open bugs related to a person.
301
302 :param extra_params: A dict that provides search params added to
303@@ -2181,7 +2191,7 @@
304
305 return context.searchTasks(
306 assignee_params, subscriber_params, owner_params,
307- commenter_params)
308+ commenter_params, prejoins=prejoins)
309
310 def getSearchPageHeading(self):
311 return "Bugs related to %s" % self.context.displayname
312@@ -2210,7 +2220,7 @@
313 page_title = 'Assigned bugs'
314
315 def searchUnbatched(self, searchtext=None, context=None,
316- extra_params=None):
317+ extra_params=None, prejoins=[]):
318 """Return the open bugs assigned to a person."""
319 if context is None:
320 context = self.context
321@@ -2222,7 +2232,8 @@
322 extra_params['assignee'] = context
323
324 sup = super(PersonAssignedBugTaskSearchListingView, self)
325- return sup.searchUnbatched(searchtext, context, extra_params)
326+ return sup.searchUnbatched(
327+ searchtext, context, extra_params, prejoins)
328
329 def shouldShowAssigneeWidget(self):
330 """Should the assignee widget be shown on the advanced search page?"""
331@@ -2267,7 +2278,7 @@
332 page_title = 'Commented bugs'
333
334 def searchUnbatched(self, searchtext=None, context=None,
335- extra_params=None):
336+ extra_params=None, prejoins=[]):
337 """Return the open bugs commented on by a person."""
338 if context is None:
339 context = self.context
340@@ -2279,7 +2290,8 @@
341 extra_params['bug_commenter'] = context
342
343 sup = super(PersonCommentedBugTaskSearchListingView, self)
344- return sup.searchUnbatched(searchtext, context, extra_params)
345+ return sup.searchUnbatched(
346+ searchtext, context, extra_params, prejoins)
347
348 def getSearchPageHeading(self):
349 """The header for the search page."""
350@@ -2312,7 +2324,7 @@
351 page_title = 'Reported bugs'
352
353 def searchUnbatched(self, searchtext=None, context=None,
354- extra_params=None):
355+ extra_params=None, prejoins=[]):
356 """Return the bugs reported by a person."""
357 if context is None:
358 context = self.context
359@@ -2327,7 +2339,8 @@
360 extra_params['bug_reporter'] = context
361
362 sup = super(PersonReportedBugTaskSearchListingView, self)
363- return sup.searchUnbatched(searchtext, context, extra_params)
364+ return sup.searchUnbatched(
365+ searchtext, context, extra_params, prejoins)
366
367 def getSearchPageHeading(self):
368 """The header for the search page."""
369@@ -2368,7 +2381,7 @@
370 page_title = 'Subscribed bugs'
371
372 def searchUnbatched(self, searchtext=None, context=None,
373- extra_params=None):
374+ extra_params=None, prejoins=[]):
375 """Return the bugs subscribed to by a person."""
376 if context is None:
377 context = self.context
378@@ -2380,7 +2393,8 @@
379 extra_params['subscriber'] = context
380
381 sup = super(PersonSubscribedBugTaskSearchListingView, self)
382- return sup.searchUnbatched(searchtext, context, extra_params)
383+ return sup.searchUnbatched(
384+ searchtext, context, extra_params, prejoins)
385
386 def getSearchPageHeading(self):
387 """The header for the search page."""
388
389=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
390--- lib/lp/registry/browser/tests/test_person_view.py 2010-10-06 08:16:37 +0000
391+++ lib/lp/registry/browser/tests/test_person_view.py 2010-11-24 10:13:32 +0000
392@@ -4,8 +4,10 @@
393 __metaclass__ = type
394
395 import transaction
396+from storm.expr import LeftJoin
397+from storm.store import Store
398+from testtools.matchers import Equals
399 from zope.component import getUtility
400-from zope.security.proxy import removeSecurityProxy
401
402 from canonical.config import config
403 from canonical.launchpad.ftests import (
404@@ -25,6 +27,7 @@
405 )
406
407 from lp.app.errors import NotFoundError
408+from lp.bugs.model.bugtask import BugTask
409 from lp.buildmaster.enums import BuildStatus
410 from lp.registry.browser.person import (
411 PersonEditView,
412@@ -43,15 +46,20 @@
413 )
414
415 from lp.registry.model.karma import KarmaCategory
416+from lp.registry.model.milestone import milestone_sort_key
417 from lp.soyuz.enums import (
418 ArchiveStatus,
419 PackagePublishingStatus,
420 )
421+from lp.registry.model.person import Person
422 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
423 from lp.testing import (
424 login_person,
425+ person_logged_in,
426+ StormStatementRecorder,
427 TestCaseWithFactory,
428 )
429+from lp.testing.matchers import HasQueryCount
430 from lp.testing.views import (
431 create_initialized_view,
432 create_view,
433@@ -809,3 +817,132 @@
434 login_person(self.person)
435 view = create_initialized_view(self.team, '+subscriptions')
436 self.assertTrue(view.canUnsubscribeFromBugTasks())
437+
438+
439+class BugTaskViewsTestBase:
440+ """A base class for bugtask search related tests."""
441+
442+ layer = DatabaseFunctionalLayer
443+
444+ def setUp(self):
445+ super(BugTaskViewsTestBase, self).setUp()
446+ self.person = self.factory.makePerson()
447+ with person_logged_in(self.person):
448+ self.subscribed_bug = self.factory.makeBug()
449+ self.subscribed_bug.subscribe(
450+ self.person, subscribed_by=self.person)
451+ self.assigned_bug = self.factory.makeBug()
452+ self.assigned_bug.default_bugtask.transitionToAssignee(
453+ self.person)
454+ self.owned_bug = self.factory.makeBug(owner=self.person)
455+ self.commented_bug = self.factory.makeBug()
456+ self.commented_bug.newMessage(owner=self.person)
457+
458+ for bug in (self.subscribed_bug, self.assigned_bug, self.owned_bug,
459+ self.commented_bug):
460+ with person_logged_in(bug.default_bugtask.product.owner):
461+ milestone = self.factory.makeMilestone(
462+ product=bug.default_bugtask.product)
463+ bug.default_bugtask.transitionToMilestone(
464+ milestone, bug.default_bugtask.product.owner)
465+
466+ def test_searchUnbatched(self):
467+ view = create_initialized_view(self.person, self.view_name)
468+ self.assertEqual(
469+ self.expected_for_search_unbatched, list(view.searchUnbatched()))
470+
471+ def test_searchUnbatched_with_prejoins(self):
472+ view = create_initialized_view(self.person, self.view_name)
473+ Store.of(self.subscribed_bug).invalidate()
474+ with StormStatementRecorder() as recorder:
475+ prejoins=[(Person, LeftJoin(Person, BugTask.owner==Person.id))]
476+ bugtasks = view.searchUnbatched(prejoins=prejoins)
477+ [bugtask.owner for bugtask in bugtasks]
478+ self.assertThat(recorder, HasQueryCount(Equals(1)))
479+
480+ def test_getMilestoneWidgetValues(self):
481+ view = create_initialized_view(self.person, self.view_name)
482+ milestones = [
483+ bugtask.milestone
484+ for bugtask in self.expected_for_search_unbatched]
485+ milestones = sorted(milestones, key=milestone_sort_key, reverse=True)
486+ expected = [
487+ {
488+ 'title': milestone.title,
489+ 'value': milestone.id,
490+ 'checked': False,
491+ }
492+ for milestone in milestones]
493+ Store.of(milestones[0]).invalidate()
494+ with StormStatementRecorder() as recorder:
495+ self.assertEqual(expected, view.getMilestoneWidgetValues())
496+ self.assertThat(recorder, HasQueryCount(Equals(1)))
497+
498+
499+class TestPersonRelatedBugTaskSearchListingView(
500+ BugTaskViewsTestBase, TestCaseWithFactory):
501+ """Tests for PersonRelatedBugTaskSearchListingView."""
502+
503+ view_name = '+bugs'
504+
505+ def setUp(self):
506+ super(TestPersonRelatedBugTaskSearchListingView, self).setUp()
507+ self.expected_for_search_unbatched = [
508+ self.subscribed_bug.default_bugtask,
509+ self.assigned_bug.default_bugtask,
510+ self.owned_bug.default_bugtask,
511+ self.commented_bug.default_bugtask,
512+ ]
513+
514+
515+class TestPersonAssignedBugTaskSearchListingView(
516+ BugTaskViewsTestBase, TestCaseWithFactory):
517+ """Tests for PersonAssignedBugTaskSearchListingView."""
518+
519+ view_name = '+assignedbugs'
520+
521+ def setUp(self):
522+ super(TestPersonAssignedBugTaskSearchListingView, self).setUp()
523+ self.expected_for_search_unbatched = [
524+ self.assigned_bug.default_bugtask,
525+ ]
526+
527+
528+class TestPersonCommentedBugTaskSearchListingView(
529+ BugTaskViewsTestBase, TestCaseWithFactory):
530+ """Tests for PersonAssignedBugTaskSearchListingView."""
531+
532+ view_name = '+commentedbugs'
533+
534+ def setUp(self):
535+ super(TestPersonCommentedBugTaskSearchListingView, self).setUp()
536+ self.expected_for_search_unbatched = [
537+ self.commented_bug.default_bugtask,
538+ ]
539+
540+
541+class TestPersonReportedBugTaskSearchListingView(
542+ BugTaskViewsTestBase, TestCaseWithFactory):
543+ """Tests for PersonAssignedBugTaskSearchListingView."""
544+
545+ view_name = '+reportedbugs'
546+
547+ def setUp(self):
548+ super(TestPersonReportedBugTaskSearchListingView, self).setUp()
549+ self.expected_for_search_unbatched = [
550+ self.owned_bug.default_bugtask,
551+ ]
552+
553+
554+class TestPersonSubscribedBugTaskSearchListingView(
555+ BugTaskViewsTestBase, TestCaseWithFactory):
556+ """Tests for PersonAssignedBugTaskSearchListingView."""
557+
558+ view_name = '+subscribedbugs'
559+
560+ def setUp(self):
561+ super(TestPersonSubscribedBugTaskSearchListingView, self).setUp()
562+ self.expected_for_search_unbatched = [
563+ self.subscribed_bug.default_bugtask,
564+ self.owned_bug.default_bugtask,
565+ ]