← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rockstar/launchpad/merge-queue-index into lp:launchpad

 

Paul Hummer has proposed merging lp:~rockstar/launchpad/merge-queue-index into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


This branch adds a (really basic) index page for branch merge queues, as long as a flow for creating a merge queue and linking a branch to it.

This work is hiding behind a feature flag, and the UI is really raw, but I need to get this (and the next branch) landed so that others can also start working on branch merge queues.
-- 
https://code.launchpad.net/~rockstar/launchpad/merge-queue-index/+merge/39488
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rockstar/launchpad/merge-queue-index 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-10-28 02:53:10 +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.

=== 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-10-28 02:53:10 +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)

=== 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-10-28 02:53:10 +0000
@@ -1326,4 +1326,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-10-28 02:53:10 +0000
@@ -0,0 +1,104 @@
+# 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)
+        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')

=== 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-10-28 02:53:10 +0000
@@ -67,11 +67,16 @@
             raise InvalidMergeQueueConfig
 
     @classmethod
-    def new(cls, name, owner, registrant, description=None,
+    def new(cls, name, owner, registrant, description,
             configuration=None):
         """See `IBranchMergeQueueSource`."""
         store = IMasterStore(BranchMergeQueue)
 
+        if description is None:
+            description = description
+        if configuration is None:
+            configuration = unicode(simplejson.dumps({}))
+
         queue = cls()
         queue.name = name
         queue.owner = owner

=== 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-10-28 02:53:10 +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,19 @@
       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-10-28 02:53:10 +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>

=== 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-10-28 02:53:10 +0000
@@ -431,7 +431,8 @@
         pattern = re.compile(
             normalise_whitespace(regular_expression_txt), re.S)
         self.assertIsNot(
-            None, pattern.search(normalise_whitespace(text)), text)
+            None, pattern.search(normalise_whitespace(text)),
+            text + '\n' + regular_expression_txt)
 
     def assertIsInstance(self, instance, assert_class):
         """Assert that an instance is an instance of assert_class.
@@ -561,6 +562,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-27 14:20:21 +0000
+++ lib/lp/testing/factory.py	2010-10-28 02:53:10 +0000
@@ -1113,7 +1113,7 @@
                 self.getUniqueString('key'): self.getUniqueString('value')}))
 
         queue = getUtility(IBranchMergeQueueSource).new(
-            name, registrant, owner, description, configuration)
+            name, owner, registrant, description, configuration)
         return queue
 
     def enableDefaultStackingForProduct(self, product, branch=None):

=== modified file 'setup.py'
--- setup.py	2010-09-18 08:00:27 +0000
+++ setup.py	2010-10-28 02:53:10 +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-23 01:59:54 +0000
+++ versions.cfg	2010-10-28 02:53:10 +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


Follow ups