Merge lp:~thumper/launchpad/trusted-reviewer into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~thumper/launchpad/trusted-reviewer
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~thumper/launchpad/trusted-reviewer
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+9737@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

= Summary =

The (Community) tag is incorrectly being shown on some reviews.

Normally when the branch is owned by a team and there is no review team
specified.

== Proposed fix ==

The browser view was looking at IBranch.reviewer instead of
IBranch.code_reviewer. Instead of just fixing this, I went to the source of
the problem and added a method 'isPersonTrustedReviewer' to the IBranch
interface. It was always intended that the trust is based on being the owner
of the branch OR in the review team. The current implementation only checked
the owner if the review team wasn't specified. This branch fixes this
deficiency.

The 'isPersonValidReviewer' is removed from IBranchMergeProposal, and call
sites are updated to check 'isPersonTrustedReviewer' on the target branch.

== Implementation details ==

The 'makeBranch' factory method is updated to be able to specify the
Branch.reviewer attribute. This is done by removing the security proxy so the
call site doesn't need to worry about launchpad.Edit for the branch.

== Tests ==

TestBranchIsPersonTrustedReviewer

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hi, looks basically fine.

Two questions:

1) is it really sane for admins to be able to review anything officially?
2) can you add a test of isPersonTrustedReviewer via the web service?

Cheers,
mwh

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/branchmergeproposal.py'
2--- lib/lp/code/browser/branchmergeproposal.py 2009-08-05 02:18:13 +0000
3+++ lib/lp/code/browser/branchmergeproposal.py 2009-08-06 03:33:44 +0000
4@@ -433,13 +433,11 @@
5
6 def __init__(self, context, user, users_vote):
7 self.context = context
8- is_mergable = self.context.branch_merge_proposal.isMergable()
9+ proposal = self.context.branch_merge_proposal
10+ is_mergable = proposal.isMergable()
11 self.can_change_review = (user == context.reviewer) and is_mergable
12- branch = context.branch_merge_proposal.source_branch
13- review_team = context.branch_merge_proposal.target_branch.reviewer
14- reviewer = context.reviewer
15- self.trusted = (reviewer is not None and reviewer.inTeam(
16- review_team))
17+ self.trusted = proposal.target_branch.isPersonTrustedReviewer(
18+ context.reviewer)
19 if user is None:
20 self.user_can_review = False
21 else:
22@@ -802,7 +800,7 @@
23 def initial_values(self):
24 # If the user is a valid reviewer, then default the revision
25 # number to be the tip.
26- if self.context.isPersonValidReviewer(self.user):
27+ if self.context.target_branch.isPersonTrustedReviewer(self.user):
28 revision_number = self.context.source_branch.revision_count
29 else:
30 revision_number = self._getRevisionNumberForRevisionId(
31@@ -820,14 +818,14 @@
32 # If the user is not a valid reviewer for the target branch,
33 # then the revision number should be read only, so an
34 # untrusted user cannot land changes that have not bee reviewed.
35- if not self.context.isPersonValidReviewer(self.user):
36+ if not self.context.target_branch.isPersonTrustedReviewer(self.user):
37 self.form_fields['revision_number'].for_display = True
38
39 @action('Enqueue', name='enqueue')
40 @update_and_notify
41 def enqueue_action(self, action, data):
42 """Update the whiteboard and enqueue the merge proposal."""
43- if self.context.isPersonValidReviewer(self.user):
44+ if self.context.target_branch.isPersonTrustedReviewer(self.user):
45 revision_id = self._getRevisionId(data)
46 else:
47 revision_id = self.context.reviewed_revision_id
48
49=== modified file 'lib/lp/code/configure.zcml'
50--- lib/lp/code/configure.zcml 2009-07-30 00:30:02 +0000
51+++ lib/lp/code/configure.zcml 2009-08-06 03:33:44 +0000
52@@ -181,7 +181,6 @@
53 votes
54 root_comment
55 all_comments
56- isPersonValidReviewer
57 isMergable
58 getComment
59 getNotificationRecipients
60@@ -348,6 +347,7 @@
61 author
62 reviewer
63 code_reviewer
64+ isPersonTrustedReviewer
65 product
66 unique_name
67 displayname
68
69=== modified file 'lib/lp/code/interfaces/branch.py'
70--- lib/lp/code/interfaces/branch.py 2009-08-05 04:02:58 +0000
71+++ lib/lp/code/interfaces/branch.py 2009-08-06 03:33:44 +0000
72@@ -482,6 +482,19 @@
73 code_reviewer = Attribute(
74 "The reviewer if set, otherwise the owner of the branch.")
75
76+ @operation_parameters(
77+ reviewer=Reference(
78+ title=_("A person for which the reviewer status is in question."),
79+ schema=IPerson))
80+ @export_read_operation()
81+ def isPersonTrustedReviewer(reviewer):
82+ """Return true if the `reviewer` is a trusted reviewer.
83+
84+ The reviewer is trusted if they are either own the branch, or are in
85+ the team that owns the branch, or they are in the review team for the
86+ branch.
87+ """
88+
89 namespace = Attribute(
90 "The namespace of this branch, as an `IBranchNamespace`.")
91
92
93=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
94--- lib/lp/code/interfaces/branchmergeproposal.py 2009-08-04 05:02:41 +0000
95+++ lib/lp/code/interfaces/branchmergeproposal.py 2009-08-06 00:15:48 +0000
96@@ -395,21 +395,6 @@
97 user-entered data like the whiteboard.
98 """
99
100- @operation_parameters(
101- reviewer=Reference(
102- title=_("A person for which the reviewer status is in question."),
103- schema=IPerson))
104- @export_read_operation()
105- def isPersonValidReviewer(reviewer):
106- """Return true if the `reviewer` is able to review the proposal.
107-
108- There is an attribute on branches called `reviewer` which allows
109- a specific person or team to be set for a branch as an authorised
110- person to approve merges for a branch. If a reviewer is not set
111- on the target branch, then the owner of the target branch is used
112- as the authorised user.
113- """
114-
115 def isMergable():
116 """Is the proposal in a state that allows it to being merged?
117
118
119=== modified file 'lib/lp/code/model/branch.py'
120--- lib/lp/code/model/branch.py 2009-07-31 01:07:04 +0000
121+++ lib/lp/code/model/branch.py 2009-08-06 03:33:44 +0000
122@@ -36,6 +36,7 @@
123
124 from canonical.launchpad import _
125 from lp.services.job.model.job import Job
126+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
127 from canonical.launchpad.mailnotification import NotificationRecipientSet
128 from canonical.launchpad.webapp import urlappend
129 from canonical.launchpad.webapp.interfaces import (
130@@ -440,6 +441,20 @@
131 else:
132 return self.owner
133
134+ def isPersonTrustedReviewer(self, reviewer):
135+ """See `IBranch`."""
136+ if reviewer is None:
137+ return False
138+ # We trust Launchpad admins.
139+ lp_admins = getUtility(ILaunchpadCelebrities).admin
140+ # Both the branch owner and the review team are checked.
141+ owner = self.owner
142+ review_team = self.code_reviewer
143+ return (
144+ reviewer.inTeam(owner) or
145+ reviewer.inTeam(review_team) or
146+ reviewer.inTeam(lp_admins))
147+
148 def latest_revisions(self, quantity=10):
149 """See `IBranch`."""
150 return self.revision_history.limit(quantity)
151
152=== modified file 'lib/lp/code/model/branchmergeproposal.py'
153--- lib/lp/code/model/branchmergeproposal.py 2009-07-23 18:08:35 +0000
154+++ lib/lp/code/model/branchmergeproposal.py 2009-08-06 00:15:48 +0000
155@@ -47,7 +47,6 @@
156 IBranchMergeProposal, IBranchMergeProposalGetter, UserNotBranchReviewer,
157 WrongBranchMergeProposal)
158 from lp.code.interfaces.branchtarget import IHasBranchTarget
159-from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
160 from lp.registry.interfaces.person import IPerson
161 from lp.registry.interfaces.product import IProduct
162 from lp.code.mail.branch import RecipientReason
163@@ -78,7 +77,7 @@
164 # Transitioning to code approved, rejected or queued from
165 # work in progress, needs review or merge failed needs the
166 # user to be a valid reviewer, other states are fine.
167- valid_reviewer = proposal.isPersonValidReviewer(user)
168+ valid_reviewer = proposal.target_branch.isPersonTrustedReviewer(user)
169 if (next_state == rejected and not valid_reviewer):
170 return False
171 # Non-reviewers can toggle between code_approved and queued, but not
172@@ -329,15 +328,6 @@
173 self._transitionToState(BranchMergeProposalStatus.NEEDS_REVIEW)
174 self.date_review_requested = UTC_NOW
175
176- def isPersonValidReviewer(self, reviewer):
177- """See `IBranchMergeProposal`."""
178- if reviewer is None:
179- return False
180- # We trust Launchpad admins.
181- lp_admins = getUtility(ILaunchpadCelebrities).admin
182- return (reviewer.inTeam(self.target_branch.code_reviewer) or
183- reviewer.inTeam(lp_admins))
184-
185 def isMergable(self):
186 """See `IBranchMergeProposal`."""
187 # As long as the source branch has not been merged, rejected
188@@ -348,7 +338,7 @@
189 """Set the proposal to one of the two review statuses."""
190 # Check the reviewer can review the code for the target branch.
191 old_state = self.queue_status
192- if not self.isPersonValidReviewer(reviewer):
193+ if not self.target_branch.isPersonTrustedReviewer(reviewer):
194 raise UserNotBranchReviewer
195 # Check the current state of the proposal.
196 self._transitionToState(next_state, reviewer)
197
198=== modified file 'lib/lp/code/model/tests/test_branch.py'
199--- lib/lp/code/model/tests/test_branch.py 2009-08-05 02:04:06 +0000
200+++ lib/lp/code/model/tests/test_branch.py 2009-08-06 03:33:44 +0000
201@@ -1651,6 +1651,73 @@
202 self.assertEqual(branch.spec_links.count(), 0)
203
204
205+class TestBranchIsPersonTrustedReviewer(TestCaseWithFactory):
206+ """Test the `IBranch.isPersonTrustedReviewer` method."""
207+
208+ layer = DatabaseFunctionalLayer
209+
210+ def assertTrustedReviewer(self, branch, person):
211+ """Assert that `person` is a trusted reviewer for the `branch`."""
212+ self.assertTrue(branch.isPersonTrustedReviewer(person))
213+
214+ def assertNotTrustedReviewer(self, branch, person):
215+ """Assert that `person` is not a trusted reviewer for the `branch`."""
216+ self.assertFalse(branch.isPersonTrustedReviewer(person))
217+
218+ def test_none_is_not_trusted(self):
219+ # If None is passed in as the person, the method returns false.
220+ branch = self.factory.makeAnyBranch()
221+ self.assertNotTrustedReviewer(branch, None)
222+
223+ def test_branch_owner_is_trusted(self):
224+ # The branch owner is a trusted reviewer.
225+ branch = self.factory.makeAnyBranch()
226+ self.assertTrustedReviewer(branch, branch.owner)
227+
228+ def test_non_branch_owner_is_not_trusted(self):
229+ # Someone other than the branch owner is not a trusted reviewer.
230+ branch = self.factory.makeAnyBranch()
231+ reviewer = self.factory.makePerson()
232+ self.assertNotTrustedReviewer(branch, reviewer)
233+
234+ def test_lp_admins_always_trusted(self):
235+ # Launchpad admins are special, and as such, are trusted.
236+ branch = self.factory.makeAnyBranch()
237+ admins = getUtility(ILaunchpadCelebrities).admin
238+ # Grab a random admin, the teamowner is good enough here.
239+ self.assertTrustedReviewer(branch, admins.teamowner)
240+
241+ def test_member_of_team_owned_branch(self):
242+ # If the branch is owned by a team, any team member is a trusted
243+ # reviewer.
244+ team = self.factory.makeTeam()
245+ branch = self.factory.makeAnyBranch(owner=team)
246+ self.assertTrustedReviewer(branch, team.teamowner)
247+
248+ def test_review_team_member_is_trusted(self):
249+ # If the reviewer is a member of the review team, but not the owner
250+ # they are still trusted.
251+ team = self.factory.makeTeam()
252+ branch = self.factory.makeAnyBranch(reviewer=team)
253+ self.assertTrustedReviewer(branch, team.teamowner)
254+
255+ def test_branch_owner_not_review_team_member_is_trusted(self):
256+ # If the owner of the branch is not in the review team, they are still
257+ # trusted.
258+ team = self.factory.makeTeam()
259+ branch = self.factory.makeAnyBranch(reviewer=team)
260+ self.assertFalse(branch.owner.inTeam(team))
261+ self.assertTrustedReviewer(branch, branch.owner)
262+
263+ def test_community_reviewer(self):
264+ # If the reviewer is not a member of the owner, or the review team,
265+ # they are not trusted reviewers.
266+ team = self.factory.makeTeam()
267+ branch = self.factory.makeAnyBranch(reviewer=team)
268+ reviewer = self.factory.makePerson()
269+ self.assertNotTrustedReviewer(branch, reviewer)
270+
271+
272 class TestBranchSetOwner(TestCaseWithFactory):
273 """Tests for IBranch.setOwner."""
274
275
276=== modified file 'lib/lp/code/model/tests/test_branchmergeproposals.py'
277--- lib/lp/code/model/tests/test_branchmergeproposals.py 2009-07-22 16:02:55 +0000
278+++ lib/lp/code/model/tests/test_branchmergeproposals.py 2009-08-06 00:15:48 +0000
279@@ -22,6 +22,7 @@
280 from canonical.testing import (
281 DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
282
283+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
284 from lp.code.model.branchmergeproposaljob import (
285 BranchMergeProposalJob, BranchMergeProposalJobType,
286 CreateMergeProposalJob, MergeProposalCreatedJob)
287@@ -32,7 +33,7 @@
288 NewBranchMergeProposalEvent, NewCodeReviewCommentEvent,
289 ReviewerNominatedEvent)
290 from canonical.launchpad.ftests import (
291- ANONYMOUS, import_secret_test_key, login, logout, syncUpdate)
292+ ANONYMOUS, import_secret_test_key, login, syncUpdate)
293 from lp.code.enums import (
294 BranchMergeProposalStatus, BranchSubscriptionNotificationLevel,
295 BranchType, CodeReviewNotificationLevel, CodeReviewVote)
296@@ -395,36 +396,6 @@
297 proposal.date_created, proposal.date_review_requested)
298
299
300-class TestBranchMergeProposalCanReview(TestCase):
301- """Test the different cases that makes a branch deletable or not."""
302-
303- layer = DatabaseFunctionalLayer
304-
305- def setUp(self):
306- login('test@canonical.com')
307-
308- factory = LaunchpadObjectFactory()
309- self.source_branch = factory.makeProductBranch()
310- self.target_branch = factory.makeProductBranch(
311- product=self.source_branch.product)
312- registrant = factory.makePerson()
313- self.proposal = self.source_branch.addLandingTarget(
314- registrant, self.target_branch)
315-
316- def tearDown(self):
317- logout()
318-
319- def test_validReviewer(self):
320- """A newly created branch can be deleted without any problems."""
321- self.assertEqual(self.proposal.isPersonValidReviewer(None),
322- False, "No user cannot review code")
323- # The owner of the target branch is a valid reviewer.
324- self.assertEqual(
325- self.proposal.isPersonValidReviewer(
326- self.target_branch.owner),
327- True, "No user cannot review code")
328-
329-
330 class TestBranchMergeProposalQueueing(TestCase):
331 """Test the enqueueing and dequeueing of merge proposals."""
332
333
334=== modified file 'lib/lp/code/stories/webservice/xx-branchmergeproposal.txt'
335--- lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-07-16 01:22:05 +0000
336+++ lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-08-06 00:15:48 +0000
337@@ -117,14 +117,7 @@
338
339 A review can be performed through the API.
340
341- >>> person = webservice.get('/~target').jsonBody()
342- >>> is_valid = webservice.named_get(
343- ... merge_proposal['self_link'], 'isPersonValidReviewer',
344- ... reviewer=person['self_link']).jsonBody()
345- >>> print is_valid
346- True
347-
348-A review can be requested of the person 'target', since he's a valid reviewer.
349+A review can be requested of the person 'target'.
350
351 >>> reviewer_webservice = webservice_for_person(
352 ... target_owner, permission=OAuthPermission.WRITE_PUBLIC)
353
354=== modified file 'lib/lp/testing/factory.py'
355--- lib/lp/testing/factory.py 2009-08-03 21:46:09 +0000
356+++ lib/lp/testing/factory.py 2009-08-06 03:33:44 +0000
357@@ -574,7 +574,7 @@
358 def makeBranch(self, branch_type=None, owner=None,
359 name=None, product=_DEFAULT, url=_DEFAULT, registrant=None,
360 private=False, stacked_on=None, sourcepackage=None,
361- **optional_branch_args):
362+ reviewer=None, **optional_branch_args):
363 """Create and return a new, arbitrary Branch of the given type.
364
365 Any parameters for `IBranchNamespace.createBranch` can be specified to
366@@ -624,6 +624,8 @@
367 removeSecurityProxy(branch).private = True
368 if stacked_on is not None:
369 removeSecurityProxy(branch).stacked_on = stacked_on
370+ if reviewer is not None:
371+ removeSecurityProxy(branch).reviewer = reviewer
372 return branch
373
374 def makePackageBranch(self, sourcepackage=None, distroseries=None,