Merge ~cjwatson/launchpad:stormify-codereview into launchpad:master
- Git
- lp:~cjwatson/launchpad
- stormify-codereview
- Merge into 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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Review via email: mp+390667@code.launchpad.net |
Commit message
Convert CodeReviewComment and CodeReviewVoteR
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-
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
1 | diff --git a/lib/lp/code/mail/tests/test_codehandler.py b/lib/lp/code/mail/tests/test_codehandler.py |
2 | index 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 |
14 | diff --git a/lib/lp/code/mail/tests/test_codereviewcomment.py b/lib/lp/code/mail/tests/test_codereviewcomment.py |
15 | index 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]) |
27 | diff --git a/lib/lp/code/model/branchcollection.py b/lib/lp/code/model/branchcollection.py |
28 | index 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, |
44 | diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py |
45 | index 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 |
176 | diff --git a/lib/lp/code/model/codereviewcomment.py b/lib/lp/code/model/codereviewcomment.py |
177 | index 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) |
251 | diff --git a/lib/lp/code/model/codereviewvote.py b/lib/lp/code/model/codereviewvote.py |
252 | index 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: |
339 | diff --git a/lib/lp/code/model/gitcollection.py b/lib/lp/code/model/gitcollection.py |
340 | index 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, |
356 | diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py |
357 | index 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.""" |
382 | diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py |
383 | index 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. |
408 | diff --git a/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt b/lib/lp/code/stories/webservice/xx-branchmergeproposal.txt |
409 | index 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' |
430 | diff --git a/lib/lp/services/mail/helpers.py b/lib/lp/services/mail/helpers.py |
431 | index 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): |