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
=== modified file 'lib/lp/code/browser/branchlisting.py'
--- lib/lp/code/browser/branchlisting.py 2010-10-29 14:36:59 +0000
+++ lib/lp/code/browser/branchlisting.py 2010-11-03 08:32:56 +0000
@@ -94,6 +94,7 @@
94 PersonActiveReviewsView,94 PersonActiveReviewsView,
95 PersonProductActiveReviewsView,95 PersonProductActiveReviewsView,
96 )96 )
97from lp.code.browser.branchmergequeuelisting import HasMergeQueuesMenuMixin
97from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin98from lp.code.browser.branchvisibilitypolicy import BranchVisibilityPolicyMixin
98from lp.code.browser.summary import BranchCountSummaryView99from lp.code.browser.summary import BranchCountSummaryView
99from lp.code.enums import (100from lp.code.enums import (
@@ -849,18 +850,19 @@
849 .scanned())850 .scanned())
850851
851852
852class PersonBranchesMenu(ApplicationMenu):853class PersonBranchesMenu(ApplicationMenu, HasMergeQueuesMenuMixin):
853854
854 usedfor = IPerson855 usedfor = IPerson
855 facet = 'branches'856 facet = 'branches'
856 links = ['registered', 'owned', 'subscribed', 'addbranch',857 links = ['registered', 'owned', 'subscribed', 'addbranch',
857 'active_reviews']858 'active_reviews', 'mergequeues']
858 extra_attributes = [859 extra_attributes = [
859 'active_review_count',860 'active_review_count',
860 'owned_branch_count',861 'owned_branch_count',
861 'registered_branch_count',862 'registered_branch_count',
862 'show_summary',863 'show_summary',
863 'subscribed_branch_count',864 'subscribed_branch_count',
865 'mergequeue_count',
864 ]866 ]
865867
866 def _getCountCollection(self):868 def _getCountCollection(self):
867869
=== 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-03 08:32:56 +0000
@@ -0,0 +1,105 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Base class view for merge queue listings."""
5
6__metaclass__ = type
7
8__all__ = [
9 'MergeQueueListingView',
10 'HasMergeQueuesMenuMixin',
11 'PersonMergeQueueListingView',
12 ]
13
14from zope.component import getUtility
15
16from canonical.launchpad.browser.feeds import FeedsMixin
17from canonical.launchpad.webapp import (
18 LaunchpadView,
19 Link,
20 )
21from lp.code.interfaces.branchmergequeuecollection import (
22 IAllBranchMergeQueues,
23 )
24from lp.services.browser_helpers import get_plural_text
25from lp.services.propertycache import cachedproperty
26
27
28class HasMergeQueuesMenuMixin:
29 """A context menus mixin for objects that can own merge queues."""
30
31 def _getCollection(self):
32 return getUtility(IAllBranchMergeQueues).visibleByUser(self.user)
33
34 @property
35 def person(self):
36 """The `IPerson` for the context of the view.
37
38 In simple cases this is the context itself, but in others, like the
39 PersonProduct, it is an attribute of the context.
40 """
41 return self.context
42
43 def mergequeues(self):
44 return Link(
45 '+merge-queues',
46 get_plural_text(
47 self.mergequeue_count,
48 'merge queue', 'merge queues'), site='code')
49
50 @cachedproperty
51 def mergequeue_count(self):
52 return self._getCollection().ownedBy(self.person).count()
53
54
55class MergeQueueListingView(LaunchpadView, FeedsMixin):
56
57 # No feeds initially
58 feed_types = ()
59
60 branch_enabled = True
61 owner_enabled = True
62
63 label_template = 'Merge Queues for %(displayname)s'
64
65 @property
66 def label(self):
67 return self.label_template % {
68 'displayname': self.context.displayname,
69 'title': getattr(self.context, 'title', 'no-title')}
70
71 # Provide a default page_title for distros and other things without
72 # breadcrumbs..
73 page_title = label
74
75 def _getCollection(self):
76 """Override this to say what queues will be in the listing."""
77 raise NotImplementedError(self._getCollection)
78
79 def getVisibleQueuesForUser(self):
80 """Branch merge queues that are visible by the logged in user."""
81 collection = self._getCollection().visibleByUser(self.user)
82 return collection.getMergeQueues()
83
84 @cachedproperty
85 def mergequeues(self):
86 return self.getVisibleQueuesForUser()
87
88 @cachedproperty
89 def mergequeue_count(self):
90 """Return the number of merge queues that will be returned."""
91 return self._getCollection().visibleByUser(self.user).count()
92
93 @property
94 def no_merge_queue_message(self):
95 """Shown when there is no table to show."""
96 return "%s has no merge queues." % self.context.displayname
97
98
99class PersonMergeQueueListingView(MergeQueueListingView):
100
101 label_template = 'Merge Queues owned by %(displayname)s'
102 owner_enabled = False
103
104 def _getCollection(self):
105 return getUtility(IAllBranchMergeQueues).ownedBy(self.context)
0106
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2010-11-03 08:32:55 +0000
+++ lib/lp/code/browser/configure.zcml 2010-11-03 08:32:56 +0000
@@ -1318,6 +1318,24 @@
1318 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"1318 for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
1319 factory="canonical.launchpad.webapp.breadcrumb.NameBreadcrumb"1319 factory="canonical.launchpad.webapp.breadcrumb.NameBreadcrumb"
1320 permission="zope.Public"/>1320 permission="zope.Public"/>
1321
1322 <browser:page
1323 for="lp.registry.interfaces.person.IPerson"
1324 layer="lp.code.publisher.CodeLayer"
1325 class="lp.code.browser.branchmergequeuelisting.PersonMergeQueueListingView"
1326 permission="zope.Public"
1327 facet="branches"
1328 name="+merge-queues"
1329 template="../templates/branchmergequeue-listing.pt"/>
1330
1331 <browser:page
1332 for="*"
1333 layer="lp.code.publisher.CodeLayer"
1334 name="+bmq-macros"
1335 permission="zope.Public"
1336 template="../templates/branchmergequeue-macros.pt"/>
1337
1338
1321 </facet>1339 </facet>
13221340
1323 <browser:url1341 <browser:url
13241342
=== 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-03 08:32:56 +0000
@@ -0,0 +1,227 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for branch listing."""
5
6__metaclass__ = type
7
8import re
9
10from mechanize import LinkNotFoundError
11import soupmatchers
12from zope.security.proxy import removeSecurityProxy
13
14from canonical.launchpad.testing.pages import (
15 extract_link_from_tag,
16 extract_text,
17 find_tag_by_id,
18 )
19from canonical.launchpad.webapp import canonical_url
20from canonical.testing.layers import DatabaseFunctionalLayer
21from lp.services.features.model import (
22 FeatureFlag,
23 getFeatureStore,
24 )
25from lp.testing import (
26 BrowserTestCase,
27 login_person,
28 person_logged_in,
29 TestCaseWithFactory,
30 )
31from lp.testing.views import create_initialized_view
32
33
34class MergeQueuesTestMixin:
35
36 def setUp(self):
37 self.branch_owner = self.factory.makePerson(name='eric')
38
39 def enable_queue_flag(self):
40 getFeatureStore().add(FeatureFlag(
41 scope=u'default', flag=u'code.branchmergequeue',
42 value=u'on', priority=1))
43
44 def _makeMergeQueues(self, nr_queues=3, nr_with_private_branches=0):
45 # We create nr_queues merge queues in total, and the first
46 # nr_with_private_branches of them will have at least one private
47 # branch in the queue.
48 with person_logged_in(self.branch_owner):
49 mergequeues = [
50 self.factory.makeBranchMergeQueue(
51 owner=self.branch_owner, branches=self._makeBranches())
52 for i in range(nr_queues-nr_with_private_branches)]
53 mergequeues_with_private_branches = [
54 self.factory.makeBranchMergeQueue(
55 owner=self.branch_owner,
56 branches=self._makeBranches(nr_private=1))
57 for i in range(nr_with_private_branches)]
58
59 return mergequeues, mergequeues_with_private_branches
60
61 def _makeBranches(self, nr_public=3, nr_private=0):
62 branches = [
63 self.factory.makeProductBranch(owner=self.branch_owner)
64 for i in range(nr_public)]
65
66 private_branches = [
67 self.factory.makeProductBranch(
68 owner=self.branch_owner, private=True)
69 for i in range(nr_private)]
70
71 branches.extend(private_branches)
72 return branches
73
74
75class TestPersonMergeQueuesView(TestCaseWithFactory, MergeQueuesTestMixin):
76
77 layer = DatabaseFunctionalLayer
78
79 def setUp(self):
80 TestCaseWithFactory.setUp(self)
81 MergeQueuesTestMixin.setUp(self)
82 self.user = self.factory.makePerson()
83
84 def test_mergequeues_with_all_public_branches(self):
85 # Anyone can see mergequeues containing all public branches.
86 mq, mq_with_private = self._makeMergeQueues()
87 login_person(self.user)
88 view = create_initialized_view(
89 self.branch_owner, name="+merge-queues", rootsite='code')
90 self.assertEqual(set(mq), set(view.mergequeues))
91
92 def test_mergequeues_with_a_private_branch_for_owner(self):
93 # Only users with access to private branches can see any queues
94 # containing such branches.
95 mq, mq_with_private = (
96 self._makeMergeQueues(nr_with_private_branches=1))
97 login_person(self.branch_owner)
98 view = create_initialized_view(
99 self.branch_owner, name="+merge-queues", rootsite='code')
100 mq.extend(mq_with_private)
101 self.assertEqual(set(mq), set(view.mergequeues))
102
103 def test_mergequeues_with_a_private_branch_for_other_user(self):
104 # Only users with access to private branches can see any queues
105 # containing such branches.
106 mq, mq_with_private = (
107 self._makeMergeQueues(nr_with_private_branches=1))
108 login_person(self.user)
109 view = create_initialized_view(
110 self.branch_owner, name="+merge-queues", rootsite='code')
111 self.assertEqual(set(mq), set(view.mergequeues))
112
113
114class TestPersonCodePage(BrowserTestCase, MergeQueuesTestMixin):
115 """Tests for the person code homepage.
116
117 This is the default page shown for a person on the code subdomain.
118 """
119
120 layer = DatabaseFunctionalLayer
121
122 def setUp(self):
123 BrowserTestCase.setUp(self)
124 MergeQueuesTestMixin.setUp(self)
125 self._makeMergeQueues()
126
127 def test_merge_queue_menu_link_without_feature_flag(self):
128 login_person(self.branch_owner)
129 browser = self.getUserBrowser(
130 canonical_url(self.branch_owner, rootsite='code'),
131 self.branch_owner)
132 self.assertRaises(
133 LinkNotFoundError,
134 browser.getLink,
135 url='+merge-queues')
136
137 def test_merge_queue_menu_link(self):
138 self.enable_queue_flag()
139 login_person(self.branch_owner)
140 browser = self.getUserBrowser(
141 canonical_url(self.branch_owner, rootsite='code'),
142 self.branch_owner)
143 browser.getLink(url='+merge-queues').click()
144 self.assertEqual(
145 'http://code.launchpad.dev/~eric/+merge-queues',
146 browser.url)
147
148
149class TestPersonMergeQueuesListPage(BrowserTestCase, MergeQueuesTestMixin):
150 """Tests for the person merge queue list page."""
151
152 layer = DatabaseFunctionalLayer
153
154 def setUp(self):
155 BrowserTestCase.setUp(self)
156 MergeQueuesTestMixin.setUp(self)
157 mq, mq_with_private = self._makeMergeQueues()
158 self.merge_queues = mq
159 self.merge_queues.extend(mq_with_private)
160
161 def test_merge_queue_list_contents_without_feature_flag(self):
162 login_person(self.branch_owner)
163 browser = self.getUserBrowser(
164 canonical_url(self.branch_owner, rootsite='code',
165 view_name='+merge-queues'), self.branch_owner)
166 table = find_tag_by_id(browser.contents, 'mergequeuetable')
167 self.assertIs(None, table)
168 noqueue_matcher = soupmatchers.HTMLContains(
169 soupmatchers.Tag(
170 'No merge queues', 'div',
171 text=re.compile(
172 '\w*No merge queues\w*')))
173 self.assertThat(browser.contents, noqueue_matcher)
174
175 def test_merge_queue_list_contents(self):
176 self.enable_queue_flag()
177 login_person(self.branch_owner)
178 browser = self.getUserBrowser(
179 canonical_url(self.branch_owner, rootsite='code',
180 view_name='+merge-queues'), self.branch_owner)
181
182 table = find_tag_by_id(browser.contents, 'mergequeuetable')
183
184 merge_queue_info = {}
185 for row in table.tbody.fetch('tr'):
186 cells = row('td')
187 row_info = {}
188 queue_name = extract_text(cells[0])
189 if not queue_name.startswith('queue'):
190 continue
191 qlink = extract_link_from_tag(cells[0].find('a'))
192 row_info['queue_link'] = qlink
193 queue_size = extract_text(cells[1])
194 row_info['queue_size'] = queue_size
195 queue_branches = cells[2]('a')
196 branch_links = set()
197 for branch_tag in queue_branches:
198 branch_links.add(extract_link_from_tag(branch_tag))
199 row_info['branch_links'] = branch_links
200 merge_queue_info[queue_name] = row_info
201
202 expected_queue_names = [queue.name for queue in self.merge_queues]
203 self.assertEqual(
204 set(expected_queue_names), set(merge_queue_info.keys()))
205
206 #TODO: when IBranchMergeQueue API is available remove '4'
207 expected_queue_sizes = dict(
208 [(queue.name, '4') for queue in self.merge_queues])
209 observed_queue_sizes = dict(
210 [(queue.name, merge_queue_info[queue.name]['queue_size'])
211 for queue in self.merge_queues])
212 self.assertEqual(
213 expected_queue_sizes, observed_queue_sizes)
214
215 def branch_links(branches):
216 return [canonical_url(removeSecurityProxy(branch),
217 force_local_path=True)
218 for branch in branches]
219
220 expected_queue_branches = dict(
221 [(queue.name, set(branch_links(queue.branches)))
222 for queue in self.merge_queues])
223 observed_queue_branches = dict(
224 [(queue.name, merge_queue_info[queue.name]['branch_links'])
225 for queue in self.merge_queues])
226 self.assertEqual(
227 expected_queue_branches, observed_queue_branches)
0228
=== 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-03 08:32:56 +0000
@@ -94,6 +94,12 @@
94 <allow attributes="browserDefault94 <allow attributes="browserDefault
95 __call__"/>95 __call__"/>
96 </class>96 </class>
97 <class class="lp.code.model.branchmergequeuecollection.GenericBranchMergeQueueCollection">
98 <allow interface="lp.code.interfaces.branchmergequeuecollection.IBranchMergeQueueCollection"/>
99 </class>
100 <class class="lp.code.model.branchmergequeuecollection.VisibleBranchMergeQueueCollection">
101 <allow interface="lp.code.interfaces.branchmergequeuecollection.IBranchMergeQueueCollection"/>
102 </class>
97 <class class="lp.code.model.branchcollection.GenericBranchCollection">103 <class class="lp.code.model.branchcollection.GenericBranchCollection">
98 <allow interface="lp.code.interfaces.branchcollection.IBranchCollection"/>104 <allow interface="lp.code.interfaces.branchcollection.IBranchCollection"/>
99 </class>105 </class>
@@ -148,6 +154,11 @@
148 provides="lp.code.interfaces.revisioncache.IRevisionCache">154 provides="lp.code.interfaces.revisioncache.IRevisionCache">
149 <allow interface="lp.code.interfaces.revisioncache.IRevisionCache"/>155 <allow interface="lp.code.interfaces.revisioncache.IRevisionCache"/>
150 </securedutility>156 </securedutility>
157 <securedutility
158 class="lp.code.model.branchmergequeuecollection.GenericBranchMergeQueueCollection"
159 provides="lp.code.interfaces.branchmergequeuecollection.IAllBranchMergeQueues">
160 <allow interface="lp.code.interfaces.branchmergequeuecollection.IAllBranchMergeQueues"/>
161 </securedutility>
151 <adapter162 <adapter
152 for="lp.registry.interfaces.person.IPerson"163 for="lp.registry.interfaces.person.IPerson"
153 provides="lp.code.interfaces.revisioncache.IRevisionCache"164 provides="lp.code.interfaces.revisioncache.IRevisionCache"
154165
=== 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-03 08:32:56 +0000
@@ -8,6 +8,7 @@
8__all__ = [8__all__ = [
9 'IBranchMergeQueue',9 'IBranchMergeQueue',
10 'IBranchMergeQueueSource',10 'IBranchMergeQueueSource',
11 'user_has_special_merge_queue_access',
11 ]12 ]
1213
13from lazr.restful.declarations import (14from lazr.restful.declarations import (
@@ -21,6 +22,7 @@
21 CollectionField,22 CollectionField,
22 Reference,23 Reference,
23 )24 )
25from zope.component import getUtility
24from zope.interface import Interface26from zope.interface import Interface
25from zope.schema import (27from zope.schema import (
26 Datetime,28 Datetime,
@@ -30,6 +32,7 @@
30 )32 )
3133
32from canonical.launchpad import _34from canonical.launchpad import _
35from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
33from lp.services.fields import (36from lp.services.fields import (
34 PersonChoice,37 PersonChoice,
35 PublicPersonChoice,38 PublicPersonChoice,
@@ -113,3 +116,14 @@
113 :param registrant: The registrant of the queue.116 :param registrant: The registrant of the queue.
114 :param branches: A list of branches to add to the queue.117 :param branches: A list of branches to add to the queue.
115 """118 """
119
120
121def user_has_special_merge_queue_access(user):
122 """Admins and bazaar experts have special access.
123
124 :param user: A 'Person' or None.
125 """
126 if user is None:
127 return False
128 celebs = getUtility(ILaunchpadCelebrities)
129 return user.inTeam(celebs.admin) or user.inTeam(celebs.bazaar_experts)
116130
=== 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-03 08:32:56 +0000
@@ -0,0 +1,64 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=E0211, E0213
5
6"""A collection of branche merge queues.
7
8See `IBranchMergeQueueCollection` for more details.
9"""
10
11__metaclass__ = type
12__all__ = [
13 'IAllBranchMergeQueues',
14 'IBranchMergeQueueCollection',
15 'InvalidFilter',
16 ]
17
18from zope.interface import Interface
19
20
21class InvalidFilter(Exception):
22 """Raised when an `IBranchMergeQueueCollection` can't apply the filter."""
23
24
25class IBranchMergeQueueCollection(Interface):
26 """A collection of branch merge queues.
27
28 An `IBranchMergeQueueCollection` is an immutable collection of branch
29 merge queues. It has two kinds of methods:
30 filter methods and query methods.
31
32 Query methods get information about the contents of collection. See
33 `IBranchMergeQueueCollection.count` and
34 `IBranchMergeQueueCollection.getMergeQueues`.
35
36 Implementations of this interface are not 'content classes'. That is, they
37 do not correspond to a particular row in the database.
38
39 This interface is intended for use within Launchpad, not to be exported as
40 a public API.
41 """
42
43 def count():
44 """The number of merge queues in this collection."""
45
46 def getMergeQueues():
47 """Return a result set of all merge queues in this collection.
48
49 The returned result set will also join across the specified tables as
50 defined by the arguments to this function. These extra tables are
51 joined specificly to allow the caller to sort on values not in the
52 Branch table itself.
53 """
54
55 def ownedBy(person):
56 """Restrict the collection to queues owned by 'person'."""
57
58 def visibleByUser(person):
59 """Restrict the collection to queues that 'person' is allowed to see.
60 """
61
62
63class IAllBranchMergeQueues(IBranchMergeQueueCollection):
64 """An `IBranchMergeQueueCollection` of all branch merge queues."""
065
=== modified file 'lib/lp/code/model/branchmergequeue.py'
--- lib/lp/code/model/branchmergequeue.py 2010-11-03 08:32:55 +0000
+++ lib/lp/code/model/branchmergequeue.py 2010-11-03 08:32:56 +0000
@@ -7,7 +7,6 @@
7__all__ = ['BranchMergeQueue']7__all__ = ['BranchMergeQueue']
88
9import simplejson9import simplejson
10
11from storm.locals import (10from storm.locals import (
12 Int,11 Int,
13 Reference,12 Reference,
@@ -68,7 +67,7 @@
6867
69 @classmethod68 @classmethod
70 def new(cls, name, owner, registrant, description=None,69 def new(cls, name, owner, registrant, description=None,
71 configuration=None):70 configuration=None, branches=None):
72 """See `IBranchMergeQueueSource`."""71 """See `IBranchMergeQueueSource`."""
73 store = IMasterStore(BranchMergeQueue)72 store = IMasterStore(BranchMergeQueue)
7473
@@ -81,6 +80,9 @@
81 queue.registrant = registrant80 queue.registrant = registrant
82 queue.description = description81 queue.description = description
83 queue.configuration = configuration82 queue.configuration = configuration
83 if branches is not None:
84 for branch in branches:
85 branch.addToQueue(queue)
8486
85 store.add(queue)87 store.add(queue)
86 return queue88 return queue
8789
=== 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-03 08:32:56 +0000
@@ -0,0 +1,174 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Implementations of `IBranchMergeQueueCollection`."""
5
6__metaclass__ = type
7__all__ = [
8 'GenericBranchCollection',
9 ]
10
11from zope.interface import implements
12
13from canonical.launchpad.interfaces.lpstorm import IMasterStore
14from lp.code.interfaces.branchmergequeue import (
15 user_has_special_merge_queue_access,
16 )
17from lp.code.interfaces.branchmergequeuecollection import (
18 IBranchMergeQueueCollection,
19 InvalidFilter,
20 )
21from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
22from lp.code.model.branchmergequeue import BranchMergeQueue
23
24
25class GenericBranchMergeQueueCollection:
26 """See `IBranchMergeQueueCollection`."""
27
28 implements(IBranchMergeQueueCollection)
29
30 def __init__(self, store=None, merge_queue_filter_expressions=None,
31 tables=None, exclude_from_search=None):
32 """Construct a `GenericBranchMergeQueueCollection`.
33
34 :param store: The store to look in for merge queues. If not specified,
35 use the default store.
36 :param merge_queue_filter_expressions: A list of Storm expressions to
37 restrict the queues in the collection. If unspecified, then
38 there will be no restrictions on the result set. That is, all
39 queues in the store will be in the collection.
40 :param tables: A dict of Storm tables to the Join expression. If an
41 expression in merge_queue_filter_expressions refers to a table,
42 then that table *must* be in this list.
43 """
44 self._store = store
45 if merge_queue_filter_expressions is None:
46 merge_queue_filter_expressions = []
47 self._merge_queue_filter_expressions = merge_queue_filter_expressions
48 if tables is None:
49 tables = {}
50 self._tables = tables
51 if exclude_from_search is None:
52 exclude_from_search = []
53 self._exclude_from_search = exclude_from_search
54
55 def count(self):
56 return self._getCount()
57
58 def _getCount(self):
59 """See `IBranchMergeQueueCollection`."""
60 return self._getMergeQueues().count()
61
62 @property
63 def store(self):
64 if self._store is None:
65 return IMasterStore(BranchMergeQueue)
66 else:
67 return self._store
68
69 def _filterBy(self, expressions, table=None, join=None,
70 exclude_from_search=None):
71 """Return a subset of this collection, filtered by 'expressions'."""
72 tables = self._tables.copy()
73 if table is not None:
74 if join is None:
75 raise InvalidFilter("Cannot specify a table without a join.")
76 tables[table] = join
77 if exclude_from_search is None:
78 exclude_from_search = []
79 if expressions is None:
80 expressions = []
81 return self.__class__(
82 self.store,
83 self._merge_queue_filter_expressions + expressions,
84 tables,
85 self._exclude_from_search + exclude_from_search)
86
87 def _getMergeQueueExpressions(self):
88 """Return the where expressions for this collection."""
89 return self._merge_queue_filter_expressions
90
91 def getMergeQueues(self):
92 return list(self._getMergeQueues())
93
94 def _getMergeQueues(self):
95 """See `IBranchMergeQueueCollection`."""
96 tables = [BranchMergeQueue] + self._tables.values()
97 expressions = self._getMergeQueueExpressions()
98 return self.store.using(*tables).find(BranchMergeQueue, *expressions)
99
100 def ownedBy(self, person):
101 """See `IBranchMergeQueueCollection`."""
102 return self._filterBy([BranchMergeQueue.owner == person])
103
104 def visibleByUser(self, person):
105 """See `IBranchMergeQueueCollection`."""
106 if (person == LAUNCHPAD_SERVICES or
107 user_has_special_merge_queue_access(person)):
108 return self
109 return VisibleBranchMergeQueueCollection(
110 person,
111 self._store, None,
112 self._tables, self._exclude_from_search)
113
114
115class VisibleBranchMergeQueueCollection(GenericBranchMergeQueueCollection):
116 """A mergequeue collection which provides queues visible by a user."""
117
118 def __init__(self, person, store=None,
119 merge_queue_filter_expressions=None, tables=None,
120 exclude_from_search=None):
121 super(VisibleBranchMergeQueueCollection, self).__init__(
122 store=store,
123 merge_queue_filter_expressions=merge_queue_filter_expressions,
124 tables=tables,
125 exclude_from_search=exclude_from_search,
126 )
127 self._user = person
128
129 def _filterBy(self, expressions, table=None, join=None,
130 exclude_from_search=None):
131 """Return a subset of this collection, filtered by 'expressions'."""
132 tables = self._tables.copy()
133 if table is not None:
134 if join is None:
135 raise InvalidFilter("Cannot specify a table without a join.")
136 tables[table] = join
137 if exclude_from_search is None:
138 exclude_from_search = []
139 if expressions is None:
140 expressions = []
141 return self.__class__(
142 self._user,
143 self.store,
144 self._merge_queue_filter_expressions + expressions,
145 tables,
146 self._exclude_from_search + exclude_from_search)
147
148 def visibleByUser(self, person):
149 """See `IBranchMergeQueueCollection`."""
150 if person == self._user:
151 return self
152 raise InvalidFilter(
153 "Cannot filter for merge queues visible by user %r, already "
154 "filtering for %r" % (person, self._user))
155
156 def _getCount(self):
157 """See `IBranchMergeQueueCollection`."""
158 return len(self._getMergeQueues())
159
160 def _getMergeQueues(self):
161 """Return the queues visible by self._user.
162
163 A queue is visible to a user if that user can see all the branches
164 associated with the queue.
165 """
166
167 def allBranchesVisible(user, branches):
168 return len([branch for branch in branches
169 if branch.visibleByUser(user)]) == branches.count()
170
171 queues = super(
172 VisibleBranchMergeQueueCollection, self)._getMergeQueues()
173 return [queue for queue in queues
174 if allBranchesVisible(self._user, queue.branches)]
0175
=== 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-03 08:32:56 +0000
@@ -0,0 +1,201 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for branch merge queue collections."""
5
6__metaclass__ = type
7
8from zope.component import getUtility
9from zope.security.proxy import removeSecurityProxy
10
11from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
12from canonical.launchpad.interfaces.lpstorm import IMasterStore
13from canonical.testing.layers import DatabaseFunctionalLayer
14from lp.code.interfaces.branchmergequeuecollection import (
15 IAllBranchMergeQueues,
16 IBranchMergeQueueCollection,
17 )
18from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
19from lp.code.model.branchmergequeue import BranchMergeQueue
20from lp.code.model.branchmergequeuecollection import (
21 GenericBranchMergeQueueCollection,
22 )
23from lp.testing import TestCaseWithFactory
24
25
26class TestGenericBranchMergeQueueCollection(TestCaseWithFactory):
27
28 layer = DatabaseFunctionalLayer
29
30 def setUp(self):
31 TestCaseWithFactory.setUp(self)
32 self.store = IMasterStore(BranchMergeQueue)
33
34 def test_provides_branchmergequeuecollection(self):
35 # `GenericBranchMergeQueueCollection`
36 # provides the `IBranchMergeQueueCollection` interface.
37 self.assertProvides(
38 GenericBranchMergeQueueCollection(self.store),
39 IBranchMergeQueueCollection)
40
41 def test_getMergeQueues_no_filter_no_queues(self):
42 # If no filter is specified, then the collection is of all branches
43 # merge queues. By default, there are no branch merge queues.
44 collection = GenericBranchMergeQueueCollection(self.store)
45 self.assertEqual([], list(collection.getMergeQueues()))
46
47 def test_getMergeQueues_no_filter(self):
48 # If no filter is specified, then the collection is of all branch
49 # merge queues.
50 collection = GenericBranchMergeQueueCollection(self.store)
51 queue = self.factory.makeBranchMergeQueue()
52 self.assertEqual([queue], list(collection.getMergeQueues()))
53
54 def test_count(self):
55 # The 'count' property of a collection is the number of elements in
56 # the collection.
57 collection = GenericBranchMergeQueueCollection(self.store)
58 self.assertEqual(0, collection.count())
59 for i in range(3):
60 self.factory.makeBranchMergeQueue()
61 self.assertEqual(3, collection.count())
62
63 def test_count_respects_filter(self):
64 # If a collection is a subset of all possible queues, then the count
65 # will be the size of that subset. That is, 'count' respects any
66 # filters that are applied.
67 person = self.factory.makePerson()
68 queue = self.factory.makeBranchMergeQueue(owner=person)
69 queue2 = self.factory.makeAnyBranch()
70 collection = GenericBranchMergeQueueCollection(
71 self.store, [BranchMergeQueue.owner == person])
72 self.assertEqual(1, collection.count())
73
74
75class TestBranchMergeQueueCollectionFilters(TestCaseWithFactory):
76
77 layer = DatabaseFunctionalLayer
78
79 def setUp(self):
80 TestCaseWithFactory.setUp(self)
81 self.all_queues = getUtility(IAllBranchMergeQueues)
82
83 def test_count_respects_visibleByUser_filter(self):
84 # IBranchMergeQueueCollection.count() returns the number of queues
85 # that getMergeQueues() yields, even when the visibleByUser filter is
86 # applied.
87 branch = self.factory.makeAnyBranch(private=True)
88 naked_branch = removeSecurityProxy(branch)
89 queue = self.factory.makeBranchMergeQueue(branches=[naked_branch])
90 branch2 = self.factory.makeAnyBranch(private=True)
91 naked_branch2 = removeSecurityProxy(branch2)
92 queue2 = self.factory.makeBranchMergeQueue(branches=[naked_branch2])
93 collection = self.all_queues.visibleByUser(naked_branch.owner)
94 self.assertEqual(1, len(collection.getMergeQueues()))
95 self.assertEqual(1, collection.count())
96
97 def test_ownedBy(self):
98 # 'ownedBy' returns a new collection restricted to queues owned by
99 # the given person.
100 queue = self.factory.makeBranchMergeQueue()
101 queue2 = self.factory.makeBranchMergeQueue()
102 collection = self.all_queues.ownedBy(queue.owner)
103 self.assertEqual([queue], collection.getMergeQueues())
104
105
106class TestGenericBranchMergeQueueCollectionVisibleFilter(TestCaseWithFactory):
107
108 layer = DatabaseFunctionalLayer
109
110 def setUp(self):
111 TestCaseWithFactory.setUp(self)
112 public_branch = self.factory.makeAnyBranch(name='public')
113 self.queue_with_public_branch = self.factory.makeBranchMergeQueue(
114 branches=[removeSecurityProxy(public_branch)])
115 private_branch1 = self.factory.makeAnyBranch(
116 private=True, name='private1')
117 naked_private_branch1 = removeSecurityProxy(private_branch1)
118 self.private_branch1_owner = naked_private_branch1.owner
119 self.queue1_with_private_branch = self.factory.makeBranchMergeQueue(
120 branches=[naked_private_branch1])
121 private_branch2 = self.factory.makeAnyBranch(
122 private=True, name='private2')
123 self.queue2_with_private_branch = self.factory.makeBranchMergeQueue(
124 branches=[removeSecurityProxy(private_branch2)])
125 self.all_queues = getUtility(IAllBranchMergeQueues)
126
127 def test_all_queues(self):
128 # Without the visibleByUser filter, all queues are in the
129 # collection.
130 self.assertEqual(
131 sorted([self.queue_with_public_branch,
132 self.queue1_with_private_branch,
133 self.queue2_with_private_branch]),
134 sorted(self.all_queues.getMergeQueues()))
135
136 def test_anonymous_sees_only_public(self):
137 # Anonymous users can see only queues with public branches.
138 queues = self.all_queues.visibleByUser(None)
139 self.assertEqual([self.queue_with_public_branch],
140 list(queues.getMergeQueues()))
141
142 def test_random_person_sees_only_public(self):
143 # Logged in users with no special permissions can see only queues with
144 # public branches.
145 person = self.factory.makePerson()
146 queues = self.all_queues.visibleByUser(person)
147 self.assertEqual([self.queue_with_public_branch],
148 list(queues.getMergeQueues()))
149
150 def test_owner_sees_own_branches(self):
151 # Users can always see the queues with branches that they own, as well
152 # as queues with public branches.
153 queues = self.all_queues.visibleByUser(self.private_branch1_owner)
154 self.assertEqual(
155 sorted([self.queue_with_public_branch,
156 self.queue1_with_private_branch]),
157 sorted(queues.getMergeQueues()))
158
159 def test_owner_member_sees_own_queues(self):
160 # Members of teams that own queues can see queues owned by those
161 # teams, as well as public branches.
162 team_owner = self.factory.makePerson()
163 team = self.factory.makeTeam(team_owner)
164 private_branch = self.factory.makeAnyBranch(
165 owner=team, private=True, name='team')
166 queue_with_private_branch = self.factory.makeBranchMergeQueue(
167 branches=[removeSecurityProxy(private_branch)])
168 queues = self.all_queues.visibleByUser(team_owner)
169 self.assertEqual(
170 sorted([self.queue_with_public_branch,
171 queue_with_private_branch]),
172 sorted(queues.getMergeQueues()))
173
174 def test_launchpad_services_sees_all(self):
175 # The LAUNCHPAD_SERVICES special user sees *everything*.
176 queues = self.all_queues.visibleByUser(LAUNCHPAD_SERVICES)
177 self.assertEqual(
178 sorted(self.all_queues.getMergeQueues()),
179 sorted(queues.getMergeQueues()))
180
181 def test_admins_see_all(self):
182 # Launchpad administrators see *everything*.
183 admin = self.factory.makePerson()
184 admin_team = removeSecurityProxy(
185 getUtility(ILaunchpadCelebrities).admin)
186 admin_team.addMember(admin, admin_team.teamowner)
187 queues = self.all_queues.visibleByUser(admin)
188 self.assertEqual(
189 sorted(self.all_queues.getMergeQueues()),
190 sorted(queues.getMergeQueues()))
191
192 def test_bazaar_experts_see_all(self):
193 # Members of the bazaar_experts team see *everything*.
194 bzr_experts = removeSecurityProxy(
195 getUtility(ILaunchpadCelebrities).bazaar_experts)
196 expert = self.factory.makePerson()
197 bzr_experts.addMember(expert, bzr_experts.teamowner)
198 queues = self.all_queues.visibleByUser(expert)
199 self.assertEqual(
200 sorted(self.all_queues.getMergeQueues()),
201 sorted(queues.getMergeQueues()))
0202
=== 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-03 08:32:56 +0000
@@ -0,0 +1,68 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8
9<body>
10
11 <div metal:fill-slot="main">
12
13 <div tal:condition="not: features/code.branchmergequeue">
14 <em>
15 No merge queues
16 </em>
17 </div>
18
19 <div tal:condition="features/code.branchmergequeue">
20
21 <tal:has-queues condition="view/mergequeue_count">
22
23 <table id="mergequeuetable" class="listing sortable">
24 <thead>
25 <tr>
26 <th colspan="2">Name</th>
27 <th tal:condition="view/owner_enabled">Owner</th>
28 <th>Queue Size</th>
29 <th>Associated Branches</th>
30 </tr>
31 </thead>
32 <tbody>
33 <tal:mergequeues repeat="mergeQueue view/mergequeues">
34 <tr>
35 <td colspan="2">
36 <a tal:attributes="href mergeQueue/fmt:url"
37 tal:content="mergeQueue/name">Merge queue name</a>
38 </td>
39 <td tal:condition="view/owner_enabled">
40 <a tal:replace="structure mergeQueue/owner/fmt:link">
41 Owner
42 </a>
43 </td>
44 <td>4</td>
45 <td>
46 <metal:display-branches
47 use-macro="context/@@+bmq-macros/merge_queue_branches"/>
48 </td>
49 </tr>
50 </tal:mergequeues>
51 </tbody>
52 </table>
53
54 </tal:has-queues>
55
56 <em id="no-queues"
57 tal:condition="not: view/mergequeue_count"
58 tal:content="view/no_merge_queue_message">
59 No merge queues
60 </em>
61
62 </div>
63
64 </div>
65
66</body>
67</html>
68
069
=== 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-03 08:32:56 +0000
@@ -0,0 +1,20 @@
1 <tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 omit-tag="">
5
6<metal:merge_queue_branches define-macro="merge_queue_branches">
7 <table class="listing">
8 <tbody>
9 <tal:mergequeue-branches repeat="branch mergeQueue/branches">
10 <tr>
11 <td>
12 <a tal:attributes="href branch/fmt:url"
13 tal:content="branch/name">Branch name</a>
14 </td>
15 </tr>
16 </tal:mergequeue-branches>
17 </tbody>
18 </table>
19</metal:merge_queue_branches>
20</tal:root>
0\ No newline at end of file21\ No newline at end of file
122
=== 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-03 08:32:56 +0000
@@ -4,7 +4,8 @@
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 id="portlet-person-codesummary"5 id="portlet-person-codesummary"
6 class="portlet"6 class="portlet"
7 tal:define="menu context/menu:branches"7 tal:define="menu context/menu:branches;
8 features request/features"
8 tal:condition="menu/show_summary">9 tal:condition="menu/show_summary">
910
10 <table>11 <table>
@@ -32,5 +33,11 @@
32 tal:content="structure menu/active_reviews/render"33 tal:content="structure menu/active_reviews/render"
33 />34 />
34 </tr>35 </tr>
36 <tr tal:condition="features/code.branchmergequeue" id="mergequeue-counts">
37 <td class="code-count" tal:content="menu/mergequeue_count">5</td>
38 <td tal:condition="menu"
39 tal:content="structure menu/mergequeues/render"
40 />
41 </tr>
35 </table>42 </table>
36</div>43</div>
3744
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-11-03 08:32:55 +0000
+++ lib/lp/testing/factory.py 2010-11-03 08:32:56 +0000
@@ -37,7 +37,6 @@
37 )37 )
38import os38import os
39from random import randint39from random import randint
40import simplejson
41from StringIO import StringIO40from StringIO import StringIO
42from textwrap import dedent41from textwrap import dedent
43from threading import local42from threading import local
@@ -47,6 +46,7 @@
47from bzrlib.merge_directive import MergeDirective246from bzrlib.merge_directive import MergeDirective2
48from bzrlib.plugins.builder.recipe import BaseRecipeBranch47from bzrlib.plugins.builder.recipe import BaseRecipeBranch
49import pytz48import pytz
49import simplejson
50from twisted.python.util import mergeFunctionMetadata50from twisted.python.util import mergeFunctionMetadata
51from zope.component import (51from zope.component import (
52 ComponentLookupError,52 ComponentLookupError,
@@ -1113,7 +1113,8 @@
1113 return namespace.createBranch(branch_type, name, creator)1113 return namespace.createBranch(branch_type, name, creator)
11141114
1115 def makeBranchMergeQueue(self, registrant=None, owner=None, name=None,1115 def makeBranchMergeQueue(self, registrant=None, owner=None, name=None,
1116 description=None, configuration=None):1116 description=None, configuration=None,
1117 branches=None):
1117 """Create a BranchMergeQueue."""1118 """Create a BranchMergeQueue."""
1118 if name is None:1119 if name is None:
1119 name = unicode(self.getUniqueString('queue'))1120 name = unicode(self.getUniqueString('queue'))
@@ -1128,7 +1129,7 @@
1128 self.getUniqueString('key'): self.getUniqueString('value')}))1129 self.getUniqueString('key'): self.getUniqueString('value')}))
11291130
1130 queue = getUtility(IBranchMergeQueueSource).new(1131 queue = getUtility(IBranchMergeQueueSource).new(
1131 name, owner, registrant, description, configuration)1132 name, owner, registrant, description, configuration, branches)
1132 return queue1133 return queue
11331134
1134 def enableDefaultStackingForProduct(self, product, branch=None):1135 def enableDefaultStackingForProduct(self, product, branch=None):

Subscribers

People subscribed via source and target branches

to status/vote changes: