launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01106
[Merge] lp:~lifeless/launchpad/malone into lp:launchpad/devel
Robert Collins has proposed merging lp:~lifeless/launchpad/malone into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#497386 ScopedCollection:CollectionResource:#message-page-resource timeouts for bugs with many messages
https://bugs.launchpad.net/bugs/497386
#607776 +filebug-show-similar FTI query timeouts
https://bugs.launchpad.net/bugs/607776
#618375 BugTrackerSet:+index consistently slower than 10 seconds
https://bugs.launchpad.net/bugs/618375
Batch the bug tracker lists - theres no point showing 1000 by default; the batching will also shrink the time massively; we can actually tune it later (this was sensible to do regardless and will give more headroom than just tuning it in the short term).
--
https://code.launchpad.net/~lifeless/launchpad/malone/+merge/35941
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/malone into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/webapp/batching.py'
--- lib/canonical/launchpad/webapp/batching.py 2010-09-06 18:52:45 +0000
+++ lib/canonical/launchpad/webapp/batching.py 2010-09-19 03:09:53 +0000
@@ -77,6 +77,24 @@
return self.batch.total() > self.batch.size
+class ActiveBatchNavigator(BatchNavigator):
+ """A paginator for active items.
+
+ Used when a view needs to display more than one BatchNavigator of items.
+ """
+ start_variable_name = 'active_start'
+ batch_variable_name = 'active_batch'
+
+
+class InactiveBatchNavigator(BatchNavigator):
+ """A paginator for inactive items.
+
+ Used when a view needs to display more than one BatchNavigator of items.
+ """
+ start_variable_name = 'inactive_start'
+ batch_variable_name = 'inactive_batch'
+
+
class TableBatchNavigator(BatchNavigator):
"""See canonical.launchpad.interfaces.ITableBatchNavigator."""
implements(ITableBatchNavigator)
=== modified file 'lib/lp/blueprints/browser/tests/test_specificationtarget.py'
--- lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-09-17 14:04:29 +0000
+++ lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-09-19 03:09:53 +0000
@@ -3,11 +3,14 @@
__metaclass__ = type
+<<<<<<< TREE
import unittest
from zope.security.proxy import removeSecurityProxy
from canonical.launchpad.webapp.batching import BatchNavigator
+=======
+>>>>>>> MERGE-SOURCE
from canonical.launchpad.testing.pages import find_tag_by_id
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.blueprints.interfaces.specificationtarget import (
@@ -21,6 +24,7 @@
login_person,
TestCaseWithFactory,
)
+from lp.testing.matchers import IsConfiguredBatchNavigator
from lp.testing.views import (
create_view,
create_initialized_view,
@@ -108,22 +112,11 @@
# subclass.
person = self.factory.makePerson()
view = create_initialized_view(person, name='+assignments')
- self.assertIsInstance(view.specs_batched, BatchNavigator)
-
- def test_batch_headings(self):
- person = self.factory.makePerson()
- view = create_initialized_view(person, name='+assignments')
- navigator = view.specs_batched
- self.assertEqual('specification', navigator._singular_heading)
- self.assertEqual('specifications', navigator._plural_heading)
-
- def test_batch_size(self):
# Because +assignments is meant to provide an overview, we default to
# 500 as the default batch size.
- person = self.factory.makePerson()
- view = create_initialized_view(person, name='+assignments')
- navigator = view.specs_batched
- self.assertEqual(500, view.specs_batched.default_size)
+ matcher = IsConfiguredBatchNavigator(
+ 'specification', 'specifications', batch_size=500)
+ self.assertThat(view.specs_batched, matcher)
class TestAssignments(TestCaseWithFactory):
=== modified file 'lib/lp/bugs/browser/bugtracker.py'
--- lib/lp/bugs/browser/bugtracker.py 2010-09-03 15:02:39 +0000
+++ lib/lp/bugs/browser/bugtracker.py 2010-09-19 03:09:53 +0000
@@ -54,7 +54,11 @@
structured,
)
from canonical.launchpad.webapp.authorization import check_permission
-from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.webapp.batching import (
+ ActiveBatchNavigator,
+ BatchNavigator,
+ InactiveBatchNavigator,
+ )
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
from canonical.launchpad.webapp.menu import NavigationMenu
from canonical.lazr.utils import smartquote
@@ -154,21 +158,30 @@
pillar_limit = 3
def initialize(self):
- # Sort the bug trackers into active and inactive lists so that
- # we can display them separately.
- all_bug_trackers = list(self.context)
- self.active_bug_trackers = [
- bug_tracker for bug_tracker in all_bug_trackers
- if bug_tracker.active]
- self.inactive_bug_trackers = [
- bug_tracker for bug_tracker in all_bug_trackers
- if not bug_tracker.active]
-
- bugtrackerset = getUtility(IBugTrackerSet)
- # The caching of bugtracker pillars here avoids us hitting the
- # database multiple times for each bugtracker.
- self._pillar_cache = bugtrackerset.getPillarsForBugtrackers(
- all_bug_trackers)
+ # eager load related pillars. In future we should do this for
+ # just the rendered trackers, and also use group by to get
+ # bug watch counts per tracker. However the batching makes
+ # the inefficiency tolerable for now. Robert Collins 20100919.
+ self._pillar_cache = self.context.getPillarsForBugtrackers(
+ list(self.context.trackers()))
+
+ @property
+ def inactive_tracker_count(self):
+ return self.inactive_trackers.currentBatch().listlength
+
+ @cachedproperty
+ def active_trackers(self):
+ results = self.context.trackers(active=True)
+ navigator = ActiveBatchNavigator(results, self.request)
+ navigator.setHeadings('tracker', 'trackers')
+ return navigator
+
+ @cachedproperty
+ def inactive_trackers(self):
+ results = self.context.trackers(active=False)
+ navigator = InactiveBatchNavigator(results, self.request)
+ navigator.setHeadings('tracker', 'trackers')
+ return navigator
def getPillarData(self, bugtracker):
"""Return dict of pillars and booleans indicating ellipsis.
=== modified file 'lib/lp/bugs/browser/tests/test_bugtracker_views.py'
--- lib/lp/bugs/browser/tests/test_bugtracker_views.py 2010-08-26 20:08:43 +0000
+++ lib/lp/bugs/browser/tests/test_bugtracker_views.py 2010-09-19 03:09:53 +0000
@@ -3,27 +3,61 @@
"""Tests for BugTracker views."""
+from __future__ import with_statement
+
__metaclass__ = type
-import unittest
-
-from datetime import datetime, timedelta
-from pytz import utc
-
from zope.component import getUtility
-from zope.security.interfaces import Unauthorized
-from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.testing.pages import find_tag_by_id
+from canonical.launchpad.webapp import canonical_url
from canonical.testing.layers import DatabaseFunctionalLayer
-
-from lp.bugs.browser.bugtracker import (
- BugTrackerEditView)
+from lp.bugs.interfaces.bugtracker import IBugTrackerSet
from lp.registry.interfaces.person import IPersonSet
-from lp.testing import login, TestCaseWithFactory
-from lp.testing.sampledata import ADMIN_EMAIL, NO_PRIVILEGE_EMAIL
+from lp.testing import (
+ login,
+ TestCaseWithFactory,
+ )
from lp.testing.views import create_initialized_view
-
-
-
-def test_suite():
- return unittest.TestLoader().loadTestsFromName(__name__)
+from lp.testing.matchers import IsConfiguredBatchNavigator
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.sampledata import ADMIN_EMAIL
+
+
+class TestBugTrackerSetView(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_trackers_are_batch_navigators(self):
+ trackers = getUtility(IBugTrackerSet)
+ view = create_initialized_view(trackers, name='+index')
+ matcher = IsConfiguredBatchNavigator('tracker', 'trackers')
+ self.assertThat(view.active_trackers, matcher)
+ self.assertThat(view.inactive_trackers, matcher)
+
+ def test_page_is_batched(self):
+ active_tracker1 = self.factory.makeBugTracker()
+ active_tracker2 = self.factory.makeBugTracker()
+ inactive_tracker1 = self.factory.makeBugTracker()
+ inactive_tracker2 = self.factory.makeBugTracker()
+ admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any()
+ with person_logged_in(admin):
+ inactive_tracker1.active = False
+ inactive_tracker2.active = False
+ trackers = getUtility(IBugTrackerSet)
+ url = (canonical_url(trackers) +
+ "/+index?active_batch=1&inactive_batch=1")
+ browser = self.getUserBrowser(url)
+ content = browser.contents
+ # XXX RobertCollns 20100919 bug=642504. The support for multiple batches
+ # isn't complete and the id for the nav links gets duplicated.
+ #self.assertEqual('next',
+ # find_tag_by_id(content, 'upper-batch-nav-batchnav-next')['class'])
+ #self.assertEqual('next',
+ # find_tag_by_id(content, 'lower-batch-nav-batchnav-next')['class'])
+ # Instead we check the string appears.
+ self.assertTrue('upper-batch-nav-batchnav-next' in content)
+
=== modified file 'lib/lp/bugs/interfaces/bugtracker.py'
--- lib/lp/bugs/interfaces/bugtracker.py 2010-08-26 20:08:43 +0000
+++ lib/lp/bugs/interfaces/bugtracker.py 2010-09-19 03:09:53 +0000
@@ -432,6 +432,13 @@
def getPillarsForBugtrackers(bug_trackers):
"""Return dict mapping bugtrackers to lists of pillars."""
+ def trackers(active=None):
+ """Return a ResultSet of bugtrackers.
+
+ :param active: If True, only active trackers are returned, if False
+ only inactive trackers are returned.
+ """
+
class IBugTrackerAlias(Interface):
"""Another URL for a remote bug system.
=== modified file 'lib/lp/bugs/model/bugtracker.py'
--- lib/lp/bugs/model/bugtracker.py 2010-08-26 20:08:43 +0000
+++ lib/lp/bugs/model/bugtracker.py 2010-09-19 03:09:53 +0000
@@ -8,7 +8,8 @@
'BugTracker',
'BugTrackerAlias',
'BugTrackerAliasSet',
- 'BugTrackerSet']
+ 'BugTrackerSet',
+ ]
from datetime import datetime
@@ -66,6 +67,8 @@
IBugTrackerSet,
SINGLE_PRODUCT_BUGTRACKERTYPES,
)
+from canonical.launchpad.webapp.interfaces import (
+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
from lp.bugs.interfaces.bugtrackerperson import BugTrackerPersonAlreadyExists
from lp.bugs.model.bug import Bug
from lp.bugs.model.bugmessage import BugMessage
@@ -577,6 +580,17 @@
"""See `IBugTrackerSet`."""
return BugTracker.select()
+ def trackers(self, active=None):
+ # Without context, cannot tell what store flavour is desirable.
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ if active is not None:
+ clauses = [BugTracker.active==active]
+ else:
+ clauses = []
+ results = store.find(BugTracker, *clauses)
+ results.order_by(BugTracker.name)
+ return results
+
def ensureBugTracker(self, baseurl, owner, bugtrackertype,
title=None, summary=None, contactdetails=None, name=None):
"""See `IBugTrackerSet`."""
=== modified file 'lib/lp/bugs/templates/bugtrackers-index.pt'
--- lib/lp/bugs/templates/bugtrackers-index.pt 2009-09-01 16:24:52 +0000
+++ lib/lp/bugs/templates/bugtrackers-index.pt 2010-09-19 03:09:53 +0000
@@ -13,6 +13,7 @@
</tal:comment>
<metal:macros fill-slot="bogus">
<metal:macro define-macro="tracker-table">
+ <tal:navigation content="structure trackers/@@+navigation-links-upper" />
<table class="sortable listing" tal:attributes="id id">
<thead>
<tr>
@@ -24,7 +25,7 @@
</tr>
</thead>
<tbody>
- <tr tal:repeat="tracker trackers">
+ <tr tal:repeat="tracker trackers/currentBatch">
<td>
<a tal:replace="structure tracker/fmt:link" />
</td>
@@ -53,6 +54,7 @@
</tr>
</tbody>
</table>
+ <tal:navigation content="structure trackers/@@+navigation-links-lower" />
</metal:macro>
</metal:macros>
@@ -70,7 +72,7 @@
the external status changes.
</p>
<tal:table define="id string:trackers;
- trackers view/active_bug_trackers">
+ trackers view/active_trackers">
<metal:table use-macro="template/macros/tracker-table" />
</tal:table>
<p style="margin-top: 1em"
@@ -81,11 +83,11 @@
</p>
</div>
</div>
- <div class="yui-u" tal:condition="view/inactive_bug_trackers">
+ <div class="yui-u" tal:condition="view/inactive_tracker_count">
<div class="portlet">
<h2>Inactive bug trackers</h2>
<tal:table define="id string:inactive-trackers;
- trackers view/inactive_bug_trackers">
+ trackers view/inactive_trackers">
<metal:table use-macro="template/macros/tracker-table" />
</tal:table>
</div>
=== modified file 'lib/lp/bugs/tests/test_bugtracker.py'
--- lib/lp/bugs/tests/test_bugtracker.py 2010-08-26 20:08:43 +0000
+++ lib/lp/bugs/tests/test_bugtracker.py 2010-09-19 03:09:53 +0000
@@ -28,7 +28,10 @@
from canonical.launchpad.ftests import login_person
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
-from canonical.testing import LaunchpadFunctionalLayer
+from canonical.testing import (
+ DatabaseFunctionalLayer,
+ LaunchpadFunctionalLayer,
+ )
from lp.bugs.externalbugtracker import (
BugTrackerConnectError,
Mantis,
@@ -38,12 +41,36 @@
BugTrackerType,
IBugTracker,
)
+from lp.bugs.model.bugtracker import BugTrackerSet
from lp.bugs.tests.externalbugtracker import Urlib2TransportTestHandler
from lp.registry.interfaces.person import IPersonSet
from lp.testing import login, login_person, TestCaseWithFactory
from lp.testing.sampledata import ADMIN_EMAIL
+class TestBugTrackerSet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_trackers(self):
+ tracker = self.factory.makeBugTracker()
+ trackers = BugTrackerSet()
+ # Active trackers are in all trackers,
+ self.assertTrue(tracker in trackers.trackers())
+ # and active,
+ self.assertTrue(tracker in trackers.trackers(active=True))
+ # But not inactive.
+ self.assertFalse(tracker in trackers.trackers(active=False))
+ login(ADMIN_EMAIL)
+ tracker.active = False
+ # Inactive trackers are in all trackers
+ self.assertTrue(tracker in trackers.trackers())
+ # and inactive,
+ self.assertTrue(tracker in trackers.trackers(active=False))
+ # but not in active.
+ self.assertFalse(tracker in trackers.trackers(active=True))
+
+
class BugTrackerTestCase(TestCaseWithFactory):
"""Unit tests for the `BugTracker` class."""
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-09-15 16:44:41 +0000
+++ lib/lp/registry/browser/person.py 2010-09-19 03:09:53 +0000
@@ -194,7 +194,11 @@
structured,
)
from canonical.launchpad.webapp.authorization import check_permission
-from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.webapp.batching import (
+ ActiveBatchNavigator,
+ BatchNavigator,
+ InactiveBatchNavigator,
+ )
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
from canonical.launchpad.webapp.interfaces import (
ILaunchBag,
@@ -1385,24 +1389,6 @@
links = ['profile', 'polls', 'members', 'ppas']
-class ActiveBatchNavigator(BatchNavigator):
- """A paginator for active items.
-
- Used when a view needs to display more than one BatchNavigator of items.
- """
- start_variable_name = 'active_start'
- batch_variable_name = 'active_batch'
-
-
-class InactiveBatchNavigator(BatchNavigator):
- """A paginator for inactive items.
-
- Used when a view needs to display more than one BatchNavigator of items.
- """
- start_variable_name = 'inactive_start'
- batch_variable_name = 'inactive_batch'
-
-
class TeamMembershipView(LaunchpadView):
"""The view behind ITeam/+members."""
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2010-09-17 18:32:23 +0000
+++ lib/lp/registry/browser/team.py 2010-09-19 03:09:53 +0000
@@ -829,8 +829,6 @@
"""
results = self.mailing_list.getReviewableMessages()
navigator = BatchNavigator(results, self.request)
- # Subclasses often set the singular and plural headings,
- # but we can use the generic class too.
navigator.setHeadings('message', 'messages')
return navigator
=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py 2010-09-14 00:21:04 +0000
+++ lib/lp/registry/browser/tests/test_team.py 2010-09-19 03:09:53 +0000
@@ -3,10 +3,10 @@
__metaclass__ = type
-from canonical.launchpad.webapp.batching import BatchNavigator
from canonical.testing import DatabaseFunctionalLayer
from lp.registry.browser.person import TeamOverviewMenu
from lp.testing import TestCaseWithFactory
+from lp.testing.matchers import IsConfiguredBatchNavigator
from lp.testing.menu import check_menu_links
from lp.testing.views import create_initialized_view
@@ -47,12 +47,6 @@
team = self.factory.makeTeam()
self.factory.makeMailingList(team, team.teamowner)
view = create_initialized_view(team, name='+mailinglist-moderate')
- self.assertIsInstance(view.held_messages, BatchNavigator)
-
- def test_held_message_headings(self):
- team = self.factory.makeTeam()
- self.factory.makeMailingList(team, team.teamowner)
- view = create_initialized_view(team, name='+mailinglist-moderate')
- navigator = view.held_messages
- self.assertEqual('message', navigator._singular_heading)
- self.assertEqual('messages', navigator._plural_heading)
+ self.assertThat(
+ view.held_messages,
+ IsConfiguredBatchNavigator('message', 'messages'))
=== modified file 'lib/lp/testing/matchers.py'
--- lib/lp/testing/matchers.py 2010-08-25 20:04:40 +0000
+++ lib/lp/testing/matchers.py 2010-09-19 03:09:53 +0000
@@ -17,9 +17,12 @@
from testtools.content import Content
from testtools.content_type import UTF8_TEXT
from testtools.matchers import (
+ Equals,
Matcher,
Mismatch,
+ MismatchesAll,
)
+from testtools import matchers
from zope.interface.exceptions import (
BrokenImplementation,
BrokenMethodImplementation,
@@ -31,6 +34,8 @@
Proxy,
)
+from canonical.launchpad.webapp.batching import BatchNavigator
+
class DoesNotProvide(Mismatch):
"""An object does not provide an interface."""
@@ -220,3 +225,44 @@
if not matchee.startswith(self.expected):
return DoesNotStartWith(matchee, self.expected)
return None
+
+
+class IsConfiguredBatchNavigator(Matcher):
+ """Check that an object is a batch navigator."""
+
+ def __init__(self, singular, plural, batch_size=None):
+ """Create a ConfiguredBatchNavigator.
+
+ :param singular: The singular header the batch should be using.
+ :param plural: The plural header the batch should be using.
+ """
+ self._single = Equals(singular)
+ self._plural = Equals(plural)
+ self._batch = None
+ if batch_size:
+ self._batch = Equals(batch_size)
+ self.matchers = dict(
+ _singular_heading=self._single, _plural_heading=self._plural)
+ if self._batch:
+ self.matchers['default_size'] = self._batch
+
+ def __str__(self):
+ if self._batch:
+ batch = ", %r" % self._batch.expected
+ else:
+ batch = ''
+ return "ConfiguredBatchNavigator(%r, %r%s)" % (
+ self._single.expected, self._plural.expected, batch)
+
+ def match(self, matchee):
+ if not isinstance(matchee, BatchNavigator):
+ # Testtools doesn't have an IsInstanceMismatch yet.
+ return matchers._BinaryMismatch(
+ BatchNavigator, 'isinstance', matchee)
+ mismatches = []
+ for attrname, matcher in self.matchers.items():
+ mismatches.append(matcher.match(getattr(matchee, attrname)))
+ if not mismatches[-1]:
+ mismatches.pop(-1)
+ if mismatches:
+ return MismatchesAll(mismatches)
=== modified file 'lib/lp/testing/views.py'
--- lib/lp/testing/views.py 2010-08-24 01:57:53 +0000
+++ lib/lp/testing/views.py 2010-09-19 03:09:53 +0000
@@ -12,6 +12,10 @@
import os
+from testtools.matchers import (
+ Matcher,
+ Mismatch,
+ )
from zope.component import (
getMultiAdapter,
getUtility,