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 | 127 | def __init__(self): | 127 | def __init__(self): |
6 | 128 | self.title = 'Malone: the Launchpad bug tracker' | 128 | self.title = 'Malone: the Launchpad bug tracker' |
7 | 129 | 129 | ||
9 | 130 | def searchTasks(self, search_params): | 130 | def searchTasks(self, search_params, prejoins=[]): |
10 | 131 | """See `IMaloneApplication`.""" | 131 | """See `IMaloneApplication`.""" |
12 | 132 | return getUtility(IBugTaskSet).search(search_params) | 132 | return getUtility(IBugTaskSet).search( |
13 | 133 | search_params, prejoins=prejoins) | ||
14 | 133 | 134 | ||
15 | 134 | def createBug(self, owner, title, description, target, | 135 | def createBug(self, owner, title, description, target, |
16 | 135 | security_related=False, private=False, tags=None): | 136 | security_related=False, private=False, tags=None): |
17 | @@ -235,8 +236,10 @@ | |||
18 | 235 | """See `IRosettaApplication`.""" | 236 | """See `IRosettaApplication`.""" |
19 | 236 | projects = getUtility(ITranslationsOverview) | 237 | projects = getUtility(ITranslationsOverview) |
20 | 237 | for project in projects.getMostTranslatedPillars(): | 238 | for project in projects.getMostTranslatedPillars(): |
23 | 238 | yield { 'pillar' : project['pillar'], | 239 | yield { |
24 | 239 | 'font_size' : project['weight'] * 10 } | 240 | 'pillar': project['pillar'], |
25 | 241 | 'font_size': project['weight'] * 10, | ||
26 | 242 | } | ||
27 | 240 | 243 | ||
28 | 241 | def translatable_distroseriess(self): | 244 | def translatable_distroseriess(self): |
29 | 242 | """See `IRosettaApplication`.""" | 245 | """See `IRosettaApplication`.""" |
30 | 243 | 246 | ||
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 | 2679 | return self._getBatchNavigator(unbatchedTasks) | 2679 | return self._getBatchNavigator(unbatchedTasks) |
36 | 2680 | 2680 | ||
37 | 2681 | def searchUnbatched(self, searchtext=None, context=None, | 2681 | def searchUnbatched(self, searchtext=None, context=None, |
39 | 2682 | extra_params=None): | 2682 | extra_params=None, prejoins=[]): |
40 | 2683 | """Return a `SelectResults` object for the GET search criteria. | 2683 | """Return a `SelectResults` object for the GET search criteria. |
41 | 2684 | 2684 | ||
42 | 2685 | :param searchtext: Text that must occur in the bug report. If | 2685 | :param searchtext: Text that must occur in the bug report. If |
43 | @@ -2697,7 +2697,7 @@ | |||
44 | 2697 | search_params = self.buildSearchParams( | 2697 | search_params = self.buildSearchParams( |
45 | 2698 | searchtext=searchtext, extra_params=extra_params) | 2698 | searchtext=searchtext, extra_params=extra_params) |
46 | 2699 | search_params.user = self.user | 2699 | search_params.user = self.user |
48 | 2700 | tasks = context.searchTasks(search_params) | 2700 | tasks = context.searchTasks(search_params, prejoins=prejoins) |
49 | 2701 | return tasks | 2701 | return tasks |
50 | 2702 | 2702 | ||
51 | 2703 | def getWidgetValues( | 2703 | def getWidgetValues( |
52 | 2704 | 2704 | ||
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 | 3 | 3 | ||
58 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
59 | 5 | 5 | ||
60 | 6 | from storm.expr import LeftJoin | ||
61 | 7 | from storm.store import Store | ||
62 | 8 | from testtools.matchers import Equals | ||
63 | 6 | from zope.component import getUtility | 9 | from zope.component import getUtility |
64 | 7 | 10 | ||
65 | 8 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 11 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
66 | @@ -13,12 +16,17 @@ | |||
67 | 13 | ) | 16 | ) |
68 | 14 | from canonical.launchpad.webapp.publisher import canonical_url | 17 | from canonical.launchpad.webapp.publisher import canonical_url |
69 | 15 | from canonical.testing.layers import DatabaseFunctionalLayer | 18 | from canonical.testing.layers import DatabaseFunctionalLayer |
70 | 19 | from lp.bugs.model.bugtask import BugTask | ||
71 | 20 | from lp.registry.model.person import Person | ||
72 | 16 | from lp.testing import ( | 21 | from lp.testing import ( |
73 | 17 | BrowserTestCase, | 22 | BrowserTestCase, |
74 | 18 | login_person, | 23 | login_person, |
75 | 19 | person_logged_in, | 24 | person_logged_in, |
76 | 25 | StormStatementRecorder, | ||
77 | 20 | TestCaseWithFactory, | 26 | TestCaseWithFactory, |
78 | 21 | ) | 27 | ) |
79 | 28 | |||
80 | 29 | from lp.testing.matchers import HasQueryCount | ||
81 | 22 | from lp.testing.views import create_initialized_view | 30 | from lp.testing.views import create_initialized_view |
82 | 23 | 31 | ||
83 | 24 | 32 | ||
84 | @@ -130,6 +138,24 @@ | |||
85 | 130 | find_tag_by_id(browser.contents, 'portlet-tags'), | 138 | find_tag_by_id(browser.contents, 'portlet-tags'), |
86 | 131 | "portlet-tags should not be shown.") | 139 | "portlet-tags should not be shown.") |
87 | 132 | 140 | ||
88 | 141 | def test_searchUnbatched_can_preload_objects(self): | ||
89 | 142 | # BugTaskSearchListingView.searchUnbatched() can optionally | ||
90 | 143 | # preload objects while retureving the bugtasks. | ||
91 | 144 | product = self.factory.makeProduct() | ||
92 | 145 | bugtask_1 = self.factory.makeBug(product=product).default_bugtask | ||
93 | 146 | bugtask_2 = self.factory.makeBug(product=product).default_bugtask | ||
94 | 147 | view = create_initialized_view(product, '+bugs') | ||
95 | 148 | Store.of(product).invalidate() | ||
96 | 149 | with StormStatementRecorder() as recorder: | ||
97 | 150 | prejoins=[(Person, LeftJoin(Person, BugTask.owner==Person.id))] | ||
98 | 151 | bugtasks = list(view.searchUnbatched(prejoins=prejoins)) | ||
99 | 152 | self.assertEqual( | ||
100 | 153 | [bugtask_1, bugtask_2], bugtasks) | ||
101 | 154 | # If the table prejoin failed, then this will issue two | ||
102 | 155 | # additional SQL queries | ||
103 | 156 | [bugtask.owner for bugtask in bugtasks] | ||
104 | 157 | self.assertThat(recorder, HasQueryCount(Equals(1))) | ||
105 | 158 | |||
106 | 133 | 159 | ||
107 | 134 | class BugTargetTestCase(TestCaseWithFactory): | 160 | class BugTargetTestCase(TestCaseWithFactory): |
108 | 135 | """Test helpers for setting up `IBugTarget` tests.""" | 161 | """Test helpers for setting up `IBugTarget` tests.""" |
109 | 136 | 162 | ||
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 | 1 | = Discover the assigned milestones from a BugTask search = | ||
115 | 2 | |||
116 | 3 | The IBugTaskSet method getAssignedMilestonesFromSearch() accepts a | ||
117 | 4 | result set that yields BugTasks. It attempt to efficiently find all | ||
118 | 5 | the distinct milestones assigned to those BugTasks. Typically this | ||
119 | 6 | should be called with the results from calling IHasBugs.searchTasks() | ||
120 | 7 | or IBugTaskSet.search(). | ||
121 | 8 | |||
122 | 9 | >>> from lp.bugs.interfaces.bugtask import ( | ||
123 | 10 | ... BugTaskSearchParams, IBugTaskSet) | ||
124 | 11 | |||
125 | 12 | >>> person = factory.makePerson() | ||
126 | 13 | >>> login(person.preferredemail.email) | ||
127 | 14 | |||
128 | 15 | >>> milestone = factory.makeMilestone() | ||
129 | 16 | |||
130 | 17 | >>> bugtask = factory.makeBugTask(target=milestone.product) | ||
131 | 18 | >>> bugtask.milestone = milestone | ||
132 | 19 | >>> bugtask.subscribe(person, person) | ||
133 | 20 | <lp.bugs.model.bugsubscription.BugSubscription ...> | ||
134 | 21 | >>> transaction.commit() | ||
135 | 22 | |||
136 | 23 | The results of a search for all bug tasks related to a person can be | ||
137 | 24 | passed to getAssignedMilestonesFromSearch() to discover all the | ||
138 | 25 | milestones used. | ||
139 | 26 | |||
140 | 27 | >>> bugtask_set = getUtility(IBugTaskSet) | ||
141 | 28 | |||
142 | 29 | >>> bugtasks = bugtask_set.search( | ||
143 | 30 | ... BugTaskSearchParams(person, assignee=person), | ||
144 | 31 | ... BugTaskSearchParams(person, subscriber=person), | ||
145 | 32 | ... BugTaskSearchParams(person, owner=person, bug_reporter=person), | ||
146 | 33 | ... BugTaskSearchParams(person, bug_commenter=person)) | ||
147 | 34 | |||
148 | 35 | >>> milestones = bugtask_set.getAssignedMilestonesFromSearch( | ||
149 | 36 | ... bugtasks) | ||
150 | 37 | |||
151 | 38 | >>> milestones | ||
152 | 39 | [<Milestone ...>] | ||
153 | 40 | >>> len(milestones) | ||
154 | 41 | 1 | ||
155 | 42 | >>> milestone in milestones | ||
156 | 43 | True | ||
157 | 44 | |||
158 | 45 | Because get_assigned_milestones_from_bugtasks() attempts to be | ||
159 | 46 | efficient, by customising the result set passed in, it rejects | ||
160 | 47 | attempts to pass in lists of bug tasks. | ||
161 | 48 | |||
162 | 49 | >>> milestones = bugtask_set.getAssignedMilestonesFromSearch( | ||
163 | 50 | ... list(bugtasks)) | ||
164 | 51 | Traceback (most recent call last): | ||
165 | 52 | ... | ||
166 | 53 | AssertionError: search_results must provide | ||
167 | 54 | IResultSet or ISQLObjectResultSet | ||
168 | 55 | 0 | ||
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 | 1414 | :param params: the BugTaskSearchParams to search on. | 1414 | :param params: the BugTaskSearchParams to search on. |
174 | 1415 | """ | 1415 | """ |
175 | 1416 | 1416 | ||
176 | 1417 | def getAssignedMilestonesFromSearch(search_results): | ||
177 | 1418 | """Returns distinct milestones for the given tasks. | ||
178 | 1419 | |||
179 | 1420 | :param search_results: A result set yielding BugTask objects, | ||
180 | 1421 | typically the result of calling `BugTaskSet.search()`. | ||
181 | 1422 | """ | ||
182 | 1423 | |||
183 | 1424 | def getStatusCountsForProductSeries(user, product_series): | 1417 | def getStatusCountsForProductSeries(user, product_series): |
184 | 1425 | """Returns status counts for a product series' bugs. | 1418 | """Returns status counts for a product series' bugs. |
185 | 1426 | 1419 | ||
186 | 1427 | 1420 | ||
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 | 32 | """Application root for malone.""" | 32 | """Application root for malone.""" |
192 | 33 | export_as_webservice_collection(IBug) | 33 | export_as_webservice_collection(IBug) |
193 | 34 | 34 | ||
195 | 35 | def searchTasks(search_params): | 35 | def searchTasks(search_params, prejoins=[]): |
196 | 36 | """Search IBugTasks with the given search parameters.""" | 36 | """Search IBugTasks with the given search parameters.""" |
197 | 37 | 37 | ||
198 | 38 | bug_count = Attribute("The number of bugs recorded in Launchpad") | 38 | bug_count = Attribute("The number of bugs recorded in Launchpad") |
199 | 39 | 39 | ||
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 | 2309 | """See `IBugTaskSet`.""" | 2309 | """See `IBugTaskSet`.""" |
205 | 2310 | return self._search(BugTask.bugID, [], params).result_set | 2310 | return self._search(BugTask.bugID, [], params).result_set |
206 | 2311 | 2311 | ||
207 | 2312 | def getAssignedMilestonesFromSearch(self, search_results): | ||
208 | 2313 | """See `IBugTaskSet`.""" | ||
209 | 2314 | # XXX: Gavin Panella 2009-03-05 bug=338184: There is currently | ||
210 | 2315 | # no clean way to get the underlying Storm ResultSet from an | ||
211 | 2316 | # SQLObjectResultSet, so we must remove the security proxy for | ||
212 | 2317 | # a moment. | ||
213 | 2318 | if ISQLObjectResultSet.providedBy(search_results): | ||
214 | 2319 | search_results = removeSecurityProxy(search_results)._result_set | ||
215 | 2320 | # Check that we have a Storm result set before we start doing | ||
216 | 2321 | # things with it. | ||
217 | 2322 | assert IResultSet.providedBy(search_results), ( | ||
218 | 2323 | "search_results must provide IResultSet or ISQLObjectResultSet") | ||
219 | 2324 | # Remove ordering and make distinct. | ||
220 | 2325 | search_results = search_results.order_by().config(distinct=True) | ||
221 | 2326 | # Get milestone IDs. | ||
222 | 2327 | milestone_ids = [ | ||
223 | 2328 | milestone_id for milestone_id in ( | ||
224 | 2329 | search_results.values(BugTask.milestoneID)) | ||
225 | 2330 | if milestone_id is not None] | ||
226 | 2331 | # Query for milestones. | ||
227 | 2332 | if len(milestone_ids) == 0: | ||
228 | 2333 | return [] | ||
229 | 2334 | else: | ||
230 | 2335 | # Import here because of cyclic references. | ||
231 | 2336 | from lp.registry.model.milestone import ( | ||
232 | 2337 | Milestone, milestone_sort_key) | ||
233 | 2338 | # We need the store that was used, we have no objects to key off | ||
234 | 2339 | # of other than the search result, and Store.of() doesn't | ||
235 | 2340 | # currently work on result sets. Additionally it may be a | ||
236 | 2341 | # DecoratedResultSet. | ||
237 | 2342 | if zope_isinstance(search_results, DecoratedResultSet): | ||
238 | 2343 | store = removeSecurityProxy(search_results).result_set._store | ||
239 | 2344 | else: | ||
240 | 2345 | store = search_results._store | ||
241 | 2346 | milestones = store.find( | ||
242 | 2347 | Milestone, Milestone.id.is_in(milestone_ids)) | ||
243 | 2348 | return sorted(milestones, key=milestone_sort_key, reverse=True) | ||
244 | 2349 | |||
245 | 2350 | def createTask(self, bug, owner, product=None, productseries=None, | 2312 | def createTask(self, bug, owner, product=None, productseries=None, |
246 | 2351 | distribution=None, distroseries=None, | 2313 | distribution=None, distroseries=None, |
247 | 2352 | sourcepackagename=None, | 2314 | sourcepackagename=None, |
248 | 2353 | 2315 | ||
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 | 105 | URI, | 105 | URI, |
254 | 106 | ) | 106 | ) |
255 | 107 | import pytz | 107 | import pytz |
256 | 108 | from storm.expr import Join | ||
257 | 108 | from storm.zope.interfaces import IResultSet | 109 | from storm.zope.interfaces import IResultSet |
258 | 109 | from z3c.ptcompat import ViewPageTemplateFile | 110 | from z3c.ptcompat import ViewPageTemplateFile |
259 | 110 | from zope.app.form.browser import ( | 111 | from zope.app.form.browser import ( |
260 | @@ -242,6 +243,7 @@ | |||
261 | 242 | IBugTaskSet, | 243 | IBugTaskSet, |
262 | 243 | UNRESOLVED_BUGTASK_STATUSES, | 244 | UNRESOLVED_BUGTASK_STATUSES, |
263 | 244 | ) | 245 | ) |
264 | 246 | from lp.bugs.model.bugtask import BugTask | ||
265 | 245 | from lp.buildmaster.enums import BuildStatus | 247 | from lp.buildmaster.enums import BuildStatus |
266 | 246 | from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin | 248 | from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin |
267 | 247 | from lp.code.errors import InvalidNamespace | 249 | from lp.code.errors import InvalidNamespace |
268 | @@ -304,6 +306,10 @@ | |||
269 | 304 | IWikiName, | 306 | IWikiName, |
270 | 305 | IWikiNameSet, | 307 | IWikiNameSet, |
271 | 306 | ) | 308 | ) |
272 | 309 | from lp.registry.model.milestone import ( | ||
273 | 310 | Milestone, | ||
274 | 311 | milestone_sort_key, | ||
275 | 312 | ) | ||
276 | 307 | from lp.services.fields import LocationField | 313 | from lp.services.fields import LocationField |
277 | 308 | from lp.services.geoip.interfaces import IRequestPreferredLanguages | 314 | from lp.services.geoip.interfaces import IRequestPreferredLanguages |
278 | 309 | from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint | 315 | from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint |
279 | @@ -2133,8 +2139,12 @@ | |||
280 | 2133 | 2139 | ||
281 | 2134 | def getMilestoneWidgetValues(self): | 2140 | def getMilestoneWidgetValues(self): |
282 | 2135 | """Return data used to render the milestone checkboxes.""" | 2141 | """Return data used to render the milestone checkboxes.""" |
285 | 2136 | milestones = getUtility(IBugTaskSet).getAssignedMilestonesFromSearch( | 2142 | prejoins = [ |
286 | 2137 | self.searchUnbatched()) | 2143 | (Milestone, Join(Milestone, BugTask.milestone == Milestone.id))] |
287 | 2144 | milestones = [ | ||
288 | 2145 | bugtask.milestone | ||
289 | 2146 | for bugtask in self.searchUnbatched(prejoins=prejoins)] | ||
290 | 2147 | milestones = sorted(milestones, key=milestone_sort_key, reverse=True) | ||
291 | 2138 | return [ | 2148 | return [ |
292 | 2139 | dict(title=milestone.title, value=milestone.id, checked=False) | 2149 | dict(title=milestone.title, value=milestone.id, checked=False) |
293 | 2140 | for milestone in milestones] | 2150 | for milestone in milestones] |
294 | @@ -2150,7 +2160,7 @@ | |||
295 | 2150 | page_title = 'Related bugs' | 2160 | page_title = 'Related bugs' |
296 | 2151 | 2161 | ||
297 | 2152 | def searchUnbatched(self, searchtext=None, context=None, | 2162 | def searchUnbatched(self, searchtext=None, context=None, |
299 | 2153 | extra_params=None): | 2163 | extra_params=None, prejoins=[]): |
300 | 2154 | """Return the open bugs related to a person. | 2164 | """Return the open bugs related to a person. |
301 | 2155 | 2165 | ||
302 | 2156 | :param extra_params: A dict that provides search params added to | 2166 | :param extra_params: A dict that provides search params added to |
303 | @@ -2181,7 +2191,7 @@ | |||
304 | 2181 | 2191 | ||
305 | 2182 | return context.searchTasks( | 2192 | return context.searchTasks( |
306 | 2183 | assignee_params, subscriber_params, owner_params, | 2193 | assignee_params, subscriber_params, owner_params, |
308 | 2184 | commenter_params) | 2194 | commenter_params, prejoins=prejoins) |
309 | 2185 | 2195 | ||
310 | 2186 | def getSearchPageHeading(self): | 2196 | def getSearchPageHeading(self): |
311 | 2187 | return "Bugs related to %s" % self.context.displayname | 2197 | return "Bugs related to %s" % self.context.displayname |
312 | @@ -2210,7 +2220,7 @@ | |||
313 | 2210 | page_title = 'Assigned bugs' | 2220 | page_title = 'Assigned bugs' |
314 | 2211 | 2221 | ||
315 | 2212 | def searchUnbatched(self, searchtext=None, context=None, | 2222 | def searchUnbatched(self, searchtext=None, context=None, |
317 | 2213 | extra_params=None): | 2223 | extra_params=None, prejoins=[]): |
318 | 2214 | """Return the open bugs assigned to a person.""" | 2224 | """Return the open bugs assigned to a person.""" |
319 | 2215 | if context is None: | 2225 | if context is None: |
320 | 2216 | context = self.context | 2226 | context = self.context |
321 | @@ -2222,7 +2232,8 @@ | |||
322 | 2222 | extra_params['assignee'] = context | 2232 | extra_params['assignee'] = context |
323 | 2223 | 2233 | ||
324 | 2224 | sup = super(PersonAssignedBugTaskSearchListingView, self) | 2234 | sup = super(PersonAssignedBugTaskSearchListingView, self) |
326 | 2225 | return sup.searchUnbatched(searchtext, context, extra_params) | 2235 | return sup.searchUnbatched( |
327 | 2236 | searchtext, context, extra_params, prejoins) | ||
328 | 2226 | 2237 | ||
329 | 2227 | def shouldShowAssigneeWidget(self): | 2238 | def shouldShowAssigneeWidget(self): |
330 | 2228 | """Should the assignee widget be shown on the advanced search page?""" | 2239 | """Should the assignee widget be shown on the advanced search page?""" |
331 | @@ -2267,7 +2278,7 @@ | |||
332 | 2267 | page_title = 'Commented bugs' | 2278 | page_title = 'Commented bugs' |
333 | 2268 | 2279 | ||
334 | 2269 | def searchUnbatched(self, searchtext=None, context=None, | 2280 | def searchUnbatched(self, searchtext=None, context=None, |
336 | 2270 | extra_params=None): | 2281 | extra_params=None, prejoins=[]): |
337 | 2271 | """Return the open bugs commented on by a person.""" | 2282 | """Return the open bugs commented on by a person.""" |
338 | 2272 | if context is None: | 2283 | if context is None: |
339 | 2273 | context = self.context | 2284 | context = self.context |
340 | @@ -2279,7 +2290,8 @@ | |||
341 | 2279 | extra_params['bug_commenter'] = context | 2290 | extra_params['bug_commenter'] = context |
342 | 2280 | 2291 | ||
343 | 2281 | sup = super(PersonCommentedBugTaskSearchListingView, self) | 2292 | sup = super(PersonCommentedBugTaskSearchListingView, self) |
345 | 2282 | return sup.searchUnbatched(searchtext, context, extra_params) | 2293 | return sup.searchUnbatched( |
346 | 2294 | searchtext, context, extra_params, prejoins) | ||
347 | 2283 | 2295 | ||
348 | 2284 | def getSearchPageHeading(self): | 2296 | def getSearchPageHeading(self): |
349 | 2285 | """The header for the search page.""" | 2297 | """The header for the search page.""" |
350 | @@ -2312,7 +2324,7 @@ | |||
351 | 2312 | page_title = 'Reported bugs' | 2324 | page_title = 'Reported bugs' |
352 | 2313 | 2325 | ||
353 | 2314 | def searchUnbatched(self, searchtext=None, context=None, | 2326 | def searchUnbatched(self, searchtext=None, context=None, |
355 | 2315 | extra_params=None): | 2327 | extra_params=None, prejoins=[]): |
356 | 2316 | """Return the bugs reported by a person.""" | 2328 | """Return the bugs reported by a person.""" |
357 | 2317 | if context is None: | 2329 | if context is None: |
358 | 2318 | context = self.context | 2330 | context = self.context |
359 | @@ -2327,7 +2339,8 @@ | |||
360 | 2327 | extra_params['bug_reporter'] = context | 2339 | extra_params['bug_reporter'] = context |
361 | 2328 | 2340 | ||
362 | 2329 | sup = super(PersonReportedBugTaskSearchListingView, self) | 2341 | sup = super(PersonReportedBugTaskSearchListingView, self) |
364 | 2330 | return sup.searchUnbatched(searchtext, context, extra_params) | 2342 | return sup.searchUnbatched( |
365 | 2343 | searchtext, context, extra_params, prejoins) | ||
366 | 2331 | 2344 | ||
367 | 2332 | def getSearchPageHeading(self): | 2345 | def getSearchPageHeading(self): |
368 | 2333 | """The header for the search page.""" | 2346 | """The header for the search page.""" |
369 | @@ -2368,7 +2381,7 @@ | |||
370 | 2368 | page_title = 'Subscribed bugs' | 2381 | page_title = 'Subscribed bugs' |
371 | 2369 | 2382 | ||
372 | 2370 | def searchUnbatched(self, searchtext=None, context=None, | 2383 | def searchUnbatched(self, searchtext=None, context=None, |
374 | 2371 | extra_params=None): | 2384 | extra_params=None, prejoins=[]): |
375 | 2372 | """Return the bugs subscribed to by a person.""" | 2385 | """Return the bugs subscribed to by a person.""" |
376 | 2373 | if context is None: | 2386 | if context is None: |
377 | 2374 | context = self.context | 2387 | context = self.context |
378 | @@ -2380,7 +2393,8 @@ | |||
379 | 2380 | extra_params['subscriber'] = context | 2393 | extra_params['subscriber'] = context |
380 | 2381 | 2394 | ||
381 | 2382 | sup = super(PersonSubscribedBugTaskSearchListingView, self) | 2395 | sup = super(PersonSubscribedBugTaskSearchListingView, self) |
383 | 2383 | return sup.searchUnbatched(searchtext, context, extra_params) | 2396 | return sup.searchUnbatched( |
384 | 2397 | searchtext, context, extra_params, prejoins) | ||
385 | 2384 | 2398 | ||
386 | 2385 | def getSearchPageHeading(self): | 2399 | def getSearchPageHeading(self): |
387 | 2386 | """The header for the search page.""" | 2400 | """The header for the search page.""" |
388 | 2387 | 2401 | ||
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 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
394 | 5 | 5 | ||
395 | 6 | import transaction | 6 | import transaction |
396 | 7 | from storm.expr import LeftJoin | ||
397 | 8 | from storm.store import Store | ||
398 | 9 | from testtools.matchers import Equals | ||
399 | 7 | from zope.component import getUtility | 10 | from zope.component import getUtility |
400 | 8 | from zope.security.proxy import removeSecurityProxy | ||
401 | 9 | 11 | ||
402 | 10 | from canonical.config import config | 12 | from canonical.config import config |
403 | 11 | from canonical.launchpad.ftests import ( | 13 | from canonical.launchpad.ftests import ( |
404 | @@ -25,6 +27,7 @@ | |||
405 | 25 | ) | 27 | ) |
406 | 26 | 28 | ||
407 | 27 | from lp.app.errors import NotFoundError | 29 | from lp.app.errors import NotFoundError |
408 | 30 | from lp.bugs.model.bugtask import BugTask | ||
409 | 28 | from lp.buildmaster.enums import BuildStatus | 31 | from lp.buildmaster.enums import BuildStatus |
410 | 29 | from lp.registry.browser.person import ( | 32 | from lp.registry.browser.person import ( |
411 | 30 | PersonEditView, | 33 | PersonEditView, |
412 | @@ -43,15 +46,20 @@ | |||
413 | 43 | ) | 46 | ) |
414 | 44 | 47 | ||
415 | 45 | from lp.registry.model.karma import KarmaCategory | 48 | from lp.registry.model.karma import KarmaCategory |
416 | 49 | from lp.registry.model.milestone import milestone_sort_key | ||
417 | 46 | from lp.soyuz.enums import ( | 50 | from lp.soyuz.enums import ( |
418 | 47 | ArchiveStatus, | 51 | ArchiveStatus, |
419 | 48 | PackagePublishingStatus, | 52 | PackagePublishingStatus, |
420 | 49 | ) | 53 | ) |
421 | 54 | from lp.registry.model.person import Person | ||
422 | 50 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher | 55 | from lp.soyuz.tests.test_publishing import SoyuzTestPublisher |
423 | 51 | from lp.testing import ( | 56 | from lp.testing import ( |
424 | 52 | login_person, | 57 | login_person, |
425 | 58 | person_logged_in, | ||
426 | 59 | StormStatementRecorder, | ||
427 | 53 | TestCaseWithFactory, | 60 | TestCaseWithFactory, |
428 | 54 | ) | 61 | ) |
429 | 62 | from lp.testing.matchers import HasQueryCount | ||
430 | 55 | from lp.testing.views import ( | 63 | from lp.testing.views import ( |
431 | 56 | create_initialized_view, | 64 | create_initialized_view, |
432 | 57 | create_view, | 65 | create_view, |
433 | @@ -809,3 +817,132 @@ | |||
434 | 809 | login_person(self.person) | 817 | login_person(self.person) |
435 | 810 | view = create_initialized_view(self.team, '+subscriptions') | 818 | view = create_initialized_view(self.team, '+subscriptions') |
436 | 811 | self.assertTrue(view.canUnsubscribeFromBugTasks()) | 819 | self.assertTrue(view.canUnsubscribeFromBugTasks()) |
437 | 820 | |||
438 | 821 | |||
439 | 822 | class BugTaskViewsTestBase: | ||
440 | 823 | """A base class for bugtask search related tests.""" | ||
441 | 824 | |||
442 | 825 | layer = DatabaseFunctionalLayer | ||
443 | 826 | |||
444 | 827 | def setUp(self): | ||
445 | 828 | super(BugTaskViewsTestBase, self).setUp() | ||
446 | 829 | self.person = self.factory.makePerson() | ||
447 | 830 | with person_logged_in(self.person): | ||
448 | 831 | self.subscribed_bug = self.factory.makeBug() | ||
449 | 832 | self.subscribed_bug.subscribe( | ||
450 | 833 | self.person, subscribed_by=self.person) | ||
451 | 834 | self.assigned_bug = self.factory.makeBug() | ||
452 | 835 | self.assigned_bug.default_bugtask.transitionToAssignee( | ||
453 | 836 | self.person) | ||
454 | 837 | self.owned_bug = self.factory.makeBug(owner=self.person) | ||
455 | 838 | self.commented_bug = self.factory.makeBug() | ||
456 | 839 | self.commented_bug.newMessage(owner=self.person) | ||
457 | 840 | |||
458 | 841 | for bug in (self.subscribed_bug, self.assigned_bug, self.owned_bug, | ||
459 | 842 | self.commented_bug): | ||
460 | 843 | with person_logged_in(bug.default_bugtask.product.owner): | ||
461 | 844 | milestone = self.factory.makeMilestone( | ||
462 | 845 | product=bug.default_bugtask.product) | ||
463 | 846 | bug.default_bugtask.transitionToMilestone( | ||
464 | 847 | milestone, bug.default_bugtask.product.owner) | ||
465 | 848 | |||
466 | 849 | def test_searchUnbatched(self): | ||
467 | 850 | view = create_initialized_view(self.person, self.view_name) | ||
468 | 851 | self.assertEqual( | ||
469 | 852 | self.expected_for_search_unbatched, list(view.searchUnbatched())) | ||
470 | 853 | |||
471 | 854 | def test_searchUnbatched_with_prejoins(self): | ||
472 | 855 | view = create_initialized_view(self.person, self.view_name) | ||
473 | 856 | Store.of(self.subscribed_bug).invalidate() | ||
474 | 857 | with StormStatementRecorder() as recorder: | ||
475 | 858 | prejoins=[(Person, LeftJoin(Person, BugTask.owner==Person.id))] | ||
476 | 859 | bugtasks = view.searchUnbatched(prejoins=prejoins) | ||
477 | 860 | [bugtask.owner for bugtask in bugtasks] | ||
478 | 861 | self.assertThat(recorder, HasQueryCount(Equals(1))) | ||
479 | 862 | |||
480 | 863 | def test_getMilestoneWidgetValues(self): | ||
481 | 864 | view = create_initialized_view(self.person, self.view_name) | ||
482 | 865 | milestones = [ | ||
483 | 866 | bugtask.milestone | ||
484 | 867 | for bugtask in self.expected_for_search_unbatched] | ||
485 | 868 | milestones = sorted(milestones, key=milestone_sort_key, reverse=True) | ||
486 | 869 | expected = [ | ||
487 | 870 | { | ||
488 | 871 | 'title': milestone.title, | ||
489 | 872 | 'value': milestone.id, | ||
490 | 873 | 'checked': False, | ||
491 | 874 | } | ||
492 | 875 | for milestone in milestones] | ||
493 | 876 | Store.of(milestones[0]).invalidate() | ||
494 | 877 | with StormStatementRecorder() as recorder: | ||
495 | 878 | self.assertEqual(expected, view.getMilestoneWidgetValues()) | ||
496 | 879 | self.assertThat(recorder, HasQueryCount(Equals(1))) | ||
497 | 880 | |||
498 | 881 | |||
499 | 882 | class TestPersonRelatedBugTaskSearchListingView( | ||
500 | 883 | BugTaskViewsTestBase, TestCaseWithFactory): | ||
501 | 884 | """Tests for PersonRelatedBugTaskSearchListingView.""" | ||
502 | 885 | |||
503 | 886 | view_name = '+bugs' | ||
504 | 887 | |||
505 | 888 | def setUp(self): | ||
506 | 889 | super(TestPersonRelatedBugTaskSearchListingView, self).setUp() | ||
507 | 890 | self.expected_for_search_unbatched = [ | ||
508 | 891 | self.subscribed_bug.default_bugtask, | ||
509 | 892 | self.assigned_bug.default_bugtask, | ||
510 | 893 | self.owned_bug.default_bugtask, | ||
511 | 894 | self.commented_bug.default_bugtask, | ||
512 | 895 | ] | ||
513 | 896 | |||
514 | 897 | |||
515 | 898 | class TestPersonAssignedBugTaskSearchListingView( | ||
516 | 899 | BugTaskViewsTestBase, TestCaseWithFactory): | ||
517 | 900 | """Tests for PersonAssignedBugTaskSearchListingView.""" | ||
518 | 901 | |||
519 | 902 | view_name = '+assignedbugs' | ||
520 | 903 | |||
521 | 904 | def setUp(self): | ||
522 | 905 | super(TestPersonAssignedBugTaskSearchListingView, self).setUp() | ||
523 | 906 | self.expected_for_search_unbatched = [ | ||
524 | 907 | self.assigned_bug.default_bugtask, | ||
525 | 908 | ] | ||
526 | 909 | |||
527 | 910 | |||
528 | 911 | class TestPersonCommentedBugTaskSearchListingView( | ||
529 | 912 | BugTaskViewsTestBase, TestCaseWithFactory): | ||
530 | 913 | """Tests for PersonAssignedBugTaskSearchListingView.""" | ||
531 | 914 | |||
532 | 915 | view_name = '+commentedbugs' | ||
533 | 916 | |||
534 | 917 | def setUp(self): | ||
535 | 918 | super(TestPersonCommentedBugTaskSearchListingView, self).setUp() | ||
536 | 919 | self.expected_for_search_unbatched = [ | ||
537 | 920 | self.commented_bug.default_bugtask, | ||
538 | 921 | ] | ||
539 | 922 | |||
540 | 923 | |||
541 | 924 | class TestPersonReportedBugTaskSearchListingView( | ||
542 | 925 | BugTaskViewsTestBase, TestCaseWithFactory): | ||
543 | 926 | """Tests for PersonAssignedBugTaskSearchListingView.""" | ||
544 | 927 | |||
545 | 928 | view_name = '+reportedbugs' | ||
546 | 929 | |||
547 | 930 | def setUp(self): | ||
548 | 931 | super(TestPersonReportedBugTaskSearchListingView, self).setUp() | ||
549 | 932 | self.expected_for_search_unbatched = [ | ||
550 | 933 | self.owned_bug.default_bugtask, | ||
551 | 934 | ] | ||
552 | 935 | |||
553 | 936 | |||
554 | 937 | class TestPersonSubscribedBugTaskSearchListingView( | ||
555 | 938 | BugTaskViewsTestBase, TestCaseWithFactory): | ||
556 | 939 | """Tests for PersonAssignedBugTaskSearchListingView.""" | ||
557 | 940 | |||
558 | 941 | view_name = '+subscribedbugs' | ||
559 | 942 | |||
560 | 943 | def setUp(self): | ||
561 | 944 | super(TestPersonSubscribedBugTaskSearchListingView, self).setUp() | ||
562 | 945 | self.expected_for_search_unbatched = [ | ||
563 | 946 | self.subscribed_bug.default_bugtask, | ||
564 | 947 | self.owned_bug.default_bugtask, | ||
565 | 948 | ] |
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