Merge lp:~thumper/launchpad/reassign-review-into-model into lp:launchpad/db-devel
- reassign-review-into-model
- Merge into db-devel
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 |
Related bugs: |
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.
Description of the change
Tim Penhey (thumper) wrote : | # |
Jonathan Lange (jml) wrote : | # |
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:/
>
>
> This branch moves the logic for review reassignment into the model code.
>
> tests:
> TestCodeReviewV
> TestReassignRev
>
> --
> https:/
> Your team Launchpad code reviewers from Canonical is subscribed to branch lp:launchpad.
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
...
> @@ -35,8 +32,5 @@
> @action('Reassign', name='reassign')
> def reassign_
> """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.
> - removeSecurityP
> + self.context.
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/
> --- lib/lp/
> +++ lib/lp/
> @@ -10,6 +10,7 @@
> 'BranchMergePro
> 'ClaimReviewFai
> 'InvalidBranchM
> + 'ReassignReview
> 'ReviewNotPending',
> 'UserNotBranchR
> 'WrongBranchMer
> @@ -39,6 +40,10 @@
> """Raised if there is already a matching BranchMergeProp
>
>
> +class ReassignReviewF
> + """Failed to reassign the review request."""
> +
> +
> class ReviewNotPendin
> """The requested review is not in a pending state."""
>
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -17,10 +17,11 @@
> IBranchMergePro
> from lp.code.
> ICodeReviewComment)
> +from lp.registry.
> from lazr.restful.fields import Reference
> from lazr.restful.
> call_with, export_
> - export_
> + export_
>
>
> class ICodeReviewVote
> @@ -86,6 +87,22 @@
> not pending.
> """
>
> + @operation_
> + reviewer=Reference(
> + title=_("The person or ...
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
Tim Penhey (thumper) 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.
> Hooray for removing the removeSecurityProxy call.
>
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -10,6 +10,7 @@
> > 'BranchMergePro
> > 'ClaimReviewFai
> > 'InvalidBranchM
> > + 'ReassignReview
> > 'ReviewNotPending',
> > 'UserNotBranchR
> > 'WrongBranchMer
> > @@ -39,6 +40,10 @@
> > """Raised if there is already a matching BranchMergeProp
> >
> >
> > +class ReassignReviewF
> > + """Failed to reassign the review request."""
> > +
> > +
> > class ReviewNotPendin
> > """The requested review is not in a pending state."""
> >
> >
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -17,10 +17,11 @@
> > IBranchMergePro
> > from lp.code.
> > ICodeReviewComment)
> > +from lp.registry.
> > from lazr.restful.fields import Reference
> > from lazr.restful.
> > call_with, export_
> > - export_
> > + export_
> > REQUEST_USER)
> >
> >
> > class ICodeReviewVote
> > @@ -86,6 +87,22 @@
> > not pending.
> > """
> >
> > + @operation_
> > + reviewer=Reference(
> > + title=_("The person or team to assign to do the review."),
> > + schema=IPerson))
> > + @export_
> > + def reassignReview(
> > + """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 ReassignReviewF
> > and + already has a personal review.
> > + """
> > +
> > @export_
> > def delete():
> > """Delete the pending review.
> >
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
>
> ...
>
> > @@ -66,6 +67,22 @@
> > error_str % claimant.
> > ...
Jonathan Lange (jml) wrote : | # |
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_
>> > if existing_review is not None:
>> > + if existing_
>> > + 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
>> ReassignReviewe
>> strings. Can you please either drop the condition or add a test?
>
> Yeah, can do. I was considering this.
>
Thanks.
>> > + raise ReassignReviewF
>> > + error_str % reviewer.
>> > + 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_
>> > + # If a user has an existing review, they cannot have another
>> > + # pending review assigned to them.
>> > + bmp, review = self.makeMergeP
>> > + reviewer = self.factory.
>> > + user_review = bmp.nominateRev
>> > + reviewer=reviewer, registrant=
>> > + self.assertRaises(
>> > + ReassignReviewF
>> > +
>> > + def test_reassign_
>> > + # 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...
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 :)
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_
> >> > if existing_review is not None:
> >> > + if existing_
> >> > + 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
> >> ReassignReviewe
> >> strings. Can you please either drop the condition or add a test?
> >
> > Yeah, can do. I was considering this.
>
> Thanks.
Done
> >> > + raise ReassignReviewF
> >> > + error_str % reviewer.
> >> > + 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 |
Preview Diff
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 |
This branch moves the logic for review reassignment into the model code.
tests: wVoteReferenceR eassignReview eviewer
TestCodeRevie
TestReassignR