Merge lp:~lifeless/launchpad/milestones into lp:launchpad
- milestones
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Curtis Hovey | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 11409 | ||||
Proposed branch: | lp:~lifeless/launchpad/milestones | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
1014 lines (+349/-154) 20 files modified
lib/canonical/launchpad/interfaces/validation.py (+1/-1) lib/canonical/launchpad/security.py (+5/-32) lib/canonical/launchpad/utilities/personroles.py (+4/-0) lib/lp/bugs/browser/bugtarget.py (+0/-5) lib/lp/bugs/browser/bugtask.py (+2/-19) lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt (+1/-1) lib/lp/bugs/browser/tests/test_bugtarget_filebug.py (+1/-1) lib/lp/bugs/interfaces/bugtask.py (+8/-0) lib/lp/bugs/model/bug.py (+28/-5) lib/lp/bugs/model/bugtarget.py (+1/-7) lib/lp/bugs/model/bugtask.py (+191/-72) lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt (+2/-2) lib/lp/bugs/tests/test_bugtask.py (+24/-1) lib/lp/registry/browser/tests/test_milestone.py (+72/-4) lib/lp/registry/browser/tests/test_views.py (+1/-0) lib/lp/registry/doc/distribution.txt (+1/-1) lib/lp/registry/doc/sourcepackage.txt (+1/-1) lib/lp/registry/model/distroseries.py (+3/-0) lib/lp/registry/model/sourcepackage.py (+2/-1) lib/lp/testing/factory.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~lifeless/launchpad/milestones | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Deryck Hodge (community) | code | Approve | |
Graham Binns (community) | code | Approve | |
Review via email: mp+32855@code.launchpad.net |
Commit message
Convert BugTaskSet.search to return a storm resultset and cache the user queried with as a viewer on returned Bug objects.
Description of the change
Hopefully fix the current instantiation of https:/
Private bug access checking is rather slow: https:/
Solution - a cached attribute recording people that we've calculated should have view access, prepopulated by BugTaskSet.
Added tests for the low level behaviour and that it fixes the observed scaling problem in a simple milestone view.
\o/
Robert Collins (lifeless) wrote : | # |
Graham Binns (gmb) wrote : | # |
Hi Rob,
Couple of nitpicks, but otherwise okay. Marking it needs fixing because
of the over-long test (see below) but if we can't split that up I'm
happy for it to land as-is.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -792,6 +796,28 @@
> result = target.
> self.assertEqua
>
> + def test_private_
> + """private bugs from a search know the user can see the bugs."""
Captialise at the start of the sentence ;).
> + target = self.makeBugTar
> + person = self.login()
> + self.factory.
> + self.factory.
> + # Search style and parameters taken from the milestone index view where
> + # the issue was discovered.
> + login_person(
> + tasks =target.
Needs a space after the =.
> + orderby=['status', '-importance', 'id']))
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -105,5 +116,61 @@
> self.assertEqual(0, product.
>
>
> -def test_suite():
> - return unittest.
> +class TestMilestoneIn
> +
> + layer = DatabaseFunctio
> +
> + def test_more_
The test should have a comment at the start of it to save me having to
parse the method name.
> + product = self.factory.
> + login_person(
> + milestone = self.factory.
> + productseries=
> + bug1 = self.factory.
> + owner=product.
> + bug1.bugtasks[
> + # We look at the page as someone who is a member of a team and the team
> + # is subscribed to the bugs, so that we don't get trivial shortcuts
> + # avoiding queries : test the worst case.
> + subscribed_team = self.factory.
> + viewer = self.factory.
> + with person_
> + subscribed_
> + bug1.subscribe(
> + bug1_url = canonical_url(bug1)
> + milestone_url = canonical_
> + browser = self.getUserBro
> + # Seed the cookie cache and any other cross-request state we may gain
> + # in future. See canonical.
> + browser.
> + collector = QueryC...
Robert Collins (lifeless) wrote : | # |
I'll do the tweaks; I don't see a good way (for now) of shrinking the
test; two separate tests would require a magic constant to be shared
between them rather than being derived on the fly; that would be more
fragile.
-Rob
Graham Binns (gmb) wrote : | # |
On 17 August 2010 11:48, Robert Collins <email address hidden> wrote:
> I'll do the tweaks; I don't see a good way (for now) of shrinking the
> test; two separate tests would require a magic constant to be shared
> between them rather than being derived on the fly; that would be more
> fragile.
>
Right, that's pretty much what I figured.
Graham Binns (gmb) : | # |
Curtis Hovey (sinzui) wrote : | # |
MilestoneView.
MilestoneView.
Robert Collins (lifeless) wrote : | # |
On Wed, Aug 18, 2010 at 2:18 AM, Curtis Hovey
<email address hidden> wrote:
> Review: Needs Information
> MilestoneView.
For objects queries via that search path, the precaching be removed. I
do not know if getConjoinedMaster accesses the related objects via
that code path - and I suspect in fact that it does not.
> MilestoneView.
We can refactor the conjoined master stuff too, but its a separate issue IMO.
-Rob
Curtis Hovey (sinzui) wrote : | # |
I do not understand the first part of your answer. I see this in MilestoneView.
...
for task in tasks:
if task.getConjoin
# Checking bug permissions is expensive. We know from the query that
# the user has at least launchpad.View on the bugtasks and their bugs.
return non_conjoined_
Since you modified the permission rules on bugtask, I wonder if the first precache_
We stopped adding enahncements to the milestone page when the preformance problems started, and we cannot entertain allowing users to edit the bugtasks unless we know the user has launchpad.Edit. I wonder if this permission change allows us to consider enhancements.
Robert Collins (lifeless) wrote : | # |
I certainly think you can make enchancements, and yes we should be
able to remove the precache permissions for nonconjoinedslaves.
I'd like to do that separately from this branch, as a mitigation : I
know what I've researched works, I'm not sure that there aren't other
gremlins in there.
Curtis Hovey (sinzui) wrote : | # |
Yes. I agree that the removal of precache_
Deryck Hodge (deryck) wrote : | # |
Looks good to me, Robert. Thanks for doing this work!
Cheers,
deryck
Robert Collins (lifeless) wrote : | # |
I hate to ask for a rereview, but I'll ask anyhow. This branch ran into incompatibilities with SQLObjectResultSet so it now converts buildQuery and thing things on it to be less SQLObject and more Storm. This involved finding addressing a bunch of differences; it still has literal SQL and so on, but at least at the surface its now sensible. I'm running it through EC2 test now.
Curtis Hovey (sinzui) wrote : | # |
Hi Robert.
Thanks for this refactoring.
Can you place a comment before ``if False and True:`` in BugtaskSet.search() to explain the dagnostic output? I had to think about the old and new guard.
I think you can remove the naked taskset in BugTaskSet.
Robert Collins (lifeless) wrote : | # |
Thank you curtis; I've deleted the naked taskset, and deleted the
print statements I had accidentally left in which had that confusing
if statement.
This is the only failing test ec2 told me about
FAIL: productseries-
-------
Text attachment: traceback
------------
Traceback (most recent call last):
File "/usr/lib/
testMethod()
File "/usr/lib/
raise self.failureExc
AssertionError: Failed doctest test for productseries-
File "lib/lp/
-------
File "lib/lp/
in productseries-
Failed example:
script = find_tag_
Differences (ndiff with -expected +actual):
+ WARNING:
pt:testrunner:
-------
File "lib/lp/
102, in productseries-
Failed example:
content = view.render()
Differences (ndiff with -expected +actual):
+ WARNING:
pt:testrunner:
-------
File "lib/lp/
117, in productseries-
Failed example:
table = find_tag_
Differences (ndiff with -expected +actual):
+ WARNING:
pt:testrunner:
If you have any ideas about it, I'd love to know.
Curtis Hovey (sinzui) wrote : | # |
productseries-
Add this to special_test_layer in lp/registry/
'productser
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/validation.py' |
2 | --- lib/canonical/launchpad/interfaces/validation.py 2010-08-20 20:31:18 +0000 |
3 | +++ lib/canonical/launchpad/interfaces/validation.py 2010-08-22 18:34:46 +0000 |
4 | @@ -312,7 +312,7 @@ |
5 | errors = [] |
6 | user = getUtility(ILaunchBag).user |
7 | params = BugTaskSearchParams(user, bug=bug) |
8 | - if product.searchTasks(params): |
9 | + if not product.searchTasks(params).is_empty(): |
10 | errors.append(LaunchpadValidationError(_( |
11 | 'A fix for this bug has already been requested for ${product}', |
12 | mapping={'product': product.displayname}))) |
13 | |
14 | === modified file 'lib/canonical/launchpad/security.py' |
15 | --- lib/canonical/launchpad/security.py 2010-08-20 20:31:18 +0000 |
16 | +++ lib/canonical/launchpad/security.py 2010-08-22 18:34:46 +0000 |
17 | @@ -988,24 +988,8 @@ |
18 | usedfor = IHasBug |
19 | |
20 | def checkAuthenticated(self, user): |
21 | - |
22 | - if user.in_admin: |
23 | - # Admins can always edit bugtasks, whether they're reported on a |
24 | - # private bug or not. |
25 | - return True |
26 | - |
27 | - if not self.obj.bug.private: |
28 | - # This is a public bug, so anyone can edit it. |
29 | - return True |
30 | - else: |
31 | - # This is a private bug, and we know the user isn't an admin, so |
32 | - # we'll only allow editing if the user is explicitly subscribed to |
33 | - # this bug. |
34 | - for subscription in self.obj.bug.subscriptions: |
35 | - if user.inTeam(subscription.person): |
36 | - return True |
37 | - |
38 | - return False |
39 | + # Delegated entirely to the bug. |
40 | + return self.obj.bug.userCanView(user) |
41 | |
42 | |
43 | class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase): |
44 | @@ -1027,21 +1011,10 @@ |
45 | |
46 | def checkAuthenticated(self, user): |
47 | """Allow any logged in user to edit a public bug, and only |
48 | - explicit subscribers to edit private bugs. |
49 | + explicit subscribers to edit private bugs. Any bug that can be seen can |
50 | + be edited. |
51 | """ |
52 | - if not self.obj.private: |
53 | - # This is a public bug. |
54 | - return True |
55 | - elif user.in_admin: |
56 | - # Admins can edit all bugs. |
57 | - return True |
58 | - else: |
59 | - # This is a private bug. Only explicit subscribers may edit it. |
60 | - for subscription in self.obj.subscriptions: |
61 | - if user.inTeam(subscription.person): |
62 | - return True |
63 | - |
64 | - return False |
65 | + return self.obj.userCanView(user) |
66 | |
67 | def checkUnauthenticated(self): |
68 | """Never allow unauthenticated users to edit a bug.""" |
69 | |
70 | === modified file 'lib/canonical/launchpad/utilities/personroles.py' |
71 | --- lib/canonical/launchpad/utilities/personroles.py 2010-08-20 20:31:18 +0000 |
72 | +++ lib/canonical/launchpad/utilities/personroles.py 2010-08-22 18:34:46 +0000 |
73 | @@ -41,6 +41,10 @@ |
74 | except AttributeError: |
75 | raise AttributeError(errortext) |
76 | |
77 | + @property |
78 | + def id(self): |
79 | + return self.person.id |
80 | + |
81 | def isOwner(self, obj): |
82 | """See IPersonRoles.""" |
83 | return self.person.inTeam(obj.owner) |
84 | |
85 | === modified file 'lib/lp/bugs/browser/bugtarget.py' |
86 | --- lib/lp/bugs/browser/bugtarget.py 2010-08-20 20:31:18 +0000 |
87 | +++ lib/lp/bugs/browser/bugtarget.py 2010-08-22 18:34:46 +0000 |
88 | @@ -966,13 +966,8 @@ |
89 | |
90 | matching_bugtasks = getUtility(IBugTaskSet).findSimilar( |
91 | self.user, title, **context_params) |
92 | - # Remove all the prejoins, since we won't use them and they slow |
93 | - # down the query significantly. |
94 | - matching_bugtasks = matching_bugtasks.prejoin([]) |
95 | - |
96 | matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks( |
97 | matching_bugtasks, self.user, self._MATCHING_BUGS_LIMIT) |
98 | - |
99 | return matching_bugs |
100 | |
101 | @property |
102 | |
103 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
104 | --- lib/lp/bugs/browser/bugtask.py 2010-08-20 20:31:18 +0000 |
105 | +++ lib/lp/bugs/browser/bugtask.py 2010-08-22 18:34:46 +0000 |
106 | @@ -3197,25 +3197,8 @@ |
107 | else: |
108 | raise AssertionError('Uknown context type: %s' % self.context) |
109 | |
110 | - # XXX flacoste 2008/04/25 bug=221947 This should be moved to an |
111 | - # IBugTaskSet.findBugIds(search_params) method. |
112 | - # buildQuery() is part of the internal API. |
113 | - taskset = removeSecurityProxy(getUtility(IBugTaskSet)) |
114 | - query, clauseTables, orderBy = taskset.buildQuery(search_params) |
115 | - |
116 | - # Convert list of SQLObject order by spec into SQL. |
117 | - order_by_clause = [] |
118 | - for field in orderBy: |
119 | - if field[0] == '-': |
120 | - field = "%s DESC" % field[1:] |
121 | - order_by_clause.append(field) |
122 | - |
123 | - sql = 'SELECT BugTask.bug FROM %s WHERE %s ORDER BY %s' % ( |
124 | - ', '.join(clauseTables), query, ', '.join(order_by_clause)) |
125 | - |
126 | - cur = cursor() |
127 | - cur.execute(sql) |
128 | - return u"".join("%d\n" % row[0] for row in cur.fetchall()) |
129 | + return u"".join("%d\n" % bug_id for bug_id in |
130 | + getUtility(IBugTaskSet).searchBugIds(search_params)) |
131 | |
132 | |
133 | def _by_targetname(bugtask): |
134 | |
135 | === modified file 'lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt' |
136 | --- lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt 2009-06-12 16:36:02 +0000 |
137 | +++ lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt 2010-08-22 18:34:46 +0000 |
138 | @@ -73,7 +73,7 @@ |
139 | >>> published_distro_series = factory.makeDistroRelease( |
140 | ... published_distro, name='published-distro-series') |
141 | >>> publisher.setUpDefaultDistroSeries(published_distro_series) |
142 | - <DistroSeries at ...> |
143 | + <DistroSeries u'published-distro-series'> |
144 | >>> print publisher.person.unique_displayname |
145 | Foo Bar (name16) |
146 | >>> no_priv = getUtility(IPersonSet).getByName('no-priv') |
147 | |
148 | === modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py' |
149 | --- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2010-08-20 20:31:18 +0000 |
150 | +++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2010-08-22 18:34:46 +0000 |
151 | @@ -212,7 +212,7 @@ |
152 | |
153 | def test_getAcknowledgementMessage_dsp_custom_distro_message(self): |
154 | # If a distribution has a customized conformatom message, it |
155 | - # is used for bugs filed on DsitributionSourcePackages. |
156 | + # is used for bugs filed on DistributionSourcePackages. |
157 | dsp = self.factory.makeDistributionSourcePackage() |
158 | dsp.distribution.bug_reported_acknowledgement = ( |
159 | u"Thank you for filing a bug in our distribution") |
160 | |
161 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
162 | --- lib/lp/bugs/interfaces/bugtask.py 2010-08-20 20:31:18 +0000 |
163 | +++ lib/lp/bugs/interfaces/bugtask.py 2010-08-22 18:34:46 +0000 |
164 | @@ -1398,6 +1398,14 @@ |
165 | orderby specified in the first BugTaskSearchParams object. |
166 | """ |
167 | |
168 | + def searchBugIds(params): |
169 | + """Search bug ids. |
170 | + |
171 | + This is a variation on IBugTaskSet.search that returns only the bug ids. |
172 | + |
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 | |
180 | === modified file 'lib/lp/bugs/model/bug.py' |
181 | --- lib/lp/bugs/model/bug.py 2010-08-20 20:31:18 +0000 |
182 | +++ lib/lp/bugs/model/bug.py 2010-08-22 18:34:46 +0000 |
183 | @@ -70,6 +70,10 @@ |
184 | providedBy, |
185 | ) |
186 | |
187 | +from canonical.cachedproperty import ( |
188 | + cachedproperty, |
189 | + clear_property, |
190 | + ) |
191 | from canonical.config import config |
192 | from canonical.database.constants import UTC_NOW |
193 | from canonical.database.datetimecol import UtcDateTimeCol |
194 | @@ -85,7 +89,10 @@ |
195 | MessageSet, |
196 | ) |
197 | from canonical.launchpad.helpers import shortlist |
198 | -from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
199 | +from canonical.launchpad.interfaces.launchpad import ( |
200 | + ILaunchpadCelebrities, |
201 | + IPersonRoles, |
202 | + ) |
203 | from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet |
204 | from canonical.launchpad.interfaces.lpstorm import IStore |
205 | from canonical.launchpad.interfaces.message import ( |
206 | @@ -626,6 +633,7 @@ |
207 | # disabled see the change. |
208 | store.flush() |
209 | self.updateHeat() |
210 | + clear_property(self, '_cached_viewers') |
211 | return |
212 | |
213 | def unsubscribeFromDupes(self, person, unsubscribed_by): |
214 | @@ -1618,21 +1626,31 @@ |
215 | self, self.messages[comment_number]) |
216 | bug_message.visible = visible |
217 | |
218 | + @cachedproperty('_cached_viewers') |
219 | + def _known_viewers(self): |
220 | + """A dict of of known persons able to view this bug.""" |
221 | + return set() |
222 | + |
223 | def userCanView(self, user): |
224 | - """See `IBug`.""" |
225 | - admins = getUtility(ILaunchpadCelebrities).admin |
226 | + """See `IBug`. |
227 | + |
228 | + Note that Editing is also controlled by this check, |
229 | + because we permit editing of any bug one can see. |
230 | + """ |
231 | + if user.id in self._known_viewers: |
232 | + return True |
233 | if not self.private: |
234 | # This is a public bug. |
235 | return True |
236 | - elif user.inTeam(admins): |
237 | + elif IPersonRoles(user).in_admin: |
238 | # Admins can view all bugs. |
239 | return True |
240 | else: |
241 | # This is a private bug. Only explicit subscribers may view it. |
242 | for subscription in self.subscriptions: |
243 | if user.inTeam(subscription.person): |
244 | + self._known_viewers.add(user.id) |
245 | return True |
246 | - |
247 | return False |
248 | |
249 | def linkHWSubmission(self, submission): |
250 | @@ -1706,6 +1724,7 @@ |
251 | |
252 | self.heat = SQL("calculate_bug_heat(%s)" % sqlvalues(self)) |
253 | self.heat_last_updated = UTC_NOW |
254 | + store.flush() |
255 | |
256 | @property |
257 | def attachments(self): |
258 | @@ -1942,6 +1961,10 @@ |
259 | # Transaction.iterSelect() will try to listify the results. |
260 | # This can be fixed by selecting from Bugs directly, but |
261 | # that's non-trivial. |
262 | + # ---: Robert Collins 20100818: if bug_tasks implements IResultSset |
263 | + # then it should be very possible to improve on it, though |
264 | + # DecoratedResultSets would need careful handling (e.g. type |
265 | + # driven callbacks on columns) |
266 | # We select more than :limit: since if a bug affects more than |
267 | # one source package, it will be returned more than one time. 4 |
268 | # is an arbitrary number that should be large enough. |
269 | |
270 | === modified file 'lib/lp/bugs/model/bugtarget.py' |
271 | --- lib/lp/bugs/model/bugtarget.py 2010-08-20 20:31:18 +0000 |
272 | +++ lib/lp/bugs/model/bugtarget.py 2010-08-22 18:34:46 +0000 |
273 | @@ -207,13 +207,7 @@ |
274 | @property |
275 | def has_bugtasks(self): |
276 | """See `IHasBugs`.""" |
277 | - # Check efficiently if any bugtasks exist. We should avoid |
278 | - # expensive calls like all_bugtasks.count(). all_bugtasks |
279 | - # returns a storm.SQLObjectResultSet instance, and this |
280 | - # class does not provide methods like is_empty(). But we can |
281 | - # indirectly call SQLObjectResultSet._result_set.is_empty() |
282 | - # by converting all_bugtasks into a boolean object. |
283 | - return bool(self.all_bugtasks) |
284 | + return not self.all_bugtasks.is_empty() |
285 | |
286 | def getBugCounts(self, user, statuses=None): |
287 | """See `IHasBugs`.""" |
288 | |
289 | === modified file 'lib/lp/bugs/model/bugtask.py' |
290 | --- lib/lp/bugs/model/bugtask.py 2010-08-20 20:31:18 +0000 |
291 | +++ lib/lp/bugs/model/bugtask.py 2010-08-22 18:34:46 +0000 |
292 | @@ -21,7 +21,7 @@ |
293 | |
294 | |
295 | import datetime |
296 | -from operator import attrgetter |
297 | +from operator import attrgetter, itemgetter |
298 | |
299 | from lazr.enum import DBItem |
300 | import pytz |
301 | @@ -35,6 +35,7 @@ |
302 | Alias, |
303 | And, |
304 | AutoTables, |
305 | + Desc, |
306 | In, |
307 | Join, |
308 | LeftJoin, |
309 | @@ -58,6 +59,7 @@ |
310 | removeSecurityProxy, |
311 | ) |
312 | |
313 | +from canonical.cachedproperty import cache_property |
314 | from canonical.config import config |
315 | from canonical.database.constants import UTC_NOW |
316 | from canonical.database.datetimecol import UtcDateTimeCol |
317 | @@ -72,6 +74,7 @@ |
318 | SQLBase, |
319 | sqlvalues, |
320 | ) |
321 | +from canonical.launchpad.components.decoratedresultset import DecoratedResultSet |
322 | from canonical.launchpad.helpers import shortlist |
323 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
324 | from canonical.launchpad.interfaces.lpstorm import IStore |
325 | @@ -118,6 +121,7 @@ |
326 | UserCannotEditBugTaskMilestone, |
327 | UserCannotEditBugTaskStatus, |
328 | ) |
329 | +from lp.bugs.model.bugnomination import BugNomination |
330 | from lp.bugs.model.bugsubscription import BugSubscription |
331 | from lp.registry.interfaces.distribution import ( |
332 | IDistribution, |
333 | @@ -148,7 +152,10 @@ |
334 | from lp.registry.interfaces.sourcepackage import ISourcePackage |
335 | from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet |
336 | from lp.registry.model.pillar import pillar_sort_key |
337 | +from lp.registry.model.sourcepackagename import SourcePackageName |
338 | from lp.soyuz.interfaces.publishing import PackagePublishingStatus |
339 | +from lp.soyuz.model.publishing import SourcePackagePublishingHistory |
340 | +from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
341 | |
342 | |
343 | debbugsseveritymap = {None: BugTaskImportance.UNDECIDED, |
344 | @@ -673,9 +680,6 @@ |
345 | |
346 | matching_bugtasks = getUtility(IBugTaskSet).findSimilar( |
347 | user, self.bug.title, **context_params) |
348 | - # Remove all the prejoins, since we won't use them and they slow |
349 | - # down the query significantly. |
350 | - matching_bugtasks = matching_bugtasks.prejoin([]) |
351 | |
352 | # Make sure to exclude the current BugTask from the list of |
353 | # matching tasks. We use 4*limit as an arbitrary value here to |
354 | @@ -1302,24 +1306,54 @@ |
355 | |
356 | def get_bug_privacy_filter(user): |
357 | """An SQL filter for search results that adds privacy-awareness.""" |
358 | + return get_bug_privacy_filter_with_decorator(user)[0] |
359 | + |
360 | + |
361 | +def _nocache_bug_decorator(obj): |
362 | + """A pass through decorator for consistency. |
363 | + |
364 | + :seealso: get_bug_privacy_filter_with_decorator |
365 | + """ |
366 | + return obj |
367 | + |
368 | + |
369 | +def _make_cache_user_can_view_bug(user): |
370 | + """Curry a decorator for bugtask queries to cache permissions. |
371 | + |
372 | + :seealso: get_bug_privacy_filter_with_decorator |
373 | + """ |
374 | + userid = user.id |
375 | + def cache_user_can_view_bug(bugtask): |
376 | + cache_property(bugtask.bug, '_cached_viewers', set([userid])) |
377 | + return bugtask |
378 | + return cache_user_can_view_bug |
379 | + |
380 | + |
381 | +def get_bug_privacy_filter_with_decorator(user): |
382 | + """Return a SQL filter to limit returned bug tasks. |
383 | + |
384 | + :return: A SQL filter, a decorator to cache visibility in a resultset that |
385 | + returns BugTask objects. |
386 | + """ |
387 | if user is None: |
388 | - return "Bug.private = FALSE" |
389 | + return "Bug.private = FALSE", _nocache_bug_decorator |
390 | admin_team = getUtility(ILaunchpadCelebrities).admin |
391 | if user.inTeam(admin_team): |
392 | - return "" |
393 | + return "", _nocache_bug_decorator |
394 | # A subselect is used here because joining through |
395 | # TeamParticipation is only relevant to the "user-aware" |
396 | # part of the WHERE condition (i.e. the bit below.) The |
397 | # other half of this condition (see code above) does not |
398 | # use TeamParticipation at all. |
399 | - return """ |
400 | + return (""" |
401 | (Bug.private = FALSE OR EXISTS ( |
402 | SELECT BugSubscription.bug |
403 | FROM BugSubscription, TeamParticipation |
404 | WHERE TeamParticipation.person = %(personid)s AND |
405 | BugSubscription.person = TeamParticipation.team AND |
406 | BugSubscription.bug = Bug.id)) |
407 | - """ % sqlvalues(personid=user.id) |
408 | + """ % sqlvalues(personid=user.id), |
409 | + _make_cache_user_can_view_bug(user)) |
410 | |
411 | |
412 | def build_tag_set_query(joiner, tags): |
413 | @@ -1408,24 +1442,7 @@ |
414 | """See `IBugTaskSet`.""" |
415 | implements(IBugTaskSet) |
416 | |
417 | - _ORDERBY_COLUMN = { |
418 | - "id": "BugTask.bug", |
419 | - "importance": "BugTask.importance", |
420 | - "assignee": "BugTask.assignee", |
421 | - "targetname": "BugTask.targetnamecache", |
422 | - "status": "BugTask.status", |
423 | - "title": "Bug.title", |
424 | - "milestone": "BugTask.milestone", |
425 | - "dateassigned": "BugTask.dateassigned", |
426 | - "datecreated": "BugTask.datecreated", |
427 | - "date_last_updated": "Bug.date_last_updated", |
428 | - "date_closed": "BugTask.date_closed", |
429 | - "number_of_duplicates": "Bug.number_of_duplicates", |
430 | - "message_count": "Bug.message_count", |
431 | - "users_affected_count": "Bug.users_affected_count", |
432 | - "heat": "Bug.heat", |
433 | - "latest_patch_uploaded": "Bug.latest_patch_uploaded", |
434 | - } |
435 | + _ORDERBY_COLUMN = None |
436 | |
437 | _open_resolved_upstream = """ |
438 | EXISTS ( |
439 | @@ -1544,7 +1561,7 @@ |
440 | |
441 | search_params.fast_searchtext = nl_phrase_search( |
442 | summary, Bug, ' AND '.join(constraint_clauses), ['BugTask']) |
443 | - return self.search(search_params) |
444 | + return self.search(search_params, _noprejoins=True) |
445 | |
446 | def _buildStatusClause(self, status): |
447 | """Return the SQL query fragment for search by status. |
448 | @@ -1591,11 +1608,15 @@ |
449 | """Build and return an SQL query with the given parameters. |
450 | |
451 | Also return the clauseTables and orderBy for the generated query. |
452 | + |
453 | + :return: A query, the tables to query, ordering expression and a |
454 | + decorator to call on each returned row. |
455 | """ |
456 | assert isinstance(params, BugTaskSearchParams) |
457 | - |
458 | + from lp.bugs.model.bug import Bug |
459 | extra_clauses = ['Bug.id = BugTask.bug'] |
460 | - clauseTables = ['BugTask', 'Bug'] |
461 | + clauseTables = [BugTask, Bug] |
462 | + decorators = [] |
463 | |
464 | # These arguments can be processed in a loop without any other |
465 | # special handling. |
466 | @@ -1651,7 +1672,9 @@ |
467 | extra_clauses.append("BugTask.milestone %s" % where_cond) |
468 | |
469 | if params.project: |
470 | - clauseTables.append("Product") |
471 | + # Circular. |
472 | + from lp.registry.model.product import Product |
473 | + clauseTables.append(Product) |
474 | extra_clauses.append("BugTask.product = Product.id") |
475 | if isinstance(params.project, any): |
476 | extra_clauses.append("Product.project IN (%s)" % ",".join( |
477 | @@ -1694,7 +1717,7 @@ |
478 | extra_clauses.append(self._buildFastSearchTextClause(params)) |
479 | |
480 | if params.subscriber is not None: |
481 | - clauseTables.append('BugSubscription') |
482 | + clauseTables.append(BugSubscription) |
483 | extra_clauses.append("""Bug.id = BugSubscription.bug AND |
484 | BugSubscription.person = %(personid)s""" % |
485 | sqlvalues(personid=params.subscriber.id)) |
486 | @@ -1742,8 +1765,8 @@ |
487 | extra_clauses.append(structural_subscriber_clause) |
488 | |
489 | if params.component: |
490 | - clauseTables += ["SourcePackagePublishingHistory", |
491 | - "SourcePackageRelease"] |
492 | + clauseTables += [SourcePackagePublishingHistory, |
493 | + SourcePackageRelease] |
494 | distroseries = None |
495 | if params.distribution: |
496 | distroseries = params.distribution.currentseries |
497 | @@ -1866,11 +1889,12 @@ |
498 | BugNomination.status = %(nomination_status)s |
499 | """ % mappings |
500 | extra_clauses.append(nominated_for_clause) |
501 | - clauseTables.append('BugNomination') |
502 | + clauseTables.append(BugNomination) |
503 | |
504 | - clause = get_bug_privacy_filter(params.user) |
505 | + clause, decorator = get_bug_privacy_filter_with_decorator(params.user) |
506 | if clause: |
507 | extra_clauses.append(clause) |
508 | + decorators.append(decorator) |
509 | |
510 | hw_clause = self._buildHardwareRelatedClause(params) |
511 | if hw_clause is not None: |
512 | @@ -1899,7 +1923,15 @@ |
513 | orderby_arg = self._processOrderBy(params) |
514 | |
515 | query = " AND ".join(extra_clauses) |
516 | - return query, clauseTables, orderby_arg |
517 | + |
518 | + if not decorators: |
519 | + decorator = lambda x:x |
520 | + else: |
521 | + def decorator(obj): |
522 | + for decor in decorators: |
523 | + obj = decor(obj) |
524 | + return obj |
525 | + return query, clauseTables, orderby_arg, decorator |
526 | |
527 | def _buildUpstreamClause(self, params): |
528 | """Return an clause for returning upstream data if the data exists. |
529 | @@ -2127,34 +2159,79 @@ |
530 | ', '.join(tables), ' AND '.join(clauses)) |
531 | return clause |
532 | |
533 | - def search(self, params, *args): |
534 | - """See `IBugTaskSet`.""" |
535 | - store_selector = getUtility(IStoreSelector) |
536 | - store = store_selector.get(MAIN_STORE, SLAVE_FLAVOR) |
537 | - query, clauseTables, orderby = self.buildQuery(params) |
538 | + def search(self, params, *args, **kwargs): |
539 | + """See `IBugTaskSet`. |
540 | + |
541 | + :param _noprejoins: Private internal parameter to BugTaskSet which |
542 | + disables all use of prejoins : consolidated from code paths that |
543 | + claim they were inefficient and unwanted. |
544 | + """ |
545 | + # Circular. |
546 | + from lp.registry.model.product import Product |
547 | + from lp.bugs.model.bug import Bug |
548 | + _noprejoins = kwargs.get('_noprejoins', False) |
549 | + store = IStore(BugTask) |
550 | + query, clauseTables, orderby, bugtask_decorator = self.buildQuery(params) |
551 | if len(args) == 0: |
552 | - # Do normal prejoins, if we don't have to do any UNION |
553 | - # queries. Prejoins don't work well with UNION, and the way |
554 | - # we prefetch objects without prejoins cause problems with |
555 | - # COUNT(*) queries, which get inefficient. |
556 | - return BugTask.select( |
557 | - query, clauseTables=clauseTables, orderBy=orderby, |
558 | - prejoins=['product', 'sourcepackagename'], |
559 | - prejoinClauseTables=['Bug']) |
560 | + if _noprejoins: |
561 | + resultset = store.find(BugTask, |
562 | + AutoTables(SQL("1=1"), clauseTables), |
563 | + query) |
564 | + decorator = bugtask_decorator |
565 | + else: |
566 | + tables = clauseTables + [Product, SourcePackageName] |
567 | + origin = [ |
568 | + BugTask, |
569 | + LeftJoin(Bug, BugTask.bug == Bug.id), |
570 | + LeftJoin(Product, BugTask.product == Product.id), |
571 | + LeftJoin( |
572 | + SourcePackageName, |
573 | + BugTask.sourcepackagename == SourcePackageName.id), |
574 | + ] |
575 | + # NB: these may work with AutoTables, but its hard to tell, |
576 | + # this way is known to work. |
577 | + if BugNomination in tables: |
578 | + # The relation is already in query. |
579 | + origin.append(BugNomination) |
580 | + if BugSubscription in tables: |
581 | + # The relation is already in query. |
582 | + origin.append(BugSubscription) |
583 | + if SourcePackageRelease in tables: |
584 | + origin.append(SourcePackageRelease) |
585 | + if SourcePackagePublishingHistory in tables: |
586 | + origin.append(SourcePackagePublishingHistory) |
587 | + resultset = store.using(*origin).find( |
588 | + (BugTask, Product, SourcePackageName, Bug), |
589 | + AutoTables(SQL("1=1"), tables), |
590 | + query) |
591 | + decorator=lambda row:bugtask_decorator(row[0]) |
592 | + resultset.order_by(orderby) |
593 | + return DecoratedResultSet(resultset, result_decorator=decorator) |
594 | |
595 | bugtask_fti = SQL('BugTask.fti') |
596 | result = store.find((BugTask, bugtask_fti), query, |
597 | AutoTables(SQL("1=1"), clauseTables)) |
598 | + decorators = [bugtask_decorator] |
599 | for arg in args: |
600 | - query, clauseTables, dummy = self.buildQuery(arg) |
601 | + query, clauseTables, dummy, decorator = self.buildQuery(arg) |
602 | result = result.union( |
603 | store.find((BugTask, bugtask_fti), query, |
604 | AutoTables(SQL("1=1"), clauseTables))) |
605 | + # NB: assumes the decorators are all compatible. |
606 | + # This may need revisiting if e.g. searches on behalf of different |
607 | + # users are combined. |
608 | + decorators.append(decorator) |
609 | + def decorator(row): |
610 | + bugtask = row[0] |
611 | + for decorator in decorators: |
612 | + bugtask = decorator(bugtask) |
613 | + return bugtask |
614 | |
615 | - # Build up the joins |
616 | - from lp.bugs.model.bug import Bug |
617 | - from lp.registry.model.product import Product |
618 | - from lp.registry.model.sourcepackagename import SourcePackageName |
619 | + # Build up the joins. |
620 | + # TODO: implement _noprejoins for this code path: as of 20100818 it has |
621 | + # been silently disabled because clients of the API were setting |
622 | + # prejoins=[] which had no effect; this TODO simply notes the reality |
623 | + # already existing when it was added. |
624 | joins = Alias(result._get_select(), "BugTask") |
625 | joins = Join(joins, Bug, BugTask.bug == Bug.id) |
626 | joins = LeftJoin(joins, Product, BugTask.product == Product.id) |
627 | @@ -2163,9 +2240,18 @@ |
628 | |
629 | result = store.using(joins).find( |
630 | (BugTask, Bug, Product, SourcePackageName)) |
631 | - bugtasks = SQLObjectResultSet(BugTask, orderBy=orderby, |
632 | - prepared_result_set=result) |
633 | - return bugtasks |
634 | + result.order_by(orderby) |
635 | + return DecoratedResultSet(result, result_decorator=decorator) |
636 | + |
637 | + def searchBugIds(self, params): |
638 | + """See `IBugTaskSet`.""" |
639 | + query, clauseTables, orderby, decorator = self.buildQuery( |
640 | + params) |
641 | + store = IStore(BugTask) |
642 | + resultset = store.find(BugTask.bugID, |
643 | + AutoTables(SQL("1=1"), clauseTables), query) |
644 | + resultset.order_by(orderby) |
645 | + return resultset |
646 | |
647 | def getAssignedMilestonesFromSearch(self, search_results): |
648 | """See `IBugTaskSet`.""" |
649 | @@ -2193,7 +2279,14 @@ |
650 | # Import here because of cyclic references. |
651 | from lp.registry.model.milestone import ( |
652 | Milestone, milestone_sort_key) |
653 | - milestones = search_results._store.find( |
654 | + # We need the store that was used, we have no objects to key off of |
655 | + # other than the search result, and Store.of() doesn't currently |
656 | + # work on result sets. Additionally it may be a DecoratedResultSet. |
657 | + if zope_isinstance(search_results, DecoratedResultSet): |
658 | + store = removeSecurityProxy(search_results).result_set._store |
659 | + else: |
660 | + store = search_results._store |
661 | + milestones = store.find( |
662 | Milestone, In(Milestone.id, milestone_ids)) |
663 | return sorted(milestones, key=milestone_sort_key, reverse=True) |
664 | |
665 | @@ -2514,14 +2607,40 @@ |
666 | |
667 | def getOrderByColumnDBName(self, col_name): |
668 | """See `IBugTaskSet`.""" |
669 | - return self._ORDERBY_COLUMN[col_name] |
670 | + if BugTaskSet._ORDERBY_COLUMN is None: |
671 | + # Local import of Bug to avoid import loop. |
672 | + from lp.bugs.model.bug import Bug |
673 | + BugTaskSet._ORDERBY_COLUMN = { |
674 | + "id": BugTask.bugID, |
675 | + "importance": BugTask.importance, |
676 | + # TODO: sort by their name? |
677 | + "assignee": BugTask.assigneeID, |
678 | + "targetname": BugTask.targetnamecache, |
679 | + "status": BugTask.status, |
680 | + "title": Bug.title, |
681 | + "milestone": BugTask.milestoneID, |
682 | + "dateassigned": BugTask.date_assigned, |
683 | + "datecreated": BugTask.datecreated, |
684 | + "date_last_updated": Bug.date_last_updated, |
685 | + "date_closed": BugTask.date_closed, |
686 | + "number_of_duplicates": Bug.number_of_duplicates, |
687 | + "message_count": Bug.message_count, |
688 | + "users_affected_count": Bug.users_affected_count, |
689 | + "heat": Bug.heat, |
690 | + "latest_patch_uploaded": Bug.latest_patch_uploaded, |
691 | + } |
692 | + return BugTaskSet._ORDERBY_COLUMN[col_name] |
693 | |
694 | def _processOrderBy(self, params): |
695 | """Process the orderby parameter supplied to search(). |
696 | |
697 | This method ensures the sort order will be stable, and converting |
698 | the string supplied to actual column names. |
699 | + |
700 | + :return: A Storm order_by tuple. |
701 | """ |
702 | + # Local import of Bug to avoid import loop. |
703 | + from lp.bugs.model.bug import Bug |
704 | orderby = params.orderby |
705 | if orderby is None: |
706 | orderby = [] |
707 | @@ -2536,10 +2655,10 @@ |
708 | # columns to make the sort consistent over runs -- which is good |
709 | # for the user and essential for the test suite. |
710 | unambiguous_cols = set([ |
711 | - "BugTask.dateassigned", |
712 | - "BugTask.datecreated", |
713 | - "Bug.datecreated", |
714 | - "Bug.date_last_updated"]) |
715 | + BugTask.date_assigned, |
716 | + BugTask.datecreated, |
717 | + Bug.datecreated, |
718 | + Bug.date_last_updated]) |
719 | # Bug ID is unique within bugs on a product or source package. |
720 | if (params.product or |
721 | (params.distribution and params.sourcepackagename) or |
722 | @@ -2549,7 +2668,7 @@ |
723 | in_unique_context = False |
724 | |
725 | if in_unique_context: |
726 | - unambiguous_cols.add("BugTask.bug") |
727 | + unambiguous_cols.add(BugTask.bug) |
728 | |
729 | # Translate orderby keys into corresponding Table.attribute |
730 | # strings. |
731 | @@ -2559,22 +2678,22 @@ |
732 | orderby_arg.append(orderby_col) |
733 | continue |
734 | if orderby_col.startswith("-"): |
735 | - col_name = self.getOrderByColumnDBName(orderby_col[1:]) |
736 | - order_clause = "-" + col_name |
737 | + col = self.getOrderByColumnDBName(orderby_col[1:]) |
738 | + order_clause = Desc(col) |
739 | else: |
740 | - col_name = self.getOrderByColumnDBName(orderby_col) |
741 | - order_clause = col_name |
742 | - if col_name in unambiguous_cols: |
743 | + col = self.getOrderByColumnDBName(orderby_col) |
744 | + order_clause = col |
745 | + if col in unambiguous_cols: |
746 | ambiguous = False |
747 | orderby_arg.append(order_clause) |
748 | |
749 | if ambiguous: |
750 | if in_unique_context: |
751 | - orderby_arg.append('BugTask.bug') |
752 | + orderby_arg.append(BugTask.bugID) |
753 | else: |
754 | - orderby_arg.append('BugTask.id') |
755 | + orderby_arg.append(BugTask.id) |
756 | |
757 | - return orderby_arg |
758 | + return tuple(orderby_arg) |
759 | |
760 | def dangerousGetAllTasks(self): |
761 | """DO NOT USE THIS METHOD. For details, see `IBugTaskSet`""" |
762 | |
763 | === modified file 'lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt' |
764 | --- lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt 2009-08-31 15:41:15 +0000 |
765 | +++ lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt 2010-08-22 18:34:46 +0000 |
766 | @@ -11,10 +11,10 @@ |
767 | >>> launchbag = getUtility(ILaunchBag) |
768 | >>> evo = getUtility(ISourcePackageNameSet).queryByName("evolution") |
769 | >>> params = BugTaskSearchParams(user=launchbag.user, |
770 | - ... sourcepackagename=evo, orderby="id") |
771 | + ... sourcepackagename=evo, orderby="-id") |
772 | |
773 | >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu") |
774 | - >>> latest_evo_task = ubuntu.searchTasks(params)[-1] |
775 | + >>> latest_evo_task = ubuntu.searchTasks(params)[0] |
776 | >>> latest_evo_bug = latest_evo_task.bug.id |
777 | >>> logout() |
778 | |
779 | |
780 | === modified file 'lib/lp/bugs/tests/test_bugtask.py' |
781 | --- lib/lp/bugs/tests/test_bugtask.py 2010-08-20 20:31:18 +0000 |
782 | +++ lib/lp/bugs/tests/test_bugtask.py 2010-08-22 18:34:46 +0000 |
783 | @@ -25,6 +25,7 @@ |
784 | BugTaskImportance, |
785 | BugTaskSearchParams, |
786 | BugTaskStatus, |
787 | + IBugTaskSet, |
788 | ) |
789 | from lp.bugs.model.bugtask import build_tag_search_clause |
790 | from lp.hardwaredb.interfaces.hwdb import ( |
791 | @@ -32,7 +33,7 @@ |
792 | IHWDeviceSet, |
793 | ) |
794 | from lp.registry.interfaces.distribution import IDistributionSet |
795 | -from lp.registry.interfaces.person import IPersonSet |
796 | +from lp.registry.interfaces.person import IPerson, IPersonSet |
797 | from lp.testing import ( |
798 | ANONYMOUS, |
799 | login, |
800 | @@ -807,6 +808,28 @@ |
801 | result = target.searchTasks(None, modified_since=date) |
802 | self.assertEqual([task2], list(result)) |
803 | |
804 | + def test_private_bug_view_permissions_cached(self): |
805 | + """Private bugs from a search know the user can see the bugs.""" |
806 | + target = self.makeBugTarget() |
807 | + person = self.login() |
808 | + self.factory.makeBug(product=target, private=True, owner=person) |
809 | + self.factory.makeBug(product=target, private=True, owner=person) |
810 | + # Search style and parameters taken from the milestone index view where |
811 | + # the issue was discovered. |
812 | + login_person(person) |
813 | + tasks = target.searchTasks(BugTaskSearchParams(person, omit_dupes=True, |
814 | + orderby=['status', '-importance', 'id'])) |
815 | + # We must be finding the bugs. |
816 | + self.assertEqual(2, tasks.count()) |
817 | + # Cache in the storm cache the account->person lookup so its not |
818 | + # distorting what we're testing. |
819 | + _ = IPerson(person.account, None) |
820 | + # One query and only one should be issued to get the tasks, bugs and |
821 | + # allow access to getConjoinedMaster attribute - an attribute that |
822 | + # triggers a permission check (nb: id does not trigger such a check) |
823 | + self.assertStatementCount(1, |
824 | + lambda:[task.getConjoinedMaster for task in tasks]) |
825 | + |
826 | |
827 | def test_suite(): |
828 | suite = unittest.TestSuite() |
829 | |
830 | === modified file 'lib/lp/registry/browser/tests/test_milestone.py' |
831 | --- lib/lp/registry/browser/tests/test_milestone.py 2010-08-20 20:31:18 +0000 |
832 | +++ lib/lp/registry/browser/tests/test_milestone.py 2010-08-22 18:34:46 +0000 |
833 | @@ -1,24 +1,30 @@ |
834 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
835 | # GNU Affero General Public License version 3 (see the file LICENSE). |
836 | |
837 | +from __future__ import with_statement |
838 | + |
839 | """Test milestone views.""" |
840 | |
841 | __metaclass__ = type |
842 | |
843 | -import unittest |
844 | - |
845 | +from testtools.matchers import LessThan |
846 | from zope.component import getUtility |
847 | |
848 | +from canonical.launchpad.webapp import canonical_url |
849 | from canonical.testing.layers import DatabaseFunctionalLayer |
850 | from lp.bugs.interfaces.bugtask import IBugTaskSet |
851 | from lp.testing import ( |
852 | ANONYMOUS, |
853 | login, |
854 | login_person, |
855 | + logout, |
856 | + person_logged_in, |
857 | TestCaseWithFactory, |
858 | ) |
859 | +from lp.testing.matchers import HasQueryCount |
860 | from lp.testing.memcache import MemcacheTestCase |
861 | from lp.testing.views import create_initialized_view |
862 | +from lp.testing._webservice import QueryCollector |
863 | |
864 | |
865 | class TestMilestoneViews(TestCaseWithFactory): |
866 | @@ -158,5 +164,67 @@ |
867 | self.assertEqual(0, product.development_focus.all_bugtasks.count()) |
868 | |
869 | |
870 | -def test_suite(): |
871 | - return unittest.TestLoader().loadTestsFromName(__name__) |
872 | +class TestMilestoneIndex(TestCaseWithFactory): |
873 | + |
874 | + layer = DatabaseFunctionalLayer |
875 | + |
876 | + def test_more_private_bugs_query_count_is_constant(self): |
877 | + # This test tests that as we add more private bugs to a milestone index |
878 | + # page, the number of queries issued by the page does not change. |
879 | + # It also sets a cap on the queries for this page: if the baseline |
880 | + # were to increase, the test would fail. As the baseline is very large |
881 | + # already, if the test fails due to such a change, please cut some more |
882 | + # of the existing fat out of it rather than increasing the cap. |
883 | + product = self.factory.makeProduct() |
884 | + login_person(product.owner) |
885 | + milestone = self.factory.makeMilestone( |
886 | + productseries=product.development_focus) |
887 | + bug1 = self.factory.makeBug(product=product, private=True, |
888 | + owner=product.owner) |
889 | + bug1.bugtasks[0].transitionToMilestone(milestone, product.owner) |
890 | + # We look at the page as someone who is a member of a team and the team |
891 | + # is subscribed to the bugs, so that we don't get trivial shortcuts |
892 | + # avoiding queries : test the worst case. |
893 | + subscribed_team = self.factory.makeTeam() |
894 | + viewer = self.factory.makePerson(password="test") |
895 | + with person_logged_in(subscribed_team.teamowner): |
896 | + subscribed_team.addMember(viewer, subscribed_team.teamowner) |
897 | + bug1.subscribe(subscribed_team, product.owner) |
898 | + bug1_url = canonical_url(bug1) |
899 | + milestone_url = canonical_url(milestone) |
900 | + browser = self.getUserBrowser(user=viewer) |
901 | + # Seed the cookie cache and any other cross-request state we may gain |
902 | + # in future. See canonical.launchpad.webapp.serssion: _get_secret. |
903 | + browser.open(milestone_url) |
904 | + collector = QueryCollector() |
905 | + collector.register() |
906 | + self.addCleanup(collector.unregister) |
907 | + # This page is rather high in queries : 46 as a baseline. 20100817 |
908 | + page_query_limit = 46 |
909 | + browser.open(milestone_url) |
910 | + # Check that the test found the bug |
911 | + self.assertTrue(bug1_url in browser.contents) |
912 | + self.assertThat(collector, HasQueryCount(LessThan(page_query_limit))) |
913 | + with_1_private_bug = collector.count |
914 | + with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in |
915 | + enumerate(collector.queries)] |
916 | + login_person(product.owner) |
917 | + bug2 = self.factory.makeBug(product=product, private=True, |
918 | + owner=product.owner) |
919 | + bug2.bugtasks[0].transitionToMilestone(milestone, product.owner) |
920 | + bug2.subscribe(subscribed_team, product.owner) |
921 | + bug2_url = canonical_url(bug2) |
922 | + bug3 = self.factory.makeBug(product=product, private=True, |
923 | + owner=product.owner) |
924 | + bug3.bugtasks[0].transitionToMilestone(milestone, product.owner) |
925 | + bug3.subscribe(subscribed_team, product.owner) |
926 | + logout() |
927 | + browser.open(milestone_url) |
928 | + self.assertTrue(bug2_url in browser.contents) |
929 | + self.assertThat(collector, HasQueryCount(LessThan(page_query_limit))) |
930 | + with_3_private_bugs = collector.count |
931 | + with_3_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in |
932 | + enumerate(collector.queries)] |
933 | + self.assertEqual(with_1_private_bug, with_3_private_bugs, |
934 | + "different query count: \n%s\n******************\n%s\n" % ( |
935 | + '\n'.join(with_1_queries), '\n'.join(with_3_queries))) |
936 | |
937 | === modified file 'lib/lp/registry/browser/tests/test_views.py' |
938 | --- lib/lp/registry/browser/tests/test_views.py 2010-08-20 20:31:18 +0000 |
939 | +++ lib/lp/registry/browser/tests/test_views.py 2010-08-22 18:34:46 +0000 |
940 | @@ -35,6 +35,7 @@ |
941 | 'product-edit-people-view.txt': LaunchpadFunctionalLayer, |
942 | 'product-files-views.txt': LaunchpadFunctionalLayer, |
943 | 'product-views.txt': LaunchpadFunctionalLayer, |
944 | + 'productseries-views.txt': LaunchpadFunctionalLayer, |
945 | 'projectgroup-views.txt': LaunchpadFunctionalLayer, |
946 | 'user-to-user-views.txt': LaunchpadFunctionalLayer, |
947 | } |
948 | |
949 | === modified file 'lib/lp/registry/doc/distribution.txt' |
950 | --- lib/lp/registry/doc/distribution.txt 2010-08-06 14:49:35 +0000 |
951 | +++ lib/lp/registry/doc/distribution.txt 2010-08-22 18:34:46 +0000 |
952 | @@ -585,7 +585,7 @@ |
953 | ... name='impossible', dateexpected=datetime(2028, 10, 1)) |
954 | Traceback (most recent call last): |
955 | ... |
956 | - Unauthorized: (<DistroSeries at ...>, 'newMilestone', 'launchpad.Edit') |
957 | + Unauthorized: (<DistroSeries u'woody'>, 'newMilestone', 'launchpad.Edit') |
958 | >>> login('mark@example.com') |
959 | >>> debian_milestone = woody.newMilestone( |
960 | ... name='woody-rc1', dateexpected=datetime(2028, 10, 1)) |
961 | |
962 | === modified file 'lib/lp/registry/doc/sourcepackage.txt' |
963 | --- lib/lp/registry/doc/sourcepackage.txt 2010-07-02 20:32:58 +0000 |
964 | +++ lib/lp/registry/doc/sourcepackage.txt 2010-08-22 18:34:46 +0000 |
965 | @@ -71,7 +71,7 @@ |
966 | >>> firefox_warty_package = SourcePackage(sourcepackagename=firefox, |
967 | ... distroseries=ubuntu_warty) |
968 | >>> firefox_warty_package |
969 | - <SourcePackage ubuntu/warty/mozilla-firefox> |
970 | + <SourcePackage ...'Ubuntu'...'warty'...'mozilla-firefox'...> |
971 | |
972 | An instance is commonly retrieved from a distroseries. |
973 | |
974 | |
975 | === modified file 'lib/lp/registry/model/distroseries.py' |
976 | --- lib/lp/registry/model/distroseries.py 2010-08-20 20:31:18 +0000 |
977 | +++ lib/lp/registry/model/distroseries.py 2010-08-22 18:34:46 +0000 |
978 | @@ -270,6 +270,9 @@ |
979 | """See `IDistroSeries`.""" |
980 | return self.getDistroArchSeries(archtag) |
981 | |
982 | + def __repr__(self): |
983 | + return '<%s %r>' % (self.__class__.__name__, self.name) |
984 | + |
985 | def __str__(self): |
986 | return '%s %s' % (self.distribution.name, self.name) |
987 | |
988 | |
989 | === modified file 'lib/lp/registry/model/sourcepackage.py' |
990 | --- lib/lp/registry/model/sourcepackage.py 2010-08-20 20:31:18 +0000 |
991 | +++ lib/lp/registry/model/sourcepackage.py 2010-08-22 18:34:46 +0000 |
992 | @@ -213,7 +213,8 @@ |
993 | return cls(sourcepackagename, distroseries) |
994 | |
995 | def __repr__(self): |
996 | - return '<%s %s>' % (self.__class__.__name__, self.path) |
997 | + return '<%s %r %r %r>' % (self.__class__.__name__, |
998 | + self.distribution, self.distroseries, self.sourcepackagename) |
999 | |
1000 | def _get_ubuntu(self): |
1001 | # XXX: kiko 2006-03-20: Ideally, it would be possible to just do |
1002 | |
1003 | === modified file 'lib/lp/testing/factory.py' |
1004 | --- lib/lp/testing/factory.py 2010-08-20 20:31:18 +0000 |
1005 | +++ lib/lp/testing/factory.py 2010-08-22 18:34:46 +0000 |
1006 | @@ -7,7 +7,7 @@ |
1007 | |
1008 | """Testing infrastructure for the Launchpad application. |
1009 | |
1010 | -This module should not have any actual tests. |
1011 | +This module should not contain tests (but it should be tested). |
1012 | """ |
1013 | |
1014 | __metaclass__ = type |
Deryck, Curtis, would appreciate sanity checks from you :)