Merge lp:~wallyworld/launchpad/person-mergequeue-listview into lp:launchpad/db-devel

Proposed by Ian Booth
Status: Superseded
Proposed branch: lp:~wallyworld/launchpad/person-mergequeue-listview
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~rockstar/launchpad/merge-queue-index
Diff against target: 1125 lines (+922/-8)
14 files modified
lib/lp/code/browser/branchlisting.py (+4/-2)
lib/lp/code/browser/branchmergequeuelisting.py (+105/-0)
lib/lp/code/browser/configure.zcml (+18/-0)
lib/lp/code/browser/tests/test_branchmergequeuelisting.py (+227/-0)
lib/lp/code/configure.zcml (+11/-0)
lib/lp/code/interfaces/branchmergequeue.py (+14/-0)
lib/lp/code/interfaces/branchmergequeuecollection.py (+64/-0)
lib/lp/code/model/branchmergequeue.py (+4/-2)
lib/lp/code/model/branchmergequeuecollection.py (+174/-0)
lib/lp/code/model/tests/test_branchmergequeuecollection.py (+201/-0)
lib/lp/code/templates/branchmergequeue-listing.pt (+68/-0)
lib/lp/code/templates/branchmergequeue-macros.pt (+20/-0)
lib/lp/code/templates/person-codesummary.pt (+8/-1)
lib/lp/testing/factory.py (+4/-3)
To merge this branch: bzr merge lp:~wallyworld/launchpad/person-mergequeue-listview
Reviewer Review Type Date Requested Status
Paul Hummer (community) ui Approve
Tim Penhey (community) mentor Approve
Steve Kowalik (community) code* Approve
Review via email: mp+39933@code.launchpad.net

This proposal has been superseded by a proposal from 2010-11-18.

Commit message

Add person merge queue listing functionality. Work in progress merge queue development.

Description of the change

This branch delivers functionality for the merge queue development project. It adds a person merge queue list view and associated model functionality.

= Implementation =

The usual suspects were developed:
- view implementation class
- page template
- zcml changes

To supply data for the view, an IBranchMergeQueueCollection implementation was developed. The implementation is similar to IBranchCollection. A key difference is that the main collection getter, in this case getMergeQueues(), returns a list() rather than a ResultSet(). This is due to how the filtering is done in the VisibleBranchMergeQueueCollection subclass - it's done by iterating over the queue branches in memory rather than as a Storm SQL query. The required query will be non-trivial and can be implemented during another coding iteration if required. The IBranchMergeQueueCollection implementation doesn't have any search() APIs yet - this can be added later if required.

The merge queue list view shows:
- queue name
- queue size
- queue branches

NB the queue size is hard coded pending the required API being developed for IBranchMergeQueue

The menu to access the merge queue list is including alongside the other person branch menu links.

A feature flag is used to hide this functionality as the whole development effort is still WIP. This branch is sufficiently complete in what it delivers and needs to allow collaboration between separate development efforts.

= Screenshot =

Here's a screenshot of the list page:

http://people.canonical.com/~ianb/mergequeuelist.png

= Tests =

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

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  setup.py
  versions.cfg
  lib/lp/code/configure.zcml
  lib/lp/code/browser/branch.py
  lib/lp/code/browser/branchlisting.py
  lib/lp/code/browser/branchmergequeue.py
  lib/lp/code/browser/branchmergequeuelisting.py
  lib/lp/code/browser/configure.zcml
  lib/lp/code/browser/tests/test_branchmergequeue.py
  lib/lp/code/browser/tests/test_branchmergequeuelisting.py
  lib/lp/code/interfaces/branchmergequeue.py
  lib/lp/code/interfaces/branchmergequeuecollection.py
  lib/lp/code/model/branchmergequeue.py
  lib/lp/code/model/branchmergequeuecollection.py
  lib/lp/code/model/tests/test_branchmergequeuecollection.py
  lib/lp/code/templates/branch-pending-merges.pt
  lib/lp/code/templates/branchmergequeue-index.pt
  lib/lp/code/templates/branchmergequeue-listing.pt
  lib/lp/code/templates/branchmergequeue-macros.pt
  lib/lp/code/templates/person-codesummary.pt
  lib/lp/testing/__init__.py
  lib/lp/testing/factory.py

./setup.py
     131: E202 whitespace before ']'
./lib/lp/testing/__init__.py
     129: 'anonymous_logged_in' imported but unused
     129: 'with_anonymous_login' imported but unused
     129: 'is_logged_in' imported but unused
     148: 'launchpadlib_for' imported but unused
     148: 'launchpadlib_credentials_for' imported but unused
     129: 'person_logged_in' imported but unused
     148: 'oauth_access_token_for' imported but unused
     129: 'login_celebrity' imported but unused
     129: 'with_celebrity_logged_in' imported but unused
     147: 'test_tales' imported but unused
     129: 'celebrity_logged_in' imported but unused
     129: 'run_with_login' imported but unused
     129: 'with_person_logged_in' imported but unused
     129: 'login_team' imported but unused
     129: 'login_person' imported but unused
     129: 'login_as' imported but unused
     429: E301 expected 1 blank line, found 0
     861: E301 expected 1 blank line, found 0
     887: E302 expected 2 blank lines, found 1
     963: E302 expected 2 blank lines, found 1

Process finished with exit code 0

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

Adding back comments from old incorrect merge proposal:

SteveK wrote:

Hi Ian,

Firstly, thanks for this awesome work! I've been looking forward to Merge Queues for a while.

I have some comments below:

* 1,500 lines? Why!? I'd suggest in future you look at splitting up work into two separate branches for plumbing and browser code, for example.
* import with_statement isn't needed any more, we're on Python 2.6!
* I'd suggest you run the import formatter over your branch.
* Why use Star Wars names in the tests, I'd prefer descriptive names, such as self.branch_owner, which is less distracting.
* You should use XXX, rather than TODO, and file a bug per the XXX Policy.
* getUtility can be imported from zope.component directly, and indeed you mostly are, except in one place.
* Investigate usage of IMasterStore, rather than getUtility(IStoreSelector), which is deprecated-ish
* You're adding BeautifulSoup and soupmatchers to setup.py and versions.cfg, I'd suggest that get split out into an earlier branch so you can be certain they are available before landing this one.

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

Hi Steve

Thanks for the review.

>
> Firstly, thanks for this awesome work! I've been looking forward to Merge Queues for a while.
>

Actually, rockstar did the modelling work and is the champion of this
stuff. My stuff merely piggy backs onto his :-)

> I have some comments below:
>
> * 1,500 lines? Why!? I'd suggest in future you look at splitting up work into two separate branches for plumbing and browser code, for example.

Yeah. It wasn't meant to be so big - the core code changes were quite
manageable. But by the time I finished writing all the unit tests, the
line count sky rocketed. Sorry.

> * import with_statement isn't needed any more, we're on Python 2.6!

Aaargh. That will teach me to cut'n'paste.

> * I'd suggest you run the import formatter over your branch.

I did that - utilities/format-imports is the right one to use? Not sure
what happened if there are still issues.

> * Why use Star Wars names in the tests, I'd prefer descriptive names, such as self.branch_owner, which is less distracting.

Ok. Others use names like eric which are equally non-descriptive :-) I
was trying to make writing tests a bit lest tedious :-)

> * You should use XXX, rather than TODO, and file a bug per the XXX Policy.

In this case, it's not so much a bug per say as something rockstar is
picking up and running with for the next iteration of work. So do these
collaborative development efforts still need a bug report for what is in
effect a handover of work?

> * getUtility can be imported from zope.component directly, and indeed you mostly are, except in one place.

Agggh. IDE auto import bites me again.

> * Investigate usage of IMasterStore, rather than getUtility(IStoreSelector), which is deprecated-ish

Will do. Thanks for the tip. I was basing my work on what I saw had been
done before.

> * You're adding BeautifulSoup and soupmatchers to setup.py and versions.cfg, I'd suggest that get split out into an earlier branch so you can be certain they are available before landing this one.

Hmmm. rockstar did this in his branch and mine is based on his. But
because he had trouble getting his through ec2, I merged from his
working copy to bootstrap my work. I didn't actually add those packages.
So perhaps there's a merge issue biting us?

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

Adding back comments from old incorrect merge proposal

lifeless wrote:

When you have a lower layer branch, use the 'dependent branch' feature
in LP's merge proposal facility.

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

I set the merge proposal's Prerequisite Branch to
lp:~rockstar/launchpad/merge-queues-model

This was the only option available when creating the merge proposal. Is
this what you mean by the 'dependent branch' facility? If so, I think I
did what I was supposed to do in this regard? Or not?

On 02/11/10 15:01, Robert Collins wrote:
> When you have a lower layer branch, use the 'dependent branch' feature
> in LP's merge proposal facility.
>
> _Rob

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

Hi Steve

I've done the requested fixes. However, I also looked again at the diff
as shown by the merge proposal and it seems there's a bit of a mix up
there. When I created the mp I included rockstar's branch as a
prerequisite to mine, but the diff contains some of his stuff eg
test_branchmergequeue. This is why the diff is so large. The other code
is also the culprit for the "import with_statement" stuff. So I would
love to find out what happened there.

On 02/11/10 13:22, Steve Kowalik wrote:
> Review: Needs Fixing code*
> Hi Ian,
>
> Firstly, thanks for this awesome work! I've been looking forward to Merge Queues for a while.
>
> I have some comments below:
>
> * 1,500 lines? Why!? I'd suggest in future you look at splitting up work into two separate branches for plumbing and browser code, for example.
> * import with_statement isn't needed any more, we're on Python 2.6!
> * I'd suggest you run the import formatter over your branch.
> * Why use Star Wars names in the tests, I'd prefer descriptive names, such as self.branch_owner, which is less distracting.
> * You should use XXX, rather than TODO, and file a bug per the XXX Policy.
> * getUtility can be imported from zope.component directly, and indeed you mostly are, except in one place.
> * Investigate usage of IMasterStore, rather than getUtility(IStoreSelector), which is deprecated-ish
> * You're adding BeautifulSoup and soupmatchers to setup.py and versions.cfg, I'd suggest that get split out into an earlier branch so you can be certain they are available before landing this one.

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

Ok. Got to the bottom of it. All the confusion in the above comments is because I chose the wrong pre-requisite branch. This mp fixes that. The diff should now be correct.

Revision history for this message
Steve Kowalik (stevenk) wrote :

Hi Ian,

You've lost some of the earlier changes for some reason -- the importing getUtility from the wrong place, and not using IMasterStore.

review: Needs Fixing (code*)
Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Steve

I fixed it again locally and pushed the changes. The diff now reflects
the correct code.

Thanks.

On 03/11/10 16:49, Steve Kowalik wrote:
> Review: Needs Fixing code*
> Hi Ian,
>
> You've lost some of the earlier changes for some reason -- the importing getUtility from the wrong place, and not using IMasterStore.

Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code*)
Revision history for this message
Tim Penhey (thumper) wrote :

Steve's review looks good.

review: Approve (mentor)
Revision history for this message
Paul Hummer (rockstar) wrote :

This looks fine, although a chat I had with jml has me questioning the usefulness of these pages in general. Maybe we can devise a nice javascript-y way of doing it in the future.

review: Approve (ui)

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-10-29 14:36:59 +0000
3+++ lib/lp/code/browser/branchlisting.py 2010-11-03 08:32:56 +0000
4@@ -94,6 +94,7 @@
5 PersonActiveReviewsView,
6 PersonProductActiveReviewsView,
7 )
8+from lp.code.browser.branchmergequeuelisting import HasMergeQueuesMenuMixin
9 from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin
10 from lp.code.browser.summary import BranchCountSummaryView
11 from lp.code.enums import (
12@@ -849,18 +850,19 @@
13 .scanned())
14
15
16-class PersonBranchesMenu(ApplicationMenu):
17+class PersonBranchesMenu(ApplicationMenu, HasMergeQueuesMenuMixin):
18
19 usedfor = IPerson
20 facet = 'branches'
21 links = ['registered', 'owned', 'subscribed', 'addbranch',
22- 'active_reviews']
23+ 'active_reviews', 'mergequeues']
24 extra_attributes = [
25 'active_review_count',
26 'owned_branch_count',
27 'registered_branch_count',
28 'show_summary',
29 'subscribed_branch_count',
30+ 'mergequeue_count',
31 ]
32
33 def _getCountCollection(self):
34
35=== added file 'lib/lp/code/browser/branchmergequeuelisting.py'
36--- lib/lp/code/browser/branchmergequeuelisting.py 1970-01-01 00:00:00 +0000
37+++ lib/lp/code/browser/branchmergequeuelisting.py 2010-11-03 08:32:56 +0000
38@@ -0,0 +1,105 @@
39+# Copyright 2010 Canonical Ltd. This software is licensed under the
40+# GNU Affero General Public License version 3 (see the file LICENSE).
41+
42+"""Base class view for merge queue listings."""
43+
44+__metaclass__ = type
45+
46+__all__ = [
47+ 'MergeQueueListingView',
48+ 'HasMergeQueuesMenuMixin',
49+ 'PersonMergeQueueListingView',
50+ ]
51+
52+from zope.component import getUtility
53+
54+from canonical.launchpad.browser.feeds import FeedsMixin
55+from canonical.launchpad.webapp import (
56+ LaunchpadView,
57+ Link,
58+ )
59+from lp.code.interfaces.branchmergequeuecollection import (
60+ IAllBranchMergeQueues,
61+ )
62+from lp.services.browser_helpers import get_plural_text
63+from lp.services.propertycache import cachedproperty
64+
65+
66+class HasMergeQueuesMenuMixin:
67+ """A context menus mixin for objects that can own merge queues."""
68+
69+ def _getCollection(self):
70+ return getUtility(IAllBranchMergeQueues).visibleByUser(self.user)
71+
72+ @property
73+ def person(self):
74+ """The `IPerson` for the context of the view.
75+
76+ In simple cases this is the context itself, but in others, like the
77+ PersonProduct, it is an attribute of the context.
78+ """
79+ return self.context
80+
81+ def mergequeues(self):
82+ return Link(
83+ '+merge-queues',
84+ get_plural_text(
85+ self.mergequeue_count,
86+ 'merge queue', 'merge queues'), site='code')
87+
88+ @cachedproperty
89+ def mergequeue_count(self):
90+ return self._getCollection().ownedBy(self.person).count()
91+
92+
93+class MergeQueueListingView(LaunchpadView, FeedsMixin):
94+
95+ # No feeds initially
96+ feed_types = ()
97+
98+ branch_enabled = True
99+ owner_enabled = True
100+
101+ label_template = 'Merge Queues for %(displayname)s'
102+
103+ @property
104+ def label(self):
105+ return self.label_template % {
106+ 'displayname': self.context.displayname,
107+ 'title': getattr(self.context, 'title', 'no-title')}
108+
109+ # Provide a default page_title for distros and other things without
110+ # breadcrumbs..
111+ page_title = label
112+
113+ def _getCollection(self):
114+ """Override this to say what queues will be in the listing."""
115+ raise NotImplementedError(self._getCollection)
116+
117+ def getVisibleQueuesForUser(self):
118+ """Branch merge queues that are visible by the logged in user."""
119+ collection = self._getCollection().visibleByUser(self.user)
120+ return collection.getMergeQueues()
121+
122+ @cachedproperty
123+ def mergequeues(self):
124+ return self.getVisibleQueuesForUser()
125+
126+ @cachedproperty
127+ def mergequeue_count(self):
128+ """Return the number of merge queues that will be returned."""
129+ return self._getCollection().visibleByUser(self.user).count()
130+
131+ @property
132+ def no_merge_queue_message(self):
133+ """Shown when there is no table to show."""
134+ return "%s has no merge queues." % self.context.displayname
135+
136+
137+class PersonMergeQueueListingView(MergeQueueListingView):
138+
139+ label_template = 'Merge Queues owned by %(displayname)s'
140+ owner_enabled = False
141+
142+ def _getCollection(self):
143+ return getUtility(IAllBranchMergeQueues).ownedBy(self.context)
144
145=== modified file 'lib/lp/code/browser/configure.zcml'
146--- lib/lp/code/browser/configure.zcml 2010-11-03 08:32:55 +0000
147+++ lib/lp/code/browser/configure.zcml 2010-11-03 08:32:56 +0000
148@@ -1318,6 +1318,24 @@
149 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
150 factory="canonical.launchpad.webapp.breadcrumb.NameBreadcrumb"
151 permission="zope.Public"/>
152+
153+ <browser:page
154+ for="lp.registry.interfaces.person.IPerson"
155+ layer="lp.code.publisher.CodeLayer"
156+ class="lp.code.browser.branchmergequeuelisting.PersonMergeQueueListingView"
157+ permission="zope.Public"
158+ facet="branches"
159+ name="+merge-queues"
160+ template="../templates/branchmergequeue-listing.pt"/>
161+
162+ <browser:page
163+ for="*"
164+ layer="lp.code.publisher.CodeLayer"
165+ name="+bmq-macros"
166+ permission="zope.Public"
167+ template="../templates/branchmergequeue-macros.pt"/>
168+
169+
170 </facet>
171
172 <browser:url
173
174=== added file 'lib/lp/code/browser/tests/test_branchmergequeuelisting.py'
175--- lib/lp/code/browser/tests/test_branchmergequeuelisting.py 1970-01-01 00:00:00 +0000
176+++ lib/lp/code/browser/tests/test_branchmergequeuelisting.py 2010-11-03 08:32:56 +0000
177@@ -0,0 +1,227 @@
178+# Copyright 2010 Canonical Ltd. This software is licensed under the
179+# GNU Affero General Public License version 3 (see the file LICENSE).
180+
181+"""Tests for branch listing."""
182+
183+__metaclass__ = type
184+
185+import re
186+
187+from mechanize import LinkNotFoundError
188+import soupmatchers
189+from zope.security.proxy import removeSecurityProxy
190+
191+from canonical.launchpad.testing.pages import (
192+ extract_link_from_tag,
193+ extract_text,
194+ find_tag_by_id,
195+ )
196+from canonical.launchpad.webapp import canonical_url
197+from canonical.testing.layers import DatabaseFunctionalLayer
198+from lp.services.features.model import (
199+ FeatureFlag,
200+ getFeatureStore,
201+ )
202+from lp.testing import (
203+ BrowserTestCase,
204+ login_person,
205+ person_logged_in,
206+ TestCaseWithFactory,
207+ )
208+from lp.testing.views import create_initialized_view
209+
210+
211+class MergeQueuesTestMixin:
212+
213+ def setUp(self):
214+ self.branch_owner = self.factory.makePerson(name='eric')
215+
216+ def enable_queue_flag(self):
217+ getFeatureStore().add(FeatureFlag(
218+ scope=u'default', flag=u'code.branchmergequeue',
219+ value=u'on', priority=1))
220+
221+ def _makeMergeQueues(self, nr_queues=3, nr_with_private_branches=0):
222+ # We create nr_queues merge queues in total, and the first
223+ # nr_with_private_branches of them will have at least one private
224+ # branch in the queue.
225+ with person_logged_in(self.branch_owner):
226+ mergequeues = [
227+ self.factory.makeBranchMergeQueue(
228+ owner=self.branch_owner, branches=self._makeBranches())
229+ for i in range(nr_queues-nr_with_private_branches)]
230+ mergequeues_with_private_branches = [
231+ self.factory.makeBranchMergeQueue(
232+ owner=self.branch_owner,
233+ branches=self._makeBranches(nr_private=1))
234+ for i in range(nr_with_private_branches)]
235+
236+ return mergequeues, mergequeues_with_private_branches
237+
238+ def _makeBranches(self, nr_public=3, nr_private=0):
239+ branches = [
240+ self.factory.makeProductBranch(owner=self.branch_owner)
241+ for i in range(nr_public)]
242+
243+ private_branches = [
244+ self.factory.makeProductBranch(
245+ owner=self.branch_owner, private=True)
246+ for i in range(nr_private)]
247+
248+ branches.extend(private_branches)
249+ return branches
250+
251+
252+class TestPersonMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):
253+
254+ layer = DatabaseFunctionalLayer
255+
256+ def setUp(self):
257+ TestCaseWithFactory.setUp(self)
258+ MergeQueuesTestMixin.setUp(self)
259+ self.user = self.factory.makePerson()
260+
261+ def test_mergequeues_with_all_public_branches(self):
262+ # Anyone can see mergequeues containing all public branches.
263+ mq, mq_with_private = self._makeMergeQueues()
264+ login_person(self.user)
265+ view = create_initialized_view(
266+ self.branch_owner, name="+merge-queues", rootsite='code')
267+ self.assertEqual(set(mq), set(view.mergequeues))
268+
269+ def test_mergequeues_with_a_private_branch_for_owner(self):
270+ # Only users with access to private branches can see any queues
271+ # containing such branches.
272+ mq, mq_with_private = (
273+ self._makeMergeQueues(nr_with_private_branches=1))
274+ login_person(self.branch_owner)
275+ view = create_initialized_view(
276+ self.branch_owner, name="+merge-queues", rootsite='code')
277+ mq.extend(mq_with_private)
278+ self.assertEqual(set(mq), set(view.mergequeues))
279+
280+ def test_mergequeues_with_a_private_branch_for_other_user(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.user)
286+ view = create_initialized_view(
287+ self.branch_owner, name="+merge-queues", rootsite='code')
288+ self.assertEqual(set(mq), set(view.mergequeues))
289+
290+
291+class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):
292+ """Tests for the person code homepage.
293+
294+ This is the default page shown for a person on the code subdomain.
295+ """
296+
297+ layer = DatabaseFunctionalLayer
298+
299+ def setUp(self):
300+ BrowserTestCase.setUp(self)
301+ MergeQueuesTestMixin.setUp(self)
302+ self._makeMergeQueues()
303+
304+ def test_merge_queue_menu_link_without_feature_flag(self):
305+ login_person(self.branch_owner)
306+ browser = self.getUserBrowser(
307+ canonical_url(self.branch_owner, rootsite='code'),
308+ self.branch_owner)
309+ self.assertRaises(
310+ LinkNotFoundError,
311+ browser.getLink,
312+ url='+merge-queues')
313+
314+ def test_merge_queue_menu_link(self):
315+ self.enable_queue_flag()
316+ login_person(self.branch_owner)
317+ browser = self.getUserBrowser(
318+ canonical_url(self.branch_owner, rootsite='code'),
319+ self.branch_owner)
320+ browser.getLink(url='+merge-queues').click()
321+ self.assertEqual(
322+ 'http://code.launchpad.dev/~eric/+merge-queues',
323+ browser.url)
324+
325+
326+class TestPersonMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
327+ """Tests for the person merge queue list page."""
328+
329+ layer = DatabaseFunctionalLayer
330+
331+ def setUp(self):
332+ BrowserTestCase.setUp(self)
333+ MergeQueuesTestMixin.setUp(self)
334+ mq, mq_with_private = self._makeMergeQueues()
335+ self.merge_queues = mq
336+ self.merge_queues.extend(mq_with_private)
337+
338+ def test_merge_queue_list_contents_without_feature_flag(self):
339+ login_person(self.branch_owner)
340+ browser = self.getUserBrowser(
341+ canonical_url(self.branch_owner, rootsite='code',
342+ view_name='+merge-queues'), self.branch_owner)
343+ table = find_tag_by_id(browser.contents, 'mergequeuetable')
344+ self.assertIs(None, table)
345+ noqueue_matcher = soupmatchers.HTMLContains(
346+ soupmatchers.Tag(
347+ 'No merge queues', 'div',
348+ text=re.compile(
349+ '\w*No merge queues\w*')))
350+ self.assertThat(browser.contents, noqueue_matcher)
351+
352+ def test_merge_queue_list_contents(self):
353+ self.enable_queue_flag()
354+ login_person(self.branch_owner)
355+ browser = self.getUserBrowser(
356+ canonical_url(self.branch_owner, rootsite='code',
357+ view_name='+merge-queues'), self.branch_owner)
358+
359+ table = find_tag_by_id(browser.contents, 'mergequeuetable')
360+
361+ merge_queue_info = {}
362+ for row in table.tbody.fetch('tr'):
363+ cells = row('td')
364+ row_info = {}
365+ queue_name = extract_text(cells[0])
366+ if not queue_name.startswith('queue'):
367+ continue
368+ qlink = extract_link_from_tag(cells[0].find('a'))
369+ row_info['queue_link'] = qlink
370+ queue_size = extract_text(cells[1])
371+ row_info['queue_size'] = queue_size
372+ queue_branches = cells[2]('a')
373+ branch_links = set()
374+ for branch_tag in queue_branches:
375+ branch_links.add(extract_link_from_tag(branch_tag))
376+ row_info['branch_links'] = branch_links
377+ merge_queue_info[queue_name] = row_info
378+
379+ expected_queue_names = [queue.name for queue in self.merge_queues]
380+ self.assertEqual(
381+ set(expected_queue_names), set(merge_queue_info.keys()))
382+
383+ #TODO: when IBranchMergeQueue API is available remove '4'
384+ expected_queue_sizes = dict(
385+ [(queue.name, '4') for queue in self.merge_queues])
386+ observed_queue_sizes = dict(
387+ [(queue.name, merge_queue_info[queue.name]['queue_size'])
388+ for queue in self.merge_queues])
389+ self.assertEqual(
390+ expected_queue_sizes, observed_queue_sizes)
391+
392+ def branch_links(branches):
393+ return [canonical_url(removeSecurityProxy(branch),
394+ force_local_path=True)
395+ for branch in branches]
396+
397+ expected_queue_branches = dict(
398+ [(queue.name, set(branch_links(queue.branches)))
399+ for queue in self.merge_queues])
400+ observed_queue_branches = dict(
401+ [(queue.name, merge_queue_info[queue.name]['branch_links'])
402+ for queue in self.merge_queues])
403+ self.assertEqual(
404+ expected_queue_branches, observed_queue_branches)
405
406=== modified file 'lib/lp/code/configure.zcml'
407--- lib/lp/code/configure.zcml 2010-10-26 13:52:43 +0000
408+++ lib/lp/code/configure.zcml 2010-11-03 08:32:56 +0000
409@@ -94,6 +94,12 @@
410 <allow attributes="browserDefault
411 __call__"/>
412 </class>
413+ <class class="lp.code.model.branchmergequeuecollection.GenericBranchMergeQueueCollection">
414+ <allow interface="lp.code.interfaces.branchmergequeuecollection.IBranchMergeQueueCollection"/>
415+ </class>
416+ <class class="lp.code.model.branchmergequeuecollection.VisibleBranchMergeQueueCollection">
417+ <allow interface="lp.code.interfaces.branchmergequeuecollection.IBranchMergeQueueCollection"/>
418+ </class>
419 <class class="lp.code.model.branchcollection.GenericBranchCollection">
420 <allow interface="lp.code.interfaces.branchcollection.IBranchCollection"/>
421 </class>
422@@ -148,6 +154,11 @@
423 provides="lp.code.interfaces.revisioncache.IRevisionCache">
424 <allow interface="lp.code.interfaces.revisioncache.IRevisionCache"/>
425 </securedutility>
426+ <securedutility
427+ class="lp.code.model.branchmergequeuecollection.GenericBranchMergeQueueCollection"
428+ provides="lp.code.interfaces.branchmergequeuecollection.IAllBranchMergeQueues">
429+ <allow interface="lp.code.interfaces.branchmergequeuecollection.IAllBranchMergeQueues"/>
430+ </securedutility>
431 <adapter
432 for="lp.registry.interfaces.person.IPerson"
433 provides="lp.code.interfaces.revisioncache.IRevisionCache"
434
435=== modified file 'lib/lp/code/interfaces/branchmergequeue.py'
436--- lib/lp/code/interfaces/branchmergequeue.py 2010-10-20 15:32:38 +0000
437+++ lib/lp/code/interfaces/branchmergequeue.py 2010-11-03 08:32:56 +0000
438@@ -8,6 +8,7 @@
439 __all__ = [
440 'IBranchMergeQueue',
441 'IBranchMergeQueueSource',
442+ 'user_has_special_merge_queue_access',
443 ]
444
445 from lazr.restful.declarations import (
446@@ -21,6 +22,7 @@
447 CollectionField,
448 Reference,
449 )
450+from zope.component import getUtility
451 from zope.interface import Interface
452 from zope.schema import (
453 Datetime,
454@@ -30,6 +32,7 @@
455 )
456
457 from canonical.launchpad import _
458+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
459 from lp.services.fields import (
460 PersonChoice,
461 PublicPersonChoice,
462@@ -113,3 +116,14 @@
463 :param registrant: The registrant of the queue.
464 :param branches: A list of branches to add to the queue.
465 """
466+
467+
468+def user_has_special_merge_queue_access(user):
469+ """Admins and bazaar experts have special access.
470+
471+ :param user: A 'Person' or None.
472+ """
473+ if user is None:
474+ return False
475+ celebs = getUtility(ILaunchpadCelebrities)
476+ return user.inTeam(celebs.admin) or user.inTeam(celebs.bazaar_experts)
477
478=== added file 'lib/lp/code/interfaces/branchmergequeuecollection.py'
479--- lib/lp/code/interfaces/branchmergequeuecollection.py 1970-01-01 00:00:00 +0000
480+++ lib/lp/code/interfaces/branchmergequeuecollection.py 2010-11-03 08:32:56 +0000
481@@ -0,0 +1,64 @@
482+# Copyright 2010 Canonical Ltd. This software is licensed under the
483+# GNU Affero General Public License version 3 (see the file LICENSE).
484+
485+# pylint: disable-msg=E0211, E0213
486+
487+"""A collection of branche merge queues.
488+
489+See `IBranchMergeQueueCollection` for more details.
490+"""
491+
492+__metaclass__ = type
493+__all__ = [
494+ 'IAllBranchMergeQueues',
495+ 'IBranchMergeQueueCollection',
496+ 'InvalidFilter',
497+ ]
498+
499+from zope.interface import Interface
500+
501+
502+class InvalidFilter(Exception):
503+ """Raised when an `IBranchMergeQueueCollection` can't apply the filter."""
504+
505+
506+class IBranchMergeQueueCollection(Interface):
507+ """A collection of branch merge queues.
508+
509+ An `IBranchMergeQueueCollection` is an immutable collection of branch
510+ merge queues. It has two kinds of methods:
511+ filter methods and query methods.
512+
513+ Query methods get information about the contents of collection. See
514+ `IBranchMergeQueueCollection.count` and
515+ `IBranchMergeQueueCollection.getMergeQueues`.
516+
517+ Implementations of this interface are not 'content classes'. That is, they
518+ do not correspond to a particular row in the database.
519+
520+ This interface is intended for use within Launchpad, not to be exported as
521+ a public API.
522+ """
523+
524+ def count():
525+ """The number of merge queues in this collection."""
526+
527+ def getMergeQueues():
528+ """Return a result set of all merge queues in this collection.
529+
530+ The returned result set will also join across the specified tables as
531+ defined by the arguments to this function. These extra tables are
532+ joined specificly to allow the caller to sort on values not in the
533+ Branch table itself.
534+ """
535+
536+ def ownedBy(person):
537+ """Restrict the collection to queues owned by 'person'."""
538+
539+ def visibleByUser(person):
540+ """Restrict the collection to queues that 'person' is allowed to see.
541+ """
542+
543+
544+class IAllBranchMergeQueues(IBranchMergeQueueCollection):
545+ """An `IBranchMergeQueueCollection` of all branch merge queues."""
546
547=== modified file 'lib/lp/code/model/branchmergequeue.py'
548--- lib/lp/code/model/branchmergequeue.py 2010-11-03 08:32:55 +0000
549+++ lib/lp/code/model/branchmergequeue.py 2010-11-03 08:32:56 +0000
550@@ -7,7 +7,6 @@
551 __all__ = ['BranchMergeQueue']
552
553 import simplejson
554-
555 from storm.locals import (
556 Int,
557 Reference,
558@@ -68,7 +67,7 @@
559
560 @classmethod
561 def new(cls, name, owner, registrant, description=None,
562- configuration=None):
563+ configuration=None, branches=None):
564 """See `IBranchMergeQueueSource`."""
565 store = IMasterStore(BranchMergeQueue)
566
567@@ -81,6 +80,9 @@
568 queue.registrant = registrant
569 queue.description = description
570 queue.configuration = configuration
571+ if branches is not None:
572+ for branch in branches:
573+ branch.addToQueue(queue)
574
575 store.add(queue)
576 return queue
577
578=== added file 'lib/lp/code/model/branchmergequeuecollection.py'
579--- lib/lp/code/model/branchmergequeuecollection.py 1970-01-01 00:00:00 +0000
580+++ lib/lp/code/model/branchmergequeuecollection.py 2010-11-03 08:32:56 +0000
581@@ -0,0 +1,174 @@
582+# Copyright 2010 Canonical Ltd. This software is licensed under the
583+# GNU Affero General Public License version 3 (see the file LICENSE).
584+
585+"""Implementations of `IBranchMergeQueueCollection`."""
586+
587+__metaclass__ = type
588+__all__ = [
589+ 'GenericBranchCollection',
590+ ]
591+
592+from zope.interface import implements
593+
594+from canonical.launchpad.interfaces.lpstorm import IMasterStore
595+from lp.code.interfaces.branchmergequeue import (
596+ user_has_special_merge_queue_access,
597+ )
598+from lp.code.interfaces.branchmergequeuecollection import (
599+ IBranchMergeQueueCollection,
600+ InvalidFilter,
601+ )
602+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
603+from lp.code.model.branchmergequeue import BranchMergeQueue
604+
605+
606+class GenericBranchMergeQueueCollection:
607+ """See `IBranchMergeQueueCollection`."""
608+
609+ implements(IBranchMergeQueueCollection)
610+
611+ def __init__(self, store=None, merge_queue_filter_expressions=None,
612+ tables=None, exclude_from_search=None):
613+ """Construct a `GenericBranchMergeQueueCollection`.
614+
615+ :param store: The store to look in for merge queues. If not specified,
616+ use the default store.
617+ :param merge_queue_filter_expressions: A list of Storm expressions to
618+ restrict the queues in the collection. If unspecified, then
619+ there will be no restrictions on the result set. That is, all
620+ queues in the store will be in the collection.
621+ :param tables: A dict of Storm tables to the Join expression. If an
622+ expression in merge_queue_filter_expressions refers to a table,
623+ then that table *must* be in this list.
624+ """
625+ self._store = store
626+ if merge_queue_filter_expressions is None:
627+ merge_queue_filter_expressions = []
628+ self._merge_queue_filter_expressions = merge_queue_filter_expressions
629+ if tables is None:
630+ tables = {}
631+ self._tables = tables
632+ if exclude_from_search is None:
633+ exclude_from_search = []
634+ self._exclude_from_search = exclude_from_search
635+
636+ def count(self):
637+ return self._getCount()
638+
639+ def _getCount(self):
640+ """See `IBranchMergeQueueCollection`."""
641+ return self._getMergeQueues().count()
642+
643+ @property
644+ def store(self):
645+ if self._store is None:
646+ return IMasterStore(BranchMergeQueue)
647+ else:
648+ return self._store
649+
650+ def _filterBy(self, expressions, table=None, join=None,
651+ exclude_from_search=None):
652+ """Return a subset of this collection, filtered by 'expressions'."""
653+ tables = self._tables.copy()
654+ if table is not None:
655+ if join is None:
656+ raise InvalidFilter("Cannot specify a table without a join.")
657+ tables[table] = join
658+ if exclude_from_search is None:
659+ exclude_from_search = []
660+ if expressions is None:
661+ expressions = []
662+ return self.__class__(
663+ self.store,
664+ self._merge_queue_filter_expressions + expressions,
665+ tables,
666+ self._exclude_from_search + exclude_from_search)
667+
668+ def _getMergeQueueExpressions(self):
669+ """Return the where expressions for this collection."""
670+ return self._merge_queue_filter_expressions
671+
672+ def getMergeQueues(self):
673+ return list(self._getMergeQueues())
674+
675+ def _getMergeQueues(self):
676+ """See `IBranchMergeQueueCollection`."""
677+ tables = [BranchMergeQueue] + self._tables.values()
678+ expressions = self._getMergeQueueExpressions()
679+ return self.store.using(*tables).find(BranchMergeQueue, *expressions)
680+
681+ def ownedBy(self, person):
682+ """See `IBranchMergeQueueCollection`."""
683+ return self._filterBy([BranchMergeQueue.owner == person])
684+
685+ def visibleByUser(self, person):
686+ """See `IBranchMergeQueueCollection`."""
687+ if (person == LAUNCHPAD_SERVICES or
688+ user_has_special_merge_queue_access(person)):
689+ return self
690+ return VisibleBranchMergeQueueCollection(
691+ person,
692+ self._store, None,
693+ self._tables, self._exclude_from_search)
694+
695+
696+class VisibleBranchMergeQueueCollection(GenericBranchMergeQueueCollection):
697+ """A mergequeue collection which provides queues visible by a user."""
698+
699+ def __init__(self, person, store=None,
700+ merge_queue_filter_expressions=None, tables=None,
701+ exclude_from_search=None):
702+ super(VisibleBranchMergeQueueCollection, self).__init__(
703+ store=store,
704+ merge_queue_filter_expressions=merge_queue_filter_expressions,
705+ tables=tables,
706+ exclude_from_search=exclude_from_search,
707+ )
708+ self._user = person
709+
710+ def _filterBy(self, expressions, table=None, join=None,
711+ exclude_from_search=None):
712+ """Return a subset of this collection, filtered by 'expressions'."""
713+ tables = self._tables.copy()
714+ if table is not None:
715+ if join is None:
716+ raise InvalidFilter("Cannot specify a table without a join.")
717+ tables[table] = join
718+ if exclude_from_search is None:
719+ exclude_from_search = []
720+ if expressions is None:
721+ expressions = []
722+ return self.__class__(
723+ self._user,
724+ self.store,
725+ self._merge_queue_filter_expressions + expressions,
726+ tables,
727+ self._exclude_from_search + exclude_from_search)
728+
729+ def visibleByUser(self, person):
730+ """See `IBranchMergeQueueCollection`."""
731+ if person == self._user:
732+ return self
733+ raise InvalidFilter(
734+ "Cannot filter for merge queues visible by user %r, already "
735+ "filtering for %r" % (person, self._user))
736+
737+ def _getCount(self):
738+ """See `IBranchMergeQueueCollection`."""
739+ return len(self._getMergeQueues())
740+
741+ def _getMergeQueues(self):
742+ """Return the queues visible by self._user.
743+
744+ A queue is visible to a user if that user can see all the branches
745+ associated with the queue.
746+ """
747+
748+ def allBranchesVisible(user, branches):
749+ return len([branch for branch in branches
750+ if branch.visibleByUser(user)]) == branches.count()
751+
752+ queues = super(
753+ VisibleBranchMergeQueueCollection, self)._getMergeQueues()
754+ return [queue for queue in queues
755+ if allBranchesVisible(self._user, queue.branches)]
756
757=== added file 'lib/lp/code/model/tests/test_branchmergequeuecollection.py'
758--- lib/lp/code/model/tests/test_branchmergequeuecollection.py 1970-01-01 00:00:00 +0000
759+++ lib/lp/code/model/tests/test_branchmergequeuecollection.py 2010-11-03 08:32:56 +0000
760@@ -0,0 +1,201 @@
761+# Copyright 2009 Canonical Ltd. This software is licensed under the
762+# GNU Affero General Public License version 3 (see the file LICENSE).
763+
764+"""Tests for branch merge queue collections."""
765+
766+__metaclass__ = type
767+
768+from zope.component import getUtility
769+from zope.security.proxy import removeSecurityProxy
770+
771+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
772+from canonical.launchpad.interfaces.lpstorm import IMasterStore
773+from canonical.testing.layers import DatabaseFunctionalLayer
774+from lp.code.interfaces.branchmergequeuecollection import (
775+ IAllBranchMergeQueues,
776+ IBranchMergeQueueCollection,
777+ )
778+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
779+from lp.code.model.branchmergequeue import BranchMergeQueue
780+from lp.code.model.branchmergequeuecollection import (
781+ GenericBranchMergeQueueCollection,
782+ )
783+from lp.testing import TestCaseWithFactory
784+
785+
786+class TestGenericBranchMergeQueueCollection(TestCaseWithFactory):
787+
788+ layer = DatabaseFunctionalLayer
789+
790+ def setUp(self):
791+ TestCaseWithFactory.setUp(self)
792+ self.store = IMasterStore(BranchMergeQueue)
793+
794+ def test_provides_branchmergequeuecollection(self):
795+ # `GenericBranchMergeQueueCollection`
796+ # provides the `IBranchMergeQueueCollection` interface.
797+ self.assertProvides(
798+ GenericBranchMergeQueueCollection(self.store),
799+ IBranchMergeQueueCollection)
800+
801+ def test_getMergeQueues_no_filter_no_queues(self):
802+ # If no filter is specified, then the collection is of all branches
803+ # merge queues. By default, there are no branch merge queues.
804+ collection = GenericBranchMergeQueueCollection(self.store)
805+ self.assertEqual([], list(collection.getMergeQueues()))
806+
807+ def test_getMergeQueues_no_filter(self):
808+ # If no filter is specified, then the collection is of all branch
809+ # merge queues.
810+ collection = GenericBranchMergeQueueCollection(self.store)
811+ queue = self.factory.makeBranchMergeQueue()
812+ self.assertEqual([queue], list(collection.getMergeQueues()))
813+
814+ def test_count(self):
815+ # The 'count' property of a collection is the number of elements in
816+ # the collection.
817+ collection = GenericBranchMergeQueueCollection(self.store)
818+ self.assertEqual(0, collection.count())
819+ for i in range(3):
820+ self.factory.makeBranchMergeQueue()
821+ self.assertEqual(3, collection.count())
822+
823+ def test_count_respects_filter(self):
824+ # If a collection is a subset of all possible queues, then the count
825+ # will be the size of that subset. That is, 'count' respects any
826+ # filters that are applied.
827+ person = self.factory.makePerson()
828+ queue = self.factory.makeBranchMergeQueue(owner=person)
829+ queue2 = self.factory.makeAnyBranch()
830+ collection = GenericBranchMergeQueueCollection(
831+ self.store, [BranchMergeQueue.owner == person])
832+ self.assertEqual(1, collection.count())
833+
834+
835+class TestBranchMergeQueueCollectionFilters(TestCaseWithFactory):
836+
837+ layer = DatabaseFunctionalLayer
838+
839+ def setUp(self):
840+ TestCaseWithFactory.setUp(self)
841+ self.all_queues = getUtility(IAllBranchMergeQueues)
842+
843+ def test_count_respects_visibleByUser_filter(self):
844+ # IBranchMergeQueueCollection.count() returns the number of queues
845+ # that getMergeQueues() yields, even when the visibleByUser filter is
846+ # applied.
847+ branch = self.factory.makeAnyBranch(private=True)
848+ naked_branch = removeSecurityProxy(branch)
849+ queue = self.factory.makeBranchMergeQueue(branches=[naked_branch])
850+ branch2 = self.factory.makeAnyBranch(private=True)
851+ naked_branch2 = removeSecurityProxy(branch2)
852+ queue2 = self.factory.makeBranchMergeQueue(branches=[naked_branch2])
853+ collection = self.all_queues.visibleByUser(naked_branch.owner)
854+ self.assertEqual(1, len(collection.getMergeQueues()))
855+ self.assertEqual(1, collection.count())
856+
857+ def test_ownedBy(self):
858+ # 'ownedBy' returns a new collection restricted to queues owned by
859+ # the given person.
860+ queue = self.factory.makeBranchMergeQueue()
861+ queue2 = self.factory.makeBranchMergeQueue()
862+ collection = self.all_queues.ownedBy(queue.owner)
863+ self.assertEqual([queue], collection.getMergeQueues())
864+
865+
866+class TestGenericBranchMergeQueueCollectionVisibleFilter(TestCaseWithFactory):
867+
868+ layer = DatabaseFunctionalLayer
869+
870+ def setUp(self):
871+ TestCaseWithFactory.setUp(self)
872+ public_branch = self.factory.makeAnyBranch(name='public')
873+ self.queue_with_public_branch = self.factory.makeBranchMergeQueue(
874+ branches=[removeSecurityProxy(public_branch)])
875+ private_branch1 = self.factory.makeAnyBranch(
876+ private=True, name='private1')
877+ naked_private_branch1 = removeSecurityProxy(private_branch1)
878+ self.private_branch1_owner = naked_private_branch1.owner
879+ self.queue1_with_private_branch = self.factory.makeBranchMergeQueue(
880+ branches=[naked_private_branch1])
881+ private_branch2 = self.factory.makeAnyBranch(
882+ private=True, name='private2')
883+ self.queue2_with_private_branch = self.factory.makeBranchMergeQueue(
884+ branches=[removeSecurityProxy(private_branch2)])
885+ self.all_queues = getUtility(IAllBranchMergeQueues)
886+
887+ def test_all_queues(self):
888+ # Without the visibleByUser filter, all queues are in the
889+ # collection.
890+ self.assertEqual(
891+ sorted([self.queue_with_public_branch,
892+ self.queue1_with_private_branch,
893+ self.queue2_with_private_branch]),
894+ sorted(self.all_queues.getMergeQueues()))
895+
896+ def test_anonymous_sees_only_public(self):
897+ # Anonymous users can see only queues with public branches.
898+ queues = self.all_queues.visibleByUser(None)
899+ self.assertEqual([self.queue_with_public_branch],
900+ list(queues.getMergeQueues()))
901+
902+ def test_random_person_sees_only_public(self):
903+ # Logged in users with no special permissions can see only queues with
904+ # public branches.
905+ person = self.factory.makePerson()
906+ queues = self.all_queues.visibleByUser(person)
907+ self.assertEqual([self.queue_with_public_branch],
908+ list(queues.getMergeQueues()))
909+
910+ def test_owner_sees_own_branches(self):
911+ # Users can always see the queues with branches that they own, as well
912+ # as queues with public branches.
913+ queues = self.all_queues.visibleByUser(self.private_branch1_owner)
914+ self.assertEqual(
915+ sorted([self.queue_with_public_branch,
916+ self.queue1_with_private_branch]),
917+ sorted(queues.getMergeQueues()))
918+
919+ def test_owner_member_sees_own_queues(self):
920+ # Members of teams that own queues can see queues owned by those
921+ # teams, as well as public branches.
922+ team_owner = self.factory.makePerson()
923+ team = self.factory.makeTeam(team_owner)
924+ private_branch = self.factory.makeAnyBranch(
925+ owner=team, private=True, name='team')
926+ queue_with_private_branch = self.factory.makeBranchMergeQueue(
927+ branches=[removeSecurityProxy(private_branch)])
928+ queues = self.all_queues.visibleByUser(team_owner)
929+ self.assertEqual(
930+ sorted([self.queue_with_public_branch,
931+ queue_with_private_branch]),
932+ sorted(queues.getMergeQueues()))
933+
934+ def test_launchpad_services_sees_all(self):
935+ # The LAUNCHPAD_SERVICES special user sees *everything*.
936+ queues = self.all_queues.visibleByUser(LAUNCHPAD_SERVICES)
937+ self.assertEqual(
938+ sorted(self.all_queues.getMergeQueues()),
939+ sorted(queues.getMergeQueues()))
940+
941+ def test_admins_see_all(self):
942+ # Launchpad administrators see *everything*.
943+ admin = self.factory.makePerson()
944+ admin_team = removeSecurityProxy(
945+ getUtility(ILaunchpadCelebrities).admin)
946+ admin_team.addMember(admin, admin_team.teamowner)
947+ queues = self.all_queues.visibleByUser(admin)
948+ self.assertEqual(
949+ sorted(self.all_queues.getMergeQueues()),
950+ sorted(queues.getMergeQueues()))
951+
952+ def test_bazaar_experts_see_all(self):
953+ # Members of the bazaar_experts team see *everything*.
954+ bzr_experts = removeSecurityProxy(
955+ getUtility(ILaunchpadCelebrities).bazaar_experts)
956+ expert = self.factory.makePerson()
957+ bzr_experts.addMember(expert, bzr_experts.teamowner)
958+ queues = self.all_queues.visibleByUser(expert)
959+ self.assertEqual(
960+ sorted(self.all_queues.getMergeQueues()),
961+ sorted(queues.getMergeQueues()))
962
963=== added file 'lib/lp/code/templates/branchmergequeue-listing.pt'
964--- lib/lp/code/templates/branchmergequeue-listing.pt 1970-01-01 00:00:00 +0000
965+++ lib/lp/code/templates/branchmergequeue-listing.pt 2010-11-03 08:32:56 +0000
966@@ -0,0 +1,68 @@
967+<html
968+ xmlns="http://www.w3.org/1999/xhtml"
969+ xmlns:tal="http://xml.zope.org/namespaces/tal"
970+ xmlns:metal="http://xml.zope.org/namespaces/metal"
971+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
972+ metal:use-macro="view/macro:page/main_only"
973+ i18n:domain="launchpad">
974+
975+<body>
976+
977+ <div metal:fill-slot="main">
978+
979+ <div tal:condition="not: features/code.branchmergequeue">
980+ <em>
981+ No merge queues
982+ </em>
983+ </div>
984+
985+ <div tal:condition="features/code.branchmergequeue">
986+
987+ <tal:has-queues condition="view/mergequeue_count">
988+
989+ <table id="mergequeuetable" class="listing sortable">
990+ <thead>
991+ <tr>
992+ <th colspan="2">Name</th>
993+ <th tal:condition="view/owner_enabled">Owner</th>
994+ <th>Queue Size</th>
995+ <th>Associated Branches</th>
996+ </tr>
997+ </thead>
998+ <tbody>
999+ <tal:mergequeues repeat="mergeQueue view/mergequeues">
1000+ <tr>
1001+ <td colspan="2">
1002+ <a tal:attributes="href mergeQueue/fmt:url"
1003+ tal:content="mergeQueue/name">Merge queue name</a>
1004+ </td>
1005+ <td tal:condition="view/owner_enabled">
1006+ <a tal:replace="structure mergeQueue/owner/fmt:link">
1007+ Owner
1008+ </a>
1009+ </td>
1010+ <td>4</td>
1011+ <td>
1012+ <metal:display-branches
1013+ use-macro="context/@@+bmq-macros/merge_queue_branches"/>
1014+ </td>
1015+ </tr>
1016+ </tal:mergequeues>
1017+ </tbody>
1018+ </table>
1019+
1020+ </tal:has-queues>
1021+
1022+ <em id="no-queues"
1023+ tal:condition="not: view/mergequeue_count"
1024+ tal:content="view/no_merge_queue_message">
1025+ No merge queues
1026+ </em>
1027+
1028+ </div>
1029+
1030+ </div>
1031+
1032+</body>
1033+</html>
1034+
1035
1036=== added file 'lib/lp/code/templates/branchmergequeue-macros.pt'
1037--- lib/lp/code/templates/branchmergequeue-macros.pt 1970-01-01 00:00:00 +0000
1038+++ lib/lp/code/templates/branchmergequeue-macros.pt 2010-11-03 08:32:56 +0000
1039@@ -0,0 +1,20 @@
1040+ <tal:root
1041+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1042+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1043+ omit-tag="">
1044+
1045+<metal:merge_queue_branches define-macro="merge_queue_branches">
1046+ <table class="listing">
1047+ <tbody>
1048+ <tal:mergequeue-branches repeat="branch mergeQueue/branches">
1049+ <tr>
1050+ <td>
1051+ <a tal:attributes="href branch/fmt:url"
1052+ tal:content="branch/name">Branch name</a>
1053+ </td>
1054+ </tr>
1055+ </tal:mergequeue-branches>
1056+ </tbody>
1057+ </table>
1058+</metal:merge_queue_branches>
1059+</tal:root>
1060\ No newline at end of file
1061
1062=== modified file 'lib/lp/code/templates/person-codesummary.pt'
1063--- lib/lp/code/templates/person-codesummary.pt 2010-10-15 01:48:05 +0000
1064+++ lib/lp/code/templates/person-codesummary.pt 2010-11-03 08:32:56 +0000
1065@@ -4,7 +4,8 @@
1066 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1067 id="portlet-person-codesummary"
1068 class="portlet"
1069- tal:define="menu context/menu:branches"
1070+ tal:define="menu context/menu:branches;
1071+ features request/features"
1072 tal:condition="menu/show_summary">
1073
1074 <table>
1075@@ -32,5 +33,11 @@
1076 tal:content="structure menu/active_reviews/render"
1077 />
1078 </tr>
1079+ <tr tal:condition="features/code.branchmergequeue" id="mergequeue-counts">
1080+ <td class="code-count" tal:content="menu/mergequeue_count">5</td>
1081+ <td tal:condition="menu"
1082+ tal:content="structure menu/mergequeues/render"
1083+ />
1084+ </tr>
1085 </table>
1086 </div>
1087
1088=== modified file 'lib/lp/testing/factory.py'
1089--- lib/lp/testing/factory.py 2010-11-03 08:32:55 +0000
1090+++ lib/lp/testing/factory.py 2010-11-03 08:32:56 +0000
1091@@ -37,7 +37,6 @@
1092 )
1093 import os
1094 from random import randint
1095-import simplejson
1096 from StringIO import StringIO
1097 from textwrap import dedent
1098 from threading import local
1099@@ -47,6 +46,7 @@
1100 from bzrlib.merge_directive import MergeDirective2
1101 from bzrlib.plugins.builder.recipe import BaseRecipeBranch
1102 import pytz
1103+import simplejson
1104 from twisted.python.util import mergeFunctionMetadata
1105 from zope.component import (
1106 ComponentLookupError,
1107@@ -1113,7 +1113,8 @@
1108 return namespace.createBranch(branch_type, name, creator)
1109
1110 def makeBranchMergeQueue(self, registrant=None, owner=None, name=None,
1111- description=None, configuration=None):
1112+ description=None, configuration=None,
1113+ branches=None):
1114 """Create a BranchMergeQueue."""
1115 if name is None:
1116 name = unicode(self.getUniqueString('queue'))
1117@@ -1128,7 +1129,7 @@
1118 self.getUniqueString('key'): self.getUniqueString('value')}))
1119
1120 queue = getUtility(IBranchMergeQueueSource).new(
1121- name, owner, registrant, description, configuration)
1122+ name, owner, registrant, description, configuration, branches)
1123 return queue
1124
1125 def enableDefaultStackingForProduct(self, product, branch=None):

Subscribers

People subscribed via source and target branches

to status/vote changes: