Merge lp:~adeuring/launchpad/bug-675595-2 into lp:launchpad
- bug-675595-2
- Merge into devel
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 |
Related bugs: |
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 BugTaskSearchLi
Description of the change
This branch fixes bug 675595: "BugTaskSet.
can call BugTaskSet.
one SQL query instead of two"
Working on the branch, I realized that the method
BugTaskSet.
The only callsite was RelevantMilesto
since we can now retrieve milestones by pre-joining the table in
BugTaskSet.
getMilestoneWid
BugTaskSearchLi
"prejoins" to this method.
tests:
./bin/test -vvt test_buglisting -t test_person_view
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.
I am not if it is worth the effort: Basically, we can now pre-join more
or less arbitrary tables in calls of BugTaskSet.
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 TaskSearchListi
>
> With the above taken into consideration, I think you can land this.
>
> Maris
Preview Diff
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 | + ] |
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 TaskSearchListi ngView 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