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