← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/person-mergequeue-listview into lp:launchpad

 

You have been requested to review the proposed merge of lp:~wallyworld/launchpad/person-mergequeue-listview into lp:launchpad.

= Summary =

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. 

= 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


-- 
https://code.launchpad.net/~wallyworld/launchpad/person-mergequeue-listview/+merge/39745
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/person-mergequeue-listview into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2010-09-30 00:37:25 +0000
+++ lib/lp/code/browser/branch.py	2010-11-01 13:09:42 +0000
@@ -296,7 +296,8 @@
     links = [
         'add_subscriber', 'browse_revisions', 'create_recipe', 'link_bug',
         'link_blueprint', 'register_merge', 'source', 'subscription',
-        'edit_status', 'edit_import', 'upgrade_branch', 'view_recipes']
+        'edit_status', 'edit_import', 'upgrade_branch', 'view_recipes',
+        'create_queue']
 
     @enabled_with_permission('launchpad.Edit')
     def edit_status(self):
@@ -388,6 +389,10 @@
         text = 'Create packaging recipe'
         return Link('+new-recipe', text, enabled=enabled, icon='add')
 
+    @enabled_with_permission('launchpad.Edit')
+    def create_queue(self):
+        return Link('+create-queue', 'Create a new queue', icon='add')
+
 
 class BranchMirrorMixin:
     """Provide mirror_location property.

=== modified file 'lib/lp/code/browser/branchlisting.py'
--- lib/lp/code/browser/branchlisting.py	2010-10-27 08:51:05 +0000
+++ lib/lp/code/browser/branchlisting.py	2010-11-01 13:09:42 +0000
@@ -94,6 +94,7 @@
     PersonActiveReviewsView,
     PersonProductActiveReviewsView,
     )
+from lp.code.browser.branchmergequeuelisting import HasMergeQueuesMenuMixin
 from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin
 from lp.code.browser.summary import BranchCountSummaryView
 from lp.code.enums import (
@@ -849,18 +850,19 @@
                 .scanned())
 
 
-class PersonBranchesMenu(ApplicationMenu):
+class PersonBranchesMenu(ApplicationMenu, HasMergeQueuesMenuMixin):
 
     usedfor = IPerson
     facet = 'branches'
     links = ['registered', 'owned', 'subscribed', 'addbranch',
-             'active_reviews']
+             'active_reviews', 'mergequeues']
     extra_attributes = [
         'active_review_count',
         'owned_branch_count',
         'registered_branch_count',
         'show_summary',
         'subscribed_branch_count',
+        'mergequeue_count',
         ]
 
     def _getCountCollection(self):

=== added file 'lib/lp/code/browser/branchmergequeue.py'
--- lib/lp/code/browser/branchmergequeue.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/branchmergequeue.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,79 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""SourcePackageRecipe views."""
+
+__metaclass__ = type
+
+__all__ = [
+    'BranchMergeQueueContextMenu',
+    'BranchMergeQueueView',
+    ]
+
+from lazr.restful.interface import copy_field
+from zope.component import getUtility
+from zope.interface import Interface
+
+from canonical.launchpad.webapp import (
+    action,
+    canonical_url,
+    ContextMenu,
+    LaunchpadFormView,
+    LaunchpadView,
+    )
+from lp.code.interfaces.branchmergequeue import (
+    IBranchMergeQueue,
+    IBranchMergeQueueSource,
+    )
+
+
+class BranchMergeQueueContextMenu(ContextMenu):
+    """Context menu for sourcepackage recipes."""
+
+    usedfor = IBranchMergeQueue
+
+    facet = 'branches'
+
+    links = ()
+
+
+class BranchMergeQueueView(LaunchpadView):
+    """Default view of a SourcePackageRecipe."""
+
+    @property
+    def page_title(self):
+        return "%(queue_name)s queue owned by %(name)s" % {
+            'name': self.context.owner.displayname,
+            'queue_name': self.context.name}
+
+    label = page_title
+
+
+class BranchMergeQueueAddView(LaunchpadFormView):
+
+    title = label = 'Create a new branch merge queue'
+
+    class schema(Interface):
+        name = copy_field(IBranchMergeQueue['name'], readonly=False)
+        owner = copy_field(IBranchMergeQueue['owner'], readonly=False)
+        description = copy_field(IBranchMergeQueue['description'],
+            readonly=False)
+
+    def initialize(self):
+        super(BranchMergeQueueAddView, self).initialize()
+
+    @property
+    def initial_values(self):
+        return {}
+
+    @property
+    def cancel_url(self):
+        return canonical_url(self.context)
+
+    @action('Create Queue', name='create')
+    def request_action(self, action, data):
+        merge_queue = getUtility(IBranchMergeQueueSource).new(
+            data['name'], data['owner'], self.user, data['description'])
+        self.context.addToQueue(merge_queue)
+
+        self.next_url = canonical_url(merge_queue)

=== added file 'lib/lp/code/browser/branchmergequeuelisting.py'
--- lib/lp/code/browser/branchmergequeuelisting.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/branchmergequeuelisting.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,105 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Base class view for merge queue listings."""
+
+__metaclass__ = type
+
+__all__ = [
+    'MergeQueueListingView',
+    'HasMergeQueuesMenuMixin',
+    'PersonMergeQueueListingView',
+    ]
+
+from zope.component._api import getUtility
+
+from canonical.launchpad.browser.feeds import FeedsMixin
+from canonical.launchpad.webapp import (
+    LaunchpadView,
+    Link,
+    )
+from lp.code.interfaces.branchmergequeuecollection import (
+    IAllBranchMergeQueues,
+    )
+from lp.services.browser_helpers import get_plural_text
+from lp.services.propertycache import cachedproperty
+
+
+class HasMergeQueuesMenuMixin:
+    """A context menus mixin for objects that can own merge queues."""
+
+    def _getCollection(self):
+        return getUtility(IAllBranchMergeQueues).visibleByUser(self.user)
+
+    @property
+    def person(self):
+        """The `IPerson` for the context of the view.
+
+        In simple cases this is the context itself, but in others, like the
+        PersonProduct, it is an attribute of the context.
+        """
+        return self.context
+
+    def mergequeues(self):
+        return Link(
+            '+merge-queues',
+            get_plural_text(
+                self.mergequeue_count,
+                'merge queue', 'merge queues'), site='code')
+
+    @cachedproperty
+    def mergequeue_count(self):
+        return self._getCollection().ownedBy(self.person).count()
+
+
+class MergeQueueListingView(LaunchpadView, FeedsMixin):
+
+    # No feeds initially
+    feed_types = ()
+
+    branch_enabled = True
+    owner_enabled = True
+
+    label_template = 'Merge Queues for %(displayname)s'
+
+    @property
+    def label(self):
+        return self.label_template % {
+            'displayname': self.context.displayname,
+            'title': getattr(self.context, 'title', 'no-title')}
+
+    # Provide a default page_title for distros and other things without
+    # breadcrumbs..
+    page_title = label
+
+    def _getCollection(self):
+        """Override this to say what queues will be in the listing."""
+        raise NotImplementedError(self._getCollection)
+
+    def getVisibleQueuesForUser(self):
+        """Branch merge queues that are visible by the logged in user."""
+        collection = self._getCollection().visibleByUser(self.user)
+        return collection.getMergeQueues()
+
+    @cachedproperty
+    def mergequeues(self):
+        return self.getVisibleQueuesForUser()
+
+    @cachedproperty
+    def mergequeue_count(self):
+        """Return the number of merge queues that will be returned."""
+        return self._getCollection().visibleByUser(self.user).count()
+
+    @property
+    def no_merge_queue_message(self):
+        """Shown when there is no table to show."""
+        return "%s has no merge queues." % self.context.displayname
+
+
+class PersonMergeQueueListingView(MergeQueueListingView):
+
+    label_template = 'Merge Queues owned by %(displayname)s'
+    owner_enabled = False
+
+    def _getCollection(self):
+        return getUtility(IAllBranchMergeQueues).ownedBy(self.context)

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2010-10-21 02:53:17 +0000
+++ lib/lp/code/browser/configure.zcml	2010-11-01 13:09:42 +0000
@@ -1318,6 +1318,24 @@
             for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
             factory="canonical.launchpad.webapp.breadcrumb.NameBreadcrumb"
             permission="zope.Public"/>
+
+        <browser:page
+            for="lp.registry.interfaces.person.IPerson"
+            layer="lp.code.publisher.CodeLayer"
+            class="lp.code.browser.branchmergequeuelisting.PersonMergeQueueListingView"
+            permission="zope.Public"
+            facet="branches"
+            name="+merge-queues"
+            template="../templates/branchmergequeue-listing.pt"/>
+
+        <browser:page
+            for="*"
+            layer="lp.code.publisher.CodeLayer"
+            name="+bmq-macros"
+            permission="zope.Public"
+            template="../templates/branchmergequeue-macros.pt"/>
+
+
     </facet>
 
     <browser:url
@@ -1326,4 +1344,30 @@
         path_expression="string:+merge-queues/${name}"
         rootsite="code" />
 
+    <browser:menus
+        classes="BranchMergeQueueContextMenu"
+        module="lp.code.browser.branchmergequeue"/>
+
+    <facet facet="branches">
+        <browser:defaultView
+            for="lp.code.interfaces.branchmergequeue.IBranchMergeQueue"
+            name="+index"
+            layer="lp.code.publisher.CodeLayer" />
+        <browser:page
+            for="lp.code.interfaces.branchmergequeue.IBranchMergeQueue"
+            layer="lp.code.publisher.CodeLayer"
+            class="lp.code.browser.branchmergequeue.BranchMergeQueueView"
+            name="+index"
+            template="../templates/branchmergequeue-index.pt"
+            permission="zope.Public" />
+        <browser:page
+            for="lp.code.interfaces.branch.IBranch"
+            layer="lp.code.publisher.CodeLayer"
+            class="lp.code.browser.branchmergequeue.BranchMergeQueueAddView"
+            name="+create-queue"
+            template="../../app/templates/generic-edit.pt"
+            permission="launchpad.Edit" />
+
+    </facet>
+
 </configure>

=== added file 'lib/lp/code/browser/tests/test_branchmergequeue.py'
--- lib/lp/code/browser/tests/test_branchmergequeue.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/tests/test_branchmergequeue.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,127 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the branch merge queue view classes and templates."""
+
+from __future__ import with_statement
+
+__metaclass__ = type
+
+from mechanize import LinkNotFoundError
+import re
+
+import soupmatchers
+
+from canonical.launchpad.webapp import canonical_url
+from canonical.testing.layers import (
+    DatabaseFunctionalLayer,
+    )
+from lp.services.features.model import FeatureFlag, getFeatureStore
+from lp.testing import (
+    ANONYMOUS,
+    BrowserTestCase,
+    person_logged_in,
+    )
+
+
+class TestBranchMergeQueue(BrowserTestCase):
+    """Test the Branch Merge Queue index page."""
+
+    layer = DatabaseFunctionalLayer
+
+    def enable_queue_flag(self):
+        getFeatureStore().add(FeatureFlag(
+            scope=u'default', flag=u'code.branchmergequeue',
+            value=u'on', priority=1))
+
+    def test_index(self):
+        """Test the index page of a branch merge queue."""
+        with person_logged_in(ANONYMOUS):
+            queue = self.factory.makeBranchMergeQueue()
+            queue_owner = queue.owner.displayname
+            queue_registrant = queue.registrant.displayname
+            queue_description = queue.description
+            queue_url = canonical_url(queue)
+
+            branch = self.factory.makeBranch()
+            branch_name = branch.bzr_identity
+            with person_logged_in(branch.owner):
+                branch.addToQueue(queue)
+
+        # XXX: rockstar - bug #666979 - The text argument should really ignore
+        # whitespace, but it currently doesn't.  Now I have two problems.
+        queue_matcher = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Page title', 'h1',
+                text=re.compile('\w*%s queue owned by %s\w*' % (
+                    queue.name, queue.owner.displayname))),
+            soupmatchers.Tag(
+                'Description Label', 'dt',
+                text=re.compile('\w*Description\w*')),
+            soupmatchers.Tag(
+                'Description Value', 'dd',
+                text=re.compile('\w*%s\w*' % queue.description)),
+            soupmatchers.Tag(
+                'Branch link', 'a',
+                text=re.compile('\w*%s\w*' % branch.bzr_identity)))
+
+        browser = self.getUserBrowser(canonical_url(queue), user=queue.owner)
+
+        self.assertThat(browser.contents, queue_matcher)
+
+    def test_create(self):
+        """Test that branch merge queues can be created from a branch."""
+        self.enable_queue_flag()
+        with person_logged_in(ANONYMOUS):
+            rockstar = self.factory.makePerson(name='rockstar')
+            branch = self.factory.makeBranch(owner=rockstar)
+            self.factory.makeBranch(product=branch.product)
+            owner_name = branch.owner.name
+
+        browser = self.getUserBrowser(canonical_url(branch), user=rockstar)
+
+        # There shouldn't be a merge queue linked here.
+        noqueue_matcher = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'Not managed', 'div',
+                text=re.compile(
+                    '\w*This branch is not managed by a queue.\w*')))
+        self.assertThat(browser.contents, noqueue_matcher)
+
+        browser.getLink('Create a new queue').click()
+
+        browser.getControl('Name').value = 'libbob-queue'
+        browser.getControl('Description').value = (
+            'This is a queue for the libbob projects.')
+        browser.getControl('Create Queue').click()
+
+        self.assertEqual(
+            'http://code.launchpad.dev/~rockstar/+merge-queues/libbob-queue',
+            browser.url)
+
+    def test_create_unauthorized(self):
+        """Test that queues can't be created by unauthorized users."""
+        self.enable_queue_flag()
+        with person_logged_in(ANONYMOUS):
+            branch = self.factory.makeBranch()
+            self.factory.makeBranch(product=branch.product)
+
+        browser = self.getUserBrowser(canonical_url(branch))
+        self.assertRaises(
+            LinkNotFoundError,
+            browser.getLink,
+            'Create a new queue')
+
+    def test_create_featureflag(self):
+        """Test that the feature flag hides the "create" link."""
+        with person_logged_in(ANONYMOUS):
+            rockstar = self.factory.makePerson(name='rockstar')
+            branch = self.factory.makeBranch(owner=rockstar)
+            self.factory.makeBranch(product=branch.product)
+            owner_name = branch.owner.name
+
+        browser = self.getUserBrowser(canonical_url(branch), user=rockstar)
+        self.assertRaises(
+            LinkNotFoundError,
+            browser.getLink,
+            'Create a new queue')

=== added file 'lib/lp/code/browser/tests/test_branchmergequeuelisting.py'
--- lib/lp/code/browser/tests/test_branchmergequeuelisting.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/tests/test_branchmergequeuelisting.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,229 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for branch listing."""
+
+__metaclass__ = type
+
+import re
+
+from mechanize import LinkNotFoundError
+import soupmatchers
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.launchpad.testing.pages import (
+    extract_link_from_tag,
+    extract_text,
+    find_tag_by_id,
+    )
+from canonical.launchpad.webapp import canonical_url
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.services.features.model import (
+    FeatureFlag,
+    getFeatureStore,
+    )
+from lp.testing import (
+    BrowserTestCase,
+    login_person,
+    person_logged_in,
+    TestCaseWithFactory,
+    )
+from lp.testing.views import create_initialized_view
+
+
+class MergeQueuesTestMixin:
+
+    def setUp(self):
+        self.darth_vader = self.factory.makePerson(name='darthvader')
+        self.death_star = self.factory.makeProduct(name='deathstar')
+
+    def enable_queue_flag(self):
+        getFeatureStore().add(FeatureFlag(
+            scope=u'default', flag=u'code.branchmergequeue',
+            value=u'on', priority=1))
+
+    def _makeMergeQueues(self, nr_queues=3, nr_with_private_branches=0):
+        # We create nr_queues merge queues in total, and the first
+        # nr_with_private_branches of them will have at least one private
+        # branch in the queue.
+        with person_logged_in(self.darth_vader):
+            mergequeues = [
+                self.factory.makeBranchMergeQueue(
+                    owner=self.darth_vader, branches=self._makeBranches())
+                for i in range(nr_queues-nr_with_private_branches)]
+            mergequeues_with_private_branches = [
+                self.factory.makeBranchMergeQueue(
+                    owner=self.darth_vader,
+                    branches=self._makeBranches(nr_private=1))
+                for i in range(nr_with_private_branches)]
+
+            return mergequeues, mergequeues_with_private_branches
+
+    def _makeBranches(self, nr_public=3, nr_private=0):
+        branches = [
+            self.factory.makeProductBranch(
+                product=self.death_star, owner=self.darth_vader)
+            for i in range(nr_public)]
+
+        private_branches = [
+            self.factory.makeProductBranch(
+                product=self.death_star, owner=self.darth_vader, private=True)
+            for i in range(nr_private)]
+
+        branches.extend(private_branches)
+        return branches
+
+
+class TestPersonMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        TestCaseWithFactory.setUp(self)
+        MergeQueuesTestMixin.setUp(self)
+        self.user = self.factory.makePerson()
+
+    def test_mergequeues_with_all_public_branches(self):
+        # Anyone can see mergequeues containing all public branches.
+        mq, mq_with_private = self._makeMergeQueues()
+        login_person(self.user)
+        view = create_initialized_view(
+            self.darth_vader, name="+merge-queues", rootsite='code')
+        self.assertEqual(set(mq), set(view.mergequeues))
+
+    def test_mergequeues_with_a_private_branch_for_owner(self):
+        # Only users with access to private branches can see any queues
+        # containing such branches.
+        mq, mq_with_private = (
+            self._makeMergeQueues(nr_with_private_branches=1))
+        login_person(self.darth_vader)
+        view = create_initialized_view(
+            self.darth_vader, name="+merge-queues", rootsite='code')
+        mq.extend(mq_with_private)
+        self.assertEqual(set(mq), set(view.mergequeues))
+
+    def test_mergequeues_with_a_private_branch_for_other_user(self):
+        # Only users with access to private branches can see any queues
+        # containing such branches.
+        mq, mq_with_private = (
+            self._makeMergeQueues(nr_with_private_branches=1))
+        login_person(self.user)
+        view = create_initialized_view(
+            self.darth_vader, name="+merge-queues", rootsite='code')
+        self.assertEqual(set(mq), set(view.mergequeues))
+
+
+class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):
+    """Tests for the person code homepage.
+
+    This is the default page shown for a person on the code subdomain.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        BrowserTestCase.setUp(self)
+        MergeQueuesTestMixin.setUp(self)
+        self._makeMergeQueues()
+
+    def test_merge_queue_menu_link_without_feature_flag(self):
+        login_person(self.darth_vader)
+        browser = self.getUserBrowser(
+            canonical_url(self.darth_vader, rootsite='code'),
+            self.darth_vader)
+        self.assertRaises(
+            LinkNotFoundError,
+            browser.getLink,
+            url='+merge-queues')
+
+    def test_merge_queue_menu_link(self):
+        self.enable_queue_flag()
+        login_person(self.darth_vader)
+        browser = self.getUserBrowser(
+            canonical_url(self.darth_vader, rootsite='code'),
+            self.darth_vader)
+        browser.getLink(url='+merge-queues').click()
+        self.assertEqual(
+            'http://code.launchpad.dev/~darthvader/+merge-queues',
+            browser.url)
+
+
+class TestPersonMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
+    """Tests for the person merge queue list page."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        BrowserTestCase.setUp(self)
+        MergeQueuesTestMixin.setUp(self)
+        mq, mq_with_private = self._makeMergeQueues()
+        self.merge_queues = mq
+        self.merge_queues.extend(mq_with_private)
+
+    def test_merge_queue_list_contents_without_feature_flag(self):
+        login_person(self.darth_vader)
+        browser = self.getUserBrowser(
+            canonical_url(self.darth_vader, rootsite='code',
+                          view_name='+merge-queues'), self.darth_vader)
+        table = find_tag_by_id(browser.contents, 'mergequeuetable')
+        self.assertIs(None, table)
+        noqueue_matcher = soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                'No merge queues', 'div',
+                text=re.compile(
+                    '\w*No merge queues\w*')))
+        self.assertThat(browser.contents, noqueue_matcher)
+
+    def test_merge_queue_list_contents(self):
+        self.enable_queue_flag()
+        login_person(self.darth_vader)
+        browser = self.getUserBrowser(
+            canonical_url(self.darth_vader, rootsite='code',
+                          view_name='+merge-queues'), self.darth_vader)
+
+        table = find_tag_by_id(browser.contents, 'mergequeuetable')
+
+        merge_queue_info = {}
+        for row in table.tbody.fetch('tr'):
+            cells = row('td')
+            row_info = {}
+            queue_name = extract_text(cells[0])
+            if not queue_name.startswith('queue'):
+                continue
+            qlink = extract_link_from_tag(cells[0].find('a'))
+            row_info['queue_link'] = qlink
+            queue_size = extract_text(cells[1])
+            row_info['queue_size'] = queue_size
+            queue_branches = cells[2]('a')
+            branch_links = set()
+            for branch_tag in queue_branches:
+                branch_links.add(extract_link_from_tag(branch_tag))
+            row_info['branch_links'] = branch_links
+            merge_queue_info[queue_name] = row_info
+
+        expected_queue_names = [queue.name for queue in self.merge_queues]
+        self.assertEqual(
+            set(expected_queue_names), set(merge_queue_info.keys()))
+
+        #TODO: when IBranchMergeQueue API is available remove '4'
+        expected_queue_sizes = dict(
+            [(queue.name, '4') for queue in self.merge_queues])
+        observed_queue_sizes = dict(
+            [(queue.name, merge_queue_info[queue.name]['queue_size'])
+             for queue in self.merge_queues])
+        self.assertEqual(
+            expected_queue_sizes, observed_queue_sizes)
+
+        def branch_links(branches):
+            return [canonical_url(removeSecurityProxy(branch),
+                                  force_local_path=True)
+                    for branch in branches]
+
+        expected_queue_branches = dict(
+            [(queue.name, set(branch_links(queue.branches)))
+             for queue in self.merge_queues])
+        observed_queue_branches = dict(
+            [(queue.name, merge_queue_info[queue.name]['branch_links'])
+             for queue in self.merge_queues])
+        self.assertEqual(
+            expected_queue_branches, observed_queue_branches)

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-10-26 13:52:43 +0000
+++ lib/lp/code/configure.zcml	2010-11-01 13:09:42 +0000
@@ -94,6 +94,12 @@
     <allow attributes="browserDefault
                        __call__"/>
   </class>
+  <class class="lp.code.model.branchmergequeuecollection.GenericBranchMergeQueueCollection">
+    <allow interface="lp.code.interfaces.branchmergequeuecollection.IBranchMergeQueueCollection"/>
+  </class>
+  <class class="lp.code.model.branchmergequeuecollection.VisibleBranchMergeQueueCollection">
+    <allow interface="lp.code.interfaces.branchmergequeuecollection.IBranchMergeQueueCollection"/>
+  </class>
   <class class="lp.code.model.branchcollection.GenericBranchCollection">
     <allow interface="lp.code.interfaces.branchcollection.IBranchCollection"/>
   </class>
@@ -148,6 +154,11 @@
       provides="lp.code.interfaces.revisioncache.IRevisionCache">
     <allow interface="lp.code.interfaces.revisioncache.IRevisionCache"/>
   </securedutility>
+  <securedutility
+      class="lp.code.model.branchmergequeuecollection.GenericBranchMergeQueueCollection"
+      provides="lp.code.interfaces.branchmergequeuecollection.IAllBranchMergeQueues">
+    <allow interface="lp.code.interfaces.branchmergequeuecollection.IAllBranchMergeQueues"/>
+  </securedutility>
   <adapter
       for="lp.registry.interfaces.person.IPerson"
       provides="lp.code.interfaces.revisioncache.IRevisionCache"

=== modified file 'lib/lp/code/interfaces/branchmergequeue.py'
--- lib/lp/code/interfaces/branchmergequeue.py	2010-10-20 15:32:38 +0000
+++ lib/lp/code/interfaces/branchmergequeue.py	2010-11-01 13:09:42 +0000
@@ -8,6 +8,7 @@
 __all__ = [
     'IBranchMergeQueue',
     'IBranchMergeQueueSource',
+    'user_has_special_merge_queue_access',
     ]
 
 from lazr.restful.declarations import (
@@ -21,6 +22,7 @@
     CollectionField,
     Reference,
     )
+from zope.component._api import getUtility
 from zope.interface import Interface
 from zope.schema import (
     Datetime,
@@ -30,6 +32,7 @@
     )
 
 from canonical.launchpad import _
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from lp.services.fields import (
     PersonChoice,
     PublicPersonChoice,
@@ -113,3 +116,14 @@
         :param registrant: The registrant of the queue.
         :param branches: A list of branches to add to the queue.
         """
+
+
+def user_has_special_merge_queue_access(user):
+    """Admins and bazaar experts have special access.
+
+    :param user: A 'Person' or None.
+    """
+    if user is None:
+        return False
+    celebs = getUtility(ILaunchpadCelebrities)
+    return user.inTeam(celebs.admin) or user.inTeam(celebs.bazaar_experts)

=== added file 'lib/lp/code/interfaces/branchmergequeuecollection.py'
--- lib/lp/code/interfaces/branchmergequeuecollection.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/branchmergequeuecollection.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,64 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+# pylint: disable-msg=E0211, E0213
+
+"""A collection of branche merge queues.
+
+See `IBranchMergeQueueCollection` for more details.
+"""
+
+__metaclass__ = type
+__all__ = [
+    'IAllBranchMergeQueues',
+    'IBranchMergeQueueCollection',
+    'InvalidFilter',
+    ]
+
+from zope.interface import Interface
+
+
+class InvalidFilter(Exception):
+    """Raised when an `IBranchMergeQueueCollection` can't apply the filter."""
+
+
+class IBranchMergeQueueCollection(Interface):
+    """A collection of branch merge queues.
+
+    An `IBranchMergeQueueCollection` is an immutable collection of branch
+    merge queues. It has two kinds of methods:
+    filter methods and query methods.
+
+    Query methods get information about the contents of collection. See
+    `IBranchMergeQueueCollection.count` and
+    `IBranchMergeQueueCollection.getMergeQueues`.
+
+    Implementations of this interface are not 'content classes'. That is, they
+    do not correspond to a particular row in the database.
+
+    This interface is intended for use within Launchpad, not to be exported as
+    a public API.
+    """
+
+    def count():
+        """The number of merge queues in this collection."""
+
+    def getMergeQueues():
+        """Return a result set of all merge queues in this collection.
+
+        The returned result set will also join across the specified tables as
+        defined by the arguments to this function.  These extra tables are
+        joined specificly to allow the caller to sort on values not in the
+        Branch table itself.
+        """
+
+    def ownedBy(person):
+        """Restrict the collection to queues owned by 'person'."""
+
+    def visibleByUser(person):
+        """Restrict the collection to queues that 'person' is allowed to see.
+        """
+
+
+class IAllBranchMergeQueues(IBranchMergeQueueCollection):
+    """An `IBranchMergeQueueCollection` of all branch merge queues."""

=== modified file 'lib/lp/code/model/branchmergequeue.py'
--- lib/lp/code/model/branchmergequeue.py	2010-10-22 04:16:34 +0000
+++ lib/lp/code/model/branchmergequeue.py	2010-11-01 13:09:42 +0000
@@ -7,7 +7,6 @@
 __all__ = ['BranchMergeQueue']
 
 import simplejson
-
 from storm.locals import (
     Int,
     Reference,
@@ -68,16 +67,22 @@
 
     @classmethod
     def new(cls, name, owner, registrant, description=None,
-            configuration=None):
+            configuration=None, branches=None):
         """See `IBranchMergeQueueSource`."""
         store = IMasterStore(BranchMergeQueue)
 
+        if configuration is None:
+            configuration = unicode(simplejson.dumps({}))
+
         queue = cls()
         queue.name = name
         queue.owner = owner
         queue.registrant = registrant
         queue.description = description
         queue.configuration = configuration
+        if branches is not None:
+            for branch in branches:
+                branch.addToQueue(queue)
 
         store.add(queue)
         return queue

=== added file 'lib/lp/code/model/branchmergequeuecollection.py'
--- lib/lp/code/model/branchmergequeuecollection.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/branchmergequeuecollection.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,179 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Implementations of `IBranchMergeQueueCollection`."""
+
+__metaclass__ = type
+__all__ = [
+    'GenericBranchCollection',
+    ]
+
+from zope.component import getUtility
+from zope.interface import implements
+
+from canonical.launchpad.webapp.interfaces import (
+    DEFAULT_FLAVOR,
+    IStoreSelector,
+    MAIN_STORE,
+    )
+from lp.code.interfaces.branchmergequeue import (
+    user_has_special_merge_queue_access,
+    )
+from lp.code.interfaces.branchmergequeuecollection import (
+    IBranchMergeQueueCollection,
+    InvalidFilter,
+    )
+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
+from lp.code.model.branchmergequeue import BranchMergeQueue
+
+
+class GenericBranchMergeQueueCollection:
+    """See `IBranchMergeQueueCollection`."""
+
+    implements(IBranchMergeQueueCollection)
+
+    def __init__(self, store=None, merge_queue_filter_expressions=None,
+                 tables=None, exclude_from_search=None):
+        """Construct a `GenericBranchMergeQueueCollection`.
+
+        :param store: The store to look in for merge queues. If not specified,
+            use the default store.
+        :param merge_queue_filter_expressions: A list of Storm expressions to
+            restrict the queues in the collection. If unspecified, then
+            there will be no restrictions on the result set. That is, all
+            queues in the store will be in the collection.
+        :param tables: A dict of Storm tables to the Join expression.  If an
+            expression in merge_queue_filter_expressions refers to a table,
+            then that table *must* be in this list.
+        """
+        self._store = store
+        if merge_queue_filter_expressions is None:
+            merge_queue_filter_expressions = []
+        self._merge_queue_filter_expressions = merge_queue_filter_expressions
+        if tables is None:
+            tables = {}
+        self._tables = tables
+        if exclude_from_search is None:
+            exclude_from_search = []
+        self._exclude_from_search = exclude_from_search
+
+    def count(self):
+        return self._getCount()
+
+    def _getCount(self):
+        """See `IBranchMergeQueueCollection`."""
+        return self._getMergeQueues().count()
+
+    @property
+    def store(self):
+        if self._store is None:
+            return getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        else:
+            return self._store
+
+    def _filterBy(self, expressions, table=None, join=None,
+                  exclude_from_search=None):
+        """Return a subset of this collection, filtered by 'expressions'."""
+        tables = self._tables.copy()
+        if table is not None:
+            if join is None:
+                raise InvalidFilter("Cannot specify a table without a join.")
+            tables[table] = join
+        if exclude_from_search is None:
+            exclude_from_search = []
+        if expressions is None:
+            expressions = []
+        return self.__class__(
+            self.store,
+            self._merge_queue_filter_expressions + expressions,
+            tables,
+            self._exclude_from_search + exclude_from_search)
+
+    def _getMergeQueueExpressions(self):
+        """Return the where expressions for this collection."""
+        return self._merge_queue_filter_expressions
+
+    def getMergeQueues(self):
+        return list(self._getMergeQueues())
+
+    def _getMergeQueues(self):
+        """See `IBranchMergeQueueCollection`."""
+        tables = [BranchMergeQueue] + self._tables.values()
+        expressions = self._getMergeQueueExpressions()
+        return self.store.using(*tables).find(BranchMergeQueue, *expressions)
+
+    def ownedBy(self, person):
+        """See `IBranchMergeQueueCollection`."""
+        return self._filterBy([BranchMergeQueue.owner == person])
+
+    def visibleByUser(self, person):
+        """See `IBranchMergeQueueCollection`."""
+        if (person == LAUNCHPAD_SERVICES or
+            user_has_special_merge_queue_access(person)):
+            return self
+        return VisibleBranchMergeQueueCollection(
+            person,
+            self._store, None,
+            self._tables, self._exclude_from_search)
+
+
+class VisibleBranchMergeQueueCollection(GenericBranchMergeQueueCollection):
+    """A mergequeue collection which provides queues visible by a user."""
+
+    def __init__(self, person, store=None,
+                 merge_queue_filter_expressions=None, tables=None,
+                 exclude_from_search=None):
+        super(VisibleBranchMergeQueueCollection, self).__init__(
+            store=store,
+            merge_queue_filter_expressions=merge_queue_filter_expressions,
+            tables=tables,
+            exclude_from_search=exclude_from_search,
+        )
+        self._user = person
+
+    def _filterBy(self, expressions, table=None, join=None,
+                  exclude_from_search=None):
+        """Return a subset of this collection, filtered by 'expressions'."""
+        tables = self._tables.copy()
+        if table is not None:
+            if join is None:
+                raise InvalidFilter("Cannot specify a table without a join.")
+            tables[table] = join
+        if exclude_from_search is None:
+            exclude_from_search = []
+        if expressions is None:
+            expressions = []
+        return self.__class__(
+            self._user,
+            self.store,
+            self._merge_queue_filter_expressions + expressions,
+            tables,
+            self._exclude_from_search + exclude_from_search)
+
+    def visibleByUser(self, person):
+        """See `IBranchMergeQueueCollection`."""
+        if person == self._user:
+            return self
+        raise InvalidFilter(
+            "Cannot filter for merge queues visible by user %r, already "
+            "filtering for %r" % (person, self._user))
+
+    def _getCount(self):
+        """See `IBranchMergeQueueCollection`."""
+        return len(self._getMergeQueues())
+
+    def _getMergeQueues(self):
+        """Return the queues visible by self._user.
+
+        A queue is visible to a user if that user can see all the branches
+        associated with the queue.
+        """
+
+        def allBranchesVisible(user, branches):
+            return len([branch for branch in branches
+                        if branch.visibleByUser(user)]) == branches.count()
+
+        queues = super(
+            VisibleBranchMergeQueueCollection, self)._getMergeQueues()
+        return [queue for queue in queues
+                if allBranchesVisible(self._user, queue.branches)]

=== added file 'lib/lp/code/model/tests/test_branchmergequeuecollection.py'
--- lib/lp/code/model/tests/test_branchmergequeuecollection.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_branchmergequeuecollection.py	2010-11-01 13:09:42 +0000
@@ -0,0 +1,206 @@
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for branch merge queue collections."""
+
+__metaclass__ = type
+
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.webapp.interfaces import (
+    DEFAULT_FLAVOR,
+    IStoreSelector,
+    MAIN_STORE,
+    )
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.code.interfaces.branchmergequeuecollection import (
+    IAllBranchMergeQueues,
+    IBranchMergeQueueCollection,
+    )
+from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
+from lp.code.model.branchmergequeue import BranchMergeQueue
+from lp.code.model.branchmergequeuecollection import (
+    GenericBranchMergeQueueCollection,
+    )
+from lp.testing import TestCaseWithFactory
+
+
+class TestGenericBranchMergeQueueCollection(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        TestCaseWithFactory.setUp(self)
+        self.store = getUtility(IStoreSelector).get(
+            MAIN_STORE, DEFAULT_FLAVOR)
+
+    def test_provides_branchmergequeuecollection(self):
+        # `GenericBranchMergeQueueCollection`
+        # provides the `IBranchMergeQueueCollection` interface.
+        self.assertProvides(
+            GenericBranchMergeQueueCollection(self.store),
+            IBranchMergeQueueCollection)
+
+    def test_getMergeQueues_no_filter_no_queues(self):
+        # If no filter is specified, then the collection is of all branches
+        # merge queues. By default, there are no branch merge queues.
+        collection = GenericBranchMergeQueueCollection(self.store)
+        self.assertEqual([], list(collection.getMergeQueues()))
+
+    def test_getMergeQueues_no_filter(self):
+        # If no filter is specified, then the collection is of all branch
+        # merge queues.
+        collection = GenericBranchMergeQueueCollection(self.store)
+        queue = self.factory.makeBranchMergeQueue()
+        self.assertEqual([queue], list(collection.getMergeQueues()))
+
+    def test_count(self):
+        # The 'count' property of a collection is the number of elements in
+        # the collection.
+        collection = GenericBranchMergeQueueCollection(self.store)
+        self.assertEqual(0, collection.count())
+        for i in range(3):
+            self.factory.makeBranchMergeQueue()
+        self.assertEqual(3, collection.count())
+
+    def test_count_respects_filter(self):
+        # If a collection is a subset of all possible queues, then the count
+        # will be the size of that subset. That is, 'count' respects any
+        # filters that are applied.
+        person = self.factory.makePerson()
+        queue = self.factory.makeBranchMergeQueue(owner=person)
+        queue2 = self.factory.makeAnyBranch()
+        collection = GenericBranchMergeQueueCollection(
+            self.store, [BranchMergeQueue.owner == person])
+        self.assertEqual(1, collection.count())
+
+
+class TestBranchMergeQueueCollectionFilters(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        TestCaseWithFactory.setUp(self)
+        self.all_queues = getUtility(IAllBranchMergeQueues)
+
+    def test_count_respects_visibleByUser_filter(self):
+        # IBranchMergeQueueCollection.count() returns the number of queues
+        # that getMergeQueues() yields, even when the visibleByUser filter is
+        # applied.
+        branch = self.factory.makeAnyBranch(private=True)
+        naked_branch = removeSecurityProxy(branch)
+        queue = self.factory.makeBranchMergeQueue(branches=[naked_branch])
+        branch2 = self.factory.makeAnyBranch(private=True)
+        naked_branch2 = removeSecurityProxy(branch2)
+        queue2 = self.factory.makeBranchMergeQueue(branches=[naked_branch2])
+        collection = self.all_queues.visibleByUser(naked_branch.owner)
+        self.assertEqual(1, len(collection.getMergeQueues()))
+        self.assertEqual(1, collection.count())
+
+    def test_ownedBy(self):
+        # 'ownedBy' returns a new collection restricted to queues owned by
+        # the given person.
+        queue = self.factory.makeBranchMergeQueue()
+        queue2 = self.factory.makeBranchMergeQueue()
+        collection = self.all_queues.ownedBy(queue.owner)
+        self.assertEqual([queue], collection.getMergeQueues())
+
+
+class TestGenericBranchMergeQueueCollectionVisibleFilter(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        TestCaseWithFactory.setUp(self)
+        public_branch = self.factory.makeAnyBranch(name='public')
+        self.queue_with_public_branch = self.factory.makeBranchMergeQueue(
+            branches=[removeSecurityProxy(public_branch)])
+        private_branch1 = self.factory.makeAnyBranch(
+            private=True, name='private1')
+        naked_private_branch1 = removeSecurityProxy(private_branch1)
+        self.private_branch1_owner = naked_private_branch1.owner
+        self.queue1_with_private_branch = self.factory.makeBranchMergeQueue(
+            branches=[naked_private_branch1])
+        private_branch2 = self.factory.makeAnyBranch(
+            private=True, name='private2')
+        self.queue2_with_private_branch = self.factory.makeBranchMergeQueue(
+            branches=[removeSecurityProxy(private_branch2)])
+        self.all_queues = getUtility(IAllBranchMergeQueues)
+
+    def test_all_queues(self):
+        # Without the visibleByUser filter, all queues are in the
+        # collection.
+        self.assertEqual(
+            sorted([self.queue_with_public_branch,
+                    self.queue1_with_private_branch,
+                    self.queue2_with_private_branch]),
+            sorted(self.all_queues.getMergeQueues()))
+
+    def test_anonymous_sees_only_public(self):
+        # Anonymous users can see only queues with public branches.
+        queues = self.all_queues.visibleByUser(None)
+        self.assertEqual([self.queue_with_public_branch],
+                         list(queues.getMergeQueues()))
+
+    def test_random_person_sees_only_public(self):
+        # Logged in users with no special permissions can see only queues with
+        # public branches.
+        person = self.factory.makePerson()
+        queues = self.all_queues.visibleByUser(person)
+        self.assertEqual([self.queue_with_public_branch],
+                         list(queues.getMergeQueues()))
+
+    def test_owner_sees_own_branches(self):
+        # Users can always see the queues with branches that they own, as well
+        # as queues with public branches.
+        queues = self.all_queues.visibleByUser(self.private_branch1_owner)
+        self.assertEqual(
+            sorted([self.queue_with_public_branch,
+                    self.queue1_with_private_branch]),
+            sorted(queues.getMergeQueues()))
+
+    def test_owner_member_sees_own_queues(self):
+        # Members of teams that own queues can see queues owned by those
+        # teams, as well as public branches.
+        team_owner = self.factory.makePerson()
+        team = self.factory.makeTeam(team_owner)
+        private_branch = self.factory.makeAnyBranch(
+            owner=team, private=True, name='team')
+        queue_with_private_branch = self.factory.makeBranchMergeQueue(
+            branches=[removeSecurityProxy(private_branch)])
+        queues = self.all_queues.visibleByUser(team_owner)
+        self.assertEqual(
+            sorted([self.queue_with_public_branch,
+                    queue_with_private_branch]),
+            sorted(queues.getMergeQueues()))
+
+    def test_launchpad_services_sees_all(self):
+        # The LAUNCHPAD_SERVICES special user sees *everything*.
+        queues = self.all_queues.visibleByUser(LAUNCHPAD_SERVICES)
+        self.assertEqual(
+            sorted(self.all_queues.getMergeQueues()),
+            sorted(queues.getMergeQueues()))
+
+    def test_admins_see_all(self):
+        # Launchpad administrators see *everything*.
+        admin = self.factory.makePerson()
+        admin_team = removeSecurityProxy(
+            getUtility(ILaunchpadCelebrities).admin)
+        admin_team.addMember(admin, admin_team.teamowner)
+        queues = self.all_queues.visibleByUser(admin)
+        self.assertEqual(
+            sorted(self.all_queues.getMergeQueues()),
+            sorted(queues.getMergeQueues()))
+
+    def test_bazaar_experts_see_all(self):
+        # Members of the bazaar_experts team see *everything*.
+        bzr_experts = removeSecurityProxy(
+            getUtility(ILaunchpadCelebrities).bazaar_experts)
+        expert = self.factory.makePerson()
+        bzr_experts.addMember(expert, bzr_experts.teamowner)
+        queues = self.all_queues.visibleByUser(expert)
+        self.assertEqual(
+            sorted(self.all_queues.getMergeQueues()),
+            sorted(queues.getMergeQueues()))

=== modified file 'lib/lp/code/templates/branch-pending-merges.pt'
--- lib/lp/code/templates/branch-pending-merges.pt	2010-06-10 07:54:59 +0000
+++ lib/lp/code/templates/branch-pending-merges.pt	2010-11-01 13:09:42 +0000
@@ -2,7 +2,9 @@
   xmlns:tal="http://xml.zope.org/namespaces/tal";
   xmlns:metal="http://xml.zope.org/namespaces/metal";
   xmlns:i18n="http://xml.zope.org/namespaces/i18n";
-  tal:define="context_menu view/context/menu:context"
+  tal:define="
+      context_menu view/context/menu:context;
+      features request/features"
   tal:condition="view/show_merge_links">
 
   <h3>Branch merges</h3>
@@ -46,6 +48,18 @@
       tal:condition="link/enabled"
       tal:replace="structure link/render"
       />
+
+    <div tal:condition="features/code.branchmergequeue">
+      <div tal:condition="not: context/merge_queue">
+        <h4>Merge Queue</h4>
+        This branch is not managed by a queue.
+        <div
+          tal:define="link context_menu/create_queue"
+          tal:condition="link/enabled"
+          tal:content="structure link/render"
+          />
+      </div>
+    </div>
   </div>
 
 </div>

=== added file 'lib/lp/code/templates/branchmergequeue-index.pt'
--- lib/lp/code/templates/branchmergequeue-index.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/branchmergequeue-index.pt	2010-11-01 13:09:42 +0000
@@ -0,0 +1,39 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_side"
+  i18n:domain="launchpad"
+>
+
+<metal:side fill-slot="side">
+  <div tal:replace="structure context/@@+global-actions" />
+</metal:side>
+
+<tal:registering metal:fill-slot="registering">
+  Created by
+    <tal:registrant replace="structure context/registrant/fmt:link" />
+  on
+    <tal:created-on replace="structure context/date_created/fmt:date" />
+</tal:registering>
+
+<div metal:fill-slot="main">
+  <dl>
+    <dt>
+      Description
+    </dt>
+    <dd tal:content="context/description"></dd>
+  </dl>
+  <div tal:condition="context/branches">
+    The following branches are managed by this queue:
+    <ul>
+      <tal:branches repeat="branch context/branches">
+      <li>
+        <a tal:content="structure branch/fmt:link" />
+      </li>
+      </tal:branches>
+    </ul>
+  </div>
+</div>
+</html>

=== added file 'lib/lp/code/templates/branchmergequeue-listing.pt'
--- lib/lp/code/templates/branchmergequeue-listing.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/branchmergequeue-listing.pt	2010-11-01 13:09:42 +0000
@@ -0,0 +1,68 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="launchpad">
+
+<body>
+
+  <div metal:fill-slot="main">
+
+    <div tal:condition="not: features/code.branchmergequeue">
+        <em>
+         No merge queues
+        </em>
+    </div>
+
+    <div tal:condition="features/code.branchmergequeue">
+
+        <tal:has-queues condition="view/mergequeue_count">
+
+            <table id="mergequeuetable" class="listing sortable">
+              <thead>
+                <tr>
+                  <th colspan="2">Name</th>
+                  <th tal:condition="view/owner_enabled">Owner</th>
+                  <th>Queue Size</th>
+                  <th>Associated Branches</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tal:mergequeues repeat="mergeQueue view/mergequeues">
+                  <tr>
+                    <td colspan="2">
+                      <a tal:attributes="href mergeQueue/fmt:url"
+                         tal:content="mergeQueue/name">Merge queue name</a>
+                    </td>
+                    <td tal:condition="view/owner_enabled">
+                      <a tal:replace="structure mergeQueue/owner/fmt:link">
+                        Owner
+                      </a>
+                    </td>
+                    <td>4</td>
+                    <td>
+                        <metal:display-branches
+                            use-macro="context/@@+bmq-macros/merge_queue_branches"/>
+                    </td>
+                  </tr>
+                </tal:mergequeues>
+              </tbody>
+            </table>
+
+        </tal:has-queues>
+
+        <em id="no-queues"
+         tal:condition="not: view/mergequeue_count"
+         tal:content="view/no_merge_queue_message">
+         No merge queues
+        </em>
+
+    </div>
+
+  </div>
+
+</body>
+</html>
+

=== added file 'lib/lp/code/templates/branchmergequeue-macros.pt'
--- lib/lp/code/templates/branchmergequeue-macros.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/branchmergequeue-macros.pt	2010-11-01 13:09:42 +0000
@@ -0,0 +1,20 @@
+ <tal:root
+   xmlns:tal="http://xml.zope.org/namespaces/tal";
+   xmlns:metal="http://xml.zope.org/namespaces/metal";
+   omit-tag="">
+
+<metal:merge_queue_branches define-macro="merge_queue_branches">
+    <table class="listing">
+          <tbody>
+            <tal:mergequeue-branches repeat="branch mergeQueue/branches">
+              <tr>
+                <td>
+                  <a tal:attributes="href branch/fmt:url"
+                     tal:content="branch/name">Branch name</a>
+                </td>
+              </tr>
+            </tal:mergequeue-branches>
+          </tbody>
+    </table>
+</metal:merge_queue_branches>
+</tal:root>
\ No newline at end of file

=== modified file 'lib/lp/code/templates/person-codesummary.pt'
--- lib/lp/code/templates/person-codesummary.pt	2010-10-15 01:48:05 +0000
+++ lib/lp/code/templates/person-codesummary.pt	2010-11-01 13:09:42 +0000
@@ -4,7 +4,8 @@
   xmlns:i18n="http://xml.zope.org/namespaces/i18n";
   id="portlet-person-codesummary"
   class="portlet"
-  tal:define="menu context/menu:branches"
+  tal:define="menu context/menu:branches;
+      features request/features"
   tal:condition="menu/show_summary">
 
   <table>
@@ -32,5 +33,11 @@
           tal:content="structure menu/active_reviews/render"
           />
     </tr>
+    <tr tal:condition="features/code.branchmergequeue" id="mergequeue-counts">
+      <td class="code-count" tal:content="menu/mergequeue_count">5</td>
+      <td tal:condition="menu"
+          tal:content="structure menu/mergequeues/render"
+          />
+    </tr>
   </table>
 </div>

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2010-10-27 14:20:21 +0000
+++ lib/lp/testing/__init__.py	2010-11-01 13:09:42 +0000
@@ -561,6 +561,7 @@
             user = self.factory.makePerson(password=password)
         naked_user = removeSecurityProxy(user)
         email = naked_user.preferredemail.email
+        password = naked_user._password_cleartext_cached
         logout()
         browser = setupBrowser(
             auth="Basic %s:%s" % (str(email), password))

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-10-29 19:28:14 +0000
+++ lib/lp/testing/factory.py	2010-11-01 13:09:42 +0000
@@ -37,7 +37,6 @@
     )
 import os
 from random import randint
-import simplejson
 from StringIO import StringIO
 from textwrap import dedent
 from threading import local
@@ -47,6 +46,7 @@
 from bzrlib.merge_directive import MergeDirective2
 from bzrlib.plugins.builder.recipe import BaseRecipeBranch
 import pytz
+import simplejson
 from twisted.python.util import mergeFunctionMetadata
 from zope.component import (
     ComponentLookupError,
@@ -1113,7 +1113,8 @@
         return namespace.createBranch(branch_type, name, creator)
 
     def makeBranchMergeQueue(self, registrant=None, owner=None, name=None,
-                             description=None, configuration=None):
+                             description=None, configuration=None,
+                             branches=None):
         """Create a BranchMergeQueue."""
         if name is None:
             name = unicode(self.getUniqueString('queue'))
@@ -1128,7 +1129,7 @@
                 self.getUniqueString('key'): self.getUniqueString('value')}))
 
         queue = getUtility(IBranchMergeQueueSource).new(
-            name, registrant, owner, description, configuration)
+            name, owner, registrant, description, configuration, branches)
         return queue
 
     def enableDefaultStackingForProduct(self, product, branch=None):

=== modified file 'setup.py'
--- setup.py	2010-09-18 08:00:27 +0000
+++ setup.py	2010-11-01 13:09:42 +0000
@@ -25,6 +25,7 @@
     # used in zcml.
     install_requires=[
         'ampoule',
+        'BeautifulSoup',
         'bzr',
         'chameleon.core',
         'chameleon.zpt',
@@ -63,6 +64,7 @@
         'RestrictedPython',
         'setproctitle',
         'setuptools',
+        'soupmatchers',
         'sourcecodegen',
         'storm',
         'testtools',

=== modified file 'versions.cfg'
--- versions.cfg	2010-10-27 04:23:52 +0000
+++ versions.cfg	2010-11-01 13:09:42 +0000
@@ -5,6 +5,7 @@
 # Alphabetical, case-insensitive, please! :-)
 
 ampoule = 0.2.0
+BeautifulSoup = 3.1.0.1
 bzr = 2.2.0
 chameleon.core = 1.0b35
 chameleon.zpt = 1.0b17
@@ -64,6 +65,7 @@
 simplejson = 2.0.9
 simplesettings = 0.4
 SimpleTal = 4.1
+soupmatchers = 0.1r53
 sourcecodegen = 0.6.9
 storm = 0.18
 testtools = 0.9.6


References