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
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2010-04-21 02:50:20 +0000
+++ lib/lp/code/browser/branch.py 2010-05-24 04:30:51 +0000
@@ -44,6 +44,7 @@
44from zope.interface import Interface, implements, providedBy44from zope.interface import Interface, implements, providedBy
45from zope.publisher.interfaces import NotFound45from zope.publisher.interfaces import NotFound
46from zope.schema import Bool, Choice, Text46from zope.schema import Bool, Choice, Text
47from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
47from lazr.delegates import delegates48from lazr.delegates import delegates
48from lazr.enum import EnumeratedType, Item49from lazr.enum import EnumeratedType, Item
49from lazr.lifecycle.event import ObjectModifiedEvent50from lazr.lifecycle.event import ObjectModifiedEvent
@@ -97,6 +98,7 @@
97from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference98from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
98from lp.registry.interfaces.person import IPerson, IPersonSet99from lp.registry.interfaces.person import IPerson, IPersonSet
99from lp.registry.interfaces.productseries import IProductSeries100from lp.registry.interfaces.productseries import IProductSeries
101from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
100102
101103
102def quote(text):104def quote(text):
@@ -978,6 +980,27 @@
978 # Replace the normal owner field with a more permissive vocab.980 # Replace the normal owner field with a more permissive vocab.
979 self.form_fields = self.form_fields.omit('owner')981 self.form_fields = self.form_fields.omit('owner')
980 self.form_fields = any_owner_field + self.form_fields982 self.form_fields = any_owner_field + self.form_fields
983 else:
984 # For normal users, there is an edge case with package branches
985 # where the editor may not be in the team of the branch owner. In
986 # these cases we need to extend the vocabulary connected to the
987 # owner field.
988 if not self.user.inTeam(self.context.owner):
989 vocab = UserTeamsParticipationPlusSelfVocabulary()
990 owner = self.context.owner
991 terms = [SimpleTerm(
992 owner, owner.name, owner.unique_displayname)]
993 terms.extend([term for term in vocab])
994 owner_field = self.schema['owner']
995 owner_choice = Choice(
996 __name__='owner', title=owner_field.title,
997 description = owner_field.description,
998 required=True, vocabulary=SimpleVocabulary(terms))
999 new_owner_field = form.Fields(
1000 owner_choice, render_context=self.render_context)
1001 # Replace the normal owner field with a more permissive vocab.
1002 self.form_fields = self.form_fields.omit('owner')
1003 self.form_fields = new_owner_field + self.form_fields
9811004
982 def validate(self, data):1005 def validate(self, data):
983 # Check that we're not moving a team branch to the +junk1006 # Check that we're not moving a team branch to the +junk
9841007
=== modified file 'lib/lp/code/stories/branches/xx-branch-edit.txt'
--- lib/lp/code/stories/branches/xx-branch-edit.txt 2010-03-18 15:39:58 +0000
+++ lib/lp/code/stories/branches/xx-branch-edit.txt 2010-05-24 04:30:51 +0000
@@ -212,7 +212,7 @@
212 >>> from lp.code.enums import (212 >>> from lp.code.enums import (
213 ... BranchSubscriptionNotificationLevel, BranchSubscriptionDiffSize,213 ... BranchSubscriptionNotificationLevel, BranchSubscriptionDiffSize,
214 ... CodeReviewNotificationLevel)214 ... CodeReviewNotificationLevel)
215 >>> login('foo.bar@canonical.com')215 >>> login('admin@canonical.com')
216 >>> sample_person = getUtility(IPersonSet).getByName('name12')216 >>> sample_person = getUtility(IPersonSet).getByName('name12')
217 >>> foogoo = factory.makeProduct(217 >>> foogoo = factory.makeProduct(
218 ... name='foogoo', owner=sample_person)218 ... name='foogoo', owner=sample_person)
@@ -303,3 +303,49 @@
303 >>> admin_browser.getControl('Change Branch').click()303 >>> admin_browser.getControl('Change Branch').click()
304 >>> print admin_browser.url304 >>> print admin_browser.url
305 http://code.launchpad.dev/~mark/firefox/main305 http://code.launchpad.dev/~mark/firefox/main
306
307
308== Package branch editing by Uploaders ==
309
310Official branches for distro series source packages are editable by valid
311package uploaders. The normal branch owner vocabulary is the editor and the
312teams that they are a member of. Official branches may well have an owner
313that is different to the editor, or be a celebrity that the editor is not a
314member of, like ubuntu-branches.
315
316 >>> # Create an official branch owned by ~ubuntu-branches, and an editor
317 >>> # who has nothing to do with ~ubuntu-branches but is still allowed
318 >>> # to edit the branch.
319 >>> login('admin@canonical.com')
320 >>> from lp.code.tests.helpers import make_official_package_branch
321 >>> from canonical.launchpad.interfaces.launchpad import (
322 ... ILaunchpadCelebrities)
323 >>> owner = getUtility(ILaunchpadCelebrities).ubuntu_branches
324 >>> branch = make_official_package_branch(factory, owner=owner)
325 >>> editor = factory.makePerson(
326 ... name='editor', email='editor@example.com', password='test')
327 >>> # Give editor upload rights.
328 >>> archive = branch.distroseries.distribution.main_archive
329 >>> spn = branch.sourcepackage.sourcepackagename
330 >>> from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
331 >>> permission_set = getUtility(IArchivePermissionSet)
332 >>> ignored = permission_set.newPackageUploader(archive, editor, spn)
333 >>> branch_url = canonical_url(branch)
334 >>> logout()
335
336Even though the branch owner, Ubuntu branches in this example, is not related
337to the editor, they stay as the default owner for this branch.
338
339 >>> browser = setupBrowser(auth='Basic editor@example.com:test')
340 >>> browser.open(branch_url)
341 >>> browser.getLink('Change branch details').click()
342
343The owner is still ubuntu-branches.
344
345 >>> browser.getControl('Owner').displayValue
346 ['Ubuntu branches (ubuntu-branches)']
347
348But the editor has the option to change the owner to themselves.
349
350 >>> browser.getControl('Owner').displayOptions
351 ['Ubuntu branches (ubuntu-branches)', 'Editor (editor)']
306352
=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py 2010-04-06 01:13:28 +0000
+++ lib/lp/code/tests/helpers.py 2010-05-24 04:30:51 +0000
@@ -6,8 +6,9 @@
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'add_revision_to_branch',8 'add_revision_to_branch',
9 'make_erics_fooix_project',
9 'make_linked_package_branch',10 'make_linked_package_branch',
10 'make_erics_fooix_project',11 'make_official_package_branch',
11 ]12 ]
1213
1314
@@ -19,13 +20,16 @@
19from zope.security.proxy import removeSecurityProxy20from zope.security.proxy import removeSecurityProxy
20from zope.security.proxy import isinstance as zisinstance21from zope.security.proxy import isinstance as zisinstance
2122
23from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
24
22from lp.code.interfaces.branchmergeproposal import (25from lp.code.interfaces.branchmergeproposal import (
23 IBranchMergeProposalJobSource)26 IBranchMergeProposalJobSource)
27from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
24from lp.code.interfaces.seriessourcepackagebranch import (28from lp.code.interfaces.seriessourcepackagebranch import (
25 IMakeOfficialBranchLinks)29 IMakeOfficialBranchLinks)
26from lp.registry.interfaces.series import SeriesStatus30from lp.registry.interfaces.series import SeriesStatus
27from lp.registry.interfaces.pocket import PackagePublishingPocket31from lp.registry.interfaces.pocket import PackagePublishingPocket
28from lp.testing import time_counter32from lp.testing import run_with_login, time_counter
2933
3034
31def mark_all_merge_proposal_jobs_done():35def mark_all_merge_proposal_jobs_done():
@@ -224,3 +228,25 @@
224 make_package_branches(228 make_package_branches(
225 factory, series, name, branch_count, official_count,229 factory, series, name, branch_count, official_count,
226 owner=mint_team, registrant=albert)230 owner=mint_team, registrant=albert)
231
232
233def make_official_package_branch(factory, owner=None):
234 """Make a branch linked to the pocket of a source package."""
235 branch = factory.makePackageBranch(owner=owner)
236 # Make sure the (distroseries, pocket) combination used allows us to
237 # upload to it.
238 stable_states = (
239 SeriesStatus.SUPPORTED, SeriesStatus.CURRENT)
240 if branch.distroseries.status in stable_states:
241 pocket = PackagePublishingPocket.BACKPORTS
242 else:
243 pocket = PackagePublishingPocket.RELEASE
244 sourcepackage = branch.sourcepackage
245 suite_sourcepackage = sourcepackage.getSuiteSourcePackage(pocket)
246 registrant = factory.makePerson()
247 ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
248 run_with_login(
249 ubuntu_branches.teamowner,
250 ICanHasLinkedBranch(suite_sourcepackage).setBranch,
251 branch, registrant)
252 return branch
227253
=== modified file 'lib/lp/code/tests/test_branch.py'
--- lib/lp/code/tests/test_branch.py 2010-03-30 13:53:25 +0000
+++ lib/lp/code/tests/test_branch.py 2010-05-24 04:30:51 +0000
@@ -15,10 +15,8 @@
15from lp.code.enums import (15from lp.code.enums import (
16 BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,16 BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,
17 CodeReviewNotificationLevel)17 CodeReviewNotificationLevel)
18from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch18from lp.code.tests.helpers import make_official_package_branch
19from lp.code.xmlrpc.branch import PublicCodehostingAPI19from lp.code.xmlrpc.branch import PublicCodehostingAPI
20from lp.registry.interfaces.series import SeriesStatus
21from lp.registry.interfaces.pocket import PackagePublishingPocket
22from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet20from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
23from lp.testing import run_with_login, TestCaseWithFactory21from lp.testing import run_with_login, TestCaseWithFactory
2422
@@ -243,24 +241,7 @@
243241
244 def makeOfficialPackageBranch(self):242 def makeOfficialPackageBranch(self):
245 """Make a branch linked to the pocket of a source package."""243 """Make a branch linked to the pocket of a source package."""
246 branch = self.factory.makePackageBranch()244 return make_official_package_branch(self.factory)
247 # Make sure the (distroseries, pocket) combination used allows us to
248 # upload to it.
249 stable_states = (
250 SeriesStatus.SUPPORTED, SeriesStatus.CURRENT)
251 if branch.distroseries.status in stable_states:
252 pocket = PackagePublishingPocket.BACKPORTS
253 else:
254 pocket = PackagePublishingPocket.RELEASE
255 sourcepackage = branch.sourcepackage
256 suite_sourcepackage = sourcepackage.getSuiteSourcePackage(pocket)
257 registrant = self.factory.makePerson()
258 ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
259 run_with_login(
260 ubuntu_branches.teamowner,
261 ICanHasLinkedBranch(suite_sourcepackage).setBranch,
262 branch, registrant)
263 return branch
264245
265 def test_owner_can_write_to_official_package_branch(self):246 def test_owner_can_write_to_official_package_branch(self):
266 # The owner of an official package branch can write to it, just like a247 # The owner of an official package branch can write to it, just like a
@@ -305,7 +286,7 @@
305 permission_set = getUtility(IArchivePermissionSet)286 permission_set = getUtility(IArchivePermissionSet)
306 # Only admins or techboard members can add permissions normally. That287 # Only admins or techboard members can add permissions normally. That
307 # restriction isn't relevant to these tests.288 # restriction isn't relevant to these tests.
308 self.permission_set = removeSecurityProxy(permission_set)289 permission_set = removeSecurityProxy(permission_set)
309 branch = self.makeOfficialPackageBranch()290 branch = self.makeOfficialPackageBranch()
310 package = branch.sourcepackage291 package = branch.sourcepackage
311 person = self.factory.makePerson()292 person = self.factory.makePerson()
@@ -316,7 +297,7 @@
316 # Now give 'person' permission to upload to 'package'.297 # Now give 'person' permission to upload to 'package'.
317 archive = branch.distroseries.distribution.main_archive298 archive = branch.distroseries.distribution.main_archive
318 spn = package.sourcepackagename299 spn = package.sourcepackagename
319 self.permission_set.newPackageUploader(archive, person, spn)300 permission_set.newPackageUploader(archive, person, spn)
320 # Make sure person *is* authorised to upload the source package301 # Make sure person *is* authorised to upload the source package
321 # targeted by the branch at hand.302 # targeted by the branch at hand.
322 self.assertCanUpload(person, spn, archive, None)303 self.assertCanUpload(person, spn, archive, None)
323304
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-05-18 17:43:58 +0000
+++ lib/lp/registry/vocabularies.py 2010-05-24 04:30:51 +0000
@@ -289,8 +289,7 @@
289289
290 def toTerm(self, obj):290 def toTerm(self, obj):
291 """See `IVocabulary`."""291 """See `IVocabulary`."""
292 return SimpleTerm(292 return SimpleTerm(obj, obj.name, obj.unique_displayname)
293 obj, obj.name, '%s (%s)' % (obj.displayname, obj.name))
294293
295 def __iter__(self):294 def __iter__(self):
296 kw = {}295 kw = {}