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
=== modified file 'lib/lp/code/browser/branchlisting.py'
--- lib/lp/code/browser/branchlisting.py 2010-11-18 12:05:34 +0000
+++ lib/lp/code/browser/branchlisting.py 2010-11-22 11:58:12 +0000
@@ -94,7 +94,9 @@
94 PersonActiveReviewsView,94 PersonActiveReviewsView,
95 PersonProductActiveReviewsView,95 PersonProductActiveReviewsView,
96 )96 )
97from lp.code.browser.branchmergequeuelisting import HasMergeQueuesMenuMixin97from lp.code.browser.branchmergequeuelisting import (
98 HasMergeQueuesMenuMixin,
99 )
98from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin100from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin
99from lp.code.browser.summary import BranchCountSummaryView101from lp.code.browser.summary import BranchCountSummaryView
100from lp.code.enums import (102from lp.code.enums import (
@@ -110,6 +112,9 @@
110 IBranchListingQueryOptimiser,112 IBranchListingQueryOptimiser,
111 )113 )
112from lp.code.interfaces.branchcollection import IAllBranches114from lp.code.interfaces.branchcollection import IAllBranches
115from lp.code.interfaces.branchmergequeuecollection import (
116 IAllBranchMergeQueues,
117 )
113from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy118from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy
114from lp.code.interfaces.branchtarget import IBranchTarget119from lp.code.interfaces.branchtarget import IBranchTarget
115from lp.code.interfaces.revision import IRevisionSet120from lp.code.interfaces.revision import IRevisionSet
@@ -959,7 +964,8 @@
959class PersonProductBranchesMenu(PersonBranchesMenu):964class PersonProductBranchesMenu(PersonBranchesMenu):
960965
961 usedfor = IPersonProduct966 usedfor = IPersonProduct
962 links = ['registered', 'owned', 'subscribed', 'active_reviews']967 links = ['registered', 'owned', 'subscribed', 'active_reviews',
968 'mergequeues']
963969
964 def _getCountCollection(self):970 def _getCountCollection(self):
965 """See `PersonBranchesMenu`."""971 """See `PersonBranchesMenu`."""
@@ -978,6 +984,11 @@
978 self.context, self.request)984 self.context, self.request)
979 return active_reviews.getProposals().count()985 return active_reviews.getProposals().count()
980986
987 @cachedproperty
988 def mergequeue_count(self):
989 return getUtility(IAllBranchMergeQueues).ownedBy(
990 self.person).inProduct(self.context.product).count()
991
981992
982class PersonBaseBranchListingView(BranchListingView):993class PersonBaseBranchListingView(BranchListingView):
983 """Base class used for different person listing views."""994 """Base class used for different person listing views."""
@@ -1106,7 +1117,7 @@
1106 return self.context.person1117 return self.context.person
11071118
11081119
1109class ProductBranchesMenu(ApplicationMenu):1120class ProductBranchesMenu(ApplicationMenu, HasMergeQueuesMenuMixin):
11101121
1111 usedfor = IProduct1122 usedfor = IProduct
1112 facet = 'branches'1123 facet = 'branches'
@@ -1114,13 +1125,19 @@
1114 'branch_add',1125 'branch_add',
1115 'list_branches',1126 'list_branches',
1116 'active_reviews',1127 'active_reviews',
1128 'mergequeues',
1117 'code_import',1129 'code_import',
1118 'branch_visibility',1130 'branch_visibility',
1119 ]1131 ]
1120 extra_attributes = [1132 extra_attributes = [
1121 'active_review_count',1133 'active_review_count',
1134 'mergequeue_count',
1122 ]1135 ]
11231136
1137 @cachedproperty
1138 def mergequeue_count(self):
1139 return self._getMergeQueueCollection().count()
1140
1124 def branch_add(self):1141 def branch_add(self):
1125 text = 'Register a branch'1142 text = 'Register a branch'
1126 summary = 'Register a new Bazaar branch for this project'1143 summary = 'Register a new Bazaar branch for this project'
11271144
=== modified file 'lib/lp/code/browser/branchmergequeuelisting.py'
--- lib/lp/code/browser/branchmergequeuelisting.py 2010-11-02 22:41:11 +0000
+++ lib/lp/code/browser/branchmergequeuelisting.py 2010-11-22 11:58:12 +0000
@@ -28,7 +28,7 @@
28class HasMergeQueuesMenuMixin:28class HasMergeQueuesMenuMixin:
29 """A context menus mixin for objects that can own merge queues."""29 """A context menus mixin for objects that can own merge queues."""
3030
31 def _getCollection(self):31 def _getMergeQueueCollection(self):
32 return getUtility(IAllBranchMergeQueues).visibleByUser(self.user)32 return getUtility(IAllBranchMergeQueues).visibleByUser(self.user)
3333
34 @property34 @property
@@ -49,7 +49,7 @@
4949
50 @cachedproperty50 @cachedproperty
51 def mergequeue_count(self):51 def mergequeue_count(self):
52 return self._getCollection().ownedBy(self.person).count()52 return self._getMergeQueueCollection().ownedBy(self.person).count()
5353
5454
55class MergeQueueListingView(LaunchpadView, FeedsMixin):55class MergeQueueListingView(LaunchpadView, FeedsMixin):
@@ -103,3 +103,39 @@
103103
104 def _getCollection(self):104 def _getCollection(self):
105 return getUtility(IAllBranchMergeQueues).ownedBy(self.context)105 return getUtility(IAllBranchMergeQueues).ownedBy(self.context)
106
107
108class ProductMergeQueueListingView(MergeQueueListingView):
109
110 label_template = 'Merge Queues for branches of %(displayname)s'
111
112 def _getCollection(self):
113 return getUtility(IAllBranchMergeQueues).inProduct(self.context)
114
115
116class PersonProductMergeQueueListingView(MergeQueueListingView):
117
118 label_template = 'Merge Queues of %(product)s owned by %(person)s'
119 owner_enabled = False
120
121 @property
122 def person(self):
123 """Return the person from the PersonProduct context."""
124 return self.context.person
125
126 @property
127 def label(self):
128 return self.label_template % {
129 'person': self.context.person.displayname,
130 'product': self.context.product.displayname}
131
132 def _getCollection(self):
133 return getUtility(IAllBranchMergeQueues).ownedBy(
134 self.context.person).inProduct(self.context.product)
135
136 @property
137 def no_merge_queue_message(self):
138 """Shown when there is no table to show."""
139 return "%(product)s has no merge queues owned by %(person)s." % {
140 'person': self.context.person.displayname,
141 'product': self.context.product.displayname}
106142
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2010-11-18 12:05:34 +0000
+++ lib/lp/code/browser/configure.zcml 2010-11-22 11:58:12 +0000
@@ -1327,6 +1327,24 @@
1327 facet="branches"1327 facet="branches"
1328 name="+merge-queues"1328 name="+merge-queues"
1329 template="../templates/branchmergequeue-listing.pt"/>1329 template="../templates/branchmergequeue-listing.pt"/>
1330 <browser:page
1331 for="lp.registry.interfaces.product.IProduct"
1332 layer="lp.code.publisher.CodeLayer"
1333 class="lp.code.browser.branchmergequeuelisting.ProductMergeQueueListingView"
1334 permission="zope.Public"
1335 facet="branches"
1336 name="+merge-queues"
1337 template="../templates/branchmergequeue-listing.pt"
1338 />
1339 <browser:page
1340 for="lp.registry.interfaces.personproduct.IPersonProduct"
1341 layer="lp.code.publisher.CodeLayer"
1342 class="lp.code.browser.branchmergequeuelisting.PersonProductMergeQueueListingView"
1343 permission="zope.Public"
1344 facet="branches"
1345 name="+merge-queues"
1346 template="../templates/branchmergequeue-listing.pt"
1347 />
13301348
1331 <browser:page1349 <browser:page
1332 for="*"1350 for="*"
13331351
=== modified file 'lib/lp/code/browser/tests/test_branchmergequeuelisting.py'
--- lib/lp/code/browser/tests/test_branchmergequeuelisting.py 2010-11-02 22:41:11 +0000
+++ lib/lp/code/browser/tests/test_branchmergequeuelisting.py 2010-11-22 11:58:12 +0000
@@ -9,6 +9,8 @@
99
10from mechanize import LinkNotFoundError10from mechanize import LinkNotFoundError
11import soupmatchers11import soupmatchers
12
13from zope.component import getUtility
12from zope.security.proxy import removeSecurityProxy14from zope.security.proxy import removeSecurityProxy
1315
14from canonical.launchpad.testing.pages import (16from canonical.launchpad.testing.pages import (
@@ -18,6 +20,8 @@
18 )20 )
19from canonical.launchpad.webapp import canonical_url21from canonical.launchpad.webapp import canonical_url
20from canonical.testing.layers import DatabaseFunctionalLayer22from canonical.testing.layers import DatabaseFunctionalLayer
23from lp.code.enums import BranchType
24from lp.registry.interfaces.personproduct import IPersonProductFactory
21from lp.services.features.model import (25from lp.services.features.model import (
22 FeatureFlag,26 FeatureFlag,
23 getFeatureStore,27 getFeatureStore,
@@ -34,45 +38,65 @@
34class MergeQueuesTestMixin:38class MergeQueuesTestMixin:
3539
36 def setUp(self):40 def setUp(self):
37 self.branch_owner = self.factory.makePerson(name='eric')41 self.branch_owner = removeSecurityProxy(
42 self.factory.makePerson(name='eric'))
43 dev_series_branch = self.factory.makeBranch()
44 productseries = self.factory.makeProductSeries(
45 name="myproduct", branch=dev_series_branch)
46 self.product = removeSecurityProxy(productseries).product
47 removeSecurityProxy(self.product).development_focus = productseries
3848
39 def enable_queue_flag(self):49 def enable_queue_flag(self):
40 getFeatureStore().add(FeatureFlag(50 getFeatureStore().add(FeatureFlag(
41 scope=u'default', flag=u'code.branchmergequeue',51 scope=u'default', flag=u'code.branchmergequeue',
42 value=u'on', priority=1))52 value=u'on', priority=1))
4353
44 def _makeMergeQueues(self, nr_queues=3, nr_with_private_branches=0):54 def _makeMergeQueues(
55 self, nr_queues=3, nr_with_private_branches=0,
56 product=None, owner=None):
45 # We create nr_queues merge queues in total, and the first57 # We create nr_queues merge queues in total, and the first
46 # nr_with_private_branches of them will have at least one private58 # nr_with_private_branches of them will have at least one private
47 # branch in the queue.59 # branch in the queue.
48 with person_logged_in(self.branch_owner):60 if owner is None:
61 owner = self.branch_owner
62 with person_logged_in(owner):
49 mergequeues = [63 mergequeues = [
50 self.factory.makeBranchMergeQueue(64 self.factory.makeBranchMergeQueue(
51 owner=self.branch_owner, branches=self._makeBranches())65 owner=owner,
66 branches=self._makeBranches(product=product, owner=owner))
52 for i in range(nr_queues-nr_with_private_branches)]67 for i in range(nr_queues-nr_with_private_branches)]
53 mergequeues_with_private_branches = [68 mergequeues_with_private_branches = [
54 self.factory.makeBranchMergeQueue(69 self.factory.makeBranchMergeQueue(
55 owner=self.branch_owner,70 owner=owner,
56 branches=self._makeBranches(nr_private=1))71 branches=self._makeBranches(
72 nr_private=1, product=product, owner=owner))
57 for i in range(nr_with_private_branches)]73 for i in range(nr_with_private_branches)]
5874
59 return mergequeues, mergequeues_with_private_branches75 return mergequeues, mergequeues_with_private_branches
6076
61 def _makeBranches(self, nr_public=3, nr_private=0):77 def _makeBranches(
78 self, nr_public=3, nr_private=0, product=None, owner=None):
79 if product is None:
80 product = self.product
81 if owner is None:
82 owner = self.branch_owner
62 branches = [83 branches = [
63 self.factory.makeProductBranch(owner=self.branch_owner)84 self.factory.makeProductBranch(
85 owner=owner, product=product, branch_type=BranchType.HOSTED)
64 for i in range(nr_public)]86 for i in range(nr_public)]
6587
66 private_branches = [88 private_branches = [
67 self.factory.makeProductBranch(89 self.factory.makeProductBranch(
68 owner=self.branch_owner, private=True)90 owner=owner, product=product, private=True,
91 branch_type=BranchType.HOSTED)
69 for i in range(nr_private)]92 for i in range(nr_private)]
7093
71 branches.extend(private_branches)94 branches.extend(private_branches)
72 return branches95 return branches
7396
7497
75class TestPersonMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):98class BaseTestMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):
99 """ Base class for person, product, personproduct view tests."""
76100
77 layer = DatabaseFunctionalLayer101 layer = DatabaseFunctionalLayer
78102
@@ -81,73 +105,38 @@
81 MergeQueuesTestMixin.setUp(self)105 MergeQueuesTestMixin.setUp(self)
82 self.user = self.factory.makePerson()106 self.user = self.factory.makePerson()
83107
84 def test_mergequeues_with_all_public_branches(self):108 def _test_mergequeues_with_all_public_branches(self):
85 # Anyone can see mergequeues containing all public branches.109 # Anyone can see mergequeues containing all public branches.
86 mq, mq_with_private = self._makeMergeQueues()110 mq, mq_with_private = self._makeMergeQueues()
87 login_person(self.user)111 login_person(self.user)
88 view = create_initialized_view(112 view = create_initialized_view(
89 self.branch_owner, name="+merge-queues", rootsite='code')113 self.context, name="+merge-queues", rootsite='code')
90 self.assertEqual(set(mq), set(view.mergequeues))114 self.assertEqual(set(mq), set(view.mergequeues))
91115
92 def test_mergequeues_with_a_private_branch_for_owner(self):116 def _test_mergequeues_with_a_private_branch_for_owner(self):
93 # Only users with access to private branches can see any queues117 # Only users with access to private branches can see any queues
94 # containing such branches.118 # containing such branches.
95 mq, mq_with_private = (119 mq, mq_with_private = (
96 self._makeMergeQueues(nr_with_private_branches=1))120 self._makeMergeQueues(nr_with_private_branches=1))
97 login_person(self.branch_owner)121 login_person(self.branch_owner)
98 view = create_initialized_view(122 view = create_initialized_view(
99 self.branch_owner, name="+merge-queues", rootsite='code')123 self.context, name="+merge-queues", rootsite='code')
100 mq.extend(mq_with_private)124 mq.extend(mq_with_private)
101 self.assertEqual(set(mq), set(view.mergequeues))125 self.assertEqual(set(mq), set(view.mergequeues))
102126
103 def test_mergequeues_with_a_private_branch_for_other_user(self):127 def _test_mergequeues_with_a_private_branch_for_other_user(self):
104 # Only users with access to private branches can see any queues128 # Only users with access to private branches can see any queues
105 # containing such branches.129 # containing such branches.
106 mq, mq_with_private = (130 mq, mq_with_private = (
107 self._makeMergeQueues(nr_with_private_branches=1))131 self._makeMergeQueues(nr_with_private_branches=1))
108 login_person(self.user)132 login_person(self.user)
109 view = create_initialized_view(133 view = create_initialized_view(
110 self.branch_owner, name="+merge-queues", rootsite='code')134 self.context, name="+merge-queues", rootsite='code')
111 self.assertEqual(set(mq), set(view.mergequeues))135 self.assertEqual(set(mq), set(view.mergequeues))
112136
113137
114class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):138class BaseTestMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
115 """Tests for the person code homepage.139 """ Base class for person, product, personproduct list page tests."""
116
117 This is the default page shown for a person on the code subdomain.
118 """
119
120 layer = DatabaseFunctionalLayer
121
122 def setUp(self):
123 BrowserTestCase.setUp(self)
124 MergeQueuesTestMixin.setUp(self)
125 self._makeMergeQueues()
126
127 def test_merge_queue_menu_link_without_feature_flag(self):
128 login_person(self.branch_owner)
129 browser = self.getUserBrowser(
130 canonical_url(self.branch_owner, rootsite='code'),
131 self.branch_owner)
132 self.assertRaises(
133 LinkNotFoundError,
134 browser.getLink,
135 url='+merge-queues')
136
137 def test_merge_queue_menu_link(self):
138 self.enable_queue_flag()
139 login_person(self.branch_owner)
140 browser = self.getUserBrowser(
141 canonical_url(self.branch_owner, rootsite='code'),
142 self.branch_owner)
143 browser.getLink(url='+merge-queues').click()
144 self.assertEqual(
145 'http://code.launchpad.dev/~eric/+merge-queues',
146 browser.url)
147
148
149class TestPersonMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
150 """Tests for the person merge queue list page."""
151140
152 layer = DatabaseFunctionalLayer141 layer = DatabaseFunctionalLayer
153142
@@ -158,10 +147,10 @@
158 self.merge_queues = mq147 self.merge_queues = mq
159 self.merge_queues.extend(mq_with_private)148 self.merge_queues.extend(mq_with_private)
160149
161 def test_merge_queue_list_contents_without_feature_flag(self):150 def _test_merge_queue_list_contents_without_feature_flag(self):
162 login_person(self.branch_owner)151 login_person(self.branch_owner)
163 browser = self.getUserBrowser(152 browser = self.getUserBrowser(
164 canonical_url(self.branch_owner, rootsite='code',153 canonical_url(self.context, rootsite='code',
165 view_name='+merge-queues'), self.branch_owner)154 view_name='+merge-queues'), self.branch_owner)
166 table = find_tag_by_id(browser.contents, 'mergequeuetable')155 table = find_tag_by_id(browser.contents, 'mergequeuetable')
167 self.assertIs(None, table)156 self.assertIs(None, table)
@@ -172,37 +161,30 @@
172 '\w*No merge queues\w*')))161 '\w*No merge queues\w*')))
173 self.assertThat(browser.contents, noqueue_matcher)162 self.assertThat(browser.contents, noqueue_matcher)
174163
175 def test_merge_queue_list_contents(self):164 def _test_merge_queue_list_contents(self):
176 self.enable_queue_flag()165 self.enable_queue_flag()
177 login_person(self.branch_owner)166 login_person(self.branch_owner)
178 browser = self.getUserBrowser(167 browser = self.getUserBrowser(
179 canonical_url(self.branch_owner, rootsite='code',168 canonical_url(self.context, rootsite='code',
180 view_name='+merge-queues'), self.branch_owner)169 view_name='+merge-queues'), self.branch_owner)
181170
182 table = find_tag_by_id(browser.contents, 'mergequeuetable')171 table = find_tag_by_id(browser.contents, 'mergequeuetable')
183172 merge_queue_info = self.extract_merge_queue_info(table)
184 merge_queue_info = {}
185 for row in table.tbody.fetch('tr'):
186 cells = row('td')
187 row_info = {}
188 queue_name = extract_text(cells[0])
189 if not queue_name.startswith('queue'):
190 continue
191 qlink = extract_link_from_tag(cells[0].find('a'))
192 row_info['queue_link'] = qlink
193 queue_size = extract_text(cells[1])
194 row_info['queue_size'] = queue_size
195 queue_branches = cells[2]('a')
196 branch_links = set()
197 for branch_tag in queue_branches:
198 branch_links.add(extract_link_from_tag(branch_tag))
199 row_info['branch_links'] = branch_links
200 merge_queue_info[queue_name] = row_info
201173
202 expected_queue_names = [queue.name for queue in self.merge_queues]174 expected_queue_names = [queue.name for queue in self.merge_queues]
203 self.assertEqual(175 self.assertEqual(
204 set(expected_queue_names), set(merge_queue_info.keys()))176 set(expected_queue_names), set(merge_queue_info.keys()))
205177
178 if self.check_queue_owner:
179 # All queues are created with self.branch_owner as the owner.
180 expected_queue_owners = [
181 'Eric' for queue in self.merge_queues]
182 observed_queue_owners = (
183 [merge_queue_info[queue.name]['queue_owner']
184 for queue in self.merge_queues])
185 self.assertEqual(
186 expected_queue_owners, observed_queue_owners)
187
206 #TODO: when IBranchMergeQueue API is available remove '4'188 #TODO: when IBranchMergeQueue API is available remove '4'
207 expected_queue_sizes = dict(189 expected_queue_sizes = dict(
208 [(queue.name, '4') for queue in self.merge_queues])190 [(queue.name, '4') for queue in self.merge_queues])
@@ -225,3 +207,294 @@
225 for queue in self.merge_queues])207 for queue in self.merge_queues])
226 self.assertEqual(208 self.assertEqual(
227 expected_queue_branches, observed_queue_branches)209 expected_queue_branches, observed_queue_branches)
210
211
212class TestPersonMergeQueuesView(BaseTestMergeQueuesView):
213 """Tests for the person merge queue view.
214
215 The tests are all defined on the base class. This class sets up the
216 context and provides helper method(s) for the tests.
217 """
218
219 def setUp(self):
220 BaseTestMergeQueuesView.setUp(self)
221 self.context = self.branch_owner
222
223 def test_mergequeues_with_all_public_branches(self):
224 self._test_mergequeues_with_all_public_branches()
225
226 def test_mergequeues_with_a_private_branch_for_owner(self):
227 self._test_mergequeues_with_a_private_branch_for_owner()
228
229 def test_mergequeues_with_a_private_branch_for_other_user(self):
230 self._test_mergequeues_with_a_private_branch_for_other_user()
231
232
233class TestPersonMergeQueuesListPage(BaseTestMergeQueuesListPage):
234 """Tests for the person merge queue list page.
235
236 The tests are all defined on the base class. This class sets up the
237 context and provides helper method(s) for the tests.
238 """
239
240 def setUp(self):
241 BaseTestMergeQueuesListPage.setUp(self)
242 self.context = self.branch_owner
243 self.check_queue_owner = False
244
245 def extract_merge_queue_info(self, table):
246 merge_queue_info = {}
247 for row in table.tbody.fetch('tr'):
248 cells = row('td')
249 row_info = {}
250 queue_name = extract_text(cells[0])
251 if not queue_name.startswith('queue'):
252 continue
253 qlink = extract_link_from_tag(cells[0].find('a'))
254 row_info['queue_link'] = qlink
255 queue_size = extract_text(cells[1])
256 row_info['queue_size'] = queue_size
257 queue_branches = cells[2]('a')
258 branch_links = set()
259 for branch_tag in queue_branches:
260 branch_links.add(extract_link_from_tag(branch_tag))
261 row_info['branch_links'] = branch_links
262 merge_queue_info[queue_name] = row_info
263 return merge_queue_info
264
265 def test_merge_queue_list_contents_without_feature_flag(self):
266 self._test_merge_queue_list_contents_without_feature_flag()
267
268 def test_merge_queue_list_contents(self):
269 self._test_merge_queue_list_contents()
270
271
272class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):
273 """Tests for the person code homepage.
274
275 This is the default page shown for a person on the code subdomain.
276 """
277
278 layer = DatabaseFunctionalLayer
279
280 def setUp(self):
281 BrowserTestCase.setUp(self)
282 MergeQueuesTestMixin.setUp(self)
283 self._makeMergeQueues()
284
285 def test_merge_queue_menu_link_without_feature_flag(self):
286 login_person(self.branch_owner)
287 browser = self.getUserBrowser(
288 canonical_url(self.branch_owner, rootsite='code'),
289 self.branch_owner)
290 self.assertRaises(
291 LinkNotFoundError,
292 browser.getLink,
293 url='+merge-queues')
294
295 def test_merge_queue_menu_link(self):
296 self.enable_queue_flag()
297 login_person(self.branch_owner)
298 browser = self.getUserBrowser(
299 canonical_url(self.branch_owner, rootsite='code'),
300 self.branch_owner)
301 browser.getLink(url='+merge-queues').click()
302 self.assertEqual(
303 '%s/+merge-queues' % canonical_url(
304 self.branch_owner, rootsite='code'), browser.url)
305
306
307class TestProductMergeQueuesView(BaseTestMergeQueuesView):
308 """Tests for the product merge queue view.
309
310 The tests are all defined on the base class. This class sets up the
311 context and provides helper method(s) for the tests.
312 """
313
314 def setUp(self):
315 BaseTestMergeQueuesView.setUp(self)
316 self.context = self.product
317 # Make some other merge queues for a different product.
318 person = self.factory.makePerson()
319 product = self.factory.makeProduct()
320 self._makeMergeQueues(nr_queues=2, product=product, owner=person)
321
322 def test_mergequeues_with_all_public_branches(self):
323 self._test_mergequeues_with_all_public_branches()
324
325 def test_mergequeues_with_a_private_branch_for_owner(self):
326 self._test_mergequeues_with_a_private_branch_for_owner()
327
328 def test_mergequeues_with_a_private_branch_for_other_user(self):
329 self._test_mergequeues_with_a_private_branch_for_other_user()
330
331
332class TestProductMergeQueuesListPage(BaseTestMergeQueuesListPage):
333 """Tests for the product merge queue list page.
334
335 The tests are all defined on the base class. This class sets up the
336 context and provides helper method(s) for the tests.
337 """
338
339 def setUp(self):
340 BaseTestMergeQueuesListPage.setUp(self)
341 self.context = self.product
342 self.check_queue_owner = True
343 # Make some other merge queues for a different product.
344 person = self.factory.makePerson()
345 product = self.factory.makeProduct()
346 self._makeMergeQueues(nr_queues=2, product=product, owner=person)
347
348 def extract_merge_queue_info(self, table):
349 merge_queue_info = {}
350 for row in table.tbody.fetch('tr'):
351 cells = row('td')
352 row_info = {}
353 queue_name = extract_text(cells[0])
354 if not queue_name.startswith('queue'):
355 continue
356 qlink = extract_link_from_tag(cells[0].find('a'))
357 row_info['queue_link'] = qlink
358 queue_owner = extract_text(cells[1])
359 row_info['queue_owner'] = queue_owner
360 queue_size = extract_text(cells[2])
361 row_info['queue_size'] = queue_size
362 queue_branches = cells[3]('a')
363 branch_links = set()
364 for branch_tag in queue_branches:
365 branch_links.add(extract_link_from_tag(branch_tag))
366 row_info['branch_links'] = branch_links
367 merge_queue_info[queue_name] = row_info
368 return merge_queue_info
369
370 def test_merge_queue_list_contents_without_feature_flag(self):
371 self._test_merge_queue_list_contents_without_feature_flag()
372
373 def test_merge_queue_list_contents(self):
374 self._test_merge_queue_list_contents()
375
376
377class TestProductCodePage(BrowserTestCase, MergeQueuesTestMixin):
378 """Tests for the product code homepage.
379
380 This is the default page shown for a product on the code subdomain.
381 """
382
383 layer = DatabaseFunctionalLayer
384
385 def setUp(self):
386 BrowserTestCase.setUp(self)
387 MergeQueuesTestMixin.setUp(self)
388 self._makeMergeQueues()
389
390 def test_merge_queue_menu_link_without_feature_flag(self):
391 login_person(self.branch_owner)
392 browser = self.getUserBrowser(
393 canonical_url(self.product, rootsite='code'),
394 self.branch_owner)
395 self.assertRaises(
396 LinkNotFoundError,
397 browser.getLink,
398 url='+merge-queues')
399
400 def test_merge_queue_menu_link(self):
401 self.enable_queue_flag()
402 login_person(self.branch_owner)
403 browser = self.getUserBrowser(
404 canonical_url(self.product, rootsite='code'),
405 self.branch_owner)
406 browser.getLink(url='+merge-queues').click()
407 self.assertEqual(
408 '%s/+merge-queues' % canonical_url(self.product, rootsite='code'),
409 browser.url)
410
411
412class TestPersonProductMergeQueuesView(BaseTestMergeQueuesView):
413 """Tests for the person merge queue view.
414
415 The tests are all defined on the base class. This class sets up the
416 context and provides helper method(s) for the tests.
417 """
418
419 def setUp(self):
420 BaseTestMergeQueuesView.setUp(self)
421 self.context = getUtility(IPersonProductFactory).create(
422 self.branch_owner, self.product)
423 # Make some other merge queues for a different product.
424 person = self.factory.makePerson()
425 product = self.factory.makeProduct()
426 self._makeMergeQueues(nr_queues=2, product=product, owner=person)
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
438class TestPersonProductMergeQueuesListPage(TestPersonMergeQueuesListPage):
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 = getUtility(IPersonProductFactory).create(
448 self.branch_owner, self.product)
449 self.check_queue_owner = False
450 self.context = getUtility(IPersonProductFactory).create(
451 self.branch_owner, self.product)
452 # Make some other merge queues for a different product.
453 person = self.factory.makePerson()
454 product = self.factory.makeProduct()
455 self._makeMergeQueues(nr_queues=2, product=product, owner=person)
456
457 def test_merge_queue_list_contents_without_feature_flag(self):
458 self._test_merge_queue_list_contents_without_feature_flag()
459
460 def test_merge_queue_list_contents(self):
461 self._test_merge_queue_list_contents()
462
463
464class TestPersonProductCodePage(BrowserTestCase, MergeQueuesTestMixin):
465 """Tests for the person product code homepage.
466
467 This is the default page shown for a person product on the code
468 subdomain.
469 """
470
471 layer = DatabaseFunctionalLayer
472
473 def setUp(self):
474 BrowserTestCase.setUp(self)
475 MergeQueuesTestMixin.setUp(self)
476 self.context = getUtility(IPersonProductFactory).create(
477 self.branch_owner, self.product)
478 self._makeMergeQueues()
479
480 def test_merge_queue_menu_link_without_feature_flag(self):
481
482 login_person(self.branch_owner)
483 browser = self.getUserBrowser(
484 canonical_url(self.branch_owner, rootsite='code'),
485 self.branch_owner)
486 self.assertRaises(
487 LinkNotFoundError,
488 browser.getLink,
489 url='+merge-queues')
490
491 def test_merge_queue_menu_link(self):
492 self.enable_queue_flag()
493 login_person(self.branch_owner)
494 browser = self.getUserBrowser(
495 canonical_url(self.branch_owner, rootsite='code'),
496 self.branch_owner)
497 browser.getLink(url='+merge-queues').click()
498 self.assertEqual(
499 '%s/+merge-queues' % canonical_url(
500 self.branch_owner, rootsite='code'), browser.url)
228501
=== modified file 'lib/lp/code/interfaces/branchmergequeuecollection.py'
--- lib/lp/code/interfaces/branchmergequeuecollection.py 2010-11-01 12:41:14 +0000
+++ lib/lp/code/interfaces/branchmergequeuecollection.py 2010-11-22 11:58:12 +0000
@@ -59,6 +59,9 @@
59 """Restrict the collection to queues that 'person' is allowed to see.59 """Restrict the collection to queues that 'person' is allowed to see.
60 """60 """
6161
62 def inProduct(product):
63 """Restrict the collection to queues with branches for 'product'."""
64
6265
63class IAllBranchMergeQueues(IBranchMergeQueueCollection):66class IAllBranchMergeQueues(IBranchMergeQueueCollection):
64 """An `IBranchMergeQueueCollection` of all branch merge queues."""67 """An `IBranchMergeQueueCollection` of all branch merge queues."""
6568
=== modified file 'lib/lp/code/model/branchmergequeuecollection.py'
--- lib/lp/code/model/branchmergequeuecollection.py 2010-11-02 22:41:11 +0000
+++ lib/lp/code/model/branchmergequeuecollection.py 2010-11-22 11:58:12 +0000
@@ -10,6 +10,8 @@
1010
11from zope.interface import implements11from zope.interface import implements
1212
13from storm.expr import Join
14
13from canonical.launchpad.interfaces.lpstorm import IMasterStore15from canonical.launchpad.interfaces.lpstorm import IMasterStore
14from lp.code.interfaces.branchmergequeue import (16from lp.code.interfaces.branchmergequeue import (
15 user_has_special_merge_queue_access,17 user_has_special_merge_queue_access,
@@ -19,6 +21,7 @@
19 InvalidFilter,21 InvalidFilter,
20 )22 )
21from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES23from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
24from lp.code.model.branch import Branch
22from lp.code.model.branchmergequeue import BranchMergeQueue25from lp.code.model.branchmergequeue import BranchMergeQueue
2326
2427
@@ -108,9 +111,17 @@
108 return self111 return self
109 return VisibleBranchMergeQueueCollection(112 return VisibleBranchMergeQueueCollection(
110 person,113 person,
111 self._store, None,114 self._store, self._merge_queue_filter_expressions,
112 self._tables, self._exclude_from_search)115 self._tables, self._exclude_from_search)
113116
117 def inProduct(self, product):
118 """See `IBranchMergeQueueCollection`."""
119 return self._filterBy(
120 [Branch.product == product],
121 exclude_from_search=['product'],
122 table=Branch,
123 join=Join(Branch, BranchMergeQueue.id == Branch.merge_queue_id))
124
114125
115class VisibleBranchMergeQueueCollection(GenericBranchMergeQueueCollection):126class VisibleBranchMergeQueueCollection(GenericBranchMergeQueueCollection):
116 """A mergequeue collection which provides queues visible by a user."""127 """A mergequeue collection which provides queues visible by a user."""
117128
=== modified file 'lib/lp/code/model/tests/test_branchmergequeuecollection.py'
--- lib/lp/code/model/tests/test_branchmergequeuecollection.py 2010-11-03 08:28:44 +0000
+++ lib/lp/code/model/tests/test_branchmergequeuecollection.py 2010-11-22 11:58:12 +0000
@@ -102,6 +102,43 @@
102 collection = self.all_queues.ownedBy(queue.owner)102 collection = self.all_queues.ownedBy(queue.owner)
103 self.assertEqual([queue], collection.getMergeQueues())103 self.assertEqual([queue], collection.getMergeQueues())
104104
105 def test_inProduct(self):
106 # 'inProject' returns a new collection restricted to queues
107 # with branches in the given project.
108 branch = self.factory.makeProductBranch()
109 branch2 = self.factory.makeProductBranch()
110 branch3 = self.factory.makeAnyBranch()
111 queue = self.factory.makeBranchMergeQueue(
112 branches=[removeSecurityProxy(branch)])
113 queue2 = self.factory.makeBranchMergeQueue(
114 branches=[removeSecurityProxy(branch2)])
115 queue3 = self.factory.makeBranchMergeQueue(
116 branches=[removeSecurityProxy(branch3)])
117
118 collection = self.all_queues.inProduct(branch.product)
119 self.assertEqual([queue], list(collection.getMergeQueues()))
120
121 def test_ownedBy_and_inProduct(self):
122 # 'ownedBy' and 'inProduct' can combine to form a collection that is
123 # restricted to queues of a particular product and owned by a
124 # particular person.
125 person = self.factory.makePerson()
126 product = self.factory.makeProduct()
127 branch = self.factory.makeProductBranch(product=product, owner=person)
128 branch2 = self.factory.makeAnyBranch(owner=person)
129 branch3 = self.factory.makeProductBranch(product=product)
130 queue = self.factory.makeBranchMergeQueue(
131 owner=person, branches=[removeSecurityProxy(branch)])
132 queue2 = self.factory.makeBranchMergeQueue(
133 owner=person, branches=[removeSecurityProxy(branch2)])
134 queue3 = self.factory.makeBranchMergeQueue(
135 branches=[removeSecurityProxy(branch3)])
136
137 collection = self.all_queues.inProduct(product).ownedBy(person)
138 self.assertEqual([queue], list(collection.getMergeQueues()))
139 collection = self.all_queues.ownedBy(person).inProduct(product)
140 self.assertEqual([queue], list(collection.getMergeQueues()))
141
105142
106class TestGenericBranchMergeQueueCollectionVisibleFilter(TestCaseWithFactory):143class TestGenericBranchMergeQueueCollectionVisibleFilter(TestCaseWithFactory):
107144
@@ -171,6 +208,14 @@
171 queue_with_private_branch]),208 queue_with_private_branch]),
172 sorted(queues.getMergeQueues()))209 sorted(queues.getMergeQueues()))
173210
211 def test_visibility_then_product(self):
212 # We can apply other filters after applying the visibleByUser filter.
213 second_public_queue = self.factory.makeBranchMergeQueue()
214 product = self.queue_with_public_branch.branches[0].product
215 queues = self.all_queues.visibleByUser(None).inProduct(
216 product).getMergeQueues()
217 self.assertEqual([self.queue_with_public_branch], list(queues))
218
174 def test_launchpad_services_sees_all(self):219 def test_launchpad_services_sees_all(self):
175 # The LAUNCHPAD_SERVICES special user sees *everything*.220 # The LAUNCHPAD_SERVICES special user sees *everything*.
176 queues = self.all_queues.visibleByUser(LAUNCHPAD_SERVICES)221 queues = self.all_queues.visibleByUser(LAUNCHPAD_SERVICES)
177222
=== modified file 'lib/lp/code/templates/product-portlet-codestatistics.pt'
--- lib/lp/code/templates/product-portlet-codestatistics.pt 2010-10-14 23:15:25 +0000
+++ lib/lp/code/templates/product-portlet-codestatistics.pt 2010-11-22 11:58:12 +0000
@@ -2,6 +2,7 @@
2 xmlns:tal="http://xml.zope.org/namespaces/tal"2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 tal:define="features request/features"
5 id="portlet-product-codestatistics">6 id="portlet-product-codestatistics">
67
7 <p id="active-review-count"8 <p id="active-review-count"
@@ -11,7 +12,18 @@
11 <tal:project replace="context/displayname"/> has12 <tal:project replace="context/displayname"/> has
12 <tal:active-count replace="count"/>13 <tal:active-count replace="count"/>
13 <tal:link replace="structure python: link.render().lower()"/>.14 <tal:link replace="structure python: link.render().lower()"/>.
14 </p> 15 </p>
16
17 <div tal:condition="features/code.branchmergequeue">
18 <p id="merge-queue-count"
19 tal:define="count context/menu:branches/mergequeue_count;
20 link context/menu:branches/mergequeues"
21 tal:condition="python: count &gt; 0">
22 <tal:project replace="context/displayname"/> has
23 <tal:active-count replace="count"/>
24 <tal:link replace="structure python: link.render().lower()"/>.
25 </p>
26 </div>
1527
16 <!--branches-->28 <!--branches-->
17 <p>29 <p>
@@ -25,7 +37,7 @@
25 <tal:branch-count replace="count"/>37 <tal:branch-count replace="count"/>
26 <tal:branches replace="python: view.branch_text.lower()">38 <tal:branches replace="python: view.branch_text.lower()">
27 branches39 branches
28 </tal:branches 40 </tal:branches
29 ><tal:has-branches condition="view/branch_count">41 ><tal:has-branches condition="view/branch_count">
30 owned by42 owned by
31 <tal:individuals condition="view/person_owner_count">43 <tal:individuals condition="view/person_owner_count">