Merge ~cjwatson/launchpad:stormify-codereview into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 337dbdc1b13415ef4d038e21dc0c44dce9807e44
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:stormify-codereview
Merge into: launchpad:master
Diff against target: 462 lines (+122/-67)
11 files modified
lib/lp/code/mail/tests/test_codehandler.py (+2/-0)
lib/lp/code/mail/tests/test_codereviewcomment.py (+1/-1)
lib/lp/code/model/branchcollection.py (+2/-2)
lib/lp/code/model/branchmergeproposal.py (+28/-15)
lib/lp/code/model/codereviewcomment.py (+30/-14)
lib/lp/code/model/codereviewvote.py (+36/-20)
lib/lp/code/model/gitcollection.py (+2/-2)
lib/lp/code/model/tests/test_branch.py (+4/-4)
lib/lp/code/model/tests/test_gitrepository.py (+4/-4)
lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+2/-2)
lib/lp/services/mail/helpers.py (+11/-3)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Review via email: mp+390667@code.launchpad.net

Commit message

Convert CodeReviewComment and CodeReviewVoteReference to Storm

Description of the change

This requires a slight fix to the infrastructure for incoming email to decode message bodies to text at the appropriate point so that the code-review-by-email system keeps working.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/mail/tests/test_codehandler.py b/lib/lp/code/mail/tests/test_codehandler.py
2index a02cc4d..97feda0 100644
3--- a/lib/lp/code/mail/tests/test_codehandler.py
4+++ b/lib/lp/code/mail/tests/test_codehandler.py
5@@ -3,6 +3,8 @@
6
7 """Testing the CodeHandler."""
8
9+from __future__ import absolute_import, print_function, unicode_literals
10+
11 __metaclass__ = type
12
13 from textwrap import dedent
14diff --git a/lib/lp/code/mail/tests/test_codereviewcomment.py b/lib/lp/code/mail/tests/test_codereviewcomment.py
15index 930753e..a692a97 100644
16--- a/lib/lp/code/mail/tests/test_codereviewcomment.py
17+++ b/lib/lp/code/mail/tests/test_codereviewcomment.py
18@@ -243,7 +243,7 @@ class TestCodeReviewComment(TestCaseWithFactory):
19 def test_generateEmailWithVoteAndTag(self):
20 """Ensure that vote tags are displayed."""
21 mailer, subscriber = self.makeMailer(
22- vote=CodeReviewVote.APPROVE, vote_tag='DBTAG')
23+ vote=CodeReviewVote.APPROVE, vote_tag=u'DBTAG')
24 ctrl = mailer.generateEmail(
25 subscriber.preferredemail.email, subscriber)
26 self.assertEqual('Review: Approve dbtag', ctrl.body.splitlines()[0])
27diff --git a/lib/lp/code/model/branchcollection.py b/lib/lp/code/model/branchcollection.py
28index 7482778..bc3a026 100644
29--- a/lib/lp/code/model/branchcollection.py
30+++ b/lib/lp/code/model/branchcollection.py
31@@ -484,10 +484,10 @@ class GenericBranchCollection:
32 tables = [
33 BranchMergeProposal,
34 Join(CodeReviewVoteReference,
35- CodeReviewVoteReference.branch_merge_proposalID == \
36+ CodeReviewVoteReference.branch_merge_proposal ==
37 BranchMergeProposal.id),
38 LeftJoin(CodeReviewComment,
39- CodeReviewVoteReference.commentID == CodeReviewComment.id)]
40+ CodeReviewVoteReference.comment == CodeReviewComment.id)]
41
42 expressions = [
43 CodeReviewVoteReference.reviewer == reviewer,
44diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py
45index 338cc29..2a346b4 100644
46--- a/lib/lp/code/model/branchmergeproposal.py
47+++ b/lib/lp/code/model/branchmergeproposal.py
48@@ -46,6 +46,7 @@ from zope.interface import implementer
49 from zope.security.interfaces import Unauthorized
50
51 from lp.app.enums import PRIVATE_INFORMATION_TYPES
52+from lp.app.errors import NotFoundError
53 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
54 from lp.bugs.interfaces.bugtask import IBugTaskSet
55 from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
56@@ -565,11 +566,14 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
57 @property
58 def all_comments(self):
59 """See `IBranchMergeProposal`."""
60- return CodeReviewComment.selectBy(branch_merge_proposal=self.id)
61+ return IStore(CodeReviewComment).find(
62+ CodeReviewComment, branch_merge_proposal=self)
63
64 def getComment(self, id):
65 """See `IBranchMergeProposal`."""
66- comment = CodeReviewComment.get(id)
67+ comment = IStore(CodeReviewComment).get(CodeReviewComment, id)
68+ if comment is None:
69+ raise NotFoundError(id)
70 if comment.branch_merge_proposal != self:
71 raise WrongBranchMergeProposal
72 return comment
73@@ -583,7 +587,10 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
74
75 def setCommentVisibility(self, user, comment_number, visible):
76 """See `IBranchMergeProposal`."""
77- comment = CodeReviewComment.get(comment_number)
78+ comment = IStore(CodeReviewComment).get(
79+ CodeReviewComment, comment_number)
80+ if comment is None:
81+ raise NotFoundError(comment_number)
82 if comment.branch_merge_proposal != self:
83 raise WrongBranchMergeProposal
84 if not comment.userCanSetCommentVisibility(user):
85@@ -596,7 +603,9 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
86 """See `IBranchMergeProposal`.
87
88 This function can raise WrongBranchMergeProposal."""
89- vote = CodeReviewVoteReference.get(id)
90+ vote = IStore(CodeReviewVoteReference).get(CodeReviewVoteReference, id)
91+ if vote is None:
92+ raise NotFoundError(id)
93 if vote.branch_merge_proposal != self:
94 raise WrongBranchMergeProposal
95 return vote
96@@ -932,6 +941,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
97 date_created=_date_created)
98 self._ensureAssociatedBranchesVisibleToReviewer(reviewer)
99 vote_reference.review_type = review_type
100+ Store.of(vote_reference).flush()
101 if _notify_listeners:
102 notify(ReviewerNominatedEvent(vote_reference))
103 return vote_reference
104@@ -1098,11 +1108,13 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
105 if team_ref is not None:
106 return team_ref
107 # Create a new reference.
108- return CodeReviewVoteReference(
109+ vote_reference = CodeReviewVoteReference(
110 branch_merge_proposal=self,
111 registrant=user,
112 reviewer=user,
113 review_type=review_type)
114+ Store.of(vote_reference).flush()
115+ return vote_reference
116
117 def createCommentFromMessage(self, message, vote, review_type,
118 original_email, _notify_listeners=True,
119@@ -1126,6 +1138,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
120 vote_reference.reviewer = message.owner
121 vote_reference.review_type = review_type
122 vote_reference.comment = code_review_message
123+ Store.of(code_review_message).flush()
124 if _notify_listeners:
125 notify(ObjectCreatedEvent(code_review_message))
126 return code_review_message
127@@ -1389,15 +1402,15 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
128 if include_votes:
129 votes = load_referencing(
130 CodeReviewVoteReference, branch_merge_proposals,
131- ['branch_merge_proposalID'])
132+ ['branch_merge_proposal_id'])
133 votes_map = defaultdict(list)
134 for vote in votes:
135- votes_map[vote.branch_merge_proposalID].append(vote)
136+ votes_map[vote.branch_merge_proposal_id].append(vote)
137 for mp in branch_merge_proposals:
138 get_property_cache(mp).votes = votes_map[mp.id]
139- comments = load_related(CodeReviewComment, votes, ['commentID'])
140- load_related(Message, comments, ['messageID'])
141- person_ids.update(vote.reviewerID for vote in votes)
142+ comments = load_related(CodeReviewComment, votes, ['comment_id'])
143+ load_related(Message, comments, ['message_id'])
144+ person_ids.update(vote.reviewer_id for vote in votes)
145
146 # we also provide a summary of diffs, so load them
147 load_related(LibraryFileAlias, diffs, ['diff_textID'])
148@@ -1439,8 +1452,8 @@ class BranchMergeProposalGetter:
149 BranchMergeProposal.registrantID == participant.id)
150
151 review_select = Select(
152- [CodeReviewVoteReference.branch_merge_proposalID],
153- [CodeReviewVoteReference.reviewerID == participant.id])
154+ [CodeReviewVoteReference.branch_merge_proposal_id],
155+ [CodeReviewVoteReference.reviewer == participant])
156
157 query = Store.of(participant).find(
158 BranchMergeProposal,
159@@ -1463,13 +1476,13 @@ class BranchMergeProposalGetter:
160 # the actual vote for that person.
161 tables = [
162 CodeReviewVoteReference,
163- Join(Person, CodeReviewVoteReference.reviewerID == Person.id),
164+ Join(Person, CodeReviewVoteReference.reviewer == Person.id),
165 LeftJoin(
166 CodeReviewComment,
167- CodeReviewVoteReference.commentID == CodeReviewComment.id)]
168+ CodeReviewVoteReference.comment == CodeReviewComment.id)]
169 results = store.using(*tables).find(
170 (CodeReviewVoteReference, Person, CodeReviewComment),
171- CodeReviewVoteReference.branch_merge_proposalID.is_in(ids))
172+ CodeReviewVoteReference.branch_merge_proposal_id.is_in(ids))
173 for reference, person, comment in results:
174 result[reference.branch_merge_proposal].append(reference)
175 return result
176diff --git a/lib/lp/code/model/codereviewcomment.py b/lib/lp/code/model/codereviewcomment.py
177index 47f640c..1029b4e 100644
178--- a/lib/lp/code/model/codereviewcomment.py
179+++ b/lib/lp/code/model/codereviewcomment.py
180@@ -10,9 +10,11 @@ __all__ = [
181
182 from textwrap import TextWrapper
183
184-from sqlobject import (
185- ForeignKey,
186- StringCol,
187+from storm.locals import (
188+ Int,
189+ Reference,
190+ Store,
191+ Unicode,
192 )
193 from zope.interface import implementer
194
195@@ -22,8 +24,8 @@ from lp.code.interfaces.codereviewcomment import (
196 ICodeReviewComment,
197 ICodeReviewCommentDeletion,
198 )
199-from lp.services.database.enumcol import EnumCol
200-from lp.services.database.sqlbase import SQLBase
201+from lp.services.database.enumcol import DBEnum
202+from lp.services.database.stormbase import StormBase
203 from lp.services.mail.signedmessage import signed_message_from_string
204
205
206@@ -60,17 +62,27 @@ def quote_text_as_email(text, width=80):
207
208
209 @implementer(ICodeReviewComment, ICodeReviewCommentDeletion, IHasBranchTarget)
210-class CodeReviewComment(SQLBase):
211+class CodeReviewComment(StormBase):
212 """A table linking branch merge proposals and messages."""
213
214- _table = 'CodeReviewMessage'
215-
216- branch_merge_proposal = ForeignKey(
217- dbName='branch_merge_proposal', foreignKey='BranchMergeProposal',
218- notNull=True)
219- message = ForeignKey(dbName='message', foreignKey='Message', notNull=True)
220- vote = EnumCol(dbName='vote', notNull=False, schema=CodeReviewVote)
221- vote_tag = StringCol(default=None)
222+ __storm_table__ = 'CodeReviewMessage'
223+
224+ id = Int(primary=True)
225+ branch_merge_proposal_id = Int(
226+ name='branch_merge_proposal', allow_none=False)
227+ branch_merge_proposal = Reference(
228+ branch_merge_proposal_id, 'BranchMergeProposal.id')
229+ message_id = Int(name='message', allow_none=False)
230+ message = Reference(message_id, 'Message.id')
231+ vote = DBEnum(name='vote', allow_none=True, enum=CodeReviewVote)
232+ vote_tag = Unicode(default=None)
233+
234+ def __init__(self, branch_merge_proposal, message, vote=None,
235+ vote_tag=None):
236+ self.branch_merge_proposal = branch_merge_proposal
237+ self.message = message
238+ self.vote = vote
239+ self.vote_tag = vote_tag
240
241 @property
242 def author(self):
243@@ -134,3 +146,7 @@ class CodeReviewComment(SQLBase):
244 return (
245 self.branch_merge_proposal.userCanSetCommentVisibility(user) or
246 (user is not None and user.inTeam(self.author)))
247+
248+ def destroySelf(self):
249+ """Delete this comment."""
250+ Store.of(self).remove(self)
251diff --git a/lib/lp/code/model/codereviewvote.py b/lib/lp/code/model/codereviewvote.py
252index d2e5c53..b695ebe 100644
253--- a/lib/lp/code/model/codereviewvote.py
254+++ b/lib/lp/code/model/codereviewvote.py
255@@ -8,12 +8,15 @@ __all__ = [
256 'CodeReviewVoteReference',
257 ]
258
259-from sqlobject import (
260- ForeignKey,
261- StringCol,
262+import pytz
263+from storm.locals import (
264+ DateTime,
265+ Int,
266+ Reference,
267+ Store,
268+ Unicode,
269 )
270 from zope.interface import implementer
271-from zope.schema import Int
272
273 from lp.code.errors import (
274 ClaimReviewFailed,
275@@ -22,27 +25,36 @@ from lp.code.errors import (
276 )
277 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
278 from lp.services.database.constants import DEFAULT
279-from lp.services.database.datetimecol import UtcDateTimeCol
280-from lp.services.database.sqlbase import SQLBase
281+from lp.services.database.stormbase import StormBase
282
283
284 @implementer(ICodeReviewVoteReference)
285-class CodeReviewVoteReference(SQLBase):
286+class CodeReviewVoteReference(StormBase):
287 """See `ICodeReviewVote`"""
288
289- _table = 'CodeReviewVote'
290- id = Int()
291- branch_merge_proposal = ForeignKey(
292- dbName='branch_merge_proposal', foreignKey='BranchMergeProposal',
293- notNull=True)
294- date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
295- registrant = ForeignKey(
296- dbName='registrant', foreignKey='Person', notNull=True)
297- reviewer = ForeignKey(
298- dbName='reviewer', foreignKey='Person', notNull=True)
299- review_type = StringCol(default=None)
300- comment = ForeignKey(
301- dbName='vote_message', foreignKey='CodeReviewComment', default=None)
302+ __storm_table__ = 'CodeReviewVote'
303+
304+ id = Int(primary=True)
305+ branch_merge_proposal_id = Int(
306+ name='branch_merge_proposal', allow_none=False)
307+ branch_merge_proposal = Reference(
308+ branch_merge_proposal_id, 'BranchMergeProposal.id')
309+ date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
310+ registrant_id = Int(name='registrant', allow_none=False)
311+ registrant = Reference(registrant_id, 'Person.id')
312+ reviewer_id = Int(name='reviewer', allow_none=False)
313+ reviewer = Reference(reviewer_id, 'Person.id')
314+ review_type = Unicode(default=None)
315+ comment_id = Int(name='vote_message', default=None)
316+ comment = Reference(comment_id, 'CodeReviewComment.id')
317+
318+ def __init__(self, branch_merge_proposal, registrant, reviewer,
319+ review_type=None, date_created=DEFAULT):
320+ self.branch_merge_proposal = branch_merge_proposal
321+ self.registrant = registrant
322+ self.reviewer = reviewer
323+ self.review_type = review_type
324+ self.date_created = date_created
325
326 @property
327 def is_pending(self):
328@@ -96,6 +108,10 @@ class CodeReviewVoteReference(SQLBase):
329 self.validateReasignReview(reviewer)
330 self.reviewer = reviewer
331
332+ def destroySelf(self):
333+ """Delete this vote."""
334+ Store.of(self).remove(self)
335+
336 def delete(self):
337 """See `ICodeReviewVote`"""
338 if not self.is_pending:
339diff --git a/lib/lp/code/model/gitcollection.py b/lib/lp/code/model/gitcollection.py
340index 4095d5e..6447d0b 100644
341--- a/lib/lp/code/model/gitcollection.py
342+++ b/lib/lp/code/model/gitcollection.py
343@@ -412,10 +412,10 @@ class GenericGitCollection:
344 tables = [
345 BranchMergeProposal,
346 Join(CodeReviewVoteReference,
347- CodeReviewVoteReference.branch_merge_proposalID == \
348+ CodeReviewVoteReference.branch_merge_proposal ==
349 BranchMergeProposal.id),
350 LeftJoin(CodeReviewComment,
351- CodeReviewVoteReference.commentID == CodeReviewComment.id)]
352+ CodeReviewVoteReference.comment == CodeReviewComment.id)]
353
354 expressions = [
355 CodeReviewVoteReference.reviewer == reviewer,
356diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py
357index 1978e88..e2421ba 100644
358--- a/lib/lp/code/model/tests/test_branch.py
359+++ b/lib/lp/code/model/tests/test_branch.py
360@@ -1566,8 +1566,8 @@ class TestBranchDeletionConsequences(TestCase):
361 comment_id = comment.id
362 branch = comment.branch_merge_proposal.source_branch
363 branch.destroySelf(break_references=True)
364- self.assertRaises(
365- SQLObjectNotFound, CodeReviewComment.get, comment_id)
366+ self.assertIsNone(
367+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
368
369 def test_deleteTargetCodeReviewComment(self):
370 """Deletion of branches that have CodeReviewComments works."""
371@@ -1575,8 +1575,8 @@ class TestBranchDeletionConsequences(TestCase):
372 comment_id = comment.id
373 branch = comment.branch_merge_proposal.target_branch
374 branch.destroySelf(break_references=True)
375- self.assertRaises(
376- SQLObjectNotFound, CodeReviewComment.get, comment_id)
377+ self.assertIsNone(
378+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
379
380 def test_branchWithBugRequirements(self):
381 """Deletion requirements for a branch with a bug are right."""
382diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
383index a8b9c35..62bf865 100644
384--- a/lib/lp/code/model/tests/test_gitrepository.py
385+++ b/lib/lp/code/model/tests/test_gitrepository.py
386@@ -1086,8 +1086,8 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
387 comment_id = comment.id
388 repository = comment.branch_merge_proposal.source_git_repository
389 repository.destroySelf(break_references=True)
390- self.assertRaises(
391- SQLObjectNotFound, CodeReviewComment.get, comment_id)
392+ self.assertIsNone(
393+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
394
395 def test_delete_target_CodeReviewComment(self):
396 # Deletion of target repositories that have CodeReviewComments works.
397@@ -1095,8 +1095,8 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
398 comment_id = comment.id
399 repository = comment.branch_merge_proposal.target_git_repository
400 repository.destroySelf(break_references=True)
401- self.assertRaises(
402- SQLObjectNotFound, CodeReviewComment.get, comment_id)
403+ self.assertIsNone(
404+ IStore(CodeReviewComment).get(CodeReviewComment, comment_id))
405
406 def test_sourceBranchWithCodeReviewVoteReference(self):
407 # break_references handles CodeReviewVoteReference source repository.
408diff --git a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
409index 1580ea4..98483a4 100644
410--- a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
411+++ b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt
412@@ -463,7 +463,7 @@ which is the one we want the method to return.
413 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
414 ... registrant=branch_owner, source_branch=source_branch)
415 >>> proposal.nominateReviewer(target_owner, branch_owner)
416- <CodeReviewVoteReference at ...>
417+ <lp.code.model.codereviewvote.CodeReviewVoteReference object at ...>
418
419 And then we propose a merge the other way, so that the owner is target,
420 but they have not been asked to review, meaning that the method shouldn't
421@@ -474,7 +474,7 @@ return this review.
422 ... product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW,
423 ... registrant=target_owner, source_branch=target_branch)
424 >>> proposal.nominateReviewer(branch_owner, target_owner)
425- <CodeReviewVoteReference at ...>
426+ <lp.code.model.codereviewvote.CodeReviewVoteReference object at ...>
427 >>> logout()
428
429 >>> proposals = webservice.named_get('/~target', 'getRequestedReviews'
430diff --git a/lib/lp/services/mail/helpers.py b/lib/lp/services/mail/helpers.py
431index 82640aa..80f68d3 100644
432--- a/lib/lp/services/mail/helpers.py
433+++ b/lib/lp/services/mail/helpers.py
434@@ -35,7 +35,13 @@ class IncomingEmailError(Exception):
435
436
437 def get_main_body(signed_msg):
438- """Returns the first text part of the email."""
439+ """Returns the first text part of the email.
440+
441+ This always returns text (or None if the email has no text parts at
442+ all). It decodes using the character set in the text part's
443+ Content-Type, or ISO-8859-1 if unspecified (in order to minimise the
444+ chances of `UnicodeDecodeError`s).
445+ """
446 msg = getattr(signed_msg, 'signedMessage', None)
447 if msg is None:
448 # The email wasn't signed.
449@@ -43,9 +49,11 @@ def get_main_body(signed_msg):
450 if msg.is_multipart():
451 for part in msg.walk():
452 if part.get_content_type() == 'text/plain':
453- return part.get_payload(decode=True)
454+ charset = part.get_content_charset('ISO-8859-1')
455+ return part.get_payload(decode=True).decode(charset)
456 else:
457- return msg.get_payload(decode=True)
458+ charset = msg.get_content_charset('ISO-8859-1')
459+ return msg.get_payload(decode=True).decode(charset)
460
461
462 def guess_bugtask(bug, person):

Subscribers

People subscribed via source and target branches

to status/vote changes: