Merge lp:~lifeless/launchpad/malone into lp:launchpad
- malone
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Robert Collins | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 11582 | ||||
Proposed branch: | lp:~lifeless/launchpad/malone | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
598 lines (+223/-101) 13 files modified
lib/canonical/launchpad/webapp/batching.py (+18/-0) lib/lp/blueprints/browser/tests/test_specificationtarget.py (+6/-27) lib/lp/bugs/browser/bugtracker.py (+29/-16) lib/lp/bugs/browser/tests/test_bugtracker_views.py (+51/-17) lib/lp/bugs/interfaces/bugtracker.py (+8/-0) lib/lp/bugs/model/bugtracker.py (+15/-1) lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt (+6/-4) lib/lp/bugs/templates/bugtrackers-index.pt (+6/-4) lib/lp/bugs/tests/test_bugtracker.py (+28/-1) lib/lp/registry/browser/person.py (+5/-19) lib/lp/registry/browser/team.py (+0/-2) lib/lp/registry/browser/tests/test_team.py (+4/-10) lib/lp/testing/matchers.py (+47/-0) |
||||
To merge this branch: | bzr merge lp:~lifeless/launchpad/malone | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Hudson-Doyle | Approve | ||
Review via email: mp+35941@code.launchpad.net |
Commit message
Batch /bugs/bugtrackers
Description of the change
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).
Robert Collins (lifeless) wrote : | # |
Thanks for the feedback will do most of it.
I have a few quirks though:
- matchers inline - i agree, but the noddy style guide line wrapping
rules make it ugly. If you agree that inline is better even so, I'll
move it inline and wrap it the way I would in every other python
project.
- the flavor comment is there because its a deprecated API. I think
the deprecation is a mistake until we actually don't need it.
Thanks,
Rob
Michael Hudson-Doyle (mwhudson) wrote : | # |
For the first, I don't think:
is too bad, but *shrug*, indent it however you like :)
For the second, fine.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/webapp/batching.py' | |||
2 | --- lib/canonical/launchpad/webapp/batching.py 2010-09-06 18:52:45 +0000 | |||
3 | +++ lib/canonical/launchpad/webapp/batching.py 2010-09-20 05:15:57 +0000 | |||
4 | @@ -77,6 +77,24 @@ | |||
5 | 77 | return self.batch.total() > self.batch.size | 77 | return self.batch.total() > self.batch.size |
6 | 78 | 78 | ||
7 | 79 | 79 | ||
8 | 80 | class ActiveBatchNavigator(BatchNavigator): | ||
9 | 81 | """A paginator for active items. | ||
10 | 82 | |||
11 | 83 | Used when a view needs to display more than one BatchNavigator of items. | ||
12 | 84 | """ | ||
13 | 85 | start_variable_name = 'active_start' | ||
14 | 86 | batch_variable_name = 'active_batch' | ||
15 | 87 | |||
16 | 88 | |||
17 | 89 | class InactiveBatchNavigator(BatchNavigator): | ||
18 | 90 | """A paginator for inactive items. | ||
19 | 91 | |||
20 | 92 | Used when a view needs to display more than one BatchNavigator of items. | ||
21 | 93 | """ | ||
22 | 94 | start_variable_name = 'inactive_start' | ||
23 | 95 | batch_variable_name = 'inactive_batch' | ||
24 | 96 | |||
25 | 97 | |||
26 | 80 | class TableBatchNavigator(BatchNavigator): | 98 | class TableBatchNavigator(BatchNavigator): |
27 | 81 | """See canonical.launchpad.interfaces.ITableBatchNavigator.""" | 99 | """See canonical.launchpad.interfaces.ITableBatchNavigator.""" |
28 | 82 | implements(ITableBatchNavigator) | 100 | implements(ITableBatchNavigator) |
29 | 83 | 101 | ||
30 | === modified file 'lib/lp/blueprints/browser/tests/test_specificationtarget.py' | |||
31 | --- lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-09-17 14:04:29 +0000 | |||
32 | +++ lib/lp/blueprints/browser/tests/test_specificationtarget.py 2010-09-20 05:15:57 +0000 | |||
33 | @@ -3,11 +3,9 @@ | |||
34 | 3 | 3 | ||
35 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
36 | 5 | 5 | ||
37 | 6 | import unittest | ||
38 | 7 | 6 | ||
39 | 8 | from zope.security.proxy import removeSecurityProxy | 7 | from zope.security.proxy import removeSecurityProxy |
40 | 9 | 8 | ||
41 | 10 | from canonical.launchpad.webapp.batching import BatchNavigator | ||
42 | 11 | from canonical.launchpad.testing.pages import find_tag_by_id | 9 | from canonical.launchpad.testing.pages import find_tag_by_id |
43 | 12 | from canonical.testing.layers import DatabaseFunctionalLayer | 10 | from canonical.testing.layers import DatabaseFunctionalLayer |
44 | 13 | from lp.blueprints.interfaces.specificationtarget import ( | 11 | from lp.blueprints.interfaces.specificationtarget import ( |
45 | @@ -21,6 +19,7 @@ | |||
46 | 21 | login_person, | 19 | login_person, |
47 | 22 | TestCaseWithFactory, | 20 | TestCaseWithFactory, |
48 | 23 | ) | 21 | ) |
49 | 22 | from lp.testing.matchers import IsConfiguredBatchNavigator | ||
50 | 24 | from lp.testing.views import ( | 23 | from lp.testing.views import ( |
51 | 25 | create_view, | 24 | create_view, |
52 | 26 | create_initialized_view, | 25 | create_initialized_view, |
53 | @@ -103,27 +102,17 @@ | |||
54 | 103 | '<div id="involvement" class="portlet involvement">' in view()) | 102 | '<div id="involvement" class="portlet involvement">' in view()) |
55 | 104 | 103 | ||
56 | 105 | def test_specs_batch(self): | 104 | def test_specs_batch(self): |
58 | 106 | # Some pages turn up in very large contexts and patch. E.g. | 105 | # Some pages turn up in very large contexts and so batch. E.g. |
59 | 107 | # Distro:+assignments which uses SpecificationAssignmentsView, a | 106 | # Distro:+assignments which uses SpecificationAssignmentsView, a |
60 | 108 | # subclass. | 107 | # subclass. |
61 | 109 | person = self.factory.makePerson() | 108 | person = self.factory.makePerson() |
62 | 110 | view = create_initialized_view(person, name='+assignments') | 109 | view = create_initialized_view(person, name='+assignments') |
63 | 111 | self.assertIsInstance(view.specs_batched, BatchNavigator) | ||
64 | 112 | |||
65 | 113 | def test_batch_headings(self): | ||
66 | 114 | person = self.factory.makePerson() | ||
67 | 115 | view = create_initialized_view(person, name='+assignments') | ||
68 | 116 | navigator = view.specs_batched | ||
69 | 117 | self.assertEqual('specification', navigator._singular_heading) | ||
70 | 118 | self.assertEqual('specifications', navigator._plural_heading) | ||
71 | 119 | |||
72 | 120 | def test_batch_size(self): | ||
73 | 121 | # Because +assignments is meant to provide an overview, we default to | 110 | # Because +assignments is meant to provide an overview, we default to |
74 | 122 | # 500 as the default batch size. | 111 | # 500 as the default batch size. |
79 | 123 | person = self.factory.makePerson() | 112 | self.assertThat( |
80 | 124 | view = create_initialized_view(person, name='+assignments') | 113 | view.specs_batched, |
81 | 125 | navigator = view.specs_batched | 114 | IsConfiguredBatchNavigator( |
82 | 126 | self.assertEqual(500, view.specs_batched.default_size) | 115 | 'specification', 'specifications', batch_size=500)) |
83 | 127 | 116 | ||
84 | 128 | 117 | ||
85 | 129 | class TestAssignments(TestCaseWithFactory): | 118 | class TestAssignments(TestCaseWithFactory): |
86 | @@ -243,13 +232,3 @@ | |||
87 | 243 | login_person(project_group.owner) | 232 | login_person(project_group.owner) |
88 | 244 | view = create_initialized_view(project_group, '+specs') | 233 | view = create_initialized_view(project_group, '+specs') |
89 | 245 | self.assertEqual(False, view.can_configure_blueprints) | 234 | self.assertEqual(False, view.can_configure_blueprints) |
90 | 246 | |||
91 | 247 | |||
92 | 248 | def test_suite(): | ||
93 | 249 | suite = unittest.TestSuite() | ||
94 | 250 | suite.addTest(unittest.TestLoader().loadTestsFromName(__name__)) | ||
95 | 251 | return suite | ||
96 | 252 | |||
97 | 253 | |||
98 | 254 | if __name__ == '__main__': | ||
99 | 255 | unittest.TextTestRunner().run(test_suite()) | ||
100 | 256 | 235 | ||
101 | === modified file 'lib/lp/bugs/browser/bugtracker.py' | |||
102 | --- lib/lp/bugs/browser/bugtracker.py 2010-09-03 15:02:39 +0000 | |||
103 | +++ lib/lp/bugs/browser/bugtracker.py 2010-09-20 05:15:57 +0000 | |||
104 | @@ -54,7 +54,11 @@ | |||
105 | 54 | structured, | 54 | structured, |
106 | 55 | ) | 55 | ) |
107 | 56 | from canonical.launchpad.webapp.authorization import check_permission | 56 | from canonical.launchpad.webapp.authorization import check_permission |
109 | 57 | from canonical.launchpad.webapp.batching import BatchNavigator | 57 | from canonical.launchpad.webapp.batching import ( |
110 | 58 | ActiveBatchNavigator, | ||
111 | 59 | BatchNavigator, | ||
112 | 60 | InactiveBatchNavigator, | ||
113 | 61 | ) | ||
114 | 58 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb | 62 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
115 | 59 | from canonical.launchpad.webapp.menu import NavigationMenu | 63 | from canonical.launchpad.webapp.menu import NavigationMenu |
116 | 60 | from canonical.lazr.utils import smartquote | 64 | from canonical.lazr.utils import smartquote |
117 | @@ -154,21 +158,30 @@ | |||
118 | 154 | pillar_limit = 3 | 158 | pillar_limit = 3 |
119 | 155 | 159 | ||
120 | 156 | def initialize(self): | 160 | def initialize(self): |
136 | 157 | # Sort the bug trackers into active and inactive lists so that | 161 | # eager load related pillars. In future we should do this for |
137 | 158 | # we can display them separately. | 162 | # just the rendered trackers, and also use group by to get |
138 | 159 | all_bug_trackers = list(self.context) | 163 | # bug watch counts per tracker. However the batching makes |
139 | 160 | self.active_bug_trackers = [ | 164 | # the inefficiency tolerable for now. Robert Collins 20100919. |
140 | 161 | bug_tracker for bug_tracker in all_bug_trackers | 165 | self._pillar_cache = self.context.getPillarsForBugtrackers( |
141 | 162 | if bug_tracker.active] | 166 | list(self.context.trackers())) |
142 | 163 | self.inactive_bug_trackers = [ | 167 | |
143 | 164 | bug_tracker for bug_tracker in all_bug_trackers | 168 | @property |
144 | 165 | if not bug_tracker.active] | 169 | def inactive_tracker_count(self): |
145 | 166 | 170 | return self.inactive_trackers.currentBatch().listlength | |
146 | 167 | bugtrackerset = getUtility(IBugTrackerSet) | 171 | |
147 | 168 | # The caching of bugtracker pillars here avoids us hitting the | 172 | @cachedproperty |
148 | 169 | # database multiple times for each bugtracker. | 173 | def active_trackers(self): |
149 | 170 | self._pillar_cache = bugtrackerset.getPillarsForBugtrackers( | 174 | results = self.context.trackers(active=True) |
150 | 171 | all_bug_trackers) | 175 | navigator = ActiveBatchNavigator(results, self.request) |
151 | 176 | navigator.setHeadings('tracker', 'trackers') | ||
152 | 177 | return navigator | ||
153 | 178 | |||
154 | 179 | @cachedproperty | ||
155 | 180 | def inactive_trackers(self): | ||
156 | 181 | results = self.context.trackers(active=False) | ||
157 | 182 | navigator = InactiveBatchNavigator(results, self.request) | ||
158 | 183 | navigator.setHeadings('tracker', 'trackers') | ||
159 | 184 | return navigator | ||
160 | 172 | 185 | ||
161 | 173 | def getPillarData(self, bugtracker): | 186 | def getPillarData(self, bugtracker): |
162 | 174 | """Return dict of pillars and booleans indicating ellipsis. | 187 | """Return dict of pillars and booleans indicating ellipsis. |
163 | 175 | 188 | ||
164 | === modified file 'lib/lp/bugs/browser/tests/test_bugtracker_views.py' | |||
165 | --- lib/lp/bugs/browser/tests/test_bugtracker_views.py 2010-08-26 20:08:43 +0000 | |||
166 | +++ lib/lp/bugs/browser/tests/test_bugtracker_views.py 2010-09-20 05:15:57 +0000 | |||
167 | @@ -3,27 +3,61 @@ | |||
168 | 3 | 3 | ||
169 | 4 | """Tests for BugTracker views.""" | 4 | """Tests for BugTracker views.""" |
170 | 5 | 5 | ||
171 | 6 | from __future__ import with_statement | ||
172 | 7 | |||
173 | 6 | __metaclass__ = type | 8 | __metaclass__ = type |
174 | 7 | 9 | ||
175 | 8 | import unittest | ||
176 | 9 | |||
177 | 10 | from datetime import datetime, timedelta | ||
178 | 11 | from pytz import utc | ||
179 | 12 | |||
180 | 13 | from zope.component import getUtility | 10 | from zope.component import getUtility |
181 | 14 | from zope.security.interfaces import Unauthorized | ||
182 | 15 | 11 | ||
184 | 16 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 12 | from canonical.launchpad.testing.pages import find_tag_by_id |
185 | 13 | from canonical.launchpad.webapp import canonical_url | ||
186 | 17 | from canonical.testing.layers import DatabaseFunctionalLayer | 14 | from canonical.testing.layers import DatabaseFunctionalLayer |
190 | 18 | 15 | from lp.bugs.interfaces.bugtracker import IBugTrackerSet | |
188 | 19 | from lp.bugs.browser.bugtracker import ( | ||
189 | 20 | BugTrackerEditView) | ||
191 | 21 | from lp.registry.interfaces.person import IPersonSet | 16 | from lp.registry.interfaces.person import IPersonSet |
194 | 22 | from lp.testing import login, TestCaseWithFactory | 17 | from lp.testing import ( |
195 | 23 | from lp.testing.sampledata import ADMIN_EMAIL, NO_PRIVILEGE_EMAIL | 18 | login, |
196 | 19 | TestCaseWithFactory, | ||
197 | 20 | ) | ||
198 | 24 | from lp.testing.views import create_initialized_view | 21 | from lp.testing.views import create_initialized_view |
204 | 25 | 22 | from lp.testing.matchers import IsConfiguredBatchNavigator | |
205 | 26 | 23 | from lp.testing import ( | |
206 | 27 | 24 | person_logged_in, | |
207 | 28 | def test_suite(): | 25 | TestCaseWithFactory, |
208 | 29 | return unittest.TestLoader().loadTestsFromName(__name__) | 26 | ) |
209 | 27 | from lp.testing.sampledata import ADMIN_EMAIL | ||
210 | 28 | |||
211 | 29 | |||
212 | 30 | class TestBugTrackerSetView(TestCaseWithFactory): | ||
213 | 31 | |||
214 | 32 | layer = DatabaseFunctionalLayer | ||
215 | 33 | |||
216 | 34 | def test_trackers_are_batch_navigators(self): | ||
217 | 35 | trackers = getUtility(IBugTrackerSet) | ||
218 | 36 | view = create_initialized_view(trackers, name='+index') | ||
219 | 37 | matcher = IsConfiguredBatchNavigator('tracker', 'trackers') | ||
220 | 38 | self.assertThat(view.active_trackers, matcher) | ||
221 | 39 | self.assertThat(view.inactive_trackers, matcher) | ||
222 | 40 | |||
223 | 41 | def test_page_is_batched(self): | ||
224 | 42 | active_tracker1 = self.factory.makeBugTracker() | ||
225 | 43 | active_tracker2 = self.factory.makeBugTracker() | ||
226 | 44 | inactive_tracker1 = self.factory.makeBugTracker() | ||
227 | 45 | inactive_tracker2 = self.factory.makeBugTracker() | ||
228 | 46 | admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any() | ||
229 | 47 | with person_logged_in(admin): | ||
230 | 48 | inactive_tracker1.active = False | ||
231 | 49 | inactive_tracker2.active = False | ||
232 | 50 | trackers = getUtility(IBugTrackerSet) | ||
233 | 51 | url = (canonical_url(trackers) + | ||
234 | 52 | "/+index?active_batch=1&inactive_batch=1") | ||
235 | 53 | browser = self.getUserBrowser(url) | ||
236 | 54 | content = browser.contents | ||
237 | 55 | # XXX RobertCollns 20100919 bug=642504. The support for multiple batches | ||
238 | 56 | # isn't complete and the id for the nav links gets duplicated. | ||
239 | 57 | #self.assertEqual('next', | ||
240 | 58 | # find_tag_by_id(content, 'upper-batch-nav-batchnav-next')['class']) | ||
241 | 59 | #self.assertEqual('next', | ||
242 | 60 | # find_tag_by_id(content, 'lower-batch-nav-batchnav-next')['class']) | ||
243 | 61 | # Instead we check the string appears. | ||
244 | 62 | self.assertTrue('upper-batch-nav-batchnav-next' in content) | ||
245 | 63 | |||
246 | 30 | 64 | ||
247 | === modified file 'lib/lp/bugs/interfaces/bugtracker.py' | |||
248 | --- lib/lp/bugs/interfaces/bugtracker.py 2010-08-26 20:08:43 +0000 | |||
249 | +++ lib/lp/bugs/interfaces/bugtracker.py 2010-09-20 05:15:57 +0000 | |||
250 | @@ -432,6 +432,14 @@ | |||
251 | 432 | def getPillarsForBugtrackers(bug_trackers): | 432 | def getPillarsForBugtrackers(bug_trackers): |
252 | 433 | """Return dict mapping bugtrackers to lists of pillars.""" | 433 | """Return dict mapping bugtrackers to lists of pillars.""" |
253 | 434 | 434 | ||
254 | 435 | def trackers(active=None): | ||
255 | 436 | """Return a ResultSet of bugtrackers. | ||
256 | 437 | |||
257 | 438 | :param active: If True, only active trackers are returned, if False | ||
258 | 439 | only inactive trackers are returned. All trackers are returned | ||
259 | 440 | by default. | ||
260 | 441 | """ | ||
261 | 442 | |||
262 | 435 | 443 | ||
263 | 436 | class IBugTrackerAlias(Interface): | 444 | class IBugTrackerAlias(Interface): |
264 | 437 | """Another URL for a remote bug system. | 445 | """Another URL for a remote bug system. |
265 | 438 | 446 | ||
266 | === modified file 'lib/lp/bugs/model/bugtracker.py' | |||
267 | --- lib/lp/bugs/model/bugtracker.py 2010-08-26 20:08:43 +0000 | |||
268 | +++ lib/lp/bugs/model/bugtracker.py 2010-09-20 05:15:57 +0000 | |||
269 | @@ -8,7 +8,8 @@ | |||
270 | 8 | 'BugTracker', | 8 | 'BugTracker', |
271 | 9 | 'BugTrackerAlias', | 9 | 'BugTrackerAlias', |
272 | 10 | 'BugTrackerAliasSet', | 10 | 'BugTrackerAliasSet', |
274 | 11 | 'BugTrackerSet'] | 11 | 'BugTrackerSet', |
275 | 12 | ] | ||
276 | 12 | 13 | ||
277 | 13 | 14 | ||
278 | 14 | from datetime import datetime | 15 | from datetime import datetime |
279 | @@ -66,6 +67,8 @@ | |||
280 | 66 | IBugTrackerSet, | 67 | IBugTrackerSet, |
281 | 67 | SINGLE_PRODUCT_BUGTRACKERTYPES, | 68 | SINGLE_PRODUCT_BUGTRACKERTYPES, |
282 | 68 | ) | 69 | ) |
283 | 70 | from canonical.launchpad.webapp.interfaces import ( | ||
284 | 71 | DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE) | ||
285 | 69 | from lp.bugs.interfaces.bugtrackerperson import BugTrackerPersonAlreadyExists | 72 | from lp.bugs.interfaces.bugtrackerperson import BugTrackerPersonAlreadyExists |
286 | 70 | from lp.bugs.model.bug import Bug | 73 | from lp.bugs.model.bug import Bug |
287 | 71 | from lp.bugs.model.bugmessage import BugMessage | 74 | from lp.bugs.model.bugmessage import BugMessage |
288 | @@ -577,6 +580,17 @@ | |||
289 | 577 | """See `IBugTrackerSet`.""" | 580 | """See `IBugTrackerSet`.""" |
290 | 578 | return BugTracker.select() | 581 | return BugTracker.select() |
291 | 579 | 582 | ||
292 | 583 | def trackers(self, active=None): | ||
293 | 584 | # Without context, cannot tell what store flavour is desirable. | ||
294 | 585 | store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) | ||
295 | 586 | if active is not None: | ||
296 | 587 | clauses = [BugTracker.active==active] | ||
297 | 588 | else: | ||
298 | 589 | clauses = [] | ||
299 | 590 | results = store.find(BugTracker, *clauses) | ||
300 | 591 | results.order_by(BugTracker.name) | ||
301 | 592 | return results | ||
302 | 593 | |||
303 | 580 | def ensureBugTracker(self, baseurl, owner, bugtrackertype, | 594 | def ensureBugTracker(self, baseurl, owner, bugtrackertype, |
304 | 581 | title=None, summary=None, contactdetails=None, name=None): | 595 | title=None, summary=None, contactdetails=None, name=None): |
305 | 582 | """See `IBugTrackerSet`.""" | 596 | """See `IBugTrackerSet`.""" |
306 | 583 | 597 | ||
307 | === modified file 'lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt' | |||
308 | --- lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt 2010-07-26 13:30:57 +0000 | |||
309 | +++ lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt 2010-09-20 05:15:57 +0000 | |||
310 | @@ -39,11 +39,11 @@ | |||
311 | 39 | Email Address | 39 | Email Address |
312 | 40 | 0 | 40 | 0 |
313 | 41 | ------------------------ | 41 | ------------------------ |
317 | 42 | GNU Savannah Bug Tracker | 42 | T'other Gnome GBugGTracker |
318 | 43 | http://savannah.gnu.org/ | 43 | http://bugzilla.gnome.org/ |
319 | 44 | --> http://savannah.gnu.org/ | 44 | --> http://bugzilla.gnome.org/ |
320 | 45 | — | 45 | — |
322 | 46 | Savane | 46 | Bugzilla |
323 | 47 | 0 | 47 | 0 |
324 | 48 | ------------------------ | 48 | ------------------------ |
325 | 49 | ... | 49 | ... |
326 | @@ -68,6 +68,8 @@ | |||
327 | 68 | — | 68 | — |
328 | 69 | Email Address | 69 | Email Address |
329 | 70 | 0 | 70 | 0 |
330 | 71 | ------------------------ | ||
331 | 72 | ... | ||
332 | 71 | 73 | ||
333 | 72 | The watch counts match the number of bugs listed, of course: | 74 | The watch counts match the number of bugs listed, of course: |
334 | 73 | 75 | ||
335 | 74 | 76 | ||
336 | === modified file 'lib/lp/bugs/templates/bugtrackers-index.pt' | |||
337 | --- lib/lp/bugs/templates/bugtrackers-index.pt 2009-09-01 16:24:52 +0000 | |||
338 | +++ lib/lp/bugs/templates/bugtrackers-index.pt 2010-09-20 05:15:57 +0000 | |||
339 | @@ -13,6 +13,7 @@ | |||
340 | 13 | </tal:comment> | 13 | </tal:comment> |
341 | 14 | <metal:macros fill-slot="bogus"> | 14 | <metal:macros fill-slot="bogus"> |
342 | 15 | <metal:macro define-macro="tracker-table"> | 15 | <metal:macro define-macro="tracker-table"> |
343 | 16 | <tal:navigation content="structure trackers/@@+navigation-links-upper" /> | ||
344 | 16 | <table class="sortable listing" tal:attributes="id id"> | 17 | <table class="sortable listing" tal:attributes="id id"> |
345 | 17 | <thead> | 18 | <thead> |
346 | 18 | <tr> | 19 | <tr> |
347 | @@ -24,7 +25,7 @@ | |||
348 | 24 | </tr> | 25 | </tr> |
349 | 25 | </thead> | 26 | </thead> |
350 | 26 | <tbody> | 27 | <tbody> |
352 | 27 | <tr tal:repeat="tracker trackers"> | 28 | <tr tal:repeat="tracker trackers/currentBatch"> |
353 | 28 | <td> | 29 | <td> |
354 | 29 | <a tal:replace="structure tracker/fmt:link" /> | 30 | <a tal:replace="structure tracker/fmt:link" /> |
355 | 30 | </td> | 31 | </td> |
356 | @@ -53,6 +54,7 @@ | |||
357 | 53 | </tr> | 54 | </tr> |
358 | 54 | </tbody> | 55 | </tbody> |
359 | 55 | </table> | 56 | </table> |
360 | 57 | <tal:navigation content="structure trackers/@@+navigation-links-lower" /> | ||
361 | 56 | </metal:macro> | 58 | </metal:macro> |
362 | 57 | </metal:macros> | 59 | </metal:macros> |
363 | 58 | 60 | ||
364 | @@ -70,7 +72,7 @@ | |||
365 | 70 | the external status changes. | 72 | the external status changes. |
366 | 71 | </p> | 73 | </p> |
367 | 72 | <tal:table define="id string:trackers; | 74 | <tal:table define="id string:trackers; |
369 | 73 | trackers view/active_bug_trackers"> | 75 | trackers view/active_trackers"> |
370 | 74 | <metal:table use-macro="template/macros/tracker-table" /> | 76 | <metal:table use-macro="template/macros/tracker-table" /> |
371 | 75 | </tal:table> | 77 | </tal:table> |
372 | 76 | <p style="margin-top: 1em" | 78 | <p style="margin-top: 1em" |
373 | @@ -81,11 +83,11 @@ | |||
374 | 81 | </p> | 83 | </p> |
375 | 82 | </div> | 84 | </div> |
376 | 83 | </div> | 85 | </div> |
378 | 84 | <div class="yui-u" tal:condition="view/inactive_bug_trackers"> | 86 | <div class="yui-u" tal:condition="view/inactive_tracker_count"> |
379 | 85 | <div class="portlet"> | 87 | <div class="portlet"> |
380 | 86 | <h2>Inactive bug trackers</h2> | 88 | <h2>Inactive bug trackers</h2> |
381 | 87 | <tal:table define="id string:inactive-trackers; | 89 | <tal:table define="id string:inactive-trackers; |
383 | 88 | trackers view/inactive_bug_trackers"> | 90 | trackers view/inactive_trackers"> |
384 | 89 | <metal:table use-macro="template/macros/tracker-table" /> | 91 | <metal:table use-macro="template/macros/tracker-table" /> |
385 | 90 | </tal:table> | 92 | </tal:table> |
386 | 91 | </div> | 93 | </div> |
387 | 92 | 94 | ||
388 | === modified file 'lib/lp/bugs/tests/test_bugtracker.py' | |||
389 | --- lib/lp/bugs/tests/test_bugtracker.py 2010-08-26 20:08:43 +0000 | |||
390 | +++ lib/lp/bugs/tests/test_bugtracker.py 2010-09-20 05:15:57 +0000 | |||
391 | @@ -28,7 +28,10 @@ | |||
392 | 28 | 28 | ||
393 | 29 | from canonical.launchpad.ftests import login_person | 29 | from canonical.launchpad.ftests import login_person |
394 | 30 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 30 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
396 | 31 | from canonical.testing import LaunchpadFunctionalLayer | 31 | from canonical.testing import ( |
397 | 32 | DatabaseFunctionalLayer, | ||
398 | 33 | LaunchpadFunctionalLayer, | ||
399 | 34 | ) | ||
400 | 32 | from lp.bugs.externalbugtracker import ( | 35 | from lp.bugs.externalbugtracker import ( |
401 | 33 | BugTrackerConnectError, | 36 | BugTrackerConnectError, |
402 | 34 | Mantis, | 37 | Mantis, |
403 | @@ -38,12 +41,36 @@ | |||
404 | 38 | BugTrackerType, | 41 | BugTrackerType, |
405 | 39 | IBugTracker, | 42 | IBugTracker, |
406 | 40 | ) | 43 | ) |
407 | 44 | from lp.bugs.model.bugtracker import BugTrackerSet | ||
408 | 41 | from lp.bugs.tests.externalbugtracker import Urlib2TransportTestHandler | 45 | from lp.bugs.tests.externalbugtracker import Urlib2TransportTestHandler |
409 | 42 | from lp.registry.interfaces.person import IPersonSet | 46 | from lp.registry.interfaces.person import IPersonSet |
410 | 43 | from lp.testing import login, login_person, TestCaseWithFactory | 47 | from lp.testing import login, login_person, TestCaseWithFactory |
411 | 44 | from lp.testing.sampledata import ADMIN_EMAIL | 48 | from lp.testing.sampledata import ADMIN_EMAIL |
412 | 45 | 49 | ||
413 | 46 | 50 | ||
414 | 51 | class TestBugTrackerSet(TestCaseWithFactory): | ||
415 | 52 | |||
416 | 53 | layer = DatabaseFunctionalLayer | ||
417 | 54 | |||
418 | 55 | def test_trackers(self): | ||
419 | 56 | tracker = self.factory.makeBugTracker() | ||
420 | 57 | trackers = BugTrackerSet() | ||
421 | 58 | # Active trackers are in all trackers, | ||
422 | 59 | self.assertTrue(tracker in trackers.trackers()) | ||
423 | 60 | # and active, | ||
424 | 61 | self.assertTrue(tracker in trackers.trackers(active=True)) | ||
425 | 62 | # But not inactive. | ||
426 | 63 | self.assertFalse(tracker in trackers.trackers(active=False)) | ||
427 | 64 | login(ADMIN_EMAIL) | ||
428 | 65 | tracker.active = False | ||
429 | 66 | # Inactive trackers are in all trackers | ||
430 | 67 | self.assertTrue(tracker in trackers.trackers()) | ||
431 | 68 | # and inactive, | ||
432 | 69 | self.assertTrue(tracker in trackers.trackers(active=False)) | ||
433 | 70 | # but not in active. | ||
434 | 71 | self.assertFalse(tracker in trackers.trackers(active=True)) | ||
435 | 72 | |||
436 | 73 | |||
437 | 47 | class BugTrackerTestCase(TestCaseWithFactory): | 74 | class BugTrackerTestCase(TestCaseWithFactory): |
438 | 48 | """Unit tests for the `BugTracker` class.""" | 75 | """Unit tests for the `BugTracker` class.""" |
439 | 49 | 76 | ||
440 | 50 | 77 | ||
441 | === modified file 'lib/lp/registry/browser/person.py' | |||
442 | --- lib/lp/registry/browser/person.py 2010-09-15 16:44:41 +0000 | |||
443 | +++ lib/lp/registry/browser/person.py 2010-09-20 05:15:57 +0000 | |||
444 | @@ -194,7 +194,11 @@ | |||
445 | 194 | structured, | 194 | structured, |
446 | 195 | ) | 195 | ) |
447 | 196 | from canonical.launchpad.webapp.authorization import check_permission | 196 | from canonical.launchpad.webapp.authorization import check_permission |
449 | 197 | from canonical.launchpad.webapp.batching import BatchNavigator | 197 | from canonical.launchpad.webapp.batching import ( |
450 | 198 | ActiveBatchNavigator, | ||
451 | 199 | BatchNavigator, | ||
452 | 200 | InactiveBatchNavigator, | ||
453 | 201 | ) | ||
454 | 198 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb | 202 | from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
455 | 199 | from canonical.launchpad.webapp.interfaces import ( | 203 | from canonical.launchpad.webapp.interfaces import ( |
456 | 200 | ILaunchBag, | 204 | ILaunchBag, |
457 | @@ -1385,24 +1389,6 @@ | |||
458 | 1385 | links = ['profile', 'polls', 'members', 'ppas'] | 1389 | links = ['profile', 'polls', 'members', 'ppas'] |
459 | 1386 | 1390 | ||
460 | 1387 | 1391 | ||
461 | 1388 | class ActiveBatchNavigator(BatchNavigator): | ||
462 | 1389 | """A paginator for active items. | ||
463 | 1390 | |||
464 | 1391 | Used when a view needs to display more than one BatchNavigator of items. | ||
465 | 1392 | """ | ||
466 | 1393 | start_variable_name = 'active_start' | ||
467 | 1394 | batch_variable_name = 'active_batch' | ||
468 | 1395 | |||
469 | 1396 | |||
470 | 1397 | class InactiveBatchNavigator(BatchNavigator): | ||
471 | 1398 | """A paginator for inactive items. | ||
472 | 1399 | |||
473 | 1400 | Used when a view needs to display more than one BatchNavigator of items. | ||
474 | 1401 | """ | ||
475 | 1402 | start_variable_name = 'inactive_start' | ||
476 | 1403 | batch_variable_name = 'inactive_batch' | ||
477 | 1404 | |||
478 | 1405 | |||
479 | 1406 | class TeamMembershipView(LaunchpadView): | 1392 | class TeamMembershipView(LaunchpadView): |
480 | 1407 | """The view behind ITeam/+members.""" | 1393 | """The view behind ITeam/+members.""" |
481 | 1408 | 1394 | ||
482 | 1409 | 1395 | ||
483 | === modified file 'lib/lp/registry/browser/team.py' | |||
484 | --- lib/lp/registry/browser/team.py 2010-09-17 18:32:23 +0000 | |||
485 | +++ lib/lp/registry/browser/team.py 2010-09-20 05:15:57 +0000 | |||
486 | @@ -829,8 +829,6 @@ | |||
487 | 829 | """ | 829 | """ |
488 | 830 | results = self.mailing_list.getReviewableMessages() | 830 | results = self.mailing_list.getReviewableMessages() |
489 | 831 | navigator = BatchNavigator(results, self.request) | 831 | navigator = BatchNavigator(results, self.request) |
490 | 832 | # Subclasses often set the singular and plural headings, | ||
491 | 833 | # but we can use the generic class too. | ||
492 | 834 | navigator.setHeadings('message', 'messages') | 832 | navigator.setHeadings('message', 'messages') |
493 | 835 | return navigator | 833 | return navigator |
494 | 836 | 834 | ||
495 | 837 | 835 | ||
496 | === modified file 'lib/lp/registry/browser/tests/test_team.py' | |||
497 | --- lib/lp/registry/browser/tests/test_team.py 2010-09-14 00:21:04 +0000 | |||
498 | +++ lib/lp/registry/browser/tests/test_team.py 2010-09-20 05:15:57 +0000 | |||
499 | @@ -3,10 +3,10 @@ | |||
500 | 3 | 3 | ||
501 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
502 | 5 | 5 | ||
503 | 6 | from canonical.launchpad.webapp.batching import BatchNavigator | ||
504 | 7 | from canonical.testing import DatabaseFunctionalLayer | 6 | from canonical.testing import DatabaseFunctionalLayer |
505 | 8 | from lp.registry.browser.person import TeamOverviewMenu | 7 | from lp.registry.browser.person import TeamOverviewMenu |
506 | 9 | from lp.testing import TestCaseWithFactory | 8 | from lp.testing import TestCaseWithFactory |
507 | 9 | from lp.testing.matchers import IsConfiguredBatchNavigator | ||
508 | 10 | from lp.testing.menu import check_menu_links | 10 | from lp.testing.menu import check_menu_links |
509 | 11 | from lp.testing.views import create_initialized_view | 11 | from lp.testing.views import create_initialized_view |
510 | 12 | 12 | ||
511 | @@ -47,12 +47,6 @@ | |||
512 | 47 | team = self.factory.makeTeam() | 47 | team = self.factory.makeTeam() |
513 | 48 | self.factory.makeMailingList(team, team.teamowner) | 48 | self.factory.makeMailingList(team, team.teamowner) |
514 | 49 | view = create_initialized_view(team, name='+mailinglist-moderate') | 49 | view = create_initialized_view(team, name='+mailinglist-moderate') |
524 | 50 | self.assertIsInstance(view.held_messages, BatchNavigator) | 50 | self.assertThat( |
525 | 51 | 51 | view.held_messages, | |
526 | 52 | def test_held_message_headings(self): | 52 | IsConfiguredBatchNavigator('message', 'messages')) |
518 | 53 | team = self.factory.makeTeam() | ||
519 | 54 | self.factory.makeMailingList(team, team.teamowner) | ||
520 | 55 | view = create_initialized_view(team, name='+mailinglist-moderate') | ||
521 | 56 | navigator = view.held_messages | ||
522 | 57 | self.assertEqual('message', navigator._singular_heading) | ||
523 | 58 | self.assertEqual('messages', navigator._plural_heading) | ||
527 | 59 | 53 | ||
528 | === modified file 'lib/lp/testing/matchers.py' | |||
529 | --- lib/lp/testing/matchers.py 2010-08-25 20:04:40 +0000 | |||
530 | +++ lib/lp/testing/matchers.py 2010-09-20 05:15:57 +0000 | |||
531 | @@ -17,9 +17,12 @@ | |||
532 | 17 | from testtools.content import Content | 17 | from testtools.content import Content |
533 | 18 | from testtools.content_type import UTF8_TEXT | 18 | from testtools.content_type import UTF8_TEXT |
534 | 19 | from testtools.matchers import ( | 19 | from testtools.matchers import ( |
535 | 20 | Equals, | ||
536 | 20 | Matcher, | 21 | Matcher, |
537 | 21 | Mismatch, | 22 | Mismatch, |
538 | 23 | MismatchesAll, | ||
539 | 22 | ) | 24 | ) |
540 | 25 | from testtools import matchers | ||
541 | 23 | from zope.interface.exceptions import ( | 26 | from zope.interface.exceptions import ( |
542 | 24 | BrokenImplementation, | 27 | BrokenImplementation, |
543 | 25 | BrokenMethodImplementation, | 28 | BrokenMethodImplementation, |
544 | @@ -31,6 +34,8 @@ | |||
545 | 31 | Proxy, | 34 | Proxy, |
546 | 32 | ) | 35 | ) |
547 | 33 | 36 | ||
548 | 37 | from canonical.launchpad.webapp.batching import BatchNavigator | ||
549 | 38 | |||
550 | 34 | 39 | ||
551 | 35 | class DoesNotProvide(Mismatch): | 40 | class DoesNotProvide(Mismatch): |
552 | 36 | """An object does not provide an interface.""" | 41 | """An object does not provide an interface.""" |
553 | @@ -220,3 +225,45 @@ | |||
554 | 220 | if not matchee.startswith(self.expected): | 225 | if not matchee.startswith(self.expected): |
555 | 221 | return DoesNotStartWith(matchee, self.expected) | 226 | return DoesNotStartWith(matchee, self.expected) |
556 | 222 | return None | 227 | return None |
557 | 228 | |||
558 | 229 | |||
559 | 230 | class IsConfiguredBatchNavigator(Matcher): | ||
560 | 231 | """Check that an object is a batch navigator.""" | ||
561 | 232 | |||
562 | 233 | def __init__(self, singular, plural, batch_size=None): | ||
563 | 234 | """Create a ConfiguredBatchNavigator. | ||
564 | 235 | |||
565 | 236 | :param singular: The singular header the batch should be using. | ||
566 | 237 | :param plural: The plural header the batch should be using. | ||
567 | 238 | :param batch_size: The batch size that should be configured by default. | ||
568 | 239 | """ | ||
569 | 240 | self._single = Equals(singular) | ||
570 | 241 | self._plural = Equals(plural) | ||
571 | 242 | self._batch = None | ||
572 | 243 | if batch_size: | ||
573 | 244 | self._batch = Equals(batch_size) | ||
574 | 245 | self.matchers = dict( | ||
575 | 246 | _singular_heading=self._single, _plural_heading=self._plural) | ||
576 | 247 | if self._batch: | ||
577 | 248 | self.matchers['default_size'] = self._batch | ||
578 | 249 | |||
579 | 250 | def __str__(self): | ||
580 | 251 | if self._batch: | ||
581 | 252 | batch = ", %r" % self._batch.expected | ||
582 | 253 | else: | ||
583 | 254 | batch = '' | ||
584 | 255 | return "ConfiguredBatchNavigator(%r, %r%s)" % ( | ||
585 | 256 | self._single.expected, self._plural.expected, batch) | ||
586 | 257 | |||
587 | 258 | def match(self, matchee): | ||
588 | 259 | if not isinstance(matchee, BatchNavigator): | ||
589 | 260 | # Testtools doesn't have an IsInstanceMismatch yet. | ||
590 | 261 | return matchers._BinaryMismatch( | ||
591 | 262 | BatchNavigator, 'isinstance', matchee) | ||
592 | 263 | mismatches = [] | ||
593 | 264 | for attrname, matcher in self.matchers.items(): | ||
594 | 265 | mismatch = matcher.match(getattr(matchee, attrname)) | ||
595 | 266 | if mismatch is not None: | ||
596 | 267 | mismatches.append(mismatch) | ||
597 | 268 | if mismatches: | ||
598 | 269 | return MismatchesAll(mismatches) |
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' launchpad/ webapp/ batching. py 2010-09-06 18:52:45 +0000 launchpad/ webapp/ batching. py 2010-09-19 22:21:05 +0000 gator(BatchNavi gator): vigator( BatchNavigator) :
> --- lib/canonical/
> +++ lib/canonical/
> @@ -77,6 +77,24 @@
> return self.batch.total() > self.batch.size
>
>
> +class ActiveBatchNavi
> + """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 InactiveBatchNa
> + """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 TableBatchNavig ator(BatchNavig ator): launchpad. interfaces. ITableBatchNavi gator." "" ITableBatchNavi gator)
> """See canonical.
> implements(
> === modified file 'lib/lp/ blueprints/ browser/ tests/test_ specificationta rget.py' blueprints/ browser/ tests/test_ specificationta rget.py 2010-09-17 14:04:29 +0000 blueprints/ browser/ tests/test_ specificationta rget.py 2010-09-19 22:21:05 +0000 launchpad. webapp. batching import BatchNavigator launchpad. testing. pages import find_tag_by_id testing. layers import DatabaseFunctio nalLayer interfaces. specificationta rget import ( tory, chNavigator initialized_ view, makePerson( ) initialized_ view(person, name='+ assignments' )
> --- lib/lp/
> +++ lib/lp/
> @@ -3,11 +3,9 @@
>
> __metaclass__ = type
>
> -import unittest
>
> from zope.security.proxy import removeSecurityProxy
>
> -from canonical.
> from canonical.
> from canonical.
> from lp.blueprints.
> @@ -21,6 +19,7 @@
> login_person,
> TestCaseWithFac
> )
> +from lp.testing.matchers import IsConfiguredBat
> from lp.testing.views import (
> create_view,
> create_
> @@ -108,22 +107,11 @@
> # subclass.
> person = self.factory.
> view = create_
Just up from here there's a comment:
# Some pages turn up in very large contexts and patch. E.g. signmentsView, a
# Distro:+assignments which uses SpecificationAs
# subclass.
Is patch a typo for batch here?
> - self.assertIsIn stance( view.specs_ batched, BatchNavigator) headings( self): makePerson( ) initialized_ view(person, name='+ assignments' ) l('specificatio n', navigator. _singular_ heading) l('specificatio ns', navigator. _plural_ heading) size(self) :
> -
> - def test_batch_
> - person = self.factory.
> - view = create_
> - navigator = view.specs_batched
> - self.assertEqua
> - self.assertEqua
> -
> - def test_batch_
> # Because +assignments is meant to provide an overview, we default to
> # 500 as the default batch size.
> - ...