Merge lp:~wallyworld/launchpad/product-mergequeue-listview into lp:launchpad
- product-mergequeue-listview
- Merge into devel
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 |
Related bugs: |
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/
lib/lp/
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/
to cover new inProduct() filter and other new methods.
Rework implementation of tests in
lib/lp/
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_branchmerg
bin/test -vvt test_branchmerg
= Lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
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?
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.
Preview Diff
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 > 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"> |
Does this still need review? It's more than 6 months old. What's the status of it?