Merge lp:~abentley/launchpad/reply into lp:launchpad
- reply
- Merge into devel
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Approved by: | Muharem Hrnjadovic |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~abentley/launchpad/reply |
Merge into: | lp:launchpad |
Diff against target: |
435 lines 7 files modified
lib/canonical/launchpad/javascript/lp/comment.js (+51/-5) lib/lp/code/browser/codereviewcomment.py (+1/-34) lib/lp/code/browser/tests/test_codereviewcomment.py (+1/-60) lib/lp/code/interfaces/codereviewcomment.py (+5/-0) lib/lp/code/model/codereviewcomment.py (+38/-0) lib/lp/code/model/tests/test_codereviewcomment.py (+59/-1) lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+3/-0) |
To merge this branch: | bzr merge lp:~abentley/launchpad/reply |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | ui* | Approve | |
Barry Warsaw (community) | ui* | Approve | |
Muharem Hrnjadovic (community) | Approve | ||
Review via email: mp+13739@code.launchpad.net |
Commit message
Support inline replies for code review
Description of the change
To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote : | # |
Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote : | # |
Looks good, approved.
review:
Approve
Revision history for this message
Aaron Bentley (abentley) wrote : | # |
Revision history for this message
Barry Warsaw (barry) : | # |
review:
Approve
(ui*)
Revision history for this message
Paul Hummer (rockstar) : | # |
review:
Approve
(ui*)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/canonical/launchpad/javascript/lp/comment.js' | |||
2 | --- lib/canonical/launchpad/javascript/lp/comment.js 2009-10-13 15:56:13 +0000 | |||
3 | +++ lib/canonical/launchpad/javascript/lp/comment.js 2009-10-26 14:01:12 +0000 | |||
4 | @@ -85,9 +85,7 @@ | |||
5 | 85 | if (!this.validate()) { | 85 | if (!this.validate()) { |
6 | 86 | return; | 86 | return; |
7 | 87 | } | 87 | } |
11 | 88 | this.set_disabled(true); | 88 | this.activateProgressUI('Saving...'); |
9 | 89 | this.submit_button.get('parentNode').replaceChild( | ||
10 | 90 | this.progress_message, this.submit_button); | ||
12 | 91 | this.post_comment(bind(function(message_entry) { | 89 | this.post_comment(bind(function(message_entry) { |
13 | 92 | this.get_comment_HTML( | 90 | this.get_comment_HTML( |
14 | 93 | message_entry, bind(this.insert_comment_HTML, this)); | 91 | message_entry, bind(this.insert_comment_HTML, this)); |
15 | @@ -150,7 +148,18 @@ | |||
16 | 150 | this.syncUI(); | 148 | this.syncUI(); |
17 | 151 | }, | 149 | }, |
18 | 152 | /** | 150 | /** |
20 | 153 | * Stop indicating that a submission is in progress. | 151 | * Activate indications of an operation in progress. |
21 | 152 | * | ||
22 | 153 | * @param message A user message describing the operation in progress. | ||
23 | 154 | */ | ||
24 | 155 | activateProgressUI: function(message){ | ||
25 | 156 | this.progress_message.set('innerHTML', message); | ||
26 | 157 | this.set_disabled(true); | ||
27 | 158 | this.submit_button.get('parentNode').replaceChild( | ||
28 | 159 | this.progress_message, this.submit_button); | ||
29 | 160 | }, | ||
30 | 161 | /** | ||
31 | 162 | * Stop indicating that an operation is in progress. | ||
32 | 154 | * | 163 | * |
33 | 155 | * @method clearProgressUI | 164 | * @method clearProgressUI |
34 | 156 | */ | 165 | */ |
35 | @@ -202,6 +211,7 @@ | |||
36 | 202 | initializer: function() { | 211 | initializer: function() { |
37 | 203 | this.vote_input = Y.get('[id="field.vote"]'); | 212 | this.vote_input = Y.get('[id="field.vote"]'); |
38 | 204 | this.review_type = Y.get('[id="field.review_type"]'); | 213 | this.review_type = Y.get('[id="field.review_type"]'); |
39 | 214 | this.in_reply_to = null; | ||
40 | 205 | }, | 215 | }, |
41 | 206 | /** | 216 | /** |
42 | 207 | * Return the Submit button. | 217 | * Return the Submit button. |
43 | @@ -265,7 +275,8 @@ | |||
44 | 265 | content: this.comment_input.get('value'), | 275 | content: this.comment_input.get('value'), |
45 | 266 | subject: '', | 276 | subject: '', |
46 | 267 | review_type: this.review_type.get('value'), | 277 | review_type: this.review_type.get('value'), |
48 | 268 | vote: this.get_vote() | 278 | vote: this.get_vote(), |
49 | 279 | parent: this.in_reply_to.get('self_link') | ||
50 | 269 | } | 280 | } |
51 | 270 | }; | 281 | }; |
52 | 271 | this.lp_client.named_post( | 282 | this.lp_client.named_post( |
53 | @@ -290,6 +301,39 @@ | |||
54 | 290 | }); | 301 | }); |
55 | 291 | }, | 302 | }, |
56 | 292 | /** | 303 | /** |
57 | 304 | * Event handler when a "Reply" link is clicked. | ||
58 | 305 | * | ||
59 | 306 | * @param e The Event object representing the click. | ||
60 | 307 | */ | ||
61 | 308 | reply_clicked: function(e){ | ||
62 | 309 | e.halt(); | ||
63 | 310 | var reply_link = LP.client.normalize_uri(e.target.get('href')); | ||
64 | 311 | var root_url = reply_link.substr(0, | ||
65 | 312 | reply_link.length - '+reply'.length); | ||
66 | 313 | var object_url = '/api/beta' + root_url; | ||
67 | 314 | this.activateProgressUI('Loading...'); | ||
68 | 315 | window.scrollTo(0, Y.get('label [for=field.vote]').getY()); | ||
69 | 316 | this.lp_client.get(object_url, { | ||
70 | 317 | on: { | ||
71 | 318 | success: bind(function(comment){ | ||
72 | 319 | this.set_in_reply_to(comment); | ||
73 | 320 | this.clearProgressUI(); | ||
74 | 321 | this.syncUI(); | ||
75 | 322 | }, this), | ||
76 | 323 | failure: this.error_handler.getFailureHandler() | ||
77 | 324 | } | ||
78 | 325 | }); | ||
79 | 326 | }, | ||
80 | 327 | /** | ||
81 | 328 | * Set the comment that the new comment will be in reply to. | ||
82 | 329 | * | ||
83 | 330 | * @param comment The comment to be in reply to. | ||
84 | 331 | */ | ||
85 | 332 | set_in_reply_to: function(comment) { | ||
86 | 333 | this.in_reply_to = comment; | ||
87 | 334 | this.comment_input.set('value', comment.get('as_quoted_email')); | ||
88 | 335 | }, | ||
89 | 336 | /** | ||
90 | 293 | * Reset the widget to a blank state. | 337 | * Reset the widget to a blank state. |
91 | 294 | * | 338 | * |
92 | 295 | * @method reset_contents | 339 | * @method reset_contents |
93 | @@ -297,6 +341,7 @@ | |||
94 | 297 | reset_contents: function() { | 341 | reset_contents: function() { |
95 | 298 | this.review_type.set('value', ''); | 342 | this.review_type.set('value', ''); |
96 | 299 | this.vote_input.set('selectedIndex', 0); | 343 | this.vote_input.set('selectedIndex', 0); |
97 | 344 | this.in_reply_to = null; | ||
98 | 300 | CodeReviewComment.superclass.reset_contents.apply(this); | 345 | CodeReviewComment.superclass.reset_contents.apply(this); |
99 | 301 | }, | 346 | }, |
100 | 302 | /** | 347 | /** |
101 | @@ -329,6 +374,7 @@ | |||
102 | 329 | this.vote_input.on('mouseup', bind(this.syncUI, this)); | 374 | this.vote_input.on('mouseup', bind(this.syncUI, this)); |
103 | 330 | this.review_type.on('keyup', bind(this.syncUI, this)); | 375 | this.review_type.on('keyup', bind(this.syncUI, this)); |
104 | 331 | this.review_type.on('mouseup', bind(this.syncUI, this)); | 376 | this.review_type.on('mouseup', bind(this.syncUI, this)); |
105 | 377 | Y.all('a.menu-link-reply').on('click', bind(this.reply_clicked, this)); | ||
106 | 332 | }, | 378 | }, |
107 | 333 | /** | 379 | /** |
108 | 334 | * Implementation of Widget.syncUI: Update appearance according to state. | 380 | * Implementation of Widget.syncUI: Update appearance according to state. |
109 | 335 | 381 | ||
110 | === modified file 'lib/lp/code/browser/codereviewcomment.py' | |||
111 | --- lib/lp/code/browser/codereviewcomment.py 2009-10-09 02:43:04 +0000 | |||
112 | +++ lib/lp/code/browser/codereviewcomment.py 2009-10-26 14:01:12 +0000 | |||
113 | @@ -11,7 +11,6 @@ | |||
114 | 11 | 'CodeReviewCommentView', | 11 | 'CodeReviewCommentView', |
115 | 12 | 'CodeReviewDisplayComment', | 12 | 'CodeReviewDisplayComment', |
116 | 13 | ] | 13 | ] |
117 | 14 | from textwrap import TextWrapper | ||
118 | 15 | 14 | ||
119 | 16 | from zope.app.form.browser import TextAreaWidget, DropdownWidget | 15 | from zope.app.form.browser import TextAreaWidget, DropdownWidget |
120 | 17 | from zope.interface import Interface, implements | 16 | from zope.interface import Interface, implements |
121 | @@ -31,38 +30,6 @@ | |||
122 | 31 | from lp.services.comments.interfaces.conversation import IComment | 30 | from lp.services.comments.interfaces.conversation import IComment |
123 | 32 | 31 | ||
124 | 33 | 32 | ||
125 | 34 | def quote_text_as_email(text, width=80): | ||
126 | 35 | """Quote the text as if it is an email response. | ||
127 | 36 | |||
128 | 37 | Uses '> ' as a line prefix, and breaks long lines. | ||
129 | 38 | |||
130 | 39 | Trailing whitespace is stripped. | ||
131 | 40 | """ | ||
132 | 41 | # Empty text begets empty text. | ||
133 | 42 | if text is None: | ||
134 | 43 | return '' | ||
135 | 44 | text = text.rstrip() | ||
136 | 45 | if not text: | ||
137 | 46 | return '' | ||
138 | 47 | prefix = '> ' | ||
139 | 48 | # The TextWrapper's handling of code is somewhat suspect. | ||
140 | 49 | wrapper = TextWrapper( | ||
141 | 50 | initial_indent=prefix, | ||
142 | 51 | subsequent_indent=prefix, | ||
143 | 52 | width=width, | ||
144 | 53 | replace_whitespace=False) | ||
145 | 54 | result = [] | ||
146 | 55 | # Break the string into lines, and use the TextWrapper to wrap the | ||
147 | 56 | # individual lines. | ||
148 | 57 | for line in text.rstrip().split('\n'): | ||
149 | 58 | # TextWrapper won't do an indent of an empty string. | ||
150 | 59 | if line.strip() == '': | ||
151 | 60 | result.append(prefix) | ||
152 | 61 | else: | ||
153 | 62 | result.extend(wrapper.wrap(line)) | ||
154 | 63 | return '\n'.join(result) | ||
155 | 64 | |||
156 | 65 | |||
157 | 66 | class CodeReviewDisplayComment: | 33 | class CodeReviewDisplayComment: |
158 | 67 | """A code review comment or activity or both. | 34 | """A code review comment or activity or both. |
159 | 68 | 35 | ||
160 | @@ -222,7 +189,7 @@ | |||
161 | 222 | quoted comment being replied to. | 189 | quoted comment being replied to. |
162 | 223 | """ | 190 | """ |
163 | 224 | if self.is_reply: | 191 | if self.is_reply: |
165 | 225 | comment = quote_text_as_email(self.reply_to.message_body) | 192 | comment = self.reply_to.as_quoted_email |
166 | 226 | else: | 193 | else: |
167 | 227 | comment = '' | 194 | comment = '' |
168 | 228 | return {'comment': comment} | 195 | return {'comment': comment} |
169 | 229 | 196 | ||
170 | === modified file 'lib/lp/code/browser/tests/test_codereviewcomment.py' | |||
171 | --- lib/lp/code/browser/tests/test_codereviewcomment.py 2009-06-25 04:06:00 +0000 | |||
172 | +++ lib/lp/code/browser/tests/test_codereviewcomment.py 2009-10-26 14:01:12 +0000 | |||
173 | @@ -5,11 +5,9 @@ | |||
174 | 5 | 5 | ||
175 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
176 | 7 | 7 | ||
177 | 8 | from textwrap import dedent | ||
178 | 9 | import unittest | 8 | import unittest |
179 | 10 | 9 | ||
182 | 11 | from lp.code.browser.codereviewcomment import quote_text_as_email | 10 | from lp.testing import login_person, TestCaseWithFactory |
181 | 12 | from lp.testing import login_person, TestCase, TestCaseWithFactory | ||
183 | 13 | from canonical.launchpad.webapp.interfaces import IPrimaryContext | 11 | from canonical.launchpad.webapp.interfaces import IPrimaryContext |
184 | 14 | from canonical.testing import DatabaseFunctionalLayer | 12 | from canonical.testing import DatabaseFunctionalLayer |
185 | 15 | 13 | ||
186 | @@ -32,62 +30,5 @@ | |||
187 | 32 | IPrimaryContext(comment.branch_merge_proposal).context) | 30 | IPrimaryContext(comment.branch_merge_proposal).context) |
188 | 33 | 31 | ||
189 | 34 | 32 | ||
190 | 35 | class TestQuoteTextAsEmail(TestCase): | ||
191 | 36 | """Test the quote_text_as_email helper method.""" | ||
192 | 37 | |||
193 | 38 | def test_empty_string(self): | ||
194 | 39 | # Nothing just gives us an empty string. | ||
195 | 40 | self.assertEqual('', quote_text_as_email('')) | ||
196 | 41 | |||
197 | 42 | def test_none_string(self): | ||
198 | 43 | # If None is passed the quoted text is an empty string. | ||
199 | 44 | self.assertEqual('', quote_text_as_email(None)) | ||
200 | 45 | |||
201 | 46 | def test_whitespace_string(self): | ||
202 | 47 | # Just whitespace gives us an empty string. | ||
203 | 48 | self.assertEqual('', quote_text_as_email(' \t ')) | ||
204 | 49 | |||
205 | 50 | def test_long_string(self): | ||
206 | 51 | # Long lines are wrapped. | ||
207 | 52 | long_line = ('This is a very long line that needs to be wrapped ' | ||
208 | 53 | 'onto more than one line given a short length.') | ||
209 | 54 | self.assertEqual( | ||
210 | 55 | dedent("""\ | ||
211 | 56 | > This is a very long line that needs to | ||
212 | 57 | > be wrapped onto more than one line | ||
213 | 58 | > given a short length."""), | ||
214 | 59 | quote_text_as_email(long_line, 40)) | ||
215 | 60 | |||
216 | 61 | def test_code_sample(self): | ||
217 | 62 | # Initial whitespace is not trimmed. | ||
218 | 63 | code = """\ | ||
219 | 64 | def test_whitespace_string(self): | ||
220 | 65 | # Nothing just gives us the prefix. | ||
221 | 66 | self.assertEqual('', wrap_text(' \t '))""" | ||
222 | 67 | self.assertEqual( | ||
223 | 68 | dedent("""\ | ||
224 | 69 | > def test_whitespace_string(self): | ||
225 | 70 | > # Nothing just gives us the prefix. | ||
226 | 71 | > self.assertEqual('', wrap_text(' '))"""), | ||
227 | 72 | quote_text_as_email(code, 60)) | ||
228 | 73 | |||
229 | 74 | def test_empty_line_mid_string(self): | ||
230 | 75 | # Lines in the middle of the string are quoted too. | ||
231 | 76 | value = dedent("""\ | ||
232 | 77 | This is the first line. | ||
233 | 78 | |||
234 | 79 | This is the second line. | ||
235 | 80 | """) | ||
236 | 81 | expected = dedent("""\ | ||
237 | 82 | > This is the first line. | ||
238 | 83 | > | ||
239 | 84 | > This is the second line.""") | ||
240 | 85 | self.assertEqual(expected, quote_text_as_email(value)) | ||
241 | 86 | |||
242 | 87 | def test_trailing_whitespace(self): | ||
243 | 88 | # Trailing whitespace is removed. | ||
244 | 89 | self.assertEqual('> foo', quote_text_as_email(' foo \n ')) | ||
245 | 90 | |||
246 | 91 | |||
247 | 92 | def test_suite(): | 33 | def test_suite(): |
248 | 93 | return unittest.TestLoader().loadTestsFromName(__name__) | 34 | return unittest.TestLoader().loadTestsFromName(__name__) |
249 | 94 | 35 | ||
250 | === modified file 'lib/lp/code/interfaces/codereviewcomment.py' | |||
251 | --- lib/lp/code/interfaces/codereviewcomment.py 2009-07-17 00:26:05 +0000 | |||
252 | +++ lib/lp/code/interfaces/codereviewcomment.py 2009-10-26 14:01:12 +0000 | |||
253 | @@ -66,6 +66,11 @@ | |||
254 | 66 | attachments. | 66 | attachments. |
255 | 67 | """ | 67 | """ |
256 | 68 | 68 | ||
257 | 69 | as_quoted_email = exported( | ||
258 | 70 | TextLine( | ||
259 | 71 | title=_('The message as quoted in email.'), | ||
260 | 72 | readonly=True)) | ||
261 | 73 | |||
262 | 69 | 74 | ||
263 | 70 | class ICodeReviewCommentDeletion(Interface): | 75 | class ICodeReviewCommentDeletion(Interface): |
264 | 71 | """This interface provides deletion of CodeReviewComments. | 76 | """This interface provides deletion of CodeReviewComments. |
265 | 72 | 77 | ||
266 | === modified file 'lib/lp/code/model/codereviewcomment.py' | |||
267 | --- lib/lp/code/model/codereviewcomment.py 2009-07-17 00:26:05 +0000 | |||
268 | +++ lib/lp/code/model/codereviewcomment.py 2009-10-26 14:01:12 +0000 | |||
269 | @@ -8,6 +8,8 @@ | |||
270 | 8 | 'CodeReviewComment', | 8 | 'CodeReviewComment', |
271 | 9 | ] | 9 | ] |
272 | 10 | 10 | ||
273 | 11 | from textwrap import TextWrapper | ||
274 | 12 | |||
275 | 11 | from zope.interface import implements | 13 | from zope.interface import implements |
276 | 12 | 14 | ||
277 | 13 | from sqlobject import ForeignKey, StringCol | 15 | from sqlobject import ForeignKey, StringCol |
278 | @@ -21,6 +23,38 @@ | |||
279 | 21 | from lp.code.interfaces.branchtarget import IHasBranchTarget | 23 | from lp.code.interfaces.branchtarget import IHasBranchTarget |
280 | 22 | 24 | ||
281 | 23 | 25 | ||
282 | 26 | def quote_text_as_email(text, width=80): | ||
283 | 27 | """Quote the text as if it is an email response. | ||
284 | 28 | |||
285 | 29 | Uses '> ' as a line prefix, and breaks long lines. | ||
286 | 30 | |||
287 | 31 | Trailing whitespace is stripped. | ||
288 | 32 | """ | ||
289 | 33 | # Empty text begets empty text. | ||
290 | 34 | if text is None: | ||
291 | 35 | return '' | ||
292 | 36 | text = text.rstrip() | ||
293 | 37 | if not text: | ||
294 | 38 | return '' | ||
295 | 39 | prefix = '> ' | ||
296 | 40 | # The TextWrapper's handling of code is somewhat suspect. | ||
297 | 41 | wrapper = TextWrapper( | ||
298 | 42 | initial_indent=prefix, | ||
299 | 43 | subsequent_indent=prefix, | ||
300 | 44 | width=width, | ||
301 | 45 | replace_whitespace=False) | ||
302 | 46 | result = [] | ||
303 | 47 | # Break the string into lines, and use the TextWrapper to wrap the | ||
304 | 48 | # individual lines. | ||
305 | 49 | for line in text.rstrip().split('\n'): | ||
306 | 50 | # TextWrapper won't do an indent of an empty string. | ||
307 | 51 | if line.strip() == '': | ||
308 | 52 | result.append(prefix) | ||
309 | 53 | else: | ||
310 | 54 | result.extend(wrapper.wrap(line)) | ||
311 | 55 | return '\n'.join(result) | ||
312 | 56 | |||
313 | 57 | |||
314 | 24 | class CodeReviewComment(SQLBase): | 58 | class CodeReviewComment(SQLBase): |
315 | 25 | """A table linking branch merge proposals and messages.""" | 59 | """A table linking branch merge proposals and messages.""" |
316 | 26 | 60 | ||
317 | @@ -72,3 +106,7 @@ | |||
318 | 72 | attachment for attachment in attachments | 106 | attachment for attachment in attachments |
319 | 73 | if attachment not in display_attachments] | 107 | if attachment not in display_attachments] |
320 | 74 | return display_attachments, other_attachments | 108 | return display_attachments, other_attachments |
321 | 109 | |||
322 | 110 | @property | ||
323 | 111 | def as_quoted_email(self): | ||
324 | 112 | return quote_text_as_email(self.message_body) | ||
325 | 75 | 113 | ||
326 | === modified file 'lib/lp/code/model/tests/test_codereviewcomment.py' | |||
327 | --- lib/lp/code/model/tests/test_codereviewcomment.py 2009-07-23 17:47:50 +0000 | |||
328 | +++ lib/lp/code/model/tests/test_codereviewcomment.py 2009-10-26 14:01:12 +0000 | |||
329 | @@ -3,12 +3,14 @@ | |||
330 | 3 | 3 | ||
331 | 4 | """Unit tests for CodeReviewComment""" | 4 | """Unit tests for CodeReviewComment""" |
332 | 5 | 5 | ||
333 | 6 | from textwrap import dedent | ||
334 | 6 | import unittest | 7 | import unittest |
335 | 7 | 8 | ||
336 | 8 | from canonical.launchpad.database.message import MessageSet | 9 | from canonical.launchpad.database.message import MessageSet |
337 | 9 | from lp.code.enums import CodeReviewVote | 10 | from lp.code.enums import CodeReviewVote |
338 | 10 | from lp.code.event.branchmergeproposal import NewCodeReviewCommentEvent | 11 | from lp.code.event.branchmergeproposal import NewCodeReviewCommentEvent |
340 | 11 | from lp.testing import TestCaseWithFactory | 12 | from lp.code.model.codereviewcomment import quote_text_as_email |
341 | 13 | from lp.testing import TestCaseWithFactory, TestCase | ||
342 | 12 | from canonical.testing import ( | 14 | from canonical.testing import ( |
343 | 13 | DatabaseFunctionalLayer, LaunchpadFunctionalLayer) | 15 | DatabaseFunctionalLayer, LaunchpadFunctionalLayer) |
344 | 14 | 16 | ||
345 | @@ -183,6 +185,62 @@ | |||
346 | 183 | self.assertEqual(([attachment.blob], []), comment.getAttachments()) | 185 | self.assertEqual(([attachment.blob], []), comment.getAttachments()) |
347 | 184 | 186 | ||
348 | 185 | 187 | ||
349 | 188 | class TestQuoteTextAsEmail(TestCase): | ||
350 | 189 | """Test the quote_text_as_email helper method.""" | ||
351 | 190 | |||
352 | 191 | def test_empty_string(self): | ||
353 | 192 | # Nothing just gives us an empty string. | ||
354 | 193 | self.assertEqual('', quote_text_as_email('')) | ||
355 | 194 | |||
356 | 195 | def test_none_string(self): | ||
357 | 196 | # If None is passed the quoted text is an empty string. | ||
358 | 197 | self.assertEqual('', quote_text_as_email(None)) | ||
359 | 198 | |||
360 | 199 | def test_whitespace_string(self): | ||
361 | 200 | # Just whitespace gives us an empty string. | ||
362 | 201 | self.assertEqual('', quote_text_as_email(' \t ')) | ||
363 | 202 | |||
364 | 203 | def test_long_string(self): | ||
365 | 204 | # Long lines are wrapped. | ||
366 | 205 | long_line = ('This is a very long line that needs to be wrapped ' | ||
367 | 206 | 'onto more than one line given a short length.') | ||
368 | 207 | self.assertEqual( | ||
369 | 208 | dedent("""\ | ||
370 | 209 | > This is a very long line that needs to | ||
371 | 210 | > be wrapped onto more than one line | ||
372 | 211 | > given a short length."""), | ||
373 | 212 | quote_text_as_email(long_line, 40)) | ||
374 | 213 | |||
375 | 214 | def test_code_sample(self): | ||
376 | 215 | # Initial whitespace is not trimmed. | ||
377 | 216 | code = """\ | ||
378 | 217 | def test_whitespace_string(self): | ||
379 | 218 | # Nothing just gives us the prefix. | ||
380 | 219 | self.assertEqual('', wrap_text(' \t '))""" | ||
381 | 220 | self.assertEqual( | ||
382 | 221 | dedent("""\ | ||
383 | 222 | > def test_whitespace_string(self): | ||
384 | 223 | > # Nothing just gives us the prefix. | ||
385 | 224 | > self.assertEqual('', wrap_text(' '))"""), | ||
386 | 225 | quote_text_as_email(code, 60)) | ||
387 | 226 | |||
388 | 227 | def test_empty_line_mid_string(self): | ||
389 | 228 | # Lines in the middle of the string are quoted too. | ||
390 | 229 | value = dedent("""\ | ||
391 | 230 | This is the first line. | ||
392 | 231 | |||
393 | 232 | This is the second line. | ||
394 | 233 | """) | ||
395 | 234 | expected = dedent("""\ | ||
396 | 235 | > This is the first line. | ||
397 | 236 | > | ||
398 | 237 | > This is the second line.""") | ||
399 | 238 | self.assertEqual(expected, quote_text_as_email(value)) | ||
400 | 239 | |||
401 | 240 | def test_trailing_whitespace(self): | ||
402 | 241 | # Trailing whitespace is removed. | ||
403 | 242 | self.assertEqual('> foo', quote_text_as_email(' foo \n ')) | ||
404 | 243 | |||
405 | 186 | 244 | ||
406 | 187 | def test_suite(): | 245 | def test_suite(): |
407 | 188 | return unittest.TestLoader().loadTestsFromName(__name__) | 246 | return unittest.TestLoader().loadTestsFromName(__name__) |
408 | 189 | 247 | ||
409 | === modified file 'lib/lp/code/stories/webservice/xx-branchmergeproposal.txt' | |||
410 | --- lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-10-15 23:45:40 +0000 | |||
411 | +++ lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-10-26 14:01:12 +0000 | |||
412 | @@ -73,6 +73,7 @@ | |||
413 | 73 | >>> print len(all_comments['entries']) | 73 | >>> print len(all_comments['entries']) |
414 | 74 | 2 | 74 | 2 |
415 | 75 | >>> pprint_entry(all_comments['entries'][0]) | 75 | >>> pprint_entry(all_comments['entries'][0]) |
416 | 76 | as_quoted_email: u'> This is great work' | ||
417 | 76 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' | 77 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' |
418 | 77 | id: 1 | 78 | id: 1 |
419 | 78 | message_body: u'This is great work' | 79 | message_body: u'This is great work' |
420 | @@ -85,6 +86,7 @@ | |||
421 | 85 | >>> comment_2 = webservice.named_get( | 86 | >>> comment_2 = webservice.named_get( |
422 | 86 | ... merge_proposal['self_link'], 'getComment', id=2).jsonBody() | 87 | ... merge_proposal['self_link'], 'getComment', id=2).jsonBody() |
423 | 87 | >>> pprint_entry(comment_2) | 88 | >>> pprint_entry(comment_2) |
424 | 89 | as_quoted_email: u'> This is mediocre work.' | ||
425 | 88 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' | 90 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' |
426 | 89 | id: 2 | 91 | id: 2 |
427 | 90 | message_body: u'This is mediocre work.' | 92 | message_body: u'This is mediocre work.' |
428 | @@ -153,6 +155,7 @@ | |||
429 | 153 | >>> comment_link = comment_result.getHeader('Location') | 155 | >>> comment_link = comment_result.getHeader('Location') |
430 | 154 | >>> comment = reviewer_webservice.get(comment_link).jsonBody() | 156 | >>> comment = reviewer_webservice.get(comment_link).jsonBody() |
431 | 155 | >>> pprint_entry(comment) | 157 | >>> pprint_entry(comment) |
432 | 158 | as_quoted_email: u'> This is great work' | ||
433 | 156 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' | 159 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' |
434 | 157 | id: 3 | 160 | id: 3 |
435 | 158 | message_body: u'This is great work' | 161 | message_body: u'This is great work' |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
= Summary =
Support inline replies for code review
== Proposed fix ==
When a user clicks on a reply link, the review/comment box is populated
with the comment as an email-style reply and the browser is scrolled
down to the widget.
== Pre-implementation notes ==
Preimplementation was with thumper
== Implementation details ==
I had to drag the email-style-reply code into the model because I wanted
to expose it for the API.
== Tests ==
None
== Demo and Q/A ==
Create a code review comment. Reply to it. In your mail client,
observe that threading is preserved correctly.
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: code/interfaces /codereviewcomm ent.py code/browser/ codereviewcomme nt.py code/model/ codereviewcomme nt.py /launchpad/ javascript/ lp/comment. js
lib/lp/
lib/lp/
lib/lp/
lib/canonical
== JSLint notices == abentley/ launchpad/ inline- comment/ lib/canonical/ launchpad/ javascript/ lp/comment. js'.
No handlers could be found for logger "bzr"
jslint: No problem found in
'/home/
jslint: 1 file to lint.
== Pylint notices ==
lib/lp/ code/interfaces /codereviewcomm ent.py fields' (No module named declarations' (No module
22: [F0401] Unable to import 'lazr.restful.
restful)
23: [F0401] Unable to import 'lazr.restful.
named restful)
lib/lp/ code/browser/ codereviewcomme nt.py interface' (No module enigmail. mozdev. org
20: [F0401] Unable to import 'lazr.delegates' (No module named
delegates)
21: [F0401] Unable to import 'lazr.restful.
named restful)
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAkr ffF0ACgkQ0F+ nu1YWqI19OgCeOh DWoVdqbv4cEMwfC gZqMF4E F10hrUAc7S9dTWM Lk0qk64E
PEYAnReM+
=7YkU
-----END PGP SIGNATURE-----