Merge lp:~thumper/launchpad/claim-review-into-model-attempt2 into lp:launchpad/db-devel
- claim-review-into-model-attempt2
- Merge into db-devel
Proposed by
Tim Penhey
Status: | Merged |
---|---|
Merged at revision: | 8763 |
Proposed branch: | lp:~thumper/launchpad/claim-review-into-model-attempt2 |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~thumper/launchpad/lp-code-errors |
Diff against target: |
342 lines (+177/-26) 7 files modified
lib/lp/code/configure.zcml (+7/-4) lib/lp/code/errors.py (+5/-2) lib/lp/code/interfaces/codereviewvote.py (+41/-11) lib/lp/code/model/codereviewvote.py (+29/-0) lib/lp/code/model/tests/test_branchmergeproposals.py (+1/-0) lib/lp/code/model/tests/test_codereviewvote.py (+91/-9) lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+3/-0) |
To merge this branch: | bzr merge lp:~thumper/launchpad/claim-review-into-model-attempt2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | Approve | ||
Review via email: mp+15728@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote : | # |
Revision history for this message
Paul Hummer (rockstar) : | # |
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/configure.zcml' |
2 | --- lib/lp/code/configure.zcml 2009-11-16 22:53:42 +0000 |
3 | +++ lib/lp/code/configure.zcml 2009-12-07 04:01:14 +0000 |
4 | @@ -28,9 +28,13 @@ |
5 | provides="lp.code.interfaces.branchmergequeue.IBranchMergeQueueSet"> |
6 | <allow interface="lp.code.interfaces.branchmergequeue.IBranchMergeQueueSet"/> |
7 | </securedutility> |
8 | + |
9 | <class class="lp.code.model.codereviewvote.CodeReviewVoteReference"> |
10 | - <allow interface="lp.code.interfaces.codereviewvote.ICodeReviewVoteReference"/> |
11 | - <require permission="launchpad.Edit" set_attributes="reviewer" /> |
12 | + <allow interface="lp.code.interfaces.codereviewvote.ICodeReviewVoteReferencePublic"/> |
13 | + <require |
14 | + permission="launchpad.Edit" |
15 | + interface="lp.code.interfaces.codereviewvote.ICodeReviewVoteReferenceEdit" |
16 | + set_attributes="reviewer" /> |
17 | </class> |
18 | <subscriber |
19 | for="lp.code.interfaces.codereviewvote.ICodeReviewVoteReference |
20 | @@ -257,8 +261,7 @@ |
21 | updatePreviewDiff"/> |
22 | <require |
23 | permission="launchpad.AnyPerson" |
24 | - attributes=" |
25 | - createComment |
26 | + attributes="createComment |
27 | createCommentFromMessage"/> |
28 | </class> |
29 | <adapter |
30 | |
31 | === modified file 'lib/lp/code/errors.py' |
32 | --- lib/lp/code/errors.py 2009-12-07 04:01:13 +0000 |
33 | +++ lib/lp/code/errors.py 2009-12-07 04:01:14 +0000 |
34 | @@ -8,6 +8,7 @@ |
35 | 'BadBranchMergeProposalSearchContext', |
36 | 'BadStateTransition', |
37 | 'BranchMergeProposalExists', |
38 | + 'ClaimReviewFailed', |
39 | 'InvalidBranchMergeProposal', |
40 | 'UserNotBranchReviewer', |
41 | 'WrongBranchMergeProposal', |
42 | @@ -22,6 +23,10 @@ |
43 | """The user requested a state transition that is not possible.""" |
44 | |
45 | |
46 | +class ClaimReviewFailed(Exception): |
47 | + """The user cannot claim the pending review.""" |
48 | + |
49 | + |
50 | class InvalidBranchMergeProposal(Exception): |
51 | """Raised during the creation of a new branch merge proposal. |
52 | |
53 | @@ -44,5 +49,3 @@ |
54 | |
55 | class WrongBranchMergeProposal(Exception): |
56 | """The comment requested is not associated with this merge proposal.""" |
57 | - |
58 | - |
59 | |
60 | === modified file 'lib/lp/code/interfaces/codereviewvote.py' |
61 | --- lib/lp/code/interfaces/codereviewvote.py 2009-10-09 04:15:55 +0000 |
62 | +++ lib/lp/code/interfaces/codereviewvote.py 2009-12-07 04:01:14 +0000 |
63 | @@ -9,7 +9,7 @@ |
64 | ] |
65 | |
66 | from zope.interface import Interface |
67 | -from zope.schema import Datetime, Int, TextLine |
68 | +from zope.schema import Bool, Datetime, Int, TextLine |
69 | |
70 | from canonical.launchpad import _ |
71 | from canonical.launchpad.fields import PublicPersonChoice |
72 | @@ -19,16 +19,12 @@ |
73 | ICodeReviewComment) |
74 | from lazr.restful.fields import Reference |
75 | from lazr.restful.declarations import ( |
76 | - export_as_webservice_entry, exported) |
77 | - |
78 | - |
79 | -class ICodeReviewVoteReference(Interface): |
80 | - """A reference to a vote on a IBranchMergeProposal. |
81 | - |
82 | - There is at most one reference to a vote for each reviewer on a given |
83 | - branch merge proposal. |
84 | - """ |
85 | - export_as_webservice_entry() |
86 | + call_with, export_as_webservice_entry, export_write_operation, exported, |
87 | + REQUEST_USER) |
88 | + |
89 | + |
90 | +class ICodeReviewVoteReferencePublic(Interface): |
91 | + """The public attributes for code review vote references.""" |
92 | |
93 | id = Int( |
94 | title=_("The ID of the vote reference")) |
95 | @@ -66,3 +62,37 @@ |
96 | "The code review comment that contains the most recent vote." |
97 | ), |
98 | required=True, schema=ICodeReviewComment)) |
99 | + |
100 | + is_pending = exported( |
101 | + Bool(title=_("Is the pending?"), required=True, readonly=True)) |
102 | + |
103 | + |
104 | +class ICodeReviewVoteReferenceEdit(Interface): |
105 | + """Method that require edit permissions.""" |
106 | + |
107 | + @call_with(claimant=REQUEST_USER) |
108 | + @export_write_operation() |
109 | + def claimReview(claimant): |
110 | + """Change a pending review into a review for claimant. |
111 | + |
112 | + Pending team reviews can be claimed by members of that team. This |
113 | + allows reviews to be moved of the general team todo list, and onto a |
114 | + personal todo list. |
115 | + |
116 | + :param claimant: The person claiming the team review. |
117 | + :raises ClaimReviewFailed: If the claimant already has a |
118 | + personal review, if the reviewer is not a team, if the |
119 | + claimant is not in the reviewer team, or if the review is |
120 | + not pending. |
121 | + """ |
122 | + |
123 | + |
124 | +class ICodeReviewVoteReference(ICodeReviewVoteReferencePublic, |
125 | + ICodeReviewVoteReferenceEdit): |
126 | + """A reference to a vote on a IBranchMergeProposal. |
127 | + |
128 | + There is at most one reference to a vote for each reviewer on a given |
129 | + branch merge proposal. |
130 | + """ |
131 | + |
132 | + export_as_webservice_entry() |
133 | |
134 | === modified file 'lib/lp/code/model/codereviewvote.py' |
135 | --- lib/lp/code/model/codereviewvote.py 2009-06-25 04:06:00 +0000 |
136 | +++ lib/lp/code/model/codereviewvote.py 2009-12-07 04:01:14 +0000 |
137 | @@ -15,6 +15,7 @@ |
138 | from canonical.database.constants import DEFAULT |
139 | from canonical.database.datetimecol import UtcDateTimeCol |
140 | from canonical.database.sqlbase import SQLBase |
141 | +from lp.code.errors import ClaimReviewFailed |
142 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference |
143 | |
144 | |
145 | @@ -36,3 +37,31 @@ |
146 | review_type = StringCol(default=None) |
147 | comment = ForeignKey( |
148 | dbName='vote_message', foreignKey='CodeReviewComment', default=None) |
149 | + |
150 | + @property |
151 | + def is_pending(self): |
152 | + """See `ICodeReviewVote`""" |
153 | + # Reviews are pending if there is no associated comment. |
154 | + return self.comment is None |
155 | + |
156 | + def claimReview(self, claimant): |
157 | + """See `ICodeReviewVote`""" |
158 | + if not self.is_pending: |
159 | + raise ClaimReviewFailed('The review is not pending.') |
160 | + if not self.reviewer.is_team: |
161 | + raise ClaimReviewFailed('Cannot claim non-team reviews.') |
162 | + if not claimant.inTeam(self.reviewer): |
163 | + raise ClaimReviewFailed( |
164 | + '%s is not a member of %s' % |
165 | + (claimant.unique_displayname, |
166 | + self.reviewer.unique_displayname)) |
167 | + claimant_review = ( |
168 | + self.branch_merge_proposal.getUsersVoteReference(claimant)) |
169 | + if claimant_review is not None: |
170 | + if claimant_review.is_pending: |
171 | + error_str = '%s has an existing pending review' |
172 | + else: |
173 | + error_str = '%s has an existing personal review' |
174 | + raise ClaimReviewFailed( |
175 | + error_str % claimant.unique_displayname) |
176 | + self.reviewer = claimant |
177 | |
178 | === modified file 'lib/lp/code/model/tests/test_branchmergeproposals.py' |
179 | --- lib/lp/code/model/tests/test_branchmergeproposals.py 2009-12-07 04:01:13 +0000 |
180 | +++ lib/lp/code/model/tests/test_branchmergeproposals.py 2009-12-07 04:01:14 +0000 |
181 | @@ -1683,6 +1683,7 @@ |
182 | # Still only one vote. |
183 | self.assertEqual(1, len(list(merge_proposal.votes))) |
184 | |
185 | + |
186 | class TestBranchMergeProposalResubmit(TestCaseWithFactory): |
187 | |
188 | layer = DatabaseFunctionalLayer |
189 | |
190 | === modified file 'lib/lp/code/model/tests/test_codereviewvote.py' |
191 | --- lib/lp/code/model/tests/test_codereviewvote.py 2009-06-25 04:06:00 +0000 |
192 | +++ lib/lp/code/model/tests/test_codereviewvote.py 2009-12-07 04:01:14 +0000 |
193 | @@ -3,30 +3,112 @@ |
194 | |
195 | from unittest import TestLoader |
196 | |
197 | +from zope.security.interfaces import Unauthorized |
198 | + |
199 | from canonical.database.constants import UTC_NOW |
200 | +from canonical.testing import DatabaseFunctionalLayer |
201 | + |
202 | +from lp.code.enums import CodeReviewVote |
203 | +from lp.code.errors import ClaimReviewFailed |
204 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference |
205 | -from lp.testing import TestCaseWithFactory |
206 | -from canonical.launchpad.webapp.testing import verifyObject |
207 | -from canonical.testing import LaunchpadZopelessLayer |
208 | +from lp.testing import login_person, TestCaseWithFactory |
209 | + |
210 | |
211 | class TestCodeReviewVote(TestCaseWithFactory): |
212 | |
213 | - layer = LaunchpadZopelessLayer |
214 | + layer = DatabaseFunctionalLayer |
215 | |
216 | def test_create_vote(self): |
217 | """CodeReviewVotes can be created""" |
218 | merge_proposal = self.factory.makeBranchMergeProposal() |
219 | reviewer = self.factory.makePerson() |
220 | - registrant = self.factory.makePerson() |
221 | - vote = merge_proposal.nominateReviewer(reviewer, registrant) |
222 | + login_person(merge_proposal.registrant) |
223 | + vote = merge_proposal.nominateReviewer( |
224 | + reviewer, merge_proposal.registrant) |
225 | self.assertEqual(reviewer, vote.reviewer) |
226 | - self.assertEqual(registrant, vote.registrant) |
227 | + self.assertEqual(merge_proposal.registrant, vote.registrant) |
228 | self.assertEqual(merge_proposal, vote.branch_merge_proposal) |
229 | self.assertEqual([vote], list(merge_proposal.votes)) |
230 | self.assertSqlAttributeEqualsDate( |
231 | vote, 'date_created', UTC_NOW) |
232 | - assert verifyObject(ICodeReviewVoteReference, vote), ('Implements the' |
233 | - ' expected interface.') |
234 | + self.assertProvides(vote, ICodeReviewVoteReference) |
235 | + |
236 | + |
237 | +class TestCodeReviewVoteReferenceClaimReview(TestCaseWithFactory): |
238 | + """Tests for CodeReviewVoteReference.claimReview.""" |
239 | + |
240 | + layer = DatabaseFunctionalLayer |
241 | + |
242 | + def setUp(self): |
243 | + TestCaseWithFactory.setUp(self) |
244 | + # Setup the proposal, claimant and team reviewer. |
245 | + self.bmp = self.factory.makeBranchMergeProposal() |
246 | + self.claimant = self.factory.makePerson() |
247 | + self.review_team = self.factory.makeTeam() |
248 | + |
249 | + def _addPendingReview(self): |
250 | + """Add a pending review for the review_team.""" |
251 | + login_person(self.bmp.registrant) |
252 | + return self.bmp.nominateReviewer( |
253 | + reviewer=self.review_team, |
254 | + registrant=self.bmp.registrant) |
255 | + |
256 | + def _addClaimantToReviewTeam(self): |
257 | + """Add the claimant to the review team.""" |
258 | + login_person(self.review_team.teamowner) |
259 | + self.review_team.addMember( |
260 | + person=self.claimant, reviewer=self.review_team.teamowner) |
261 | + |
262 | + def test_personal_completed_review(self): |
263 | + # If the claimant has a personal review already, then they can't claim |
264 | + # a pending team review. |
265 | + login_person(self.claimant) |
266 | + # Make sure that the personal review is done before the pending team |
267 | + # review, otherwise the pending team review will be claimed by this |
268 | + # one. |
269 | + self.bmp.createComment( |
270 | + self.claimant, 'Message subject', 'Message content', |
271 | + vote=CodeReviewVote.APPROVE) |
272 | + review = self._addPendingReview() |
273 | + self._addClaimantToReviewTeam() |
274 | + self.assertRaises( |
275 | + ClaimReviewFailed, review.claimReview, self.claimant) |
276 | + |
277 | + def test_personal_pending_review(self): |
278 | + # If the claimant has a pending review already, then they can't claim |
279 | + # a pending team review. |
280 | + review = self._addPendingReview() |
281 | + self._addClaimantToReviewTeam() |
282 | + login_person(self.bmp.registrant) |
283 | + self.bmp.nominateReviewer( |
284 | + reviewer=self.claimant, registrant=self.bmp.registrant) |
285 | + login_person(self.claimant) |
286 | + self.assertRaises( |
287 | + ClaimReviewFailed, review.claimReview, self.claimant) |
288 | + |
289 | + def test_personal_not_in_review_team(self): |
290 | + # If the claimant is not in the review team, an error is raised. |
291 | + review = self._addPendingReview() |
292 | + # Since the claimant isn't in the review team, they don't have |
293 | + # launchpad.Edit on the review itself, hence Unauthorized. |
294 | + login_person(self.claimant) |
295 | + # Actually accessing claimReview triggers the security proxy. |
296 | + self.assertRaises( |
297 | + Unauthorized, getattr, review, 'claimReview') |
298 | + # The merge proposal registrant however does have edit permissions, |
299 | + # but isn't in the team, so they get ClaimReviewFailed. |
300 | + login_person(self.bmp.registrant) |
301 | + self.assertRaises( |
302 | + ClaimReviewFailed, review.claimReview, self.bmp.registrant) |
303 | + |
304 | + def test_success(self): |
305 | + # If the claimant is in the review team, and does not have a personal |
306 | + # review, pending or completed, then they can claim the team review. |
307 | + review = self._addPendingReview() |
308 | + self._addClaimantToReviewTeam() |
309 | + login_person(self.claimant) |
310 | + review.claimReview(self.claimant) |
311 | + self.assertEqual(self.claimant, review.reviewer) |
312 | |
313 | |
314 | def test_suite(): |
315 | |
316 | === modified file 'lib/lp/code/stories/webservice/xx-branchmergeproposal.txt' |
317 | --- lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-11-05 21:36:14 +0000 |
318 | +++ lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-12-07 04:01:14 +0000 |
319 | @@ -79,6 +79,7 @@ |
320 | branch_merge_proposal_link: u'http://api.launchpad.dev/beta/~.../+merge/...' |
321 | comment_link: None |
322 | date_created: u'...' |
323 | + is_pending: True |
324 | registrant_link: u'http://api.launchpad.dev/beta/~person-name...' |
325 | resource_type_link: u'http://api.launchpad.dev/beta/#code_review_vote_reference' |
326 | review_type: u'green' |
327 | @@ -194,6 +195,7 @@ |
328 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' |
329 | comment_link: u'http://.../~source/fooix/fix-it/+merge/.../comments/...' |
330 | date_created: u'...' |
331 | + is_pending: False |
332 | registrant_link: u'http://.../~person-name...' |
333 | resource_type_link: u'http://.../#code_review_vote_reference' |
334 | review_type: u'code' |
335 | @@ -221,6 +223,7 @@ |
336 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/...' |
337 | comment_link: None |
338 | date_created: u'...' |
339 | + is_pending: True |
340 | registrant_link: u'http://.../~target' |
341 | resource_type_link: u'http://.../#code_review_vote_reference' |
342 | review_type: u'code' |
Moves the logic of actually claiming a review into the model code itself and exposes it over the api.
tests: tests.test_ codereviewvote
model.
and for the is_pending bit: xx-branchmergep roposal
webservice/