Merge lp:~lifeless/launchpad/milestones into lp:launchpad

Proposed by Robert Collins
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
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://bugs.edge.launchpad.net/launchpad-registry/+bug/447418.

Private bug access checking is rather slow: https://bugs.edge.launchpad.net/malone/+bug/619039.

Solution - a cached attribute recording people that we've calculated should have view access, prepopulated by BugTaskSet.search().

Added tests for the low level behaviour and that it fixes the observed scaling problem in a simple milestone view.

\o/

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

Deryck, Curtis, would appreciate sanity checks from you :)

Revision history for this message
Graham Binns (gmb) wrote :
Download full text (5.1 KiB)

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/bugs/tests/test_bugtask.py'
> --- lib/lp/bugs/tests/test_bugtask.py 2010-07-14 14:11:15 +0000
> +++ lib/lp/bugs/tests/test_bugtask.py 2010-08-17 09:41:19 +0000
> @@ -792,6 +796,28 @@
> result = target.searchTasks(None, modified_since=date)
> self.assertEqual([task2], list(result))
>
> + def test_private_bug_view_permissions_cached(self):
> + """private bugs from a search know the user can see the bugs."""

Captialise at the start of the sentence ;).

> + target = self.makeBugTarget()
> + person = self.login()
> + self.factory.makeBug(product=target, private=True, owner=person)
> + self.factory.makeBug(product=target, private=True, owner=person)
> + # Search style and parameters taken from the milestone index view where
> + # the issue was discovered.
> + login_person(person)
> + tasks =target.searchTasks(BugTaskSearchParams(person, omit_dupes=True,

Needs a space after the =.

> + orderby=['status', '-importance', 'id']))

> === modified file 'lib/lp/registry/browser/tests/test_milestone.py'
> --- lib/lp/registry/browser/tests/test_milestone.py 2010-08-16 18:39:11 +0000
> +++ lib/lp/registry/browser/tests/test_milestone.py 2010-08-17 09:41:19 +0000
> @@ -105,5 +116,61 @@
> self.assertEqual(0, product.development_focus.all_bugtasks.count())
>
>
> -def test_suite():
> - return unittest.TestLoader().loadTestsFromName(__name__)
> +class TestMilestoneIndex(TestCaseWithFactory):
> +
> + layer = DatabaseFunctionalLayer
> +
> + def test_more_private_bugs_query_count_is_constant(self):

The test should have a comment at the start of it to save me having to
parse the method name.

> + product = self.factory.makeProduct()
> + login_person(product.owner)
> + milestone = self.factory.makeMilestone(
> + productseries=product.development_focus)
> + bug1 = self.factory.makeBug(product=product, private=True,
> + owner=product.owner)
> + bug1.bugtasks[0].transitionToMilestone(milestone, product.owner)
> + # 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.makeTeam()
> + viewer = self.factory.makePerson(password="test")
> + with person_logged_in(subscribed_team.teamowner):
> + subscribed_team.addMember(viewer, subscribed_team.teamowner)
> + bug1.subscribe(subscribed_team, product.owner)
> + bug1_url = canonical_url(bug1)
> + milestone_url = canonical_url(milestone)
> + browser = self.getUserBrowser(user=viewer)
> + # Seed the cookie cache and any other cross-request state we may gain
> + # in future. See canonical.launchpad.webapp.serssion: _get_secret.
> + browser.open(milestone_url)
> + collector = QueryC...

Read more...

review: Needs Fixing (code)
Revision history for this message
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

Revision history for this message
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.

Revision history for this message
Graham Binns (gmb) :
review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

MilestoneView._bugtasks uses precache_permission_for_objects() on the bugtasks and the conjoined bugtasks. Does this change allow us to remove precache_permission_for_objects()? Will your test fail if I remove precache_permission_for_objects() from the view?

MilestoneView._bugtasks calls getConjoinedMaster() with a prepared list (1 query) of expected ids. I think we need to keep behaviour; this branch just lowers the access cost of getConjoinedMaster()

review: Needs Information
Revision history for this message
Robert Collins (lifeless) wrote :

On Wed, Aug 18, 2010 at 2:18 AM, Curtis Hovey
<email address hidden> wrote:
> Review: Needs Information
> MilestoneView._bugtasks uses precache_permission_for_objects() on the bugtasks and the conjoined bugtasks. Does this change allow us to remove precache_permission_for_objects()? Will your test fail if I remove precache_permission_for_objects() from the view?

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._bugtasks calls getConjoinedMaster() with a prepared list (1 query) of expected ids. I think we need to keep behaviour; this branch just lowers the access cost of getConjoinedMaster()

We can refactor the conjoined master stuff too, but its a separate issue IMO.

-Rob

Revision history for this message
Curtis Hovey (sinzui) wrote :

I do not understand the first part of your answer. I see this in MilestoneView._bugtasks:
        ...
        for task in tasks:
            if task.getConjoinedMaster(bugs_and_tasks[task.bug]) is None:
                non_conjoined_slaves.append(task)
        # Checking bug permissions is expensive. We know from the query that
        # the user has at least launchpad.View on the bugtasks and their bugs.
        precache_permission_for_objects(
            self.request, 'launchpad.View', non_conjoined_slaves)
        precache_permission_for_objects(
            self.request, 'launchpad.View',
            [task.bug for task in non_conjoined_slaves])
        return non_conjoined_slaves

Since you modified the permission rules on bugtask, I wonder if the first precache_permission_for_objects() is not needed. I wonder if permission checking is fast enough.

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.

Revision history for this message
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.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Yes. I agree that the removal of precache_permissions is a separate branch. Thanks for answering my questions.

review: Approve (code)
Revision history for this message
Deryck Hodge (deryck) wrote :

Looks good to me, Robert. Thanks for doing this work!

Cheers,
deryck

review: Approve (code)
Revision history for this message
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.

Revision history for this message
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.searchBugIds(). I do not think it is being used.

review: Approve (code)
Revision history for this message
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-views_txt
----------------------------------------------------------------------
Text attachment: traceback
------------
Traceback (most recent call last):
  File "/usr/lib/python2.6/unittest.py", line 279, in run
    testMethod()
  File "/usr/lib/python2.6/doctest.py", line 2152, in runTest
    raise self.failureException(self.format_failure(new.getvalue()))
AssertionError: Failed doctest test for productseries-views.txt
  File "lib/lp/registry/browser/tests/productseries-views.txt", line 0

----------------------------------------------------------------------
File "lib/lp/registry/browser/tests/productseries-views.txt", line 80,
in productseries-views.txt
Failed example:
    script = find_tag_by_id(view.render(), 'milestone-script')
Differences (ndiff with -expected +actual):
    + WARNING:root:Memcache set failed for
pt:testrunner:lp/bugs/templates/bugtarget-portlet-latestbugs.pt,10721:16:1,0:1,http_//127.0.0.1?6RKVP3n9fovtv8CxVi8ydF
----------------------------------------------------------------------
File "lib/lp/registry/browser/tests/productseries-views.txt", line
102, in productseries-views.txt
Failed example:
    content = view.render()
Differences (ndiff with -expected +actual):
    + WARNING:root:Memcache set failed for
pt:testrunner:lp/bugs/templates/bugtarget-portlet-latestbugs.pt,10721:243656:1,0:1,http_//127.0.0.1?4Ophjjtmt5aXGqxEwNgYvP
----------------------------------------------------------------------
File "lib/lp/registry/browser/tests/productseries-views.txt", line
117, in productseries-views.txt
Failed example:
    table = find_tag_by_id(view.render(), 'series-simple')
Differences (ndiff with -expected +actual):
    + WARNING:root:Memcache set failed for
pt:testrunner:lp/bugs/templates/bugtarget-portlet-latestbugs.pt,10721:243652:1,0:1,http_//127.0.0.1?IXiDEgHZPmgHvZKrJmNvh

If you have any ideas about it, I'd love to know.

Revision history for this message
Curtis Hovey (sinzui) wrote :

productseries-views.txt is now rendering a template that uses the memcache tales directive. We need to run it on a layer that has the memcached server up.

Add this to special_test_layer in lp/registry/browser/tests/test_views.py
    'productseries-views.txt': LaunchpadFunctionalLayer,

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
--- lib/canonical/launchpad/interfaces/validation.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/interfaces/validation.py 2010-08-22 18:34:46 +0000
@@ -312,7 +312,7 @@
312 errors = []312 errors = []
313 user = getUtility(ILaunchBag).user313 user = getUtility(ILaunchBag).user
314 params = BugTaskSearchParams(user, bug=bug)314 params = BugTaskSearchParams(user, bug=bug)
315 if product.searchTasks(params):315 if not product.searchTasks(params).is_empty():
316 errors.append(LaunchpadValidationError(_(316 errors.append(LaunchpadValidationError(_(
317 'A fix for this bug has already been requested for ${product}',317 'A fix for this bug has already been requested for ${product}',
318 mapping={'product': product.displayname})))318 mapping={'product': product.displayname})))
319319
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/security.py 2010-08-22 18:34:46 +0000
@@ -988,24 +988,8 @@
988 usedfor = IHasBug988 usedfor = IHasBug
989989
990 def checkAuthenticated(self, user):990 def checkAuthenticated(self, user):
991991 # Delegated entirely to the bug.
992 if user.in_admin:992 return self.obj.bug.userCanView(user)
993 # Admins can always edit bugtasks, whether they're reported on a
994 # private bug or not.
995 return True
996
997 if not self.obj.bug.private:
998 # This is a public bug, so anyone can edit it.
999 return True
1000 else:
1001 # This is a private bug, and we know the user isn't an admin, so
1002 # we'll only allow editing if the user is explicitly subscribed to
1003 # this bug.
1004 for subscription in self.obj.bug.subscriptions:
1005 if user.inTeam(subscription.person):
1006 return True
1007
1008 return False
1009993
1010994
1011class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase):995class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase):
@@ -1027,21 +1011,10 @@
10271011
1028 def checkAuthenticated(self, user):1012 def checkAuthenticated(self, user):
1029 """Allow any logged in user to edit a public bug, and only1013 """Allow any logged in user to edit a public bug, and only
1030 explicit subscribers to edit private bugs.1014 explicit subscribers to edit private bugs. Any bug that can be seen can
1015 be edited.
1031 """1016 """
1032 if not self.obj.private:1017 return self.obj.userCanView(user)
1033 # This is a public bug.
1034 return True
1035 elif user.in_admin:
1036 # Admins can edit all bugs.
1037 return True
1038 else:
1039 # This is a private bug. Only explicit subscribers may edit it.
1040 for subscription in self.obj.subscriptions:
1041 if user.inTeam(subscription.person):
1042 return True
1043
1044 return False
10451018
1046 def checkUnauthenticated(self):1019 def checkUnauthenticated(self):
1047 """Never allow unauthenticated users to edit a bug."""1020 """Never allow unauthenticated users to edit a bug."""
10481021
=== modified file 'lib/canonical/launchpad/utilities/personroles.py'
--- lib/canonical/launchpad/utilities/personroles.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/utilities/personroles.py 2010-08-22 18:34:46 +0000
@@ -41,6 +41,10 @@
41 except AttributeError:41 except AttributeError:
42 raise AttributeError(errortext)42 raise AttributeError(errortext)
4343
44 @property
45 def id(self):
46 return self.person.id
47
44 def isOwner(self, obj):48 def isOwner(self, obj):
45 """See IPersonRoles."""49 """See IPersonRoles."""
46 return self.person.inTeam(obj.owner)50 return self.person.inTeam(obj.owner)
4751
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2010-08-22 18:34:46 +0000
@@ -966,13 +966,8 @@
966966
967 matching_bugtasks = getUtility(IBugTaskSet).findSimilar(967 matching_bugtasks = getUtility(IBugTaskSet).findSimilar(
968 self.user, title, **context_params)968 self.user, title, **context_params)
969 # Remove all the prejoins, since we won't use them and they slow
970 # down the query significantly.
971 matching_bugtasks = matching_bugtasks.prejoin([])
972
973 matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(969 matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
974 matching_bugtasks, self.user, self._MATCHING_BUGS_LIMIT)970 matching_bugtasks, self.user, self._MATCHING_BUGS_LIMIT)
975
976 return matching_bugs971 return matching_bugs
977972
978 @property973 @property
979974
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-08-22 18:34:46 +0000
@@ -3197,25 +3197,8 @@
3197 else:3197 else:
3198 raise AssertionError('Uknown context type: %s' % self.context)3198 raise AssertionError('Uknown context type: %s' % self.context)
31993199
3200 # XXX flacoste 2008/04/25 bug=221947 This should be moved to an3200 return u"".join("%d\n" % bug_id for bug_id in
3201 # IBugTaskSet.findBugIds(search_params) method.3201 getUtility(IBugTaskSet).searchBugIds(search_params))
3202 # buildQuery() is part of the internal API.
3203 taskset = removeSecurityProxy(getUtility(IBugTaskSet))
3204 query, clauseTables, orderBy = taskset.buildQuery(search_params)
3205
3206 # Convert list of SQLObject order by spec into SQL.
3207 order_by_clause = []
3208 for field in orderBy:
3209 if field[0] == '-':
3210 field = "%s DESC" % field[1:]
3211 order_by_clause.append(field)
3212
3213 sql = 'SELECT BugTask.bug FROM %s WHERE %s ORDER BY %s' % (
3214 ', '.join(clauseTables), query, ', '.join(order_by_clause))
3215
3216 cur = cursor()
3217 cur.execute(sql)
3218 return u"".join("%d\n" % row[0] for row in cur.fetchall())
32193202
32203203
3221def _by_targetname(bugtask):3204def _by_targetname(bugtask):
32223205
=== modified file 'lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt'
--- lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt 2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/browser/tests/bugtask-target-link-titles.txt 2010-08-22 18:34:46 +0000
@@ -73,7 +73,7 @@
73 >>> published_distro_series = factory.makeDistroRelease(73 >>> published_distro_series = factory.makeDistroRelease(
74 ... published_distro, name='published-distro-series')74 ... published_distro, name='published-distro-series')
75 >>> publisher.setUpDefaultDistroSeries(published_distro_series)75 >>> publisher.setUpDefaultDistroSeries(published_distro_series)
76 <DistroSeries at ...>76 <DistroSeries u'published-distro-series'>
77 >>> print publisher.person.unique_displayname77 >>> print publisher.person.unique_displayname
78 Foo Bar (name16)78 Foo Bar (name16)
79 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')79 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
8080
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2010-08-22 18:34:46 +0000
@@ -212,7 +212,7 @@
212212
213 def test_getAcknowledgementMessage_dsp_custom_distro_message(self):213 def test_getAcknowledgementMessage_dsp_custom_distro_message(self):
214 # If a distribution has a customized conformatom message, it214 # If a distribution has a customized conformatom message, it
215 # is used for bugs filed on DsitributionSourcePackages.215 # is used for bugs filed on DistributionSourcePackages.
216 dsp = self.factory.makeDistributionSourcePackage()216 dsp = self.factory.makeDistributionSourcePackage()
217 dsp.distribution.bug_reported_acknowledgement = (217 dsp.distribution.bug_reported_acknowledgement = (
218 u"Thank you for filing a bug in our distribution")218 u"Thank you for filing a bug in our distribution")
219219
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-08-22 18:34:46 +0000
@@ -1398,6 +1398,14 @@
1398 orderby specified in the first BugTaskSearchParams object.1398 orderby specified in the first BugTaskSearchParams object.
1399 """1399 """
14001400
1401 def searchBugIds(params):
1402 """Search bug ids.
1403
1404 This is a variation on IBugTaskSet.search that returns only the bug ids.
1405
1406 :param params: the BugTaskSearchParams to search on.
1407 """
1408
1401 def getAssignedMilestonesFromSearch(search_results):1409 def getAssignedMilestonesFromSearch(search_results):
1402 """Returns distinct milestones for the given tasks.1410 """Returns distinct milestones for the given tasks.
14031411
14041412
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/model/bug.py 2010-08-22 18:34:46 +0000
@@ -70,6 +70,10 @@
70 providedBy,70 providedBy,
71 )71 )
7272
73from canonical.cachedproperty import (
74 cachedproperty,
75 clear_property,
76 )
73from canonical.config import config77from canonical.config import config
74from canonical.database.constants import UTC_NOW78from canonical.database.constants import UTC_NOW
75from canonical.database.datetimecol import UtcDateTimeCol79from canonical.database.datetimecol import UtcDateTimeCol
@@ -85,7 +89,10 @@
85 MessageSet,89 MessageSet,
86 )90 )
87from canonical.launchpad.helpers import shortlist91from canonical.launchpad.helpers import shortlist
88from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities92from canonical.launchpad.interfaces.launchpad import (
93 ILaunchpadCelebrities,
94 IPersonRoles,
95 )
89from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet96from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
90from canonical.launchpad.interfaces.lpstorm import IStore97from canonical.launchpad.interfaces.lpstorm import IStore
91from canonical.launchpad.interfaces.message import (98from canonical.launchpad.interfaces.message import (
@@ -626,6 +633,7 @@
626 # disabled see the change.633 # disabled see the change.
627 store.flush()634 store.flush()
628 self.updateHeat()635 self.updateHeat()
636 clear_property(self, '_cached_viewers')
629 return637 return
630638
631 def unsubscribeFromDupes(self, person, unsubscribed_by):639 def unsubscribeFromDupes(self, person, unsubscribed_by):
@@ -1618,21 +1626,31 @@
1618 self, self.messages[comment_number])1626 self, self.messages[comment_number])
1619 bug_message.visible = visible1627 bug_message.visible = visible
16201628
1629 @cachedproperty('_cached_viewers')
1630 def _known_viewers(self):
1631 """A dict of of known persons able to view this bug."""
1632 return set()
1633
1621 def userCanView(self, user):1634 def userCanView(self, user):
1622 """See `IBug`."""1635 """See `IBug`.
1623 admins = getUtility(ILaunchpadCelebrities).admin1636
1637 Note that Editing is also controlled by this check,
1638 because we permit editing of any bug one can see.
1639 """
1640 if user.id in self._known_viewers:
1641 return True
1624 if not self.private:1642 if not self.private:
1625 # This is a public bug.1643 # This is a public bug.
1626 return True1644 return True
1627 elif user.inTeam(admins):1645 elif IPersonRoles(user).in_admin:
1628 # Admins can view all bugs.1646 # Admins can view all bugs.
1629 return True1647 return True
1630 else:1648 else:
1631 # This is a private bug. Only explicit subscribers may view it.1649 # This is a private bug. Only explicit subscribers may view it.
1632 for subscription in self.subscriptions:1650 for subscription in self.subscriptions:
1633 if user.inTeam(subscription.person):1651 if user.inTeam(subscription.person):
1652 self._known_viewers.add(user.id)
1634 return True1653 return True
1635
1636 return False1654 return False
16371655
1638 def linkHWSubmission(self, submission):1656 def linkHWSubmission(self, submission):
@@ -1706,6 +1724,7 @@
17061724
1707 self.heat = SQL("calculate_bug_heat(%s)" % sqlvalues(self))1725 self.heat = SQL("calculate_bug_heat(%s)" % sqlvalues(self))
1708 self.heat_last_updated = UTC_NOW1726 self.heat_last_updated = UTC_NOW
1727 store.flush()
17091728
1710 @property1729 @property
1711 def attachments(self):1730 def attachments(self):
@@ -1942,6 +1961,10 @@
1942 # Transaction.iterSelect() will try to listify the results.1961 # Transaction.iterSelect() will try to listify the results.
1943 # This can be fixed by selecting from Bugs directly, but1962 # This can be fixed by selecting from Bugs directly, but
1944 # that's non-trivial.1963 # that's non-trivial.
1964 # ---: Robert Collins 20100818: if bug_tasks implements IResultSset
1965 # then it should be very possible to improve on it, though
1966 # DecoratedResultSets would need careful handling (e.g. type
1967 # driven callbacks on columns)
1945 # We select more than :limit: since if a bug affects more than1968 # We select more than :limit: since if a bug affects more than
1946 # one source package, it will be returned more than one time. 41969 # one source package, it will be returned more than one time. 4
1947 # is an arbitrary number that should be large enough.1970 # is an arbitrary number that should be large enough.
19481971
=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/model/bugtarget.py 2010-08-22 18:34:46 +0000
@@ -207,13 +207,7 @@
207 @property207 @property
208 def has_bugtasks(self):208 def has_bugtasks(self):
209 """See `IHasBugs`."""209 """See `IHasBugs`."""
210 # Check efficiently if any bugtasks exist. We should avoid210 return not self.all_bugtasks.is_empty()
211 # expensive calls like all_bugtasks.count(). all_bugtasks
212 # returns a storm.SQLObjectResultSet instance, and this
213 # class does not provide methods like is_empty(). But we can
214 # indirectly call SQLObjectResultSet._result_set.is_empty()
215 # by converting all_bugtasks into a boolean object.
216 return bool(self.all_bugtasks)
217211
218 def getBugCounts(self, user, statuses=None):212 def getBugCounts(self, user, statuses=None):
219 """See `IHasBugs`."""213 """See `IHasBugs`."""
220214
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/model/bugtask.py 2010-08-22 18:34:46 +0000
@@ -21,7 +21,7 @@
2121
2222
23import datetime23import datetime
24from operator import attrgetter24from operator import attrgetter, itemgetter
2525
26from lazr.enum import DBItem26from lazr.enum import DBItem
27import pytz27import pytz
@@ -35,6 +35,7 @@
35 Alias,35 Alias,
36 And,36 And,
37 AutoTables,37 AutoTables,
38 Desc,
38 In,39 In,
39 Join,40 Join,
40 LeftJoin,41 LeftJoin,
@@ -58,6 +59,7 @@
58 removeSecurityProxy,59 removeSecurityProxy,
59 )60 )
6061
62from canonical.cachedproperty import cache_property
61from canonical.config import config63from canonical.config import config
62from canonical.database.constants import UTC_NOW64from canonical.database.constants import UTC_NOW
63from canonical.database.datetimecol import UtcDateTimeCol65from canonical.database.datetimecol import UtcDateTimeCol
@@ -72,6 +74,7 @@
72 SQLBase,74 SQLBase,
73 sqlvalues,75 sqlvalues,
74 )76 )
77from canonical.launchpad.components.decoratedresultset import DecoratedResultSet
75from canonical.launchpad.helpers import shortlist78from canonical.launchpad.helpers import shortlist
76from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities79from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
77from canonical.launchpad.interfaces.lpstorm import IStore80from canonical.launchpad.interfaces.lpstorm import IStore
@@ -118,6 +121,7 @@
118 UserCannotEditBugTaskMilestone,121 UserCannotEditBugTaskMilestone,
119 UserCannotEditBugTaskStatus,122 UserCannotEditBugTaskStatus,
120 )123 )
124from lp.bugs.model.bugnomination import BugNomination
121from lp.bugs.model.bugsubscription import BugSubscription125from lp.bugs.model.bugsubscription import BugSubscription
122from lp.registry.interfaces.distribution import (126from lp.registry.interfaces.distribution import (
123 IDistribution,127 IDistribution,
@@ -148,7 +152,10 @@
148from lp.registry.interfaces.sourcepackage import ISourcePackage152from lp.registry.interfaces.sourcepackage import ISourcePackage
149from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet153from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
150from lp.registry.model.pillar import pillar_sort_key154from lp.registry.model.pillar import pillar_sort_key
155from lp.registry.model.sourcepackagename import SourcePackageName
151from lp.soyuz.interfaces.publishing import PackagePublishingStatus156from lp.soyuz.interfaces.publishing import PackagePublishingStatus
157from lp.soyuz.model.publishing import SourcePackagePublishingHistory
158from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
152159
153160
154debbugsseveritymap = {None: BugTaskImportance.UNDECIDED,161debbugsseveritymap = {None: BugTaskImportance.UNDECIDED,
@@ -673,9 +680,6 @@
673680
674 matching_bugtasks = getUtility(IBugTaskSet).findSimilar(681 matching_bugtasks = getUtility(IBugTaskSet).findSimilar(
675 user, self.bug.title, **context_params)682 user, self.bug.title, **context_params)
676 # Remove all the prejoins, since we won't use them and they slow
677 # down the query significantly.
678 matching_bugtasks = matching_bugtasks.prejoin([])
679683
680 # Make sure to exclude the current BugTask from the list of684 # Make sure to exclude the current BugTask from the list of
681 # matching tasks. We use 4*limit as an arbitrary value here to685 # matching tasks. We use 4*limit as an arbitrary value here to
@@ -1302,24 +1306,54 @@
13021306
1303def get_bug_privacy_filter(user):1307def get_bug_privacy_filter(user):
1304 """An SQL filter for search results that adds privacy-awareness."""1308 """An SQL filter for search results that adds privacy-awareness."""
1309 return get_bug_privacy_filter_with_decorator(user)[0]
1310
1311
1312def _nocache_bug_decorator(obj):
1313 """A pass through decorator for consistency.
1314
1315 :seealso: get_bug_privacy_filter_with_decorator
1316 """
1317 return obj
1318
1319
1320def _make_cache_user_can_view_bug(user):
1321 """Curry a decorator for bugtask queries to cache permissions.
1322
1323 :seealso: get_bug_privacy_filter_with_decorator
1324 """
1325 userid = user.id
1326 def cache_user_can_view_bug(bugtask):
1327 cache_property(bugtask.bug, '_cached_viewers', set([userid]))
1328 return bugtask
1329 return cache_user_can_view_bug
1330
1331
1332def get_bug_privacy_filter_with_decorator(user):
1333 """Return a SQL filter to limit returned bug tasks.
1334
1335 :return: A SQL filter, a decorator to cache visibility in a resultset that
1336 returns BugTask objects.
1337 """
1305 if user is None:1338 if user is None:
1306 return "Bug.private = FALSE"1339 return "Bug.private = FALSE", _nocache_bug_decorator
1307 admin_team = getUtility(ILaunchpadCelebrities).admin1340 admin_team = getUtility(ILaunchpadCelebrities).admin
1308 if user.inTeam(admin_team):1341 if user.inTeam(admin_team):
1309 return ""1342 return "", _nocache_bug_decorator
1310 # A subselect is used here because joining through1343 # A subselect is used here because joining through
1311 # TeamParticipation is only relevant to the "user-aware"1344 # TeamParticipation is only relevant to the "user-aware"
1312 # part of the WHERE condition (i.e. the bit below.) The1345 # part of the WHERE condition (i.e. the bit below.) The
1313 # other half of this condition (see code above) does not1346 # other half of this condition (see code above) does not
1314 # use TeamParticipation at all.1347 # use TeamParticipation at all.
1315 return """1348 return ("""
1316 (Bug.private = FALSE OR EXISTS (1349 (Bug.private = FALSE OR EXISTS (
1317 SELECT BugSubscription.bug1350 SELECT BugSubscription.bug
1318 FROM BugSubscription, TeamParticipation1351 FROM BugSubscription, TeamParticipation
1319 WHERE TeamParticipation.person = %(personid)s AND1352 WHERE TeamParticipation.person = %(personid)s AND
1320 BugSubscription.person = TeamParticipation.team AND1353 BugSubscription.person = TeamParticipation.team AND
1321 BugSubscription.bug = Bug.id))1354 BugSubscription.bug = Bug.id))
1322 """ % sqlvalues(personid=user.id)1355 """ % sqlvalues(personid=user.id),
1356 _make_cache_user_can_view_bug(user))
13231357
13241358
1325def build_tag_set_query(joiner, tags):1359def build_tag_set_query(joiner, tags):
@@ -1408,24 +1442,7 @@
1408 """See `IBugTaskSet`."""1442 """See `IBugTaskSet`."""
1409 implements(IBugTaskSet)1443 implements(IBugTaskSet)
14101444
1411 _ORDERBY_COLUMN = {1445 _ORDERBY_COLUMN = None
1412 "id": "BugTask.bug",
1413 "importance": "BugTask.importance",
1414 "assignee": "BugTask.assignee",
1415 "targetname": "BugTask.targetnamecache",
1416 "status": "BugTask.status",
1417 "title": "Bug.title",
1418 "milestone": "BugTask.milestone",
1419 "dateassigned": "BugTask.dateassigned",
1420 "datecreated": "BugTask.datecreated",
1421 "date_last_updated": "Bug.date_last_updated",
1422 "date_closed": "BugTask.date_closed",
1423 "number_of_duplicates": "Bug.number_of_duplicates",
1424 "message_count": "Bug.message_count",
1425 "users_affected_count": "Bug.users_affected_count",
1426 "heat": "Bug.heat",
1427 "latest_patch_uploaded": "Bug.latest_patch_uploaded",
1428 }
14291446
1430 _open_resolved_upstream = """1447 _open_resolved_upstream = """
1431 EXISTS (1448 EXISTS (
@@ -1544,7 +1561,7 @@
15441561
1545 search_params.fast_searchtext = nl_phrase_search(1562 search_params.fast_searchtext = nl_phrase_search(
1546 summary, Bug, ' AND '.join(constraint_clauses), ['BugTask'])1563 summary, Bug, ' AND '.join(constraint_clauses), ['BugTask'])
1547 return self.search(search_params)1564 return self.search(search_params, _noprejoins=True)
15481565
1549 def _buildStatusClause(self, status):1566 def _buildStatusClause(self, status):
1550 """Return the SQL query fragment for search by status.1567 """Return the SQL query fragment for search by status.
@@ -1591,11 +1608,15 @@
1591 """Build and return an SQL query with the given parameters.1608 """Build and return an SQL query with the given parameters.
15921609
1593 Also return the clauseTables and orderBy for the generated query.1610 Also return the clauseTables and orderBy for the generated query.
1611
1612 :return: A query, the tables to query, ordering expression and a
1613 decorator to call on each returned row.
1594 """1614 """
1595 assert isinstance(params, BugTaskSearchParams)1615 assert isinstance(params, BugTaskSearchParams)
15961616 from lp.bugs.model.bug import Bug
1597 extra_clauses = ['Bug.id = BugTask.bug']1617 extra_clauses = ['Bug.id = BugTask.bug']
1598 clauseTables = ['BugTask', 'Bug']1618 clauseTables = [BugTask, Bug]
1619 decorators = []
15991620
1600 # These arguments can be processed in a loop without any other1621 # These arguments can be processed in a loop without any other
1601 # special handling.1622 # special handling.
@@ -1651,7 +1672,9 @@
1651 extra_clauses.append("BugTask.milestone %s" % where_cond)1672 extra_clauses.append("BugTask.milestone %s" % where_cond)
16521673
1653 if params.project:1674 if params.project:
1654 clauseTables.append("Product")1675 # Circular.
1676 from lp.registry.model.product import Product
1677 clauseTables.append(Product)
1655 extra_clauses.append("BugTask.product = Product.id")1678 extra_clauses.append("BugTask.product = Product.id")
1656 if isinstance(params.project, any):1679 if isinstance(params.project, any):
1657 extra_clauses.append("Product.project IN (%s)" % ",".join(1680 extra_clauses.append("Product.project IN (%s)" % ",".join(
@@ -1694,7 +1717,7 @@
1694 extra_clauses.append(self._buildFastSearchTextClause(params))1717 extra_clauses.append(self._buildFastSearchTextClause(params))
16951718
1696 if params.subscriber is not None:1719 if params.subscriber is not None:
1697 clauseTables.append('BugSubscription')1720 clauseTables.append(BugSubscription)
1698 extra_clauses.append("""Bug.id = BugSubscription.bug AND1721 extra_clauses.append("""Bug.id = BugSubscription.bug AND
1699 BugSubscription.person = %(personid)s""" %1722 BugSubscription.person = %(personid)s""" %
1700 sqlvalues(personid=params.subscriber.id))1723 sqlvalues(personid=params.subscriber.id))
@@ -1742,8 +1765,8 @@
1742 extra_clauses.append(structural_subscriber_clause)1765 extra_clauses.append(structural_subscriber_clause)
17431766
1744 if params.component:1767 if params.component:
1745 clauseTables += ["SourcePackagePublishingHistory",1768 clauseTables += [SourcePackagePublishingHistory,
1746 "SourcePackageRelease"]1769 SourcePackageRelease]
1747 distroseries = None1770 distroseries = None
1748 if params.distribution:1771 if params.distribution:
1749 distroseries = params.distribution.currentseries1772 distroseries = params.distribution.currentseries
@@ -1866,11 +1889,12 @@
1866 BugNomination.status = %(nomination_status)s1889 BugNomination.status = %(nomination_status)s
1867 """ % mappings1890 """ % mappings
1868 extra_clauses.append(nominated_for_clause)1891 extra_clauses.append(nominated_for_clause)
1869 clauseTables.append('BugNomination')1892 clauseTables.append(BugNomination)
18701893
1871 clause = get_bug_privacy_filter(params.user)1894 clause, decorator = get_bug_privacy_filter_with_decorator(params.user)
1872 if clause:1895 if clause:
1873 extra_clauses.append(clause)1896 extra_clauses.append(clause)
1897 decorators.append(decorator)
18741898
1875 hw_clause = self._buildHardwareRelatedClause(params)1899 hw_clause = self._buildHardwareRelatedClause(params)
1876 if hw_clause is not None:1900 if hw_clause is not None:
@@ -1899,7 +1923,15 @@
1899 orderby_arg = self._processOrderBy(params)1923 orderby_arg = self._processOrderBy(params)
19001924
1901 query = " AND ".join(extra_clauses)1925 query = " AND ".join(extra_clauses)
1902 return query, clauseTables, orderby_arg1926
1927 if not decorators:
1928 decorator = lambda x:x
1929 else:
1930 def decorator(obj):
1931 for decor in decorators:
1932 obj = decor(obj)
1933 return obj
1934 return query, clauseTables, orderby_arg, decorator
19031935
1904 def _buildUpstreamClause(self, params):1936 def _buildUpstreamClause(self, params):
1905 """Return an clause for returning upstream data if the data exists.1937 """Return an clause for returning upstream data if the data exists.
@@ -2127,34 +2159,79 @@
2127 ', '.join(tables), ' AND '.join(clauses))2159 ', '.join(tables), ' AND '.join(clauses))
2128 return clause2160 return clause
21292161
2130 def search(self, params, *args):2162 def search(self, params, *args, **kwargs):
2131 """See `IBugTaskSet`."""2163 """See `IBugTaskSet`.
2132 store_selector = getUtility(IStoreSelector)2164
2133 store = store_selector.get(MAIN_STORE, SLAVE_FLAVOR)2165 :param _noprejoins: Private internal parameter to BugTaskSet which
2134 query, clauseTables, orderby = self.buildQuery(params)2166 disables all use of prejoins : consolidated from code paths that
2167 claim they were inefficient and unwanted.
2168 """
2169 # Circular.
2170 from lp.registry.model.product import Product
2171 from lp.bugs.model.bug import Bug
2172 _noprejoins = kwargs.get('_noprejoins', False)
2173 store = IStore(BugTask)
2174 query, clauseTables, orderby, bugtask_decorator = self.buildQuery(params)
2135 if len(args) == 0:2175 if len(args) == 0:
2136 # Do normal prejoins, if we don't have to do any UNION2176 if _noprejoins:
2137 # queries. Prejoins don't work well with UNION, and the way2177 resultset = store.find(BugTask,
2138 # we prefetch objects without prejoins cause problems with2178 AutoTables(SQL("1=1"), clauseTables),
2139 # COUNT(*) queries, which get inefficient.2179 query)
2140 return BugTask.select(2180 decorator = bugtask_decorator
2141 query, clauseTables=clauseTables, orderBy=orderby,2181 else:
2142 prejoins=['product', 'sourcepackagename'],2182 tables = clauseTables + [Product, SourcePackageName]
2143 prejoinClauseTables=['Bug'])2183 origin = [
2184 BugTask,
2185 LeftJoin(Bug, BugTask.bug == Bug.id),
2186 LeftJoin(Product, BugTask.product == Product.id),
2187 LeftJoin(
2188 SourcePackageName,
2189 BugTask.sourcepackagename == SourcePackageName.id),
2190 ]
2191 # NB: these may work with AutoTables, but its hard to tell,
2192 # this way is known to work.
2193 if BugNomination in tables:
2194 # The relation is already in query.
2195 origin.append(BugNomination)
2196 if BugSubscription in tables:
2197 # The relation is already in query.
2198 origin.append(BugSubscription)
2199 if SourcePackageRelease in tables:
2200 origin.append(SourcePackageRelease)
2201 if SourcePackagePublishingHistory in tables:
2202 origin.append(SourcePackagePublishingHistory)
2203 resultset = store.using(*origin).find(
2204 (BugTask, Product, SourcePackageName, Bug),
2205 AutoTables(SQL("1=1"), tables),
2206 query)
2207 decorator=lambda row:bugtask_decorator(row[0])
2208 resultset.order_by(orderby)
2209 return DecoratedResultSet(resultset, result_decorator=decorator)
21442210
2145 bugtask_fti = SQL('BugTask.fti')2211 bugtask_fti = SQL('BugTask.fti')
2146 result = store.find((BugTask, bugtask_fti), query,2212 result = store.find((BugTask, bugtask_fti), query,
2147 AutoTables(SQL("1=1"), clauseTables))2213 AutoTables(SQL("1=1"), clauseTables))
2214 decorators = [bugtask_decorator]
2148 for arg in args:2215 for arg in args:
2149 query, clauseTables, dummy = self.buildQuery(arg)2216 query, clauseTables, dummy, decorator = self.buildQuery(arg)
2150 result = result.union(2217 result = result.union(
2151 store.find((BugTask, bugtask_fti), query,2218 store.find((BugTask, bugtask_fti), query,
2152 AutoTables(SQL("1=1"), clauseTables)))2219 AutoTables(SQL("1=1"), clauseTables)))
2220 # NB: assumes the decorators are all compatible.
2221 # This may need revisiting if e.g. searches on behalf of different
2222 # users are combined.
2223 decorators.append(decorator)
2224 def decorator(row):
2225 bugtask = row[0]
2226 for decorator in decorators:
2227 bugtask = decorator(bugtask)
2228 return bugtask
21532229
2154 # Build up the joins2230 # Build up the joins.
2155 from lp.bugs.model.bug import Bug2231 # TODO: implement _noprejoins for this code path: as of 20100818 it has
2156 from lp.registry.model.product import Product2232 # been silently disabled because clients of the API were setting
2157 from lp.registry.model.sourcepackagename import SourcePackageName2233 # prejoins=[] which had no effect; this TODO simply notes the reality
2234 # already existing when it was added.
2158 joins = Alias(result._get_select(), "BugTask")2235 joins = Alias(result._get_select(), "BugTask")
2159 joins = Join(joins, Bug, BugTask.bug == Bug.id)2236 joins = Join(joins, Bug, BugTask.bug == Bug.id)
2160 joins = LeftJoin(joins, Product, BugTask.product == Product.id)2237 joins = LeftJoin(joins, Product, BugTask.product == Product.id)
@@ -2163,9 +2240,18 @@
21632240
2164 result = store.using(joins).find(2241 result = store.using(joins).find(
2165 (BugTask, Bug, Product, SourcePackageName))2242 (BugTask, Bug, Product, SourcePackageName))
2166 bugtasks = SQLObjectResultSet(BugTask, orderBy=orderby,2243 result.order_by(orderby)
2167 prepared_result_set=result)2244 return DecoratedResultSet(result, result_decorator=decorator)
2168 return bugtasks2245
2246 def searchBugIds(self, params):
2247 """See `IBugTaskSet`."""
2248 query, clauseTables, orderby, decorator = self.buildQuery(
2249 params)
2250 store = IStore(BugTask)
2251 resultset = store.find(BugTask.bugID,
2252 AutoTables(SQL("1=1"), clauseTables), query)
2253 resultset.order_by(orderby)
2254 return resultset
21692255
2170 def getAssignedMilestonesFromSearch(self, search_results):2256 def getAssignedMilestonesFromSearch(self, search_results):
2171 """See `IBugTaskSet`."""2257 """See `IBugTaskSet`."""
@@ -2193,7 +2279,14 @@
2193 # Import here because of cyclic references.2279 # Import here because of cyclic references.
2194 from lp.registry.model.milestone import (2280 from lp.registry.model.milestone import (
2195 Milestone, milestone_sort_key)2281 Milestone, milestone_sort_key)
2196 milestones = search_results._store.find(2282 # We need the store that was used, we have no objects to key off of
2283 # other than the search result, and Store.of() doesn't currently
2284 # work on result sets. Additionally it may be a DecoratedResultSet.
2285 if zope_isinstance(search_results, DecoratedResultSet):
2286 store = removeSecurityProxy(search_results).result_set._store
2287 else:
2288 store = search_results._store
2289 milestones = store.find(
2197 Milestone, In(Milestone.id, milestone_ids))2290 Milestone, In(Milestone.id, milestone_ids))
2198 return sorted(milestones, key=milestone_sort_key, reverse=True)2291 return sorted(milestones, key=milestone_sort_key, reverse=True)
21992292
@@ -2514,14 +2607,40 @@
25142607
2515 def getOrderByColumnDBName(self, col_name):2608 def getOrderByColumnDBName(self, col_name):
2516 """See `IBugTaskSet`."""2609 """See `IBugTaskSet`."""
2517 return self._ORDERBY_COLUMN[col_name]2610 if BugTaskSet._ORDERBY_COLUMN is None:
2611 # Local import of Bug to avoid import loop.
2612 from lp.bugs.model.bug import Bug
2613 BugTaskSet._ORDERBY_COLUMN = {
2614 "id": BugTask.bugID,
2615 "importance": BugTask.importance,
2616 # TODO: sort by their name?
2617 "assignee": BugTask.assigneeID,
2618 "targetname": BugTask.targetnamecache,
2619 "status": BugTask.status,
2620 "title": Bug.title,
2621 "milestone": BugTask.milestoneID,
2622 "dateassigned": BugTask.date_assigned,
2623 "datecreated": BugTask.datecreated,
2624 "date_last_updated": Bug.date_last_updated,
2625 "date_closed": BugTask.date_closed,
2626 "number_of_duplicates": Bug.number_of_duplicates,
2627 "message_count": Bug.message_count,
2628 "users_affected_count": Bug.users_affected_count,
2629 "heat": Bug.heat,
2630 "latest_patch_uploaded": Bug.latest_patch_uploaded,
2631 }
2632 return BugTaskSet._ORDERBY_COLUMN[col_name]
25182633
2519 def _processOrderBy(self, params):2634 def _processOrderBy(self, params):
2520 """Process the orderby parameter supplied to search().2635 """Process the orderby parameter supplied to search().
25212636
2522 This method ensures the sort order will be stable, and converting2637 This method ensures the sort order will be stable, and converting
2523 the string supplied to actual column names.2638 the string supplied to actual column names.
2639
2640 :return: A Storm order_by tuple.
2524 """2641 """
2642 # Local import of Bug to avoid import loop.
2643 from lp.bugs.model.bug import Bug
2525 orderby = params.orderby2644 orderby = params.orderby
2526 if orderby is None:2645 if orderby is None:
2527 orderby = []2646 orderby = []
@@ -2536,10 +2655,10 @@
2536 # columns to make the sort consistent over runs -- which is good2655 # columns to make the sort consistent over runs -- which is good
2537 # for the user and essential for the test suite.2656 # for the user and essential for the test suite.
2538 unambiguous_cols = set([2657 unambiguous_cols = set([
2539 "BugTask.dateassigned",2658 BugTask.date_assigned,
2540 "BugTask.datecreated",2659 BugTask.datecreated,
2541 "Bug.datecreated",2660 Bug.datecreated,
2542 "Bug.date_last_updated"])2661 Bug.date_last_updated])
2543 # Bug ID is unique within bugs on a product or source package.2662 # Bug ID is unique within bugs on a product or source package.
2544 if (params.product or2663 if (params.product or
2545 (params.distribution and params.sourcepackagename) or2664 (params.distribution and params.sourcepackagename) or
@@ -2549,7 +2668,7 @@
2549 in_unique_context = False2668 in_unique_context = False
25502669
2551 if in_unique_context:2670 if in_unique_context:
2552 unambiguous_cols.add("BugTask.bug")2671 unambiguous_cols.add(BugTask.bug)
25532672
2554 # Translate orderby keys into corresponding Table.attribute2673 # Translate orderby keys into corresponding Table.attribute
2555 # strings.2674 # strings.
@@ -2559,22 +2678,22 @@
2559 orderby_arg.append(orderby_col)2678 orderby_arg.append(orderby_col)
2560 continue2679 continue
2561 if orderby_col.startswith("-"):2680 if orderby_col.startswith("-"):
2562 col_name = self.getOrderByColumnDBName(orderby_col[1:])2681 col = self.getOrderByColumnDBName(orderby_col[1:])
2563 order_clause = "-" + col_name2682 order_clause = Desc(col)
2564 else:2683 else:
2565 col_name = self.getOrderByColumnDBName(orderby_col)2684 col = self.getOrderByColumnDBName(orderby_col)
2566 order_clause = col_name2685 order_clause = col
2567 if col_name in unambiguous_cols:2686 if col in unambiguous_cols:
2568 ambiguous = False2687 ambiguous = False
2569 orderby_arg.append(order_clause)2688 orderby_arg.append(order_clause)
25702689
2571 if ambiguous:2690 if ambiguous:
2572 if in_unique_context:2691 if in_unique_context:
2573 orderby_arg.append('BugTask.bug')2692 orderby_arg.append(BugTask.bugID)
2574 else:2693 else:
2575 orderby_arg.append('BugTask.id')2694 orderby_arg.append(BugTask.id)
25762695
2577 return orderby_arg2696 return tuple(orderby_arg)
25782697
2579 def dangerousGetAllTasks(self):2698 def dangerousGetAllTasks(self):
2580 """DO NOT USE THIS METHOD. For details, see `IBugTaskSet`"""2699 """DO NOT USE THIS METHOD. For details, see `IBugTaskSet`"""
25812700
=== modified file 'lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt'
--- lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt 2009-08-31 15:41:15 +0000
+++ lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt 2010-08-22 18:34:46 +0000
@@ -11,10 +11,10 @@
11 >>> launchbag = getUtility(ILaunchBag)11 >>> launchbag = getUtility(ILaunchBag)
12 >>> evo = getUtility(ISourcePackageNameSet).queryByName("evolution")12 >>> evo = getUtility(ISourcePackageNameSet).queryByName("evolution")
13 >>> params = BugTaskSearchParams(user=launchbag.user,13 >>> params = BugTaskSearchParams(user=launchbag.user,
14 ... sourcepackagename=evo, orderby="id")14 ... sourcepackagename=evo, orderby="-id")
1515
16 >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")16 >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
17 >>> latest_evo_task = ubuntu.searchTasks(params)[-1]17 >>> latest_evo_task = ubuntu.searchTasks(params)[0]
18 >>> latest_evo_bug = latest_evo_task.bug.id18 >>> latest_evo_bug = latest_evo_task.bug.id
19 >>> logout()19 >>> logout()
2020
2121
=== modified file 'lib/lp/bugs/tests/test_bugtask.py'
--- lib/lp/bugs/tests/test_bugtask.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/tests/test_bugtask.py 2010-08-22 18:34:46 +0000
@@ -25,6 +25,7 @@
25 BugTaskImportance,25 BugTaskImportance,
26 BugTaskSearchParams,26 BugTaskSearchParams,
27 BugTaskStatus,27 BugTaskStatus,
28 IBugTaskSet,
28 )29 )
29from lp.bugs.model.bugtask import build_tag_search_clause30from lp.bugs.model.bugtask import build_tag_search_clause
30from lp.hardwaredb.interfaces.hwdb import (31from lp.hardwaredb.interfaces.hwdb import (
@@ -32,7 +33,7 @@
32 IHWDeviceSet,33 IHWDeviceSet,
33 )34 )
34from lp.registry.interfaces.distribution import IDistributionSet35from lp.registry.interfaces.distribution import IDistributionSet
35from lp.registry.interfaces.person import IPersonSet36from lp.registry.interfaces.person import IPerson, IPersonSet
36from lp.testing import (37from lp.testing import (
37 ANONYMOUS,38 ANONYMOUS,
38 login,39 login,
@@ -807,6 +808,28 @@
807 result = target.searchTasks(None, modified_since=date)808 result = target.searchTasks(None, modified_since=date)
808 self.assertEqual([task2], list(result))809 self.assertEqual([task2], list(result))
809810
811 def test_private_bug_view_permissions_cached(self):
812 """Private bugs from a search know the user can see the bugs."""
813 target = self.makeBugTarget()
814 person = self.login()
815 self.factory.makeBug(product=target, private=True, owner=person)
816 self.factory.makeBug(product=target, private=True, owner=person)
817 # Search style and parameters taken from the milestone index view where
818 # the issue was discovered.
819 login_person(person)
820 tasks = target.searchTasks(BugTaskSearchParams(person, omit_dupes=True,
821 orderby=['status', '-importance', 'id']))
822 # We must be finding the bugs.
823 self.assertEqual(2, tasks.count())
824 # Cache in the storm cache the account->person lookup so its not
825 # distorting what we're testing.
826 _ = IPerson(person.account, None)
827 # One query and only one should be issued to get the tasks, bugs and
828 # allow access to getConjoinedMaster attribute - an attribute that
829 # triggers a permission check (nb: id does not trigger such a check)
830 self.assertStatementCount(1,
831 lambda:[task.getConjoinedMaster for task in tasks])
832
810833
811def test_suite():834def test_suite():
812 suite = unittest.TestSuite()835 suite = unittest.TestSuite()
813836
=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py 2010-08-22 18:34:46 +0000
@@ -1,24 +1,30 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from __future__ import with_statement
5
4"""Test milestone views."""6"""Test milestone views."""
57
6__metaclass__ = type8__metaclass__ = type
79
8import unittest10from testtools.matchers import LessThan
9
10from zope.component import getUtility11from zope.component import getUtility
1112
13from canonical.launchpad.webapp import canonical_url
12from canonical.testing.layers import DatabaseFunctionalLayer14from canonical.testing.layers import DatabaseFunctionalLayer
13from lp.bugs.interfaces.bugtask import IBugTaskSet15from lp.bugs.interfaces.bugtask import IBugTaskSet
14from lp.testing import (16from lp.testing import (
15 ANONYMOUS,17 ANONYMOUS,
16 login,18 login,
17 login_person,19 login_person,
20 logout,
21 person_logged_in,
18 TestCaseWithFactory,22 TestCaseWithFactory,
19 )23 )
24from lp.testing.matchers import HasQueryCount
20from lp.testing.memcache import MemcacheTestCase25from lp.testing.memcache import MemcacheTestCase
21from lp.testing.views import create_initialized_view26from lp.testing.views import create_initialized_view
27from lp.testing._webservice import QueryCollector
2228
2329
24class TestMilestoneViews(TestCaseWithFactory):30class TestMilestoneViews(TestCaseWithFactory):
@@ -158,5 +164,67 @@
158 self.assertEqual(0, product.development_focus.all_bugtasks.count())164 self.assertEqual(0, product.development_focus.all_bugtasks.count())
159165
160166
161def test_suite():167class TestMilestoneIndex(TestCaseWithFactory):
162 return unittest.TestLoader().loadTestsFromName(__name__)168
169 layer = DatabaseFunctionalLayer
170
171 def test_more_private_bugs_query_count_is_constant(self):
172 # This test tests that as we add more private bugs to a milestone index
173 # page, the number of queries issued by the page does not change.
174 # It also sets a cap on the queries for this page: if the baseline
175 # were to increase, the test would fail. As the baseline is very large
176 # already, if the test fails due to such a change, please cut some more
177 # of the existing fat out of it rather than increasing the cap.
178 product = self.factory.makeProduct()
179 login_person(product.owner)
180 milestone = self.factory.makeMilestone(
181 productseries=product.development_focus)
182 bug1 = self.factory.makeBug(product=product, private=True,
183 owner=product.owner)
184 bug1.bugtasks[0].transitionToMilestone(milestone, product.owner)
185 # We look at the page as someone who is a member of a team and the team
186 # is subscribed to the bugs, so that we don't get trivial shortcuts
187 # avoiding queries : test the worst case.
188 subscribed_team = self.factory.makeTeam()
189 viewer = self.factory.makePerson(password="test")
190 with person_logged_in(subscribed_team.teamowner):
191 subscribed_team.addMember(viewer, subscribed_team.teamowner)
192 bug1.subscribe(subscribed_team, product.owner)
193 bug1_url = canonical_url(bug1)
194 milestone_url = canonical_url(milestone)
195 browser = self.getUserBrowser(user=viewer)
196 # Seed the cookie cache and any other cross-request state we may gain
197 # in future. See canonical.launchpad.webapp.serssion: _get_secret.
198 browser.open(milestone_url)
199 collector = QueryCollector()
200 collector.register()
201 self.addCleanup(collector.unregister)
202 # This page is rather high in queries : 46 as a baseline. 20100817
203 page_query_limit = 46
204 browser.open(milestone_url)
205 # Check that the test found the bug
206 self.assertTrue(bug1_url in browser.contents)
207 self.assertThat(collector, HasQueryCount(LessThan(page_query_limit)))
208 with_1_private_bug = collector.count
209 with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in
210 enumerate(collector.queries)]
211 login_person(product.owner)
212 bug2 = self.factory.makeBug(product=product, private=True,
213 owner=product.owner)
214 bug2.bugtasks[0].transitionToMilestone(milestone, product.owner)
215 bug2.subscribe(subscribed_team, product.owner)
216 bug2_url = canonical_url(bug2)
217 bug3 = self.factory.makeBug(product=product, private=True,
218 owner=product.owner)
219 bug3.bugtasks[0].transitionToMilestone(milestone, product.owner)
220 bug3.subscribe(subscribed_team, product.owner)
221 logout()
222 browser.open(milestone_url)
223 self.assertTrue(bug2_url in browser.contents)
224 self.assertThat(collector, HasQueryCount(LessThan(page_query_limit)))
225 with_3_private_bugs = collector.count
226 with_3_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in
227 enumerate(collector.queries)]
228 self.assertEqual(with_1_private_bug, with_3_private_bugs,
229 "different query count: \n%s\n******************\n%s\n" % (
230 '\n'.join(with_1_queries), '\n'.join(with_3_queries)))
163231
=== modified file 'lib/lp/registry/browser/tests/test_views.py'
--- lib/lp/registry/browser/tests/test_views.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/tests/test_views.py 2010-08-22 18:34:46 +0000
@@ -35,6 +35,7 @@
35 'product-edit-people-view.txt': LaunchpadFunctionalLayer,35 'product-edit-people-view.txt': LaunchpadFunctionalLayer,
36 'product-files-views.txt': LaunchpadFunctionalLayer,36 'product-files-views.txt': LaunchpadFunctionalLayer,
37 'product-views.txt': LaunchpadFunctionalLayer,37 'product-views.txt': LaunchpadFunctionalLayer,
38 'productseries-views.txt': LaunchpadFunctionalLayer,
38 'projectgroup-views.txt': LaunchpadFunctionalLayer,39 'projectgroup-views.txt': LaunchpadFunctionalLayer,
39 'user-to-user-views.txt': LaunchpadFunctionalLayer,40 'user-to-user-views.txt': LaunchpadFunctionalLayer,
40}41}
4142
=== modified file 'lib/lp/registry/doc/distribution.txt'
--- lib/lp/registry/doc/distribution.txt 2010-08-06 14:49:35 +0000
+++ lib/lp/registry/doc/distribution.txt 2010-08-22 18:34:46 +0000
@@ -585,7 +585,7 @@
585 ... name='impossible', dateexpected=datetime(2028, 10, 1))585 ... name='impossible', dateexpected=datetime(2028, 10, 1))
586 Traceback (most recent call last):586 Traceback (most recent call last):
587 ...587 ...
588 Unauthorized: (<DistroSeries at ...>, 'newMilestone', 'launchpad.Edit')588 Unauthorized: (<DistroSeries u'woody'>, 'newMilestone', 'launchpad.Edit')
589 >>> login('mark@example.com')589 >>> login('mark@example.com')
590 >>> debian_milestone = woody.newMilestone(590 >>> debian_milestone = woody.newMilestone(
591 ... name='woody-rc1', dateexpected=datetime(2028, 10, 1))591 ... name='woody-rc1', dateexpected=datetime(2028, 10, 1))
592592
=== modified file 'lib/lp/registry/doc/sourcepackage.txt'
--- lib/lp/registry/doc/sourcepackage.txt 2010-07-02 20:32:58 +0000
+++ lib/lp/registry/doc/sourcepackage.txt 2010-08-22 18:34:46 +0000
@@ -71,7 +71,7 @@
71 >>> firefox_warty_package = SourcePackage(sourcepackagename=firefox,71 >>> firefox_warty_package = SourcePackage(sourcepackagename=firefox,
72 ... distroseries=ubuntu_warty)72 ... distroseries=ubuntu_warty)
73 >>> firefox_warty_package73 >>> firefox_warty_package
74 <SourcePackage ubuntu/warty/mozilla-firefox>74 <SourcePackage ...'Ubuntu'...'warty'...'mozilla-firefox'...>
7575
76An instance is commonly retrieved from a distroseries.76An instance is commonly retrieved from a distroseries.
7777
7878
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/distroseries.py 2010-08-22 18:34:46 +0000
@@ -270,6 +270,9 @@
270 """See `IDistroSeries`."""270 """See `IDistroSeries`."""
271 return self.getDistroArchSeries(archtag)271 return self.getDistroArchSeries(archtag)
272272
273 def __repr__(self):
274 return '<%s %r>' % (self.__class__.__name__, self.name)
275
273 def __str__(self):276 def __str__(self):
274 return '%s %s' % (self.distribution.name, self.name)277 return '%s %s' % (self.distribution.name, self.name)
275278
276279
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/sourcepackage.py 2010-08-22 18:34:46 +0000
@@ -213,7 +213,8 @@
213 return cls(sourcepackagename, distroseries)213 return cls(sourcepackagename, distroseries)
214214
215 def __repr__(self):215 def __repr__(self):
216 return '<%s %s>' % (self.__class__.__name__, self.path)216 return '<%s %r %r %r>' % (self.__class__.__name__,
217 self.distribution, self.distroseries, self.sourcepackagename)
217218
218 def _get_ubuntu(self):219 def _get_ubuntu(self):
219 # XXX: kiko 2006-03-20: Ideally, it would be possible to just do220 # XXX: kiko 2006-03-20: Ideally, it would be possible to just do
220221
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-08-20 20:31:18 +0000
+++ lib/lp/testing/factory.py 2010-08-22 18:34:46 +0000
@@ -7,7 +7,7 @@
77
8"""Testing infrastructure for the Launchpad application.8"""Testing infrastructure for the Launchpad application.
99
10This module should not have any actual tests.10This module should not contain tests (but it should be tested).
11"""11"""
1212
13__metaclass__ = type13__metaclass__ = type