Merge lp:~thumper/launchpad/reassign-review-into-model into lp:launchpad/db-devel

Proposed by Tim Penhey
Status: Merged
Approved by: Jonathan Lange
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~thumper/launchpad/reassign-review-into-model
Merge into: lp:launchpad/db-devel
Diff against target: 394 lines (+183/-37)
6 files modified
lib/lp/code/browser/codereviewvote.py (+11/-7)
lib/lp/code/errors.py (+5/-0)
lib/lp/code/interfaces/codereviewvote.py (+35/-1)
lib/lp/code/model/codereviewvote.py (+36/-13)
lib/lp/code/model/tests/test_codereviewvote.py (+81/-6)
lib/lp/code/stories/branches/xx-branchmergeproposals.txt (+15/-10)
To merge this branch: bzr merge lp:~thumper/launchpad/reassign-review-into-model
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+15991@code.launchpad.net

Commit message

Move the review reassignment logic into the model.

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

This branch moves the logic for review reassignment into the model code.

tests:
  TestCodeReviewVoteReferenceReassignReview
  TestReassignReviewer

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (8.4 KiB)

On Fri, Dec 11, 2009 at 11:59 AM, Tim Penhey <email address hidden> wrote:
> Tim Penhey has proposed merging lp:~thumper/launchpad/reassign-review-into-model into lp:launchpad.
>
>    Requested reviews:
>    Canonical Launchpad Engineering (launchpad)
> Related bugs:
>  #495201 Review reassignment doesn't check for existing reviews
>  https://bugs.launchpad.net/bugs/495201
>
>
> This branch moves the logic for review reassignment into the model code.
>
> tests:
>  TestCodeReviewVoteReferenceReassignReview
>  TestReassignReviewer
>
> --
> https://code.launchpad.net/~thumper/launchpad/reassign-review-into-model/+merge/15991
> Your team Launchpad code reviewers from Canonical is subscribed to branch lp:launchpad.
>
> === modified file 'lib/lp/code/browser/codereviewvote.py'
> --- lib/lp/code/browser/codereviewvote.py       2009-12-10 20:46:32 +0000
> +++ lib/lp/code/browser/codereviewvote.py       2009-12-11 00:59:25 +0000
...
> @@ -35,8 +32,5 @@
>     @action('Reassign', name='reassign')
>     def reassign_action(self, action, data):
>         """Use the form data to change the review request reviewer."""
> -        # XXX TimPenhey 2009-12-11 bug=495201
> -        # This should check for existing reviews by the reviewer, and have
> -        # the logic moved into the model code.
> -        removeSecurityProxy(self.context).reviewer = data['reviewer']
> +        self.context.reassignReview(data['reviewer'])

This function can raise errors that simple assignment would not. How
are you handling those errors?

Hooray for removing the removeSecurityProxy call.

> === modified file 'lib/lp/code/errors.py'
> --- lib/lp/code/errors.py       2009-12-07 06:51:42 +0000
> +++ lib/lp/code/errors.py       2009-12-11 00:59:25 +0000
> @@ -10,6 +10,7 @@
>     'BranchMergeProposalExists',
>     'ClaimReviewFailed',
>     'InvalidBranchMergeProposal',
> +    'ReassignReviewFailed',
>     'ReviewNotPending',
>     'UserNotBranchReviewer',
>     'WrongBranchMergeProposal',
> @@ -39,6 +40,10 @@
>     """Raised if there is already a matching BranchMergeProposal."""
>
>
> +class ReassignReviewFailed(Exception):
> +    """Failed to reassign the review request."""
> +
> +
>  class ReviewNotPending(Exception):
>     """The requested review is not in a pending state."""
>
>
> === modified file 'lib/lp/code/interfaces/codereviewvote.py'
> --- lib/lp/code/interfaces/codereviewvote.py    2009-12-07 06:51:42 +0000
> +++ lib/lp/code/interfaces/codereviewvote.py    2009-12-11 00:59:25 +0000
> @@ -17,10 +17,11 @@
>     IBranchMergeProposal)
>  from lp.code.interfaces.codereviewcomment import (
>     ICodeReviewComment)
> +from lp.registry.interfaces.person import IPerson
>  from lazr.restful.fields import Reference
>  from lazr.restful.declarations import (
>     call_with, export_as_webservice_entry, export_destructor_operation,
> -    export_write_operation, exported, REQUEST_USER)
> +    export_write_operation, exported, operation_parameters, REQUEST_USER)
>
>
>  class ICodeReviewVoteReferencePublic(Interface):
> @@ -86,6 +87,22 @@
>             not pending.
>         """
>
> +    @operation_parameters(
> +        reviewer=Reference(
> +            title=_("The person or ...

Read more...

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

Hey Tim,

Thanks for doing this. It's encouraging to see XXXs and removeSecurityProxy calls disappear.

I've got a few questions, mostly around the terminology that the patch uses. I look forward to hearing back from you.

jml

review: Needs Fixing
Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (8.2 KiB)

On Fri, 11 Dec 2009 15:36:17 Jonathan Lange wrote:
> This function can raise errors that simple assignment would not. How
> are you handling those errors?

Right now I'm not. We are only one step better where we are not getting the
data into an inconsistent state, but the error handling still needs work.

I could change the XXX and file another bug if you like. The same error
handling is needed for claiming reviews.

Moving in baby steps.

> Hooray for removing the removeSecurityProxy call.
>
> > === modified file 'lib/lp/code/errors.py'
> > --- lib/lp/code/errors.py 2009-12-07 06:51:42 +0000
> > +++ lib/lp/code/errors.py 2009-12-11 00:59:25 +0000
> > @@ -10,6 +10,7 @@
> > 'BranchMergeProposalExists',
> > 'ClaimReviewFailed',
> > 'InvalidBranchMergeProposal',
> > + 'ReassignReviewFailed',
> > 'ReviewNotPending',
> > 'UserNotBranchReviewer',
> > 'WrongBranchMergeProposal',
> > @@ -39,6 +40,10 @@
> > """Raised if there is already a matching BranchMergeProposal."""
> >
> >
> > +class ReassignReviewFailed(Exception):
> > + """Failed to reassign the review request."""
> > +
> > +
> > class ReviewNotPending(Exception):
> > """The requested review is not in a pending state."""
> >
> >
> > === modified file 'lib/lp/code/interfaces/codereviewvote.py'
> > --- lib/lp/code/interfaces/codereviewvote.py 2009-12-07 06:51:42 +0000
> > +++ lib/lp/code/interfaces/codereviewvote.py 2009-12-11 00:59:25 +0000
> > @@ -17,10 +17,11 @@
> > IBranchMergeProposal)
> > from lp.code.interfaces.codereviewcomment import (
> > ICodeReviewComment)
> > +from lp.registry.interfaces.person import IPerson
> > from lazr.restful.fields import Reference
> > from lazr.restful.declarations import (
> > call_with, export_as_webservice_entry, export_destructor_operation,
> > - export_write_operation, exported, REQUEST_USER)
> > + export_write_operation, exported, operation_parameters,
> > REQUEST_USER)
> >
> >
> > class ICodeReviewVoteReferencePublic(Interface):
> > @@ -86,6 +87,22 @@
> > not pending.
> > """
> >
> > + @operation_parameters(
> > + reviewer=Reference(
> > + title=_("The person or team to assign to do the review."),
> > + schema=IPerson))
> > + @export_write_operation()
> > + def reassignReview(reviewer):
> > + """Reassign a pending review to someone else.
> > +
> > + Pending reviews can be reassigned to someone else.
> > +
> > + :param reviewer: The person to assign the pending review to.
> > + :raises ReviewNotPending: If the review is not pending.
> > + :raises ReassignReviewFailed: If the reviewer is an individual
> > and + already has a personal review.
> > + """
> > +
> > @export_destructor_operation()
> > def delete():
> > """Delete the pending review.
> >
> > === modified file 'lib/lp/code/model/codereviewvote.py'
> > --- lib/lp/code/model/codereviewvote.py 2009-12-07 06:51:42 +0000
> > +++ lib/lp/code/model/codereviewvote.py 2009-12-11 00:59:25 +0000
>
> ...
>
> > @@ -66,6 +67,22 @@
> > error_str % claimant.unique_displayname)
> > ...

Read more...

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (3.5 KiB)

On Fri, Dec 11, 2009 at 3:15 PM, Tim Penhey <email address hidden> wrote:
> On Fri, 11 Dec 2009 15:36:17 Jonathan Lange wrote:
>> This function can raise errors that simple assignment would not. How
>> are you handling those errors?
>
> Right now I'm not.  We are only one step better where we are not getting the
> data into an inconsistent state, but the error handling still needs work.
>
> I could change the XXX and file another bug if you like.  The same error
> handling is needed for claiming reviews.
>
> Moving in baby steps.
>

That means it will raise an OOPS, right? Is it really that difficult to fix it?

...
>> >  self.branch_merge_proposal.getUsersVoteReference(reviewer)) +
>> >  if existing_review is not None:
>> > +                if existing_review.is_pending:
>> > +                    error_str = '%s has an existing pending review'
>> > +                else:
>> > +                    error_str = '%s has an existing personal review'
>>
>> This code path is untested. AFAICT, you only test
>> ReassignReviewerFailed once, so you cannot possibly be testing both
>> strings. Can you please either drop the condition or add a test?
>
> Yeah, can do.  I was considering this.
>

Thanks.

>> > +                raise ReassignReviewFailed(
>> > +                    error_str % reviewer.unique_displayname)
>> > +        self.reviewer = reviewer
>> > +
>>
>> The logic here is straightforward, but it's hard to figure out what
>> the error messages actually mean.
>>
>> Why does "pending" matter? Do these error strings get displayed to the
>> user? If so, they don't give the user any indication as to what to do
>> about the problem.
>
> I wrote it with the intent of showing them to the user, and in fact they are
> shown to a user using the API.  Pending only matters for the readabiltiy of
> the error, and for no other reason.
>

In that case, the text needs to be changed. If I tried to reassign a
review to you and was told "Tim Penhey already has an existing
personal review", I wouldn't know what do about it, or really what a
personal review was.

Doesn't it really mean "Tim Penhey has already reviewed this" or "Tim
Penhey has already been asked to review this"?

...
>> > +    def test_reassign_to_user_existing(self):
>> > +        # If a user has an existing review, they cannot have another
>> > +        # pending review assigned to them.
>> > +        bmp, review = self.makeMergeProposalWithReview()
>> > +        reviewer = self.factory.makePerson()
>> > +        user_review = bmp.nominateReviewer(
>> > +            reviewer=reviewer, registrant=bmp.registrant)
>> > +        self.assertRaises(
>> > +            ReassignReviewFailed, review.reassignReview, reviewer)
>> > +
>> > +    def test_reassign_to_team_existing(self):
>> > +        # If a team has an existing review, they can have another
>> > pending +        # review assigned to them.
>>
>> It took me a while to grok that the important different between this
>> test and the previous test is that one is for users and the other is
>> for teams.
>>
>> It's odd that you use 'personal' in the code and 'user' in the tests.
>> Could you please pick one set of terms and stick with them?
>
> W...

Read more...

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

<jml> aiui, if I try to assign a thing to you for review, and you are already assigned to do it
<thumper> I'm not sure I'll get that finished tonight
<jml> the error is "Tim Penhey already has an existing pending review"
<jml> is that right?
<thumper> almost: "Tim Penhey (thumper) already has an existing pending review"
<jml> ahh ok.
<jml> how about "Tim Penhey (thumper) has already been asked to review this"
<thumper> sounds fine
<thumper> better even
<jml> and likewise, "Tim Penhey has already reviewed this"
<jml> rather than 'has a personal review'
<thumper> yeah, they sound good
<jml> cool.
<jml> makes it easier to understand what the code is for, also :)

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

On Fri, 11 Dec 2009 18:09:12 Jonathan Lange wrote:
> That means it will raise an OOPS, right? Is it really that difficult to fix
> it?

Fixed, and updated the story.

> >> > self.branch_merge_proposal.getUsersVoteReference(reviewer)) +
> >> > if existing_review is not None:
> >> > + if existing_review.is_pending:
> >> > + error_str = '%s has an existing pending review'
> >> > + else:
> >> > + error_str = '%s has an existing personal review'
> >>
> >> This code path is untested. AFAICT, you only test
> >> ReassignReviewerFailed once, so you cannot possibly be testing both
> >> strings. Can you please either drop the condition or add a test?
> >
> > Yeah, can do. I was considering this.
>
> Thanks.

Done

> >> > + raise ReassignReviewFailed(
> >> > + error_str % reviewer.unique_displayname)
> >> > + self.reviewer = reviewer
> >> > +
> >>
> >> The logic here is straightforward, but it's hard to figure out what
> >> the error messages actually mean.
> >>
> >> Why does "pending" matter? Do these error strings get displayed to the
> >> user? If so, they don't give the user any indication as to what to do
> >> about the problem.
> >
> > I wrote it with the intent of showing them to the user, and in fact they
> > are shown to a user using the API. Pending only matters for the
> > readabiltiy of the error, and for no other reason.
>
> In that case, the text needs to be changed. If I tried to reassign a
> review to you and was told "Tim Penhey already has an existing
> personal review", I wouldn't know what do about it, or really what a
> personal review was.
>
> Doesn't it really mean "Tim Penhey has already reviewed this" or "Tim
> Penhey has already been asked to review this"?

Updated following our IRC discussion.

1=== modified file 'lib/lp/code/browser/codereviewvote.py'
2--- lib/lp/code/browser/codereviewvote.py 2009-12-10 23:12:46 +0000
3+++ lib/lp/code/browser/codereviewvote.py 2009-12-14 09:08:36 +0000
4@@ -12,6 +12,7 @@
5 from canonical.launchpad.fields import PublicPersonChoice
6 from canonical.launchpad.webapp import (
7 action, canonical_url, LaunchpadFormView)
8+from lp.code.errors import ReviewNotPending, UserHasExistingReview
9
10
11 class ReassignSchema(Interface):
12@@ -34,3 +35,12 @@
13 """Use the form data to change the review request reviewer."""
14 self.context.reassignReview(data['reviewer'])
15 self.next_url = canonical_url(self.context.branch_merge_proposal)
16+
17+ def validate(self, data):
18+ """Make sure that the reassignment can happen."""
19+ reviewer = data.get('reviewer')
20+ if reviewer is not None:
21+ try:
22+ self.context.validateReasignReview(reviewer)
23+ except (ReviewNotPending, UserHasExistingReview), e:
24+ self.addError(str(e))
25
26=== modified file 'lib/lp/code/errors.py'
27--- lib/lp/code/errors.py 2009-12-10 23:12:46 +0000
28+++ lib/lp/code/errors.py 2009-12-14 03:34:57 +0000
29@@ -10,8 +10,8 @@
30 'BranchMergeProposalExists',
31 'ClaimReviewFailed',
32 'InvalidBranchMergeProposal',
33- 'ReassignReviewFailed',
34 'ReviewNotPending',
35+ 'UserHasExistingReview',
36 'UserNotBranchReviewer',
37 'WrongBranchMergeProposal',
38 ]
39@@ -40,14 +40,14 @@
40 """Raised if there is already a matching BranchMergeProposal."""
41
42
43-class ReassignReviewFailed(Exception):
44- """Failed to reassign the review request."""
45-
46-
47 class ReviewNotPending(Exception):
48 """The requested review is not in a pending state."""
49
50
51+class UserHasExistingReview(Exception):
52+ """The user has an existing review."""
53+
54+
55 class UserNotBranchReviewer(Exception):
56 """The user who attempted to review the merge proposal isn't a reviewer.
57
58
59=== modified file 'lib/lp/code/interfaces/codereviewvote.py'
60--- lib/lp/code/interfaces/codereviewvote.py 2009-12-10 23:12:46 +0000
61+++ lib/lp/code/interfaces/codereviewvote.py 2009-12-14 03:26:56 +0000
62@@ -71,6 +71,15 @@
63 class ICodeReviewVoteReferenceEdit(Interface):
64 """Method that require edit permissions."""
65
66+ def validateClaimReview(claimant):
67+ """Implements the validation for claiming a review.
68+
69+ :raises ClaimReviewFailed: If the claimant already has a
70+ personal review, if the reviewer is not a team, if the
71+ claimant is not in the reviewer team, or if the review is
72+ not pending.
73+ """
74+
75 @call_with(claimant=REQUEST_USER)
76 @export_write_operation()
77 def claimReview(claimant):
78@@ -87,6 +96,14 @@
79 not pending.
80 """
81
82+ def validateReasignReview(reviewer):
83+ """Implements the validation for reassignment.
84+
85+ :raises ReviewNotPending: If the review is not pending.
86+ :raises ReassignReviewFailed: If the reviewer is an individual and
87+ already has a personal review.
88+ """
89+
90 @operation_parameters(
91 reviewer=Reference(
92 title=_("The person or team to assign to do the review."),
93
94=== modified file 'lib/lp/code/model/codereviewvote.py'
95--- lib/lp/code/model/codereviewvote.py 2009-12-10 23:12:46 +0000
96+++ lib/lp/code/model/codereviewvote.py 2009-12-14 03:37:22 +0000
97@@ -16,7 +16,7 @@
98 from canonical.database.datetimecol import UtcDateTimeCol
99 from canonical.database.sqlbase import SQLBase
100 from lp.code.errors import (
101- ClaimReviewFailed, ReassignReviewFailed, ReviewNotPending)
102+ ClaimReviewFailed, ReviewNotPending, UserHasExistingReview)
103 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
104
105
106@@ -45,10 +45,25 @@
107 # Reviews are pending if there is no associated comment.
108 return self.comment is None
109
110- def claimReview(self, claimant):
111+ def _validatePending(self):
112+ """Raise if the review is not pending."""
113+ if not self.is_pending:
114+ raise ReviewNotPending('The review is not pending.')
115+
116+ def _validateNoReviewForUser(self, user):
117+ """Make sure there isn't an existing review for the user."""
118+ bmp = self.branch_merge_proposal
119+ existing_review = bmp.getUsersVoteReference(user)
120+ if existing_review is not None:
121+ if existing_review.is_pending:
122+ error_str = '%s has already been asked to review this'
123+ else:
124+ error_str = '%s has already reviewed this'
125+ raise UserHasExistingReview(error_str % user.unique_displayname)
126+
127+ def validateClaimReview(self, claimant):
128 """See `ICodeReviewVote`"""
129- if not self.is_pending:
130- raise ClaimReviewFailed('The review is not pending.')
131+ self._validatePending()
132 if not self.reviewer.is_team:
133 raise ClaimReviewFailed('Cannot claim non-team reviews.')
134 if not claimant.inTeam(self.reviewer):
135@@ -56,31 +71,22 @@
136 '%s is not a member of %s' %
137 (claimant.unique_displayname,
138 self.reviewer.unique_displayname))
139- claimant_review = (
140- self.branch_merge_proposal.getUsersVoteReference(claimant))
141- if claimant_review is not None:
142- if claimant_review.is_pending:
143- error_str = '%s has an existing pending review'
144- else:
145- error_str = '%s has an existing personal review'
146- raise ClaimReviewFailed(
147- error_str % claimant.unique_displayname)
148+ self._validateNoReviewForUser(claimant)
149+
150+ def claimReview(self, claimant):
151+ """See `ICodeReviewVote`"""
152+ self.validateClaimReview(claimant)
153 self.reviewer = claimant
154
155+ def validateReasignReview(self, reviewer):
156+ """See `ICodeReviewVote`"""
157+ self._validatePending()
158+ if not reviewer.is_team:
159+ self._validateNoReviewForUser(reviewer)
160+
161 def reassignReview(self, reviewer):
162 """See `ICodeReviewVote`"""
163- if not self.is_pending:
164- raise ReviewNotPending('The review is not pending.')
165- if not reviewer.is_team:
166- existing_review = (
167- self.branch_merge_proposal.getUsersVoteReference(reviewer))
168- if existing_review is not None:
169- if existing_review.is_pending:
170- error_str = '%s has an existing pending review'
171- else:
172- error_str = '%s has an existing personal review'
173- raise ReassignReviewFailed(
174- error_str % reviewer.unique_displayname)
175+ self.validateReasignReview(reviewer)
176 self.reviewer = reviewer
177
178 def delete(self):
179
180=== modified file 'lib/lp/code/model/tests/test_codereviewvote.py'
181--- lib/lp/code/model/tests/test_codereviewvote.py 2009-12-10 23:37:56 +0000
182+++ lib/lp/code/model/tests/test_codereviewvote.py 2009-12-14 04:33:25 +0000
183@@ -10,7 +10,7 @@
184
185 from lp.code.enums import CodeReviewVote
186 from lp.code.errors import (
187- ClaimReviewFailed, ReassignReviewFailed, ReviewNotPending)
188+ ClaimReviewFailed, ReviewNotPending, UserHasExistingReview)
189 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
190 from lp.testing import login_person, TestCaseWithFactory
191
192@@ -44,7 +44,7 @@
193 TestCaseWithFactory.setUp(self)
194 # Setup the proposal, claimant and team reviewer.
195 self.bmp = self.factory.makeBranchMergeProposal()
196- self.claimant = self.factory.makePerson()
197+ self.claimant = self.factory.makePerson(name='eric')
198 self.review_team = self.factory.makeTeam()
199
200 def _addPendingReview(self):
201@@ -72,8 +72,10 @@
202 vote=CodeReviewVote.APPROVE)
203 review = self._addPendingReview()
204 self._addClaimantToReviewTeam()
205- self.assertRaises(
206- ClaimReviewFailed, review.claimReview, self.claimant)
207+ self.assertRaisesWithContent(
208+ UserHasExistingReview,
209+ 'Eric (eric) has already reviewed this',
210+ review.claimReview, self.claimant)
211
212 def test_personal_pending_review(self):
213 # If the claimant has a pending review already, then they can't claim
214@@ -84,8 +86,10 @@
215 self.bmp.nominateReviewer(
216 reviewer=self.claimant, registrant=self.bmp.registrant)
217 login_person(self.claimant)
218- self.assertRaises(
219- ClaimReviewFailed, review.claimReview, self.claimant)
220+ self.assertRaisesWithContent(
221+ UserHasExistingReview,
222+ 'Eric (eric) has already been asked to review this',
223+ review.claimReview, self.claimant)
224
225 def test_personal_not_in_review_team(self):
226 # If the claimant is not in the review team, an error is raised.
227@@ -218,15 +222,30 @@
228 self.assertRaises(
229 ReviewNotPending, review.reassignReview, bmp.registrant)
230
231- def test_reassign_to_user_existing(self):
232- # If a user has an existing review, they cannot have another
233+ def test_reassign_to_user_existing_pending(self):
234+ # If a user has an existing pending review, they cannot have another
235 # pending review assigned to them.
236 bmp, review = self.makeMergeProposalWithReview()
237- reviewer = self.factory.makePerson()
238+ reviewer = self.factory.makePerson(name='eric')
239 user_review = bmp.nominateReviewer(
240 reviewer=reviewer, registrant=bmp.registrant)
241- self.assertRaises(
242- ReassignReviewFailed, review.reassignReview, reviewer)
243+ self.assertRaisesWithContent(
244+ UserHasExistingReview,
245+ 'Eric (eric) has already been asked to review this',
246+ review.reassignReview, reviewer)
247+
248+ def test_reassign_to_user_existing_completed(self):
249+ # If a user has an existing completed review, they cannot have another
250+ # pending review assigned to them.
251+ bmp, review = self.makeMergeProposalWithReview()
252+ reviewer = self.factory.makePerson(name='eric')
253+ bmp.createComment(
254+ reviewer, 'Message subject', 'Message content',
255+ vote=CodeReviewVote.APPROVE)
256+ self.assertRaisesWithContent(
257+ UserHasExistingReview,
258+ 'Eric (eric) has already reviewed this',
259+ review.reassignReview, reviewer)
260
261 def test_reassign_to_team_existing(self):
262 # If a team has an existing review, they can have another pending
263
264=== renamed file 'lib/lp/code/stories/branches/xx-branch-merge-proposals.txt' => 'lib/lp/code/stories/branches/xx-branchmergeproposals.txt'
265--- lib/lp/code/stories/branches/xx-branch-merge-proposals.txt 2009-11-23 03:41:39 +0000
266+++ lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2009-12-14 09:20:01 +0000
267@@ -232,15 +232,23 @@
268 The claimant can reassign the review to someone else.
269
270 >>> foobar_browser.getLink('Reassign').click()
271+ >>> foobar_browser.getControl('Reviewer').value = 'no-priv'
272+ >>> foobar_browser.getControl('Reassign').click()
273+
274+If the person already has a review, the user gets an error...
275+
276+ >>> print_feedback_messages(foobar_browser.contents)
277+ There is 1 error.
278+ No Privileges Person (no-priv) has already reviewed this
279+
280+... if not, the review is reassigned.
281+
282 >>> foobar_browser.getControl('Reviewer').value = 'hwdb-team'
283 >>> foobar_browser.getControl('Reassign').click()
284- >>> foobar_browser.open(klingon_proposal)
285- >>> pending = find_tag_by_id(
286- ... foobar_browser.contents, 'code-review-votes')
287
288 The review is now reassigned to the HWDB team.
289
290- >>> print extract_text(pending)
291+ >>> print_tag_with_id(foobar_browser.contents, 'code-review-votes')
292 Reviewer Review Type Date Requested Status...
293 HWDB Team claimable ... ago Pending ...
294
295@@ -293,8 +301,7 @@
296 >>> sample_browser.getControl('Merged Revision Number').value = '42'
297 >>> sample_browser.getControl('Mark as Merged').click()
298
299- >>> for message in get_feedback_messages(sample_browser.contents):
300- ... print extract_text(message)
301+ >>> print_feedback_messages(sample_browser.contents)
302 The proposal's merged revision has been updated.
303 >>> print_summary(sample_browser)
304 Status: Merged
305@@ -435,8 +442,7 @@
306 setting it gives an appropriate error.
307
308 >>> nopriv_browser.getControl('Propose Merge').click()
309- >>> for message in get_feedback_messages(nopriv_browser.contents):
310- ... print extract_text(message)
311+ >>> print_feedback_messages(nopriv_browser.contents)
312 There is 1 error.
313 Required input is missing.
314
315@@ -446,8 +452,7 @@
316 ... name='field.target_branch.target_branch').value = (
317 ... 'fooix')
318 >>> nopriv_browser.getControl('Propose Merge').click()
319- >>> for message in get_feedback_messages(nopriv_browser.contents):
320- ... print extract_text(message)
321+ >>> print_feedback_messages(nopriv_browser.contents)
322 There is 1 error.
323 Invalid value
324
Revision history for this message
Jonathan Lange (jml) wrote :

Thanks Tim.

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/codereviewvote.py'
2--- lib/lp/code/browser/codereviewvote.py 2009-12-10 20:46:32 +0000
3+++ lib/lp/code/browser/codereviewvote.py 2009-12-14 09:24:24 +0000
4@@ -1,20 +1,18 @@
5 # Copyright 2009 Canonical Ltd. This software is licensed under the
6 # GNU Affero General Public License version 3 (see the file LICENSE).
7
8-
9 """Views, navigation and actions for CodeReviewVotes."""
10
11-
12 __metaclass__ = type
13
14
15 from zope.interface import Interface
16-from zope.security.proxy import removeSecurityProxy
17
18 from canonical.launchpad import _
19 from canonical.launchpad.fields import PublicPersonChoice
20 from canonical.launchpad.webapp import (
21 action, canonical_url, LaunchpadFormView)
22+from lp.code.errors import ReviewNotPending, UserHasExistingReview
23
24
25 class ReassignSchema(Interface):
26@@ -35,8 +33,14 @@
27 @action('Reassign', name='reassign')
28 def reassign_action(self, action, data):
29 """Use the form data to change the review request reviewer."""
30- # XXX TimPenhey 2009-12-11 bug=495201
31- # This should check for existing reviews by the reviewer, and have
32- # the logic moved into the model code.
33- removeSecurityProxy(self.context).reviewer = data['reviewer']
34+ self.context.reassignReview(data['reviewer'])
35 self.next_url = canonical_url(self.context.branch_merge_proposal)
36+
37+ def validate(self, data):
38+ """Make sure that the reassignment can happen."""
39+ reviewer = data.get('reviewer')
40+ if reviewer is not None:
41+ try:
42+ self.context.validateReasignReview(reviewer)
43+ except (ReviewNotPending, UserHasExistingReview), e:
44+ self.addError(str(e))
45
46=== modified file 'lib/lp/code/errors.py'
47--- lib/lp/code/errors.py 2009-12-07 06:51:42 +0000
48+++ lib/lp/code/errors.py 2009-12-14 09:24:24 +0000
49@@ -11,6 +11,7 @@
50 'ClaimReviewFailed',
51 'InvalidBranchMergeProposal',
52 'ReviewNotPending',
53+ 'UserHasExistingReview',
54 'UserNotBranchReviewer',
55 'WrongBranchMergeProposal',
56 ]
57@@ -43,6 +44,10 @@
58 """The requested review is not in a pending state."""
59
60
61+class UserHasExistingReview(Exception):
62+ """The user has an existing review."""
63+
64+
65 class UserNotBranchReviewer(Exception):
66 """The user who attempted to review the merge proposal isn't a reviewer.
67
68
69=== modified file 'lib/lp/code/interfaces/codereviewvote.py'
70--- lib/lp/code/interfaces/codereviewvote.py 2009-12-07 06:51:42 +0000
71+++ lib/lp/code/interfaces/codereviewvote.py 2009-12-14 09:24:24 +0000
72@@ -17,10 +17,11 @@
73 IBranchMergeProposal)
74 from lp.code.interfaces.codereviewcomment import (
75 ICodeReviewComment)
76+from lp.registry.interfaces.person import IPerson
77 from lazr.restful.fields import Reference
78 from lazr.restful.declarations import (
79 call_with, export_as_webservice_entry, export_destructor_operation,
80- export_write_operation, exported, REQUEST_USER)
81+ export_write_operation, exported, operation_parameters, REQUEST_USER)
82
83
84 class ICodeReviewVoteReferencePublic(Interface):
85@@ -70,6 +71,15 @@
86 class ICodeReviewVoteReferenceEdit(Interface):
87 """Method that require edit permissions."""
88
89+ def validateClaimReview(claimant):
90+ """Implements the validation for claiming a review.
91+
92+ :raises ClaimReviewFailed: If the claimant already has a
93+ personal review, if the reviewer is not a team, if the
94+ claimant is not in the reviewer team, or if the review is
95+ not pending.
96+ """
97+
98 @call_with(claimant=REQUEST_USER)
99 @export_write_operation()
100 def claimReview(claimant):
101@@ -86,6 +96,30 @@
102 not pending.
103 """
104
105+ def validateReasignReview(reviewer):
106+ """Implements the validation for reassignment.
107+
108+ :raises ReviewNotPending: If the review is not pending.
109+ :raises ReassignReviewFailed: If the reviewer is an individual and
110+ already has a personal review.
111+ """
112+
113+ @operation_parameters(
114+ reviewer=Reference(
115+ title=_("The person or team to assign to do the review."),
116+ schema=IPerson))
117+ @export_write_operation()
118+ def reassignReview(reviewer):
119+ """Reassign a pending review to someone else.
120+
121+ Pending reviews can be reassigned to someone else.
122+
123+ :param reviewer: The person to assign the pending review to.
124+ :raises ReviewNotPending: If the review is not pending.
125+ :raises ReassignReviewFailed: If the reviewer is an individual and
126+ already has a personal review.
127+ """
128+
129 @export_destructor_operation()
130 def delete():
131 """Delete the pending review.
132
133=== modified file 'lib/lp/code/model/codereviewvote.py'
134--- lib/lp/code/model/codereviewvote.py 2009-12-07 06:51:42 +0000
135+++ lib/lp/code/model/codereviewvote.py 2009-12-14 09:24:24 +0000
136@@ -15,7 +15,8 @@
137 from canonical.database.constants import DEFAULT
138 from canonical.database.datetimecol import UtcDateTimeCol
139 from canonical.database.sqlbase import SQLBase
140-from lp.code.errors import ClaimReviewFailed, ReviewNotPending
141+from lp.code.errors import (
142+ ClaimReviewFailed, ReviewNotPending, UserHasExistingReview)
143 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
144
145
146@@ -44,10 +45,25 @@
147 # Reviews are pending if there is no associated comment.
148 return self.comment is None
149
150- def claimReview(self, claimant):
151+ def _validatePending(self):
152+ """Raise if the review is not pending."""
153+ if not self.is_pending:
154+ raise ReviewNotPending('The review is not pending.')
155+
156+ def _validateNoReviewForUser(self, user):
157+ """Make sure there isn't an existing review for the user."""
158+ bmp = self.branch_merge_proposal
159+ existing_review = bmp.getUsersVoteReference(user)
160+ if existing_review is not None:
161+ if existing_review.is_pending:
162+ error_str = '%s has already been asked to review this'
163+ else:
164+ error_str = '%s has already reviewed this'
165+ raise UserHasExistingReview(error_str % user.unique_displayname)
166+
167+ def validateClaimReview(self, claimant):
168 """See `ICodeReviewVote`"""
169- if not self.is_pending:
170- raise ClaimReviewFailed('The review is not pending.')
171+ self._validatePending()
172 if not self.reviewer.is_team:
173 raise ClaimReviewFailed('Cannot claim non-team reviews.')
174 if not claimant.inTeam(self.reviewer):
175@@ -55,17 +71,24 @@
176 '%s is not a member of %s' %
177 (claimant.unique_displayname,
178 self.reviewer.unique_displayname))
179- claimant_review = (
180- self.branch_merge_proposal.getUsersVoteReference(claimant))
181- if claimant_review is not None:
182- if claimant_review.is_pending:
183- error_str = '%s has an existing pending review'
184- else:
185- error_str = '%s has an existing personal review'
186- raise ClaimReviewFailed(
187- error_str % claimant.unique_displayname)
188+ self._validateNoReviewForUser(claimant)
189+
190+ def claimReview(self, claimant):
191+ """See `ICodeReviewVote`"""
192+ self.validateClaimReview(claimant)
193 self.reviewer = claimant
194
195+ def validateReasignReview(self, reviewer):
196+ """See `ICodeReviewVote`"""
197+ self._validatePending()
198+ if not reviewer.is_team:
199+ self._validateNoReviewForUser(reviewer)
200+
201+ def reassignReview(self, reviewer):
202+ """See `ICodeReviewVote`"""
203+ self.validateReasignReview(reviewer)
204+ self.reviewer = reviewer
205+
206 def delete(self):
207 """See `ICodeReviewVote`"""
208 if not self.is_pending:
209
210=== modified file 'lib/lp/code/model/tests/test_codereviewvote.py'
211--- lib/lp/code/model/tests/test_codereviewvote.py 2009-12-07 06:51:42 +0000
212+++ lib/lp/code/model/tests/test_codereviewvote.py 2009-12-14 09:24:24 +0000
213@@ -9,7 +9,8 @@
214 from canonical.testing import DatabaseFunctionalLayer
215
216 from lp.code.enums import CodeReviewVote
217-from lp.code.errors import ClaimReviewFailed, ReviewNotPending
218+from lp.code.errors import (
219+ ClaimReviewFailed, ReviewNotPending, UserHasExistingReview)
220 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
221 from lp.testing import login_person, TestCaseWithFactory
222
223@@ -43,7 +44,7 @@
224 TestCaseWithFactory.setUp(self)
225 # Setup the proposal, claimant and team reviewer.
226 self.bmp = self.factory.makeBranchMergeProposal()
227- self.claimant = self.factory.makePerson()
228+ self.claimant = self.factory.makePerson(name='eric')
229 self.review_team = self.factory.makeTeam()
230
231 def _addPendingReview(self):
232@@ -71,8 +72,10 @@
233 vote=CodeReviewVote.APPROVE)
234 review = self._addPendingReview()
235 self._addClaimantToReviewTeam()
236- self.assertRaises(
237- ClaimReviewFailed, review.claimReview, self.claimant)
238+ self.assertRaisesWithContent(
239+ UserHasExistingReview,
240+ 'Eric (eric) has already reviewed this',
241+ review.claimReview, self.claimant)
242
243 def test_personal_pending_review(self):
244 # If the claimant has a pending review already, then they can't claim
245@@ -83,8 +86,10 @@
246 self.bmp.nominateReviewer(
247 reviewer=self.claimant, registrant=self.bmp.registrant)
248 login_person(self.claimant)
249- self.assertRaises(
250- ClaimReviewFailed, review.claimReview, self.claimant)
251+ self.assertRaisesWithContent(
252+ UserHasExistingReview,
253+ 'Eric (eric) has already been asked to review this',
254+ review.claimReview, self.claimant)
255
256 def test_personal_not_in_review_team(self):
257 # If the claimant is not in the review team, an error is raised.
258@@ -183,5 +188,75 @@
259 self.assertRaises(ReviewNotPending, review.delete)
260
261
262+class TestCodeReviewVoteReferenceReassignReview(TestCaseWithFactory):
263+ """Tests for CodeReviewVoteReference.reassignReview."""
264+
265+ layer = DatabaseFunctionalLayer
266+
267+ def makeMergeProposalWithReview(self, completed=False):
268+ """Return a new merge proposal with a review."""
269+ bmp = self.factory.makeBranchMergeProposal()
270+ reviewer = self.factory.makePerson()
271+ if completed:
272+ login_person(reviewer)
273+ bmp.createComment(
274+ reviewer, 'Message subject', 'Message content',
275+ vote=CodeReviewVote.APPROVE)
276+ [review] = list(bmp.votes)
277+ else:
278+ login_person(bmp.registrant)
279+ review = bmp.nominateReviewer(
280+ reviewer=reviewer, registrant=bmp.registrant)
281+ return bmp, review
282+
283+ def test_reassign_pending(self):
284+ # A pending review can be reassigned to someone else.
285+ bmp, review = self.makeMergeProposalWithReview()
286+ new_reviewer = self.factory.makePerson()
287+ review.reassignReview(new_reviewer)
288+ self.assertEqual(new_reviewer, review.reviewer)
289+
290+ def test_reassign_completed_review(self):
291+ # A completed review cannot be reassigned
292+ bmp, review = self.makeMergeProposalWithReview(completed=True)
293+ self.assertRaises(
294+ ReviewNotPending, review.reassignReview, bmp.registrant)
295+
296+ def test_reassign_to_user_existing_pending(self):
297+ # If a user has an existing pending review, they cannot have another
298+ # pending review assigned to them.
299+ bmp, review = self.makeMergeProposalWithReview()
300+ reviewer = self.factory.makePerson(name='eric')
301+ user_review = bmp.nominateReviewer(
302+ reviewer=reviewer, registrant=bmp.registrant)
303+ self.assertRaisesWithContent(
304+ UserHasExistingReview,
305+ 'Eric (eric) has already been asked to review this',
306+ review.reassignReview, reviewer)
307+
308+ def test_reassign_to_user_existing_completed(self):
309+ # If a user has an existing completed review, they cannot have another
310+ # pending review assigned to them.
311+ bmp, review = self.makeMergeProposalWithReview()
312+ reviewer = self.factory.makePerson(name='eric')
313+ bmp.createComment(
314+ reviewer, 'Message subject', 'Message content',
315+ vote=CodeReviewVote.APPROVE)
316+ self.assertRaisesWithContent(
317+ UserHasExistingReview,
318+ 'Eric (eric) has already reviewed this',
319+ review.reassignReview, reviewer)
320+
321+ def test_reassign_to_team_existing(self):
322+ # If a team has an existing review, they can have another pending
323+ # review assigned to them.
324+ bmp, review = self.makeMergeProposalWithReview()
325+ reviewer_team = self.factory.makeTeam()
326+ team_review = bmp.nominateReviewer(
327+ reviewer=reviewer_team, registrant=bmp.registrant)
328+ review.reassignReview(reviewer_team)
329+ self.assertEqual(reviewer_team, review.reviewer)
330+
331+
332 def test_suite():
333 return TestLoader().loadTestsFromName(__name__)
334
335=== renamed file 'lib/lp/code/stories/branches/xx-branch-merge-proposals.txt' => 'lib/lp/code/stories/branches/xx-branchmergeproposals.txt'
336--- lib/lp/code/stories/branches/xx-branch-merge-proposals.txt 2009-11-27 01:29:10 +0000
337+++ lib/lp/code/stories/branches/xx-branchmergeproposals.txt 2009-12-14 09:24:24 +0000
338@@ -233,15 +233,23 @@
339 The claimant can reassign the review to someone else.
340
341 >>> foobar_browser.getLink('Reassign').click()
342+ >>> foobar_browser.getControl('Reviewer').value = 'no-priv'
343+ >>> foobar_browser.getControl('Reassign').click()
344+
345+If the person already has a review, the user gets an error...
346+
347+ >>> print_feedback_messages(foobar_browser.contents)
348+ There is 1 error.
349+ No Privileges Person (no-priv) has already reviewed this
350+
351+... if not, the review is reassigned.
352+
353 >>> foobar_browser.getControl('Reviewer').value = 'hwdb-team'
354 >>> foobar_browser.getControl('Reassign').click()
355- >>> foobar_browser.open(klingon_proposal)
356- >>> pending = find_tag_by_id(
357- ... foobar_browser.contents, 'code-review-votes')
358
359 The review is now reassigned to the HWDB team.
360
361- >>> print extract_text(pending)
362+ >>> print_tag_with_id(foobar_browser.contents, 'code-review-votes')
363 Reviewer Review Type Date Requested Status...
364 HWDB Team claimable ... ago Pending ...
365
366@@ -294,8 +302,7 @@
367 >>> sample_browser.getControl('Merged Revision Number').value = '42'
368 >>> sample_browser.getControl('Mark as Merged').click()
369
370- >>> for message in get_feedback_messages(sample_browser.contents):
371- ... print extract_text(message)
372+ >>> print_feedback_messages(sample_browser.contents)
373 The proposal's merged revision has been updated.
374 >>> print_summary(sample_browser)
375 Status: Merged
376@@ -436,8 +443,7 @@
377 setting it gives an appropriate error.
378
379 >>> nopriv_browser.getControl('Propose Merge').click()
380- >>> for message in get_feedback_messages(nopriv_browser.contents):
381- ... print extract_text(message)
382+ >>> print_feedback_messages(nopriv_browser.contents)
383 There is 1 error.
384 Required input is missing.
385
386@@ -447,8 +453,7 @@
387 ... name='field.target_branch.target_branch').value = (
388 ... 'fooix')
389 >>> nopriv_browser.getControl('Propose Merge').click()
390- >>> for message in get_feedback_messages(nopriv_browser.contents):
391- ... print extract_text(message)
392+ >>> print_feedback_messages(nopriv_browser.contents)
393 There is 1 error.
394 Invalid value
395

Subscribers

People subscribed via source and target branches

to status/vote changes: