Merge lp:~wallyworld/launchpad/product-mergequeue-listview into lp:launchpad

Proposed by Ian Booth
Status: Work in progress
Proposed branch: lp:~wallyworld/launchpad/product-mergequeue-listview
Merge into: lp:launchpad
Diff against target: 863 lines (+498/-83)
8 files modified
lib/lp/code/browser/branchlisting.py (+20/-3)
lib/lp/code/browser/branchmergequeuelisting.py (+38/-2)
lib/lp/code/browser/configure.zcml (+18/-0)
lib/lp/code/browser/tests/test_branchmergequeuelisting.py (+348/-75)
lib/lp/code/interfaces/branchmergequeuecollection.py (+3/-0)
lib/lp/code/model/branchmergequeuecollection.py (+12/-1)
lib/lp/code/model/tests/test_branchmergequeuecollection.py (+45/-0)
lib/lp/code/templates/product-portlet-codestatistics.pt (+14/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/product-mergequeue-listview
Reviewer Review Type Date Requested Status
Paul Hummer Pending
Review via email: mp+41445@code.launchpad.net

Commit message

Add product and person product merge queue listing pages.

Description of the change

= Summary =

Add product and person product merge queue listing pages.

= Implementation =

Extend the Person Merge Queue Listing implementation. Core code changes contained in classes:
  lib/lp/code/model/branchmergequeuecollection.py
  lib/lp/code/browser/branchmergequeuelisting.py

Add menus linking to the branch merge queue listing page to the product code page and person product code page.

= Tests =

Extend tests in
  lib/lp/code/model/tests/test_branchmergequeuecollection.py

to cover new inProduct() filter and other new methods.

Rework implementation of tests in
  lib/lp/code/browser/tests/test_branchmergequeuelisting.p

to provide base classes for the various person, product, and person product tests, which are essentially the same logic but on a different context.

  bin/test -vvt test_branchmergequeuecollection
  bin/test -vvt test_branchmergequeuelisting

= Lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/browser/branchlisting.py
  lib/lp/code/browser/branchmergequeuelisting.py
  lib/lp/code/browser/configure.zcml
  lib/lp/code/browser/tests/test_branchmergequeuelisting.py
  lib/lp/code/interfaces/branchmergequeuecollection.py
  lib/lp/code/model/branchmergequeuecollection.py
  lib/lp/code/model/tests/test_branchmergequeuecollection.py
  lib/lp/code/templates/product-portlet-codestatistics.pt

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

Does this still need review? It's more than 6 months old. What's the status of it?

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Paul

I suspect the merge queue work has stalled somewhat but it would be nice
to get this landed so that when it is picked up again everything is in
place to recommence work on it.

On 26/05/11 23:59, Paul Hummer wrote:
> Does this still need review? It's more than 6 months old. What's the status of it?

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

I'm going to put this in WIP as I suspect it will have conflicts and bitrot by now; if it doesn't thats cool - please put it back to needs review and request an LP reviewer review.

Unmerged revisions

9922. By Ian Booth

Add product merge queue listing pages and tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchlisting.py'
2--- lib/lp/code/browser/branchlisting.py 2010-11-18 12:05:34 +0000
3+++ lib/lp/code/browser/branchlisting.py 2010-11-22 11:58:12 +0000
4@@ -94,7 +94,9 @@
5 PersonActiveReviewsView,
6 PersonProductActiveReviewsView,
7 )
8-from lp.code.browser.branchmergequeuelisting import HasMergeQueuesMenuMixin
9+from lp.code.browser.branchmergequeuelisting import (
10+ HasMergeQueuesMenuMixin,
11+ )
12 from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin
13 from lp.code.browser.summary import BranchCountSummaryView
14 from lp.code.enums import (
15@@ -110,6 +112,9 @@
16 IBranchListingQueryOptimiser,
17 )
18 from lp.code.interfaces.branchcollection import IAllBranches
19+from lp.code.interfaces.branchmergequeuecollection import (
20+ IAllBranchMergeQueues,
21+ )
22 from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy
23 from lp.code.interfaces.branchtarget import IBranchTarget
24 from lp.code.interfaces.revision import IRevisionSet
25@@ -959,7 +964,8 @@
26 class PersonProductBranchesMenu(PersonBranchesMenu):
27
28 usedfor = IPersonProduct
29- links = ['registered', 'owned', 'subscribed', 'active_reviews']
30+ links = ['registered', 'owned', 'subscribed', 'active_reviews',
31+ 'mergequeues']
32
33 def _getCountCollection(self):
34 """See `PersonBranchesMenu`."""
35@@ -978,6 +984,11 @@
36 self.context, self.request)
37 return active_reviews.getProposals().count()
38
39+ @cachedproperty
40+ def mergequeue_count(self):
41+ return getUtility(IAllBranchMergeQueues).ownedBy(
42+ self.person).inProduct(self.context.product).count()
43+
44
45 class PersonBaseBranchListingView(BranchListingView):
46 """Base class used for different person listing views."""
47@@ -1106,7 +1117,7 @@
48 return self.context.person
49
50
51-class ProductBranchesMenu(ApplicationMenu):
52+class ProductBranchesMenu(ApplicationMenu, HasMergeQueuesMenuMixin):
53
54 usedfor = IProduct
55 facet = 'branches'
56@@ -1114,13 +1125,19 @@
57 'branch_add',
58 'list_branches',
59 'active_reviews',
60+ 'mergequeues',
61 'code_import',
62 'branch_visibility',
63 ]
64 extra_attributes = [
65 'active_review_count',
66+ 'mergequeue_count',
67 ]
68
69+ @cachedproperty
70+ def mergequeue_count(self):
71+ return self._getMergeQueueCollection().count()
72+
73 def branch_add(self):
74 text = 'Register a branch'
75 summary = 'Register a new Bazaar branch for this project'
76
77=== modified file 'lib/lp/code/browser/branchmergequeuelisting.py'
78--- lib/lp/code/browser/branchmergequeuelisting.py 2010-11-02 22:41:11 +0000
79+++ lib/lp/code/browser/branchmergequeuelisting.py 2010-11-22 11:58:12 +0000
80@@ -28,7 +28,7 @@
81 class HasMergeQueuesMenuMixin:
82 """A context menus mixin for objects that can own merge queues."""
83
84- def _getCollection(self):
85+ def _getMergeQueueCollection(self):
86 return getUtility(IAllBranchMergeQueues).visibleByUser(self.user)
87
88 @property
89@@ -49,7 +49,7 @@
90
91 @cachedproperty
92 def mergequeue_count(self):
93- return self._getCollection().ownedBy(self.person).count()
94+ return self._getMergeQueueCollection().ownedBy(self.person).count()
95
96
97 class MergeQueueListingView(LaunchpadView, FeedsMixin):
98@@ -103,3 +103,39 @@
99
100 def _getCollection(self):
101 return getUtility(IAllBranchMergeQueues).ownedBy(self.context)
102+
103+
104+class ProductMergeQueueListingView(MergeQueueListingView):
105+
106+ label_template = 'Merge Queues for branches of %(displayname)s'
107+
108+ def _getCollection(self):
109+ return getUtility(IAllBranchMergeQueues).inProduct(self.context)
110+
111+
112+class PersonProductMergeQueueListingView(MergeQueueListingView):
113+
114+ label_template = 'Merge Queues of %(product)s owned by %(person)s'
115+ owner_enabled = False
116+
117+ @property
118+ def person(self):
119+ """Return the person from the PersonProduct context."""
120+ return self.context.person
121+
122+ @property
123+ def label(self):
124+ return self.label_template % {
125+ 'person': self.context.person.displayname,
126+ 'product': self.context.product.displayname}
127+
128+ def _getCollection(self):
129+ return getUtility(IAllBranchMergeQueues).ownedBy(
130+ self.context.person).inProduct(self.context.product)
131+
132+ @property
133+ def no_merge_queue_message(self):
134+ """Shown when there is no table to show."""
135+ return "%(product)s has no merge queues owned by %(person)s." % {
136+ 'person': self.context.person.displayname,
137+ 'product': self.context.product.displayname}
138
139=== modified file 'lib/lp/code/browser/configure.zcml'
140--- lib/lp/code/browser/configure.zcml 2010-11-18 12:05:34 +0000
141+++ lib/lp/code/browser/configure.zcml 2010-11-22 11:58:12 +0000
142@@ -1327,6 +1327,24 @@
143 facet="branches"
144 name="+merge-queues"
145 template="../templates/branchmergequeue-listing.pt"/>
146+ <browser:page
147+ for="lp.registry.interfaces.product.IProduct"
148+ layer="lp.code.publisher.CodeLayer"
149+ class="lp.code.browser.branchmergequeuelisting.ProductMergeQueueListingView"
150+ permission="zope.Public"
151+ facet="branches"
152+ name="+merge-queues"
153+ template="../templates/branchmergequeue-listing.pt"
154+ />
155+ <browser:page
156+ for="lp.registry.interfaces.personproduct.IPersonProduct"
157+ layer="lp.code.publisher.CodeLayer"
158+ class="lp.code.browser.branchmergequeuelisting.PersonProductMergeQueueListingView"
159+ permission="zope.Public"
160+ facet="branches"
161+ name="+merge-queues"
162+ template="../templates/branchmergequeue-listing.pt"
163+ />
164
165 <browser:page
166 for="*"
167
168=== modified file 'lib/lp/code/browser/tests/test_branchmergequeuelisting.py'
169--- lib/lp/code/browser/tests/test_branchmergequeuelisting.py 2010-11-02 22:41:11 +0000
170+++ lib/lp/code/browser/tests/test_branchmergequeuelisting.py 2010-11-22 11:58:12 +0000
171@@ -9,6 +9,8 @@
172
173 from mechanize import LinkNotFoundError
174 import soupmatchers
175+
176+from zope.component import getUtility
177 from zope.security.proxy import removeSecurityProxy
178
179 from canonical.launchpad.testing.pages import (
180@@ -18,6 +20,8 @@
181 )
182 from canonical.launchpad.webapp import canonical_url
183 from canonical.testing.layers import DatabaseFunctionalLayer
184+from lp.code.enums import BranchType
185+from lp.registry.interfaces.personproduct import IPersonProductFactory
186 from lp.services.features.model import (
187 FeatureFlag,
188 getFeatureStore,
189@@ -34,45 +38,65 @@
190 class MergeQueuesTestMixin:
191
192 def setUp(self):
193- self.branch_owner = self.factory.makePerson(name='eric')
194+ self.branch_owner = removeSecurityProxy(
195+ self.factory.makePerson(name='eric'))
196+ dev_series_branch = self.factory.makeBranch()
197+ productseries = self.factory.makeProductSeries(
198+ name="myproduct", branch=dev_series_branch)
199+ self.product = removeSecurityProxy(productseries).product
200+ removeSecurityProxy(self.product).development_focus = productseries
201
202 def enable_queue_flag(self):
203 getFeatureStore().add(FeatureFlag(
204 scope=u'default', flag=u'code.branchmergequeue',
205 value=u'on', priority=1))
206
207- def _makeMergeQueues(self, nr_queues=3, nr_with_private_branches=0):
208+ def _makeMergeQueues(
209+ self, nr_queues=3, nr_with_private_branches=0,
210+ product=None, owner=None):
211 # We create nr_queues merge queues in total, and the first
212 # nr_with_private_branches of them will have at least one private
213 # branch in the queue.
214- with person_logged_in(self.branch_owner):
215+ if owner is None:
216+ owner = self.branch_owner
217+ with person_logged_in(owner):
218 mergequeues = [
219 self.factory.makeBranchMergeQueue(
220- owner=self.branch_owner, branches=self._makeBranches())
221+ owner=owner,
222+ branches=self._makeBranches(product=product, owner=owner))
223 for i in range(nr_queues-nr_with_private_branches)]
224 mergequeues_with_private_branches = [
225 self.factory.makeBranchMergeQueue(
226- owner=self.branch_owner,
227- branches=self._makeBranches(nr_private=1))
228+ owner=owner,
229+ branches=self._makeBranches(
230+ nr_private=1, product=product, owner=owner))
231 for i in range(nr_with_private_branches)]
232
233 return mergequeues, mergequeues_with_private_branches
234
235- def _makeBranches(self, nr_public=3, nr_private=0):
236+ def _makeBranches(
237+ self, nr_public=3, nr_private=0, product=None, owner=None):
238+ if product is None:
239+ product = self.product
240+ if owner is None:
241+ owner = self.branch_owner
242 branches = [
243- self.factory.makeProductBranch(owner=self.branch_owner)
244+ self.factory.makeProductBranch(
245+ owner=owner, product=product, branch_type=BranchType.HOSTED)
246 for i in range(nr_public)]
247
248 private_branches = [
249 self.factory.makeProductBranch(
250- owner=self.branch_owner, private=True)
251+ owner=owner, product=product, private=True,
252+ branch_type=BranchType.HOSTED)
253 for i in range(nr_private)]
254
255 branches.extend(private_branches)
256 return branches
257
258
259-class TestPersonMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):
260+class BaseTestMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):
261+ """ Base class for person, product, personproduct view tests."""
262
263 layer = DatabaseFunctionalLayer
264
265@@ -81,73 +105,38 @@
266 MergeQueuesTestMixin.setUp(self)
267 self.user = self.factory.makePerson()
268
269- def test_mergequeues_with_all_public_branches(self):
270+ def _test_mergequeues_with_all_public_branches(self):
271 # Anyone can see mergequeues containing all public branches.
272 mq, mq_with_private = self._makeMergeQueues()
273 login_person(self.user)
274 view = create_initialized_view(
275- self.branch_owner, name="+merge-queues", rootsite='code')
276+ self.context, name="+merge-queues", rootsite='code')
277 self.assertEqual(set(mq), set(view.mergequeues))
278
279- def test_mergequeues_with_a_private_branch_for_owner(self):
280+ def _test_mergequeues_with_a_private_branch_for_owner(self):
281 # Only users with access to private branches can see any queues
282 # containing such branches.
283 mq, mq_with_private = (
284 self._makeMergeQueues(nr_with_private_branches=1))
285 login_person(self.branch_owner)
286 view = create_initialized_view(
287- self.branch_owner, name="+merge-queues", rootsite='code')
288+ self.context, name="+merge-queues", rootsite='code')
289 mq.extend(mq_with_private)
290 self.assertEqual(set(mq), set(view.mergequeues))
291
292- def test_mergequeues_with_a_private_branch_for_other_user(self):
293+ def _test_mergequeues_with_a_private_branch_for_other_user(self):
294 # Only users with access to private branches can see any queues
295 # containing such branches.
296 mq, mq_with_private = (
297 self._makeMergeQueues(nr_with_private_branches=1))
298 login_person(self.user)
299 view = create_initialized_view(
300- self.branch_owner, name="+merge-queues", rootsite='code')
301+ self.context, name="+merge-queues", rootsite='code')
302 self.assertEqual(set(mq), set(view.mergequeues))
303
304
305-class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):
306- """Tests for the person code homepage.
307-
308- This is the default page shown for a person on the code subdomain.
309- """
310-
311- layer = DatabaseFunctionalLayer
312-
313- def setUp(self):
314- BrowserTestCase.setUp(self)
315- MergeQueuesTestMixin.setUp(self)
316- self._makeMergeQueues()
317-
318- def test_merge_queue_menu_link_without_feature_flag(self):
319- login_person(self.branch_owner)
320- browser = self.getUserBrowser(
321- canonical_url(self.branch_owner, rootsite='code'),
322- self.branch_owner)
323- self.assertRaises(
324- LinkNotFoundError,
325- browser.getLink,
326- url='+merge-queues')
327-
328- def test_merge_queue_menu_link(self):
329- self.enable_queue_flag()
330- login_person(self.branch_owner)
331- browser = self.getUserBrowser(
332- canonical_url(self.branch_owner, rootsite='code'),
333- self.branch_owner)
334- browser.getLink(url='+merge-queues').click()
335- self.assertEqual(
336- 'http://code.launchpad.dev/~eric/+merge-queues',
337- browser.url)
338-
339-
340-class TestPersonMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
341- """Tests for the person merge queue list page."""
342+class BaseTestMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
343+ """ Base class for person, product, personproduct list page tests."""
344
345 layer = DatabaseFunctionalLayer
346
347@@ -158,10 +147,10 @@
348 self.merge_queues = mq
349 self.merge_queues.extend(mq_with_private)
350
351- def test_merge_queue_list_contents_without_feature_flag(self):
352+ def _test_merge_queue_list_contents_without_feature_flag(self):
353 login_person(self.branch_owner)
354 browser = self.getUserBrowser(
355- canonical_url(self.branch_owner, rootsite='code',
356+ canonical_url(self.context, rootsite='code',
357 view_name='+merge-queues'), self.branch_owner)
358 table = find_tag_by_id(browser.contents, 'mergequeuetable')
359 self.assertIs(None, table)
360@@ -172,37 +161,30 @@
361 '\w*No merge queues\w*')))
362 self.assertThat(browser.contents, noqueue_matcher)
363
364- def test_merge_queue_list_contents(self):
365+ def _test_merge_queue_list_contents(self):
366 self.enable_queue_flag()
367 login_person(self.branch_owner)
368 browser = self.getUserBrowser(
369- canonical_url(self.branch_owner, rootsite='code',
370+ canonical_url(self.context, rootsite='code',
371 view_name='+merge-queues'), self.branch_owner)
372
373 table = find_tag_by_id(browser.contents, 'mergequeuetable')
374-
375- merge_queue_info = {}
376- for row in table.tbody.fetch('tr'):
377- cells = row('td')
378- row_info = {}
379- queue_name = extract_text(cells[0])
380- if not queue_name.startswith('queue'):
381- continue
382- qlink = extract_link_from_tag(cells[0].find('a'))
383- row_info['queue_link'] = qlink
384- queue_size = extract_text(cells[1])
385- row_info['queue_size'] = queue_size
386- queue_branches = cells[2]('a')
387- branch_links = set()
388- for branch_tag in queue_branches:
389- branch_links.add(extract_link_from_tag(branch_tag))
390- row_info['branch_links'] = branch_links
391- merge_queue_info[queue_name] = row_info
392+ merge_queue_info = self.extract_merge_queue_info(table)
393
394 expected_queue_names = [queue.name for queue in self.merge_queues]
395 self.assertEqual(
396 set(expected_queue_names), set(merge_queue_info.keys()))
397
398+ if self.check_queue_owner:
399+ # All queues are created with self.branch_owner as the owner.
400+ expected_queue_owners = [
401+ 'Eric' for queue in self.merge_queues]
402+ observed_queue_owners = (
403+ [merge_queue_info[queue.name]['queue_owner']
404+ for queue in self.merge_queues])
405+ self.assertEqual(
406+ expected_queue_owners, observed_queue_owners)
407+
408 #TODO: when IBranchMergeQueue API is available remove '4'
409 expected_queue_sizes = dict(
410 [(queue.name, '4') for queue in self.merge_queues])
411@@ -225,3 +207,294 @@
412 for queue in self.merge_queues])
413 self.assertEqual(
414 expected_queue_branches, observed_queue_branches)
415+
416+
417+class TestPersonMergeQueuesView(BaseTestMergeQueuesView):
418+ """Tests for the person merge queue view.
419+
420+ The tests are all defined on the base class. This class sets up the
421+ context and provides helper method(s) for the tests.
422+ """
423+
424+ def setUp(self):
425+ BaseTestMergeQueuesView.setUp(self)
426+ self.context = self.branch_owner
427+
428+ def test_mergequeues_with_all_public_branches(self):
429+ self._test_mergequeues_with_all_public_branches()
430+
431+ def test_mergequeues_with_a_private_branch_for_owner(self):
432+ self._test_mergequeues_with_a_private_branch_for_owner()
433+
434+ def test_mergequeues_with_a_private_branch_for_other_user(self):
435+ self._test_mergequeues_with_a_private_branch_for_other_user()
436+
437+
438+class TestPersonMergeQueuesListPage(BaseTestMergeQueuesListPage):
439+ """Tests for the person merge queue list page.
440+
441+ The tests are all defined on the base class. This class sets up the
442+ context and provides helper method(s) for the tests.
443+ """
444+
445+ def setUp(self):
446+ BaseTestMergeQueuesListPage.setUp(self)
447+ self.context = self.branch_owner
448+ self.check_queue_owner = False
449+
450+ def extract_merge_queue_info(self, table):
451+ merge_queue_info = {}
452+ for row in table.tbody.fetch('tr'):
453+ cells = row('td')
454+ row_info = {}
455+ queue_name = extract_text(cells[0])
456+ if not queue_name.startswith('queue'):
457+ continue
458+ qlink = extract_link_from_tag(cells[0].find('a'))
459+ row_info['queue_link'] = qlink
460+ queue_size = extract_text(cells[1])
461+ row_info['queue_size'] = queue_size
462+ queue_branches = cells[2]('a')
463+ branch_links = set()
464+ for branch_tag in queue_branches:
465+ branch_links.add(extract_link_from_tag(branch_tag))
466+ row_info['branch_links'] = branch_links
467+ merge_queue_info[queue_name] = row_info
468+ return merge_queue_info
469+
470+ def test_merge_queue_list_contents_without_feature_flag(self):
471+ self._test_merge_queue_list_contents_without_feature_flag()
472+
473+ def test_merge_queue_list_contents(self):
474+ self._test_merge_queue_list_contents()
475+
476+
477+class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):
478+ """Tests for the person code homepage.
479+
480+ This is the default page shown for a person on the code subdomain.
481+ """
482+
483+ layer = DatabaseFunctionalLayer
484+
485+ def setUp(self):
486+ BrowserTestCase.setUp(self)
487+ MergeQueuesTestMixin.setUp(self)
488+ self._makeMergeQueues()
489+
490+ def test_merge_queue_menu_link_without_feature_flag(self):
491+ login_person(self.branch_owner)
492+ browser = self.getUserBrowser(
493+ canonical_url(self.branch_owner, rootsite='code'),
494+ self.branch_owner)
495+ self.assertRaises(
496+ LinkNotFoundError,
497+ browser.getLink,
498+ url='+merge-queues')
499+
500+ def test_merge_queue_menu_link(self):
501+ self.enable_queue_flag()
502+ login_person(self.branch_owner)
503+ browser = self.getUserBrowser(
504+ canonical_url(self.branch_owner, rootsite='code'),
505+ self.branch_owner)
506+ browser.getLink(url='+merge-queues').click()
507+ self.assertEqual(
508+ '%s/+merge-queues' % canonical_url(
509+ self.branch_owner, rootsite='code'), browser.url)
510+
511+
512+class TestProductMergeQueuesView(BaseTestMergeQueuesView):
513+ """Tests for the product merge queue view.
514+
515+ The tests are all defined on the base class. This class sets up the
516+ context and provides helper method(s) for the tests.
517+ """
518+
519+ def setUp(self):
520+ BaseTestMergeQueuesView.setUp(self)
521+ self.context = self.product
522+ # Make some other merge queues for a different product.
523+ person = self.factory.makePerson()
524+ product = self.factory.makeProduct()
525+ self._makeMergeQueues(nr_queues=2, product=product, owner=person)
526+
527+ def test_mergequeues_with_all_public_branches(self):
528+ self._test_mergequeues_with_all_public_branches()
529+
530+ def test_mergequeues_with_a_private_branch_for_owner(self):
531+ self._test_mergequeues_with_a_private_branch_for_owner()
532+
533+ def test_mergequeues_with_a_private_branch_for_other_user(self):
534+ self._test_mergequeues_with_a_private_branch_for_other_user()
535+
536+
537+class TestProductMergeQueuesListPage(BaseTestMergeQueuesListPage):
538+ """Tests for the product merge queue list page.
539+
540+ The tests are all defined on the base class. This class sets up the
541+ context and provides helper method(s) for the tests.
542+ """
543+
544+ def setUp(self):
545+ BaseTestMergeQueuesListPage.setUp(self)
546+ self.context = self.product
547+ self.check_queue_owner = True
548+ # Make some other merge queues for a different product.
549+ person = self.factory.makePerson()
550+ product = self.factory.makeProduct()
551+ self._makeMergeQueues(nr_queues=2, product=product, owner=person)
552+
553+ def extract_merge_queue_info(self, table):
554+ merge_queue_info = {}
555+ for row in table.tbody.fetch('tr'):
556+ cells = row('td')
557+ row_info = {}
558+ queue_name = extract_text(cells[0])
559+ if not queue_name.startswith('queue'):
560+ continue
561+ qlink = extract_link_from_tag(cells[0].find('a'))
562+ row_info['queue_link'] = qlink
563+ queue_owner = extract_text(cells[1])
564+ row_info['queue_owner'] = queue_owner
565+ queue_size = extract_text(cells[2])
566+ row_info['queue_size'] = queue_size
567+ queue_branches = cells[3]('a')
568+ branch_links = set()
569+ for branch_tag in queue_branches:
570+ branch_links.add(extract_link_from_tag(branch_tag))
571+ row_info['branch_links'] = branch_links
572+ merge_queue_info[queue_name] = row_info
573+ return merge_queue_info
574+
575+ def test_merge_queue_list_contents_without_feature_flag(self):
576+ self._test_merge_queue_list_contents_without_feature_flag()
577+
578+ def test_merge_queue_list_contents(self):
579+ self._test_merge_queue_list_contents()
580+
581+
582+class TestProductCodePage(BrowserTestCase, MergeQueuesTestMixin):
583+ """Tests for the product code homepage.
584+
585+ This is the default page shown for a product on the code subdomain.
586+ """
587+
588+ layer = DatabaseFunctionalLayer
589+
590+ def setUp(self):
591+ BrowserTestCase.setUp(self)
592+ MergeQueuesTestMixin.setUp(self)
593+ self._makeMergeQueues()
594+
595+ def test_merge_queue_menu_link_without_feature_flag(self):
596+ login_person(self.branch_owner)
597+ browser = self.getUserBrowser(
598+ canonical_url(self.product, rootsite='code'),
599+ self.branch_owner)
600+ self.assertRaises(
601+ LinkNotFoundError,
602+ browser.getLink,
603+ url='+merge-queues')
604+
605+ def test_merge_queue_menu_link(self):
606+ self.enable_queue_flag()
607+ login_person(self.branch_owner)
608+ browser = self.getUserBrowser(
609+ canonical_url(self.product, rootsite='code'),
610+ self.branch_owner)
611+ browser.getLink(url='+merge-queues').click()
612+ self.assertEqual(
613+ '%s/+merge-queues' % canonical_url(self.product, rootsite='code'),
614+ browser.url)
615+
616+
617+class TestPersonProductMergeQueuesView(BaseTestMergeQueuesView):
618+ """Tests for the person merge queue view.
619+
620+ The tests are all defined on the base class. This class sets up the
621+ context and provides helper method(s) for the tests.
622+ """
623+
624+ def setUp(self):
625+ BaseTestMergeQueuesView.setUp(self)
626+ self.context = getUtility(IPersonProductFactory).create(
627+ self.branch_owner, self.product)
628+ # Make some other merge queues for a different product.
629+ person = self.factory.makePerson()
630+ product = self.factory.makeProduct()
631+ self._makeMergeQueues(nr_queues=2, product=product, owner=person)
632+
633+ def test_mergequeues_with_all_public_branches(self):
634+ self._test_mergequeues_with_all_public_branches()
635+
636+ def test_mergequeues_with_a_private_branch_for_owner(self):
637+ self._test_mergequeues_with_a_private_branch_for_owner()
638+
639+ def test_mergequeues_with_a_private_branch_for_other_user(self):
640+ self._test_mergequeues_with_a_private_branch_for_other_user()
641+
642+
643+class TestPersonProductMergeQueuesListPage(TestPersonMergeQueuesListPage):
644+ """Tests for the person merge queue list page.
645+
646+ The tests are all defined on the base class. This class sets up the
647+ context and provides helper method(s) for the tests.
648+ """
649+
650+ def setUp(self):
651+ BaseTestMergeQueuesListPage.setUp(self)
652+ self.context = getUtility(IPersonProductFactory).create(
653+ self.branch_owner, self.product)
654+ self.check_queue_owner = False
655+ self.context = getUtility(IPersonProductFactory).create(
656+ self.branch_owner, self.product)
657+ # Make some other merge queues for a different product.
658+ person = self.factory.makePerson()
659+ product = self.factory.makeProduct()
660+ self._makeMergeQueues(nr_queues=2, product=product, owner=person)
661+
662+ def test_merge_queue_list_contents_without_feature_flag(self):
663+ self._test_merge_queue_list_contents_without_feature_flag()
664+
665+ def test_merge_queue_list_contents(self):
666+ self._test_merge_queue_list_contents()
667+
668+
669+class TestPersonProductCodePage(BrowserTestCase, MergeQueuesTestMixin):
670+ """Tests for the person product code homepage.
671+
672+ This is the default page shown for a person product on the code
673+ subdomain.
674+ """
675+
676+ layer = DatabaseFunctionalLayer
677+
678+ def setUp(self):
679+ BrowserTestCase.setUp(self)
680+ MergeQueuesTestMixin.setUp(self)
681+ self.context = getUtility(IPersonProductFactory).create(
682+ self.branch_owner, self.product)
683+ self._makeMergeQueues()
684+
685+ def test_merge_queue_menu_link_without_feature_flag(self):
686+
687+ login_person(self.branch_owner)
688+ browser = self.getUserBrowser(
689+ canonical_url(self.branch_owner, rootsite='code'),
690+ self.branch_owner)
691+ self.assertRaises(
692+ LinkNotFoundError,
693+ browser.getLink,
694+ url='+merge-queues')
695+
696+ def test_merge_queue_menu_link(self):
697+ self.enable_queue_flag()
698+ login_person(self.branch_owner)
699+ browser = self.getUserBrowser(
700+ canonical_url(self.branch_owner, rootsite='code'),
701+ self.branch_owner)
702+ browser.getLink(url='+merge-queues').click()
703+ self.assertEqual(
704+ '%s/+merge-queues' % canonical_url(
705+ self.branch_owner, rootsite='code'), browser.url)
706
707=== modified file 'lib/lp/code/interfaces/branchmergequeuecollection.py'
708--- lib/lp/code/interfaces/branchmergequeuecollection.py 2010-11-01 12:41:14 +0000
709+++ lib/lp/code/interfaces/branchmergequeuecollection.py 2010-11-22 11:58:12 +0000
710@@ -59,6 +59,9 @@
711 """Restrict the collection to queues that 'person' is allowed to see.
712 """
713
714+ def inProduct(product):
715+ """Restrict the collection to queues with branches for 'product'."""
716+
717
718 class IAllBranchMergeQueues(IBranchMergeQueueCollection):
719 """An `IBranchMergeQueueCollection` of all branch merge queues."""
720
721=== modified file 'lib/lp/code/model/branchmergequeuecollection.py'
722--- lib/lp/code/model/branchmergequeuecollection.py 2010-11-02 22:41:11 +0000
723+++ lib/lp/code/model/branchmergequeuecollection.py 2010-11-22 11:58:12 +0000
724@@ -10,6 +10,8 @@
725
726 from zope.interface import implements
727
728+from storm.expr import Join
729+
730 from canonical.launchpad.interfaces.lpstorm import IMasterStore
731 from lp.code.interfaces.branchmergequeue import (
732 user_has_special_merge_queue_access,
733@@ -19,6 +21,7 @@
734 InvalidFilter,
735 )
736 from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
737+from lp.code.model.branch import Branch
738 from lp.code.model.branchmergequeue import BranchMergeQueue
739
740
741@@ -108,9 +111,17 @@
742 return self
743 return VisibleBranchMergeQueueCollection(
744 person,
745- self._store, None,
746+ self._store, self._merge_queue_filter_expressions,
747 self._tables, self._exclude_from_search)
748
749+ def inProduct(self, product):
750+ """See `IBranchMergeQueueCollection`."""
751+ return self._filterBy(
752+ [Branch.product == product],
753+ exclude_from_search=['product'],
754+ table=Branch,
755+ join=Join(Branch, BranchMergeQueue.id == Branch.merge_queue_id))
756+
757
758 class VisibleBranchMergeQueueCollection(GenericBranchMergeQueueCollection):
759 """A mergequeue collection which provides queues visible by a user."""
760
761=== modified file 'lib/lp/code/model/tests/test_branchmergequeuecollection.py'
762--- lib/lp/code/model/tests/test_branchmergequeuecollection.py 2010-11-03 08:28:44 +0000
763+++ lib/lp/code/model/tests/test_branchmergequeuecollection.py 2010-11-22 11:58:12 +0000
764@@ -102,6 +102,43 @@
765 collection = self.all_queues.ownedBy(queue.owner)
766 self.assertEqual([queue], collection.getMergeQueues())
767
768+ def test_inProduct(self):
769+ # 'inProject' returns a new collection restricted to queues
770+ # with branches in the given project.
771+ branch = self.factory.makeProductBranch()
772+ branch2 = self.factory.makeProductBranch()
773+ branch3 = self.factory.makeAnyBranch()
774+ queue = self.factory.makeBranchMergeQueue(
775+ branches=[removeSecurityProxy(branch)])
776+ queue2 = self.factory.makeBranchMergeQueue(
777+ branches=[removeSecurityProxy(branch2)])
778+ queue3 = self.factory.makeBranchMergeQueue(
779+ branches=[removeSecurityProxy(branch3)])
780+
781+ collection = self.all_queues.inProduct(branch.product)
782+ self.assertEqual([queue], list(collection.getMergeQueues()))
783+
784+ def test_ownedBy_and_inProduct(self):
785+ # 'ownedBy' and 'inProduct' can combine to form a collection that is
786+ # restricted to queues of a particular product and owned by a
787+ # particular person.
788+ person = self.factory.makePerson()
789+ product = self.factory.makeProduct()
790+ branch = self.factory.makeProductBranch(product=product, owner=person)
791+ branch2 = self.factory.makeAnyBranch(owner=person)
792+ branch3 = self.factory.makeProductBranch(product=product)
793+ queue = self.factory.makeBranchMergeQueue(
794+ owner=person, branches=[removeSecurityProxy(branch)])
795+ queue2 = self.factory.makeBranchMergeQueue(
796+ owner=person, branches=[removeSecurityProxy(branch2)])
797+ queue3 = self.factory.makeBranchMergeQueue(
798+ branches=[removeSecurityProxy(branch3)])
799+
800+ collection = self.all_queues.inProduct(product).ownedBy(person)
801+ self.assertEqual([queue], list(collection.getMergeQueues()))
802+ collection = self.all_queues.ownedBy(person).inProduct(product)
803+ self.assertEqual([queue], list(collection.getMergeQueues()))
804+
805
806 class TestGenericBranchMergeQueueCollectionVisibleFilter(TestCaseWithFactory):
807
808@@ -171,6 +208,14 @@
809 queue_with_private_branch]),
810 sorted(queues.getMergeQueues()))
811
812+ def test_visibility_then_product(self):
813+ # We can apply other filters after applying the visibleByUser filter.
814+ second_public_queue = self.factory.makeBranchMergeQueue()
815+ product = self.queue_with_public_branch.branches[0].product
816+ queues = self.all_queues.visibleByUser(None).inProduct(
817+ product).getMergeQueues()
818+ self.assertEqual([self.queue_with_public_branch], list(queues))
819+
820 def test_launchpad_services_sees_all(self):
821 # The LAUNCHPAD_SERVICES special user sees *everything*.
822 queues = self.all_queues.visibleByUser(LAUNCHPAD_SERVICES)
823
824=== modified file 'lib/lp/code/templates/product-portlet-codestatistics.pt'
825--- lib/lp/code/templates/product-portlet-codestatistics.pt 2010-10-14 23:15:25 +0000
826+++ lib/lp/code/templates/product-portlet-codestatistics.pt 2010-11-22 11:58:12 +0000
827@@ -2,6 +2,7 @@
828 xmlns:tal="http://xml.zope.org/namespaces/tal"
829 xmlns:metal="http://xml.zope.org/namespaces/metal"
830 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
831+ tal:define="features request/features"
832 id="portlet-product-codestatistics">
833
834 <p id="active-review-count"
835@@ -11,7 +12,18 @@
836 <tal:project replace="context/displayname"/> has
837 <tal:active-count replace="count"/>
838 <tal:link replace="structure python: link.render().lower()"/>.
839- </p>
840+ </p>
841+
842+ <div tal:condition="features/code.branchmergequeue">
843+ <p id="merge-queue-count"
844+ tal:define="count context/menu:branches/mergequeue_count;
845+ link context/menu:branches/mergequeues"
846+ tal:condition="python: count &gt; 0">
847+ <tal:project replace="context/displayname"/> has
848+ <tal:active-count replace="count"/>
849+ <tal:link replace="structure python: link.render().lower()"/>.
850+ </p>
851+ </div>
852
853 <!--branches-->
854 <p>
855@@ -25,7 +37,7 @@
856 <tal:branch-count replace="count"/>
857 <tal:branches replace="python: view.branch_text.lower()">
858 branches
859- </tal:branches
860+ </tal:branches
861 ><tal:has-branches condition="view/branch_count">
862 owned by
863 <tal:individuals condition="view/person_owner_count">