Merge lp:~thumper/launchpad/package-branch-edit-owner into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Jonathan Lange
Approved revision: no longer in the source branch.
Merged at revision: 10925
Proposed branch: lp:~thumper/launchpad/package-branch-edit-owner
Merge into: lp:launchpad
Diff against target: 243 lines (+103/-28)
5 files modified
lib/lp/code/browser/branch.py (+23/-0)
lib/lp/code/stories/branches/xx-branch-edit.txt (+47/-1)
lib/lp/code/tests/helpers.py (+28/-2)
lib/lp/code/tests/test_branch.py (+4/-23)
lib/lp/registry/vocabularies.py (+1/-2)
To merge this branch: bzr merge lp:~thumper/launchpad/package-branch-edit-owner
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+25291@code.launchpad.net

Commit message

Allow branch editors who are not in the owner team to edit the branch details without grabbing ownership of the branch.

Description of the change

There is a weird edge case in the editing of a branch where the editor has upload permission on the source package, and this gives the user edit permission on the official source package branch. In these cases the current owner of the branch wasn't in the vocabulary that was used for the widget to set the owner, so the default was set to the first value (the user).

Now when this edge case occurs we add the current branch owner in at the start of the vocabulary used in this situation.

pre-impl call: rockstar

tests: xx-branch-edit.txt

As a drive by fix, I noticed that the vocabularies for IPerson were duplicating the code of unique_displayname, so I fixed it.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Hey Tim,

Weird edge case. Thanks for fixing it up.

 * I wish you didn't add to the doctest, and instead added some real unit tests. Don't feel obliged to do anything about this wish.

 * If there were a function like get_allowed_owners(branch, editor) (or a method branch.get_allowed_owners(editor)) that returned a list of potential owners, then this wouldn't be an edge case. I guess what I mean is, why did you decide to fix this at the view level rather than the model level?

 * The big block of code you add in the doctest could do with some basic prose explanation, e.g. "Create an official branch owned by ~ubuntu-branches, and an editor who has nothing to do with ~ubuntu-branches but is still allowed to edit the branch."

You only need to do something about the last point as far as I'm concerned. Would very much appreciate your thoughts on the first two though.

jml

review: Needs Fixing
Revision history for this message
Tim Penhey (thumper) wrote :

OK, for the first, since I was testing a widget that was being rendered, it seemed easiest to use a page test as it has the results of the widget being rendered.

I was wanting to use a standard widget (most of the time) for the rendering of the owner drop-down, and I didn't feel like getting into defining a new custom widget type.

Providing a real model method may well be preferable, and if we get to having a special user selector (instead of the search popup widget), then we may well need this.

I've added a comment to the top of the block of commands in the doctest.

Revision history for this message
Jonathan Lange (jml) wrote :

Thanks for getting back to me. I like the new comment, and won't block on the other stuff.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2010-04-21 02:50:20 +0000
3+++ lib/lp/code/browser/branch.py 2010-05-24 04:30:51 +0000
4@@ -44,6 +44,7 @@
5 from zope.interface import Interface, implements, providedBy
6 from zope.publisher.interfaces import NotFound
7 from zope.schema import Bool, Choice, Text
8+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
9 from lazr.delegates import delegates
10 from lazr.enum import EnumeratedType, Item
11 from lazr.lifecycle.event import ObjectModifiedEvent
12@@ -97,6 +98,7 @@
13 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
14 from lp.registry.interfaces.person import IPerson, IPersonSet
15 from lp.registry.interfaces.productseries import IProductSeries
16+from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
17
18
19 def quote(text):
20@@ -978,6 +980,27 @@
21 # Replace the normal owner field with a more permissive vocab.
22 self.form_fields = self.form_fields.omit('owner')
23 self.form_fields = any_owner_field + self.form_fields
24+ else:
25+ # For normal users, there is an edge case with package branches
26+ # where the editor may not be in the team of the branch owner. In
27+ # these cases we need to extend the vocabulary connected to the
28+ # owner field.
29+ if not self.user.inTeam(self.context.owner):
30+ vocab = UserTeamsParticipationPlusSelfVocabulary()
31+ owner = self.context.owner
32+ terms = [SimpleTerm(
33+ owner, owner.name, owner.unique_displayname)]
34+ terms.extend([term for term in vocab])
35+ owner_field = self.schema['owner']
36+ owner_choice = Choice(
37+ __name__='owner', title=owner_field.title,
38+ description = owner_field.description,
39+ required=True, vocabulary=SimpleVocabulary(terms))
40+ new_owner_field = form.Fields(
41+ owner_choice, render_context=self.render_context)
42+ # Replace the normal owner field with a more permissive vocab.
43+ self.form_fields = self.form_fields.omit('owner')
44+ self.form_fields = new_owner_field + self.form_fields
45
46 def validate(self, data):
47 # Check that we're not moving a team branch to the +junk
48
49=== modified file 'lib/lp/code/stories/branches/xx-branch-edit.txt'
50--- lib/lp/code/stories/branches/xx-branch-edit.txt 2010-03-18 15:39:58 +0000
51+++ lib/lp/code/stories/branches/xx-branch-edit.txt 2010-05-24 04:30:51 +0000
52@@ -212,7 +212,7 @@
53 >>> from lp.code.enums import (
54 ... BranchSubscriptionNotificationLevel, BranchSubscriptionDiffSize,
55 ... CodeReviewNotificationLevel)
56- >>> login('foo.bar@canonical.com')
57+ >>> login('admin@canonical.com')
58 >>> sample_person = getUtility(IPersonSet).getByName('name12')
59 >>> foogoo = factory.makeProduct(
60 ... name='foogoo', owner=sample_person)
61@@ -303,3 +303,49 @@
62 >>> admin_browser.getControl('Change Branch').click()
63 >>> print admin_browser.url
64 http://code.launchpad.dev/~mark/firefox/main
65+
66+
67+== Package branch editing by Uploaders ==
68+
69+Official branches for distro series source packages are editable by valid
70+package uploaders. The normal branch owner vocabulary is the editor and the
71+teams that they are a member of. Official branches may well have an owner
72+that is different to the editor, or be a celebrity that the editor is not a
73+member of, like ubuntu-branches.
74+
75+ >>> # Create an official branch owned by ~ubuntu-branches, and an editor
76+ >>> # who has nothing to do with ~ubuntu-branches but is still allowed
77+ >>> # to edit the branch.
78+ >>> login('admin@canonical.com')
79+ >>> from lp.code.tests.helpers import make_official_package_branch
80+ >>> from canonical.launchpad.interfaces.launchpad import (
81+ ... ILaunchpadCelebrities)
82+ >>> owner = getUtility(ILaunchpadCelebrities).ubuntu_branches
83+ >>> branch = make_official_package_branch(factory, owner=owner)
84+ >>> editor = factory.makePerson(
85+ ... name='editor', email='editor@example.com', password='test')
86+ >>> # Give editor upload rights.
87+ >>> archive = branch.distroseries.distribution.main_archive
88+ >>> spn = branch.sourcepackage.sourcepackagename
89+ >>> from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
90+ >>> permission_set = getUtility(IArchivePermissionSet)
91+ >>> ignored = permission_set.newPackageUploader(archive, editor, spn)
92+ >>> branch_url = canonical_url(branch)
93+ >>> logout()
94+
95+Even though the branch owner, Ubuntu branches in this example, is not related
96+to the editor, they stay as the default owner for this branch.
97+
98+ >>> browser = setupBrowser(auth='Basic editor@example.com:test')
99+ >>> browser.open(branch_url)
100+ >>> browser.getLink('Change branch details').click()
101+
102+The owner is still ubuntu-branches.
103+
104+ >>> browser.getControl('Owner').displayValue
105+ ['Ubuntu branches (ubuntu-branches)']
106+
107+But the editor has the option to change the owner to themselves.
108+
109+ >>> browser.getControl('Owner').displayOptions
110+ ['Ubuntu branches (ubuntu-branches)', 'Editor (editor)']
111
112=== modified file 'lib/lp/code/tests/helpers.py'
113--- lib/lp/code/tests/helpers.py 2010-04-06 01:13:28 +0000
114+++ lib/lp/code/tests/helpers.py 2010-05-24 04:30:51 +0000
115@@ -6,8 +6,9 @@
116 __metaclass__ = type
117 __all__ = [
118 'add_revision_to_branch',
119+ 'make_erics_fooix_project',
120 'make_linked_package_branch',
121- 'make_erics_fooix_project',
122+ 'make_official_package_branch',
123 ]
124
125
126@@ -19,13 +20,16 @@
127 from zope.security.proxy import removeSecurityProxy
128 from zope.security.proxy import isinstance as zisinstance
129
130+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
131+
132 from lp.code.interfaces.branchmergeproposal import (
133 IBranchMergeProposalJobSource)
134+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
135 from lp.code.interfaces.seriessourcepackagebranch import (
136 IMakeOfficialBranchLinks)
137 from lp.registry.interfaces.series import SeriesStatus
138 from lp.registry.interfaces.pocket import PackagePublishingPocket
139-from lp.testing import time_counter
140+from lp.testing import run_with_login, time_counter
141
142
143 def mark_all_merge_proposal_jobs_done():
144@@ -224,3 +228,25 @@
145 make_package_branches(
146 factory, series, name, branch_count, official_count,
147 owner=mint_team, registrant=albert)
148+
149+
150+def make_official_package_branch(factory, owner=None):
151+ """Make a branch linked to the pocket of a source package."""
152+ branch = factory.makePackageBranch(owner=owner)
153+ # Make sure the (distroseries, pocket) combination used allows us to
154+ # upload to it.
155+ stable_states = (
156+ SeriesStatus.SUPPORTED, SeriesStatus.CURRENT)
157+ if branch.distroseries.status in stable_states:
158+ pocket = PackagePublishingPocket.BACKPORTS
159+ else:
160+ pocket = PackagePublishingPocket.RELEASE
161+ sourcepackage = branch.sourcepackage
162+ suite_sourcepackage = sourcepackage.getSuiteSourcePackage(pocket)
163+ registrant = factory.makePerson()
164+ ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
165+ run_with_login(
166+ ubuntu_branches.teamowner,
167+ ICanHasLinkedBranch(suite_sourcepackage).setBranch,
168+ branch, registrant)
169+ return branch
170
171=== modified file 'lib/lp/code/tests/test_branch.py'
172--- lib/lp/code/tests/test_branch.py 2010-03-30 13:53:25 +0000
173+++ lib/lp/code/tests/test_branch.py 2010-05-24 04:30:51 +0000
174@@ -15,10 +15,8 @@
175 from lp.code.enums import (
176 BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,
177 CodeReviewNotificationLevel)
178-from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
179+from lp.code.tests.helpers import make_official_package_branch
180 from lp.code.xmlrpc.branch import PublicCodehostingAPI
181-from lp.registry.interfaces.series import SeriesStatus
182-from lp.registry.interfaces.pocket import PackagePublishingPocket
183 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
184 from lp.testing import run_with_login, TestCaseWithFactory
185
186@@ -243,24 +241,7 @@
187
188 def makeOfficialPackageBranch(self):
189 """Make a branch linked to the pocket of a source package."""
190- branch = self.factory.makePackageBranch()
191- # Make sure the (distroseries, pocket) combination used allows us to
192- # upload to it.
193- stable_states = (
194- SeriesStatus.SUPPORTED, SeriesStatus.CURRENT)
195- if branch.distroseries.status in stable_states:
196- pocket = PackagePublishingPocket.BACKPORTS
197- else:
198- pocket = PackagePublishingPocket.RELEASE
199- sourcepackage = branch.sourcepackage
200- suite_sourcepackage = sourcepackage.getSuiteSourcePackage(pocket)
201- registrant = self.factory.makePerson()
202- ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
203- run_with_login(
204- ubuntu_branches.teamowner,
205- ICanHasLinkedBranch(suite_sourcepackage).setBranch,
206- branch, registrant)
207- return branch
208+ return make_official_package_branch(self.factory)
209
210 def test_owner_can_write_to_official_package_branch(self):
211 # The owner of an official package branch can write to it, just like a
212@@ -305,7 +286,7 @@
213 permission_set = getUtility(IArchivePermissionSet)
214 # Only admins or techboard members can add permissions normally. That
215 # restriction isn't relevant to these tests.
216- self.permission_set = removeSecurityProxy(permission_set)
217+ permission_set = removeSecurityProxy(permission_set)
218 branch = self.makeOfficialPackageBranch()
219 package = branch.sourcepackage
220 person = self.factory.makePerson()
221@@ -316,7 +297,7 @@
222 # Now give 'person' permission to upload to 'package'.
223 archive = branch.distroseries.distribution.main_archive
224 spn = package.sourcepackagename
225- self.permission_set.newPackageUploader(archive, person, spn)
226+ permission_set.newPackageUploader(archive, person, spn)
227 # Make sure person *is* authorised to upload the source package
228 # targeted by the branch at hand.
229 self.assertCanUpload(person, spn, archive, None)
230
231=== modified file 'lib/lp/registry/vocabularies.py'
232--- lib/lp/registry/vocabularies.py 2010-05-18 17:43:58 +0000
233+++ lib/lp/registry/vocabularies.py 2010-05-24 04:30:51 +0000
234@@ -289,8 +289,7 @@
235
236 def toTerm(self, obj):
237 """See `IVocabulary`."""
238- return SimpleTerm(
239- obj, obj.name, '%s (%s)' % (obj.displayname, obj.name))
240+ return SimpleTerm(obj, obj.name, obj.unique_displayname)
241
242 def __iter__(self):
243 kw = {}