The change is basically fine, and clearly a good idea. I have a number of code clarity comments though, see below.
> === 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 22:21:05 +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'
> +
> +
These are fine and clear enough -- are they generic enough to go in a
generic enough location though? Later: oh, I see you moved them from
other code. It's a shame you have to subclass to change these.
> 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 22:21:05 +0000
> @@ -3,11 +3,9 @@
>
> __metaclass__ = type
>
> -import unittest
>
> from zope.security.proxy import removeSecurityProxy
>
> -from canonical.launchpad.webapp.batching import BatchNavigator
> from canonical.launchpad.testing.pages import find_tag_by_id
> from canonical.testing.layers import DatabaseFunctionalLayer
> from lp.blueprints.interfaces.specificationtarget import (
> @@ -21,6 +19,7 @@
> login_person,
> TestCaseWithFactory,
> )
> +from lp.testing.matchers import IsConfiguredBatchNavigator
> from lp.testing.views import (
> create_view,
> create_initialized_view,
> @@ -108,22 +107,11 @@
> # subclass.
> person = self.factory.makePerson()
> view = create_initialized_view(person, name='+assignments')
Just up from here there's a comment:
# Some pages turn up in very large contexts and patch. E.g.
# Distro:+assignments which uses SpecificationAssignmentsView, a
# subclass.
Is patch a typo for batch here?
> - 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)
I think this reads more clearly if you construct the matcher inline.
>
> class TestAssignments(TestCaseWithFactory):
> @@ -243,13 +231,3 @@
> login_person(project_group.owner)
> view = create_initialized_view(project_group, '+specs')
> self.assertEqual(False, view.can_configure_blueprints)
> -
> -
> -def test_suite():
> - suite = unittest.TestSuite()
> - suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
> - return suite
> -
> -
> -if __name__ == '__main__':
> - unittest.TextTestRunner().run(test_suite())
> === 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 22:21:05 +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
Trailing whitespace here.
> + # 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 22:21:05 +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) +
Trailing whitespace here.
> + "/+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 22:21:05 +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.
> + """
I think you should also say "by default, all 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 22:21:05 +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.
I'm not sure this comment adds anything and suggest you remove it.
> + 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 22:21:05 +0000
> @@ -13,6 +13,7 @@
>
>
>
>
> +
> @@ -24,7 +25,7 @@
>
>
>
> -
> +
>
>
>
>
>
> @@ -53,6 +54,7 @@
>
@@ -81,11 +83,11 @@ >
> > > -