Merge lp:~abentley/launchpad/bmp-inline into lp:launchpad
- bmp-inline
- Merge into devel
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~abentley/launchpad/bmp-inline |
Merge into: | lp:launchpad |
Diff against target: |
983 lines 12 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+2/-0) lib/canonical/launchpad/javascript/bugs/bugtask-index.js (+15/-155) lib/canonical/launchpad/javascript/lp/comment.js (+349/-0) lib/canonical/launchpad/javascript/lp/errors.js (+81/-0) lib/lp/bugs/templates/bugtask-index.pt (+14/-0) lib/lp/code/browser/configure.zcml (+3/-0) lib/lp/code/interfaces/branchmergeproposal.py (+6/-4) lib/lp/code/stories/webservice/xx-branchmergeproposal.txt (+4/-2) lib/lp/code/templates/branchmergeproposal-index.pt (+23/-2) lib/lp/code/templates/codereviewcomment-fragment.pt (+2/-0) lib/lp/code/windmill/testing.py (+16/-0) lib/lp/code/windmill/tests/test_merge_proposal_commenting.py (+55/-0) |
To merge this branch: | bzr merge lp:~abentley/launchpad/bmp-inline |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | ui | Approve | |
Brad Crittenden (community) | code | Approve | |
Review via email: mp+13299@code.launchpad.net |
Commit message
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
Aaron Bentley (abentley) wrote : | # |
This branch includes changes from reuse-refactor, which was separately reviewed, so here's a diff with just the changes unique to this branch:
Revision history for this message
Brad Crittenden (bac) : | # |
review:
Approve
(code)
Revision history for this message
Paul Hummer (rockstar) wrote : | # |
This looks good to me, but you'll need someone else to look at.
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/interfaces/_schema_circular_imports.py' | |||
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-09-09 11:28:03 +0000 | |||
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-10-14 13:59:13 +0000 | |||
4 | @@ -100,6 +100,8 @@ | |||
5 | 100 | IBranchMergeProposal['createComment'].queryTaggedValue( | 100 | IBranchMergeProposal['createComment'].queryTaggedValue( |
6 | 101 | LAZR_WEBSERVICE_EXPORTED)['params']['parent'].schema = \ | 101 | LAZR_WEBSERVICE_EXPORTED)['params']['parent'].schema = \ |
7 | 102 | ICodeReviewComment | 102 | ICodeReviewComment |
8 | 103 | patch_entry_return_type( | ||
9 | 104 | IBranchMergeProposal, 'createComment', ICodeReviewComment) | ||
10 | 103 | IBranchMergeProposal['all_comments'].value_type.schema = ICodeReviewComment | 105 | IBranchMergeProposal['all_comments'].value_type.schema = ICodeReviewComment |
11 | 104 | IBranchMergeProposal['nominateReviewer'].queryTaggedValue( | 106 | IBranchMergeProposal['nominateReviewer'].queryTaggedValue( |
12 | 105 | LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = ICodeReviewVoteReference | 107 | LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = ICodeReviewVoteReference |
13 | 106 | 108 | ||
14 | === modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js' | |||
15 | --- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-10-06 21:58:10 +0000 | |||
16 | +++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-10-14 13:59:13 +0000 | |||
17 | @@ -83,7 +83,7 @@ | |||
18 | 83 | } | 83 | } |
19 | 84 | }; | 84 | }; |
20 | 85 | error_handler.showError = function(error_msg) { | 85 | error_handler.showError = function(error_msg) { |
22 | 86 | display_error(Y.get('.menu-link-addsubscriber'), error_msg); | 86 | Y.lp.display_error(Y.get('.menu-link-addsubscriber'), error_msg); |
23 | 87 | }; | 87 | }; |
24 | 88 | 88 | ||
25 | 89 | var config = { | 89 | var config = { |
26 | @@ -210,12 +210,11 @@ | |||
27 | 210 | }); | 210 | }); |
28 | 211 | privacy_link.addClass('js-action'); | 211 | privacy_link.addClass('js-action'); |
29 | 212 | } | 212 | } |
30 | 213 | create_error_overlay(); | ||
31 | 214 | setup_inline_commenting(); | ||
32 | 215 | setup_add_attachment(); | 213 | setup_add_attachment(); |
33 | 216 | }, window); | 214 | }, window); |
34 | 217 | }; | 215 | }; |
35 | 218 | 216 | ||
36 | 217 | |||
37 | 219 | /* | 218 | /* |
38 | 220 | * Clear the subscribe someone else picker. | 219 | * Clear the subscribe someone else picker. |
39 | 221 | * | 220 | * |
40 | @@ -313,7 +312,6 @@ | |||
41 | 313 | 312 | ||
42 | 314 | setup_unsubscribe_icon_handlers(subscription); | 313 | setup_unsubscribe_icon_handlers(subscription); |
43 | 315 | setup_subscribe_someone_else_handler(subscription); | 314 | setup_subscribe_someone_else_handler(subscription); |
44 | 316 | create_error_overlay(); | ||
45 | 317 | } | 315 | } |
46 | 318 | 316 | ||
47 | 319 | /* | 317 | /* |
48 | @@ -568,64 +566,6 @@ | |||
49 | 568 | lp_bug_entry.lp_save(config); | 566 | lp_bug_entry.lp_save(config); |
50 | 569 | }; | 567 | }; |
51 | 570 | 568 | ||
52 | 571 | /* | ||
53 | 572 | * Create the form overlay to use when encountering errors. | ||
54 | 573 | * | ||
55 | 574 | * @method create_error_overlay | ||
56 | 575 | */ | ||
57 | 576 | function create_error_overlay() { | ||
58 | 577 | if (error_overlay === undefined) { | ||
59 | 578 | error_overlay = new Y.lazr.FormOverlay({ | ||
60 | 579 | headerContent: '<h2>Error</h2>', | ||
61 | 580 | form_header: '', | ||
62 | 581 | form_content: '', | ||
63 | 582 | form_submit_button: Y.Node.create( | ||
64 | 583 | '<button style="display:none"></button>'), | ||
65 | 584 | form_cancel_button: cancel_form_button(), | ||
66 | 585 | centered: true, | ||
67 | 586 | visible: false | ||
68 | 587 | }); | ||
69 | 588 | error_overlay.render(); | ||
70 | 589 | } | ||
71 | 590 | } | ||
72 | 591 | |||
73 | 592 | /* | ||
74 | 593 | * Create a form button for canceling an error form | ||
75 | 594 | * that won't reload the page on submit. | ||
76 | 595 | * | ||
77 | 596 | * @method cancel_form_button | ||
78 | 597 | * @return button {Node} The form's cancel button. | ||
79 | 598 | */ | ||
80 | 599 | function cancel_form_button() { | ||
81 | 600 | var button = Y.Node.create('<button>OK</button>'); | ||
82 | 601 | button.on('click', function(e) { | ||
83 | 602 | e.preventDefault(); | ||
84 | 603 | error_overlay.hide(); | ||
85 | 604 | }); | ||
86 | 605 | return button; | ||
87 | 606 | } | ||
88 | 607 | |||
89 | 608 | /* | ||
90 | 609 | * Take an error message and display in an overlay (creating it if necessary). | ||
91 | 610 | * | ||
92 | 611 | * @method display_error | ||
93 | 612 | * @param flash_node {Node} The node to red flash. | ||
94 | 613 | * @param msg {String} The message to display. | ||
95 | 614 | */ | ||
96 | 615 | function display_error(flash_node, msg) { | ||
97 | 616 | create_error_overlay(); | ||
98 | 617 | if (flash_node) { | ||
99 | 618 | var anim = Y.lazr.anim.red_flash({ node: flash_node }); | ||
100 | 619 | anim.on('end', function(e) { | ||
101 | 620 | error_overlay.showError(msg); | ||
102 | 621 | error_overlay.show(); | ||
103 | 622 | }); | ||
104 | 623 | anim.run(); | ||
105 | 624 | } else { | ||
106 | 625 | error_overlay.showError(msg); | ||
107 | 626 | error_overlay.show(); | ||
108 | 627 | } | ||
109 | 628 | } | ||
110 | 629 | 569 | ||
111 | 630 | /* | 570 | /* |
112 | 631 | * Traverse the DOM of a given remove icon to find | 571 | * Traverse the DOM of a given remove icon to find |
113 | @@ -652,6 +592,7 @@ | |||
114 | 652 | return user_uri; | 592 | return user_uri; |
115 | 653 | } | 593 | } |
116 | 654 | 594 | ||
117 | 595 | |||
118 | 655 | /* | 596 | /* |
119 | 656 | * Build the HTML for a user link for the subscribers list. | 597 | * Build the HTML for a user link for the subscribers list. |
120 | 657 | * | 598 | * |
121 | @@ -1014,7 +955,7 @@ | |||
122 | 1014 | }; | 955 | }; |
123 | 1015 | error_handler.showError = function (error_msg) { | 956 | error_handler.showError = function (error_msg) { |
124 | 1016 | var flash_node = Y.get('.' + person.get('css_name')); | 957 | var flash_node = Y.get('.' + person.get('css_name')); |
126 | 1017 | display_error(flash_node, error_msg); | 958 | Y.lp.display_error(flash_node, error_msg); |
127 | 1018 | 959 | ||
128 | 1019 | }; | 960 | }; |
129 | 1020 | 961 | ||
130 | @@ -1089,7 +1030,7 @@ | |||
131 | 1089 | subscription.disable_spinner(); | 1030 | subscription.disable_spinner(); |
132 | 1090 | }; | 1031 | }; |
133 | 1091 | error_handler.showError = function (error_msg) { | 1032 | error_handler.showError = function (error_msg) { |
135 | 1092 | display_error(subscription_link, error_msg); | 1033 | Y.lp.display_error(subscription_link, error_msg); |
136 | 1093 | }; | 1034 | }; |
137 | 1094 | 1035 | ||
138 | 1095 | var config = { | 1036 | var config = { |
139 | @@ -1145,7 +1086,7 @@ | |||
140 | 1145 | subscription.disable_spinner(); | 1086 | subscription.disable_spinner(); |
141 | 1146 | }; | 1087 | }; |
142 | 1147 | error_handler.showError = function (error_msg) { | 1088 | error_handler.showError = function (error_msg) { |
144 | 1148 | display_error(subscription_link, error_msg); | 1089 | Y.lp.display_error(subscription_link, error_msg); |
145 | 1149 | }; | 1090 | }; |
146 | 1150 | 1091 | ||
147 | 1151 | var config = { | 1092 | var config = { |
148 | @@ -1257,7 +1198,7 @@ | |||
149 | 1257 | backgroundColor: tr.hasClass('highlight') ? '#FFFF99' : '#FFFFFF' | 1198 | backgroundColor: tr.hasClass('highlight') ? '#FFFF99' : '#FFFFFF' |
150 | 1258 | }); | 1199 | }); |
151 | 1259 | status_choice_edit.showError = function(err) { | 1200 | status_choice_edit.showError = function(err) { |
153 | 1260 | display_error(null, err); | 1201 | Y.lp.display_error(null, err); |
154 | 1261 | }; | 1202 | }; |
155 | 1262 | status_choice_edit.on('save', function(e) { | 1203 | status_choice_edit.on('save', function(e) { |
156 | 1263 | var cb = status_choice_edit.get('contentBox'); | 1204 | var cb = status_choice_edit.get('contentBox'); |
157 | @@ -1289,7 +1230,7 @@ | |||
158 | 1289 | backgroundColor: tr.hasClass('highlight') ? '#FFFF99' : '#FFFFFF' | 1230 | backgroundColor: tr.hasClass('highlight') ? '#FFFF99' : '#FFFFFF' |
159 | 1290 | }); | 1231 | }); |
160 | 1291 | importance_choice_edit.showError = function(err) { | 1232 | importance_choice_edit.showError = function(err) { |
162 | 1292 | display_error(null, err); | 1233 | Y.lp.display_error(null, err); |
163 | 1293 | }; | 1234 | }; |
164 | 1294 | importance_choice_edit.on('save', function(e) { | 1235 | importance_choice_edit.on('save', function(e) { |
165 | 1295 | var cb = importance_choice_edit.get('contentBox'); | 1236 | var cb = importance_choice_edit.get('contentBox'); |
166 | @@ -1324,7 +1265,7 @@ | |||
167 | 1324 | clickable_content: false | 1265 | clickable_content: false |
168 | 1325 | }); | 1266 | }); |
169 | 1326 | milestone_choice_edit.showError = function(err) { | 1267 | milestone_choice_edit.showError = function(err) { |
171 | 1327 | display_error(null, err); | 1268 | Y.lp.display_error(null, err); |
172 | 1328 | }; | 1269 | }; |
173 | 1329 | milestone_choice_edit.plug({ | 1270 | milestone_choice_edit.plug({ |
174 | 1330 | fn: Y.lp.client.plugins.PATCHPlugin, cfg: { | 1271 | fn: Y.lp.client.plugins.PATCHPlugin, cfg: { |
175 | @@ -1378,7 +1319,7 @@ | |||
176 | 1378 | "header": "Change assignee", | 1319 | "header": "Change assignee", |
177 | 1379 | "remove_button_text": "Remove assignee", | 1320 | "remove_button_text": "Remove assignee", |
178 | 1380 | "null_display_value": "Unassigned"}); | 1321 | "null_display_value": "Unassigned"}); |
180 | 1381 | assignee_picker.render() | 1322 | assignee_picker.render(); |
181 | 1382 | } | 1323 | } |
182 | 1383 | }; | 1324 | }; |
183 | 1384 | 1325 | ||
184 | @@ -1486,7 +1427,7 @@ | |||
185 | 1486 | }, | 1427 | }, |
186 | 1487 | 1428 | ||
187 | 1488 | showError: function(err) { | 1429 | showError: function(err) { |
189 | 1489 | display_error(null, err); | 1430 | Y.lp.display_error(null, err); |
190 | 1490 | }, | 1431 | }, |
191 | 1491 | 1432 | ||
192 | 1492 | syncUI: function() { | 1433 | syncUI: function() { |
193 | @@ -1549,7 +1490,7 @@ | |||
194 | 1549 | function check_can_be_unsubscribed(subscription) { | 1490 | function check_can_be_unsubscribed(subscription) { |
195 | 1550 | var error_handler = new LP.client.ErrorHandler(); | 1491 | var error_handler = new LP.client.ErrorHandler(); |
196 | 1551 | error_handler.showError = function (error_msg) { | 1492 | error_handler.showError = function (error_msg) { |
198 | 1552 | display_error(Y.get('.menu-link-addsubscriber'), error_msg); | 1493 | Y.lp.display_error(Y.get('.menu-link-addsubscriber'), error_msg); |
199 | 1553 | }; | 1494 | }; |
200 | 1554 | 1495 | ||
201 | 1555 | var config = { | 1496 | var config = { |
202 | @@ -1618,7 +1559,7 @@ | |||
203 | 1618 | 1559 | ||
204 | 1619 | var error_handler = new LP.client.ErrorHandler(); | 1560 | var error_handler = new LP.client.ErrorHandler(); |
205 | 1620 | error_handler.showError = function(error_msg) { | 1561 | error_handler.showError = function(error_msg) { |
207 | 1621 | display_error(Y.get('.menu-link-addsubscriber'), error_msg); | 1562 | Y.lp.display_error(Y.get('.menu-link-addsubscriber'), error_msg); |
208 | 1622 | }; | 1563 | }; |
209 | 1623 | 1564 | ||
210 | 1624 | if (subscription.is_already_subscribed()) { | 1565 | if (subscription.is_already_subscribed()) { |
211 | @@ -1631,88 +1572,6 @@ | |||
212 | 1631 | } | 1572 | } |
213 | 1632 | 1573 | ||
214 | 1633 | /* | 1574 | /* |
215 | 1634 | * Set up and handle submitting a comment inline. | ||
216 | 1635 | * | ||
217 | 1636 | * @method setup_inline_commenting | ||
218 | 1637 | */ | ||
219 | 1638 | function setup_inline_commenting() { | ||
220 | 1639 | var submit_button = Y.get('[id="field.actions.save"]'); | ||
221 | 1640 | var progress_message = Y.Node.create( | ||
222 | 1641 | '<span class="update-in-progress-message">Saving...</span>'); | ||
223 | 1642 | var comment_input = Y.get('[id="field.comment"]'); | ||
224 | 1643 | |||
225 | 1644 | var error_handler = new LP.client.ErrorHandler(); | ||
226 | 1645 | error_handler.clearProgressUI = function () { | ||
227 | 1646 | clearProgressUI(); | ||
228 | 1647 | }; | ||
229 | 1648 | error_handler.showError = function (error_msg) { | ||
230 | 1649 | display_error(submit_button, error_msg); | ||
231 | 1650 | }; | ||
232 | 1651 | |||
233 | 1652 | function clearProgressUI() { | ||
234 | 1653 | progress_message.get('parentNode').replaceChild( | ||
235 | 1654 | submit_button, progress_message); | ||
236 | 1655 | comment_input.removeAttribute('disabled'); | ||
237 | 1656 | } | ||
238 | 1657 | |||
239 | 1658 | function hide_or_show_submit_button(e) { | ||
240 | 1659 | if (comment_input.get('value') === '') { | ||
241 | 1660 | submit_button.set('disabled', true); | ||
242 | 1661 | } | ||
243 | 1662 | else { | ||
244 | 1663 | submit_button.set('disabled', false); | ||
245 | 1664 | } | ||
246 | 1665 | } | ||
247 | 1666 | /* Hook up hide_or_show_submit_button on both keyup and mouseup to | ||
248 | 1667 | handle both entering text with the keyboard, and pasting with the | ||
249 | 1668 | mouse. */ | ||
250 | 1669 | comment_input.on('keyup', hide_or_show_submit_button); | ||
251 | 1670 | comment_input.on('mouseup', hide_or_show_submit_button); | ||
252 | 1671 | hide_or_show_submit_button(null); | ||
253 | 1672 | submit_button.addClass('js-action'); | ||
254 | 1673 | submit_button.setStyle('display', 'inline'); | ||
255 | 1674 | submit_button.on('click', function(e) { | ||
256 | 1675 | e.halt(); | ||
257 | 1676 | var comment_text = comment_input.get('value'); | ||
258 | 1677 | /* Don't try to add an empty comment. */ | ||
259 | 1678 | if (trim(comment_text) === '') { | ||
260 | 1679 | return; | ||
261 | 1680 | } | ||
262 | 1681 | var config = { | ||
263 | 1682 | on: { | ||
264 | 1683 | success: function(message_entry) { | ||
265 | 1684 | var config = { | ||
266 | 1685 | on: { | ||
267 | 1686 | success: function(message_html) { | ||
268 | 1687 | var fieldset = Y.get('#add-comment-form'); | ||
269 | 1688 | var legend = Y.get('#add-comment-form legend'); | ||
270 | 1689 | var comment = Y.Node.create(message_html); | ||
271 | 1690 | fieldset.get('parentNode').insertBefore( | ||
272 | 1691 | comment, fieldset); | ||
273 | 1692 | clearProgressUI(); | ||
274 | 1693 | comment_input.set('value', ''); | ||
275 | 1694 | Y.lazr.anim.green_flash({node: comment}).run(); | ||
276 | 1695 | } | ||
277 | 1696 | }, | ||
278 | 1697 | accept: LP.client.XHTML | ||
279 | 1698 | }; | ||
280 | 1699 | lp_client.get(message_entry.get('self_link'), config); | ||
281 | 1700 | }, | ||
282 | 1701 | failure: error_handler.getFailureHandler() | ||
283 | 1702 | }, | ||
284 | 1703 | parameters: { | ||
285 | 1704 | content: comment_input.get('value') | ||
286 | 1705 | } | ||
287 | 1706 | }; | ||
288 | 1707 | comment_input.set('disabled', 'true'); | ||
289 | 1708 | submit_button.get('parentNode').replaceChild( | ||
290 | 1709 | progress_message, submit_button); | ||
291 | 1710 | lp_client.named_post( | ||
292 | 1711 | bug_repr.self_link, 'newMessage', config); | ||
293 | 1712 | }); | ||
294 | 1713 | } | ||
295 | 1714 | |||
296 | 1715 | /* | ||
297 | 1716 | * Click handling to pass comment text to the attachment | 1575 | * Click handling to pass comment text to the attachment |
298 | 1717 | * page if there is a comment. | 1576 | * page if there is a comment. |
299 | 1718 | * | 1577 | * |
300 | @@ -1734,5 +1593,6 @@ | |||
301 | 1734 | }, '0.1', {requires: ['base', 'oop', 'node', 'event', 'io-base', 'substitute', | 1593 | }, '0.1', {requires: ['base', 'oop', 'node', 'event', 'io-base', 'substitute', |
302 | 1735 | 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim', | 1594 | 'widget-position-ext', 'lazr.formoverlay', 'lazr.anim', |
303 | 1736 | 'lazr.base', 'lazr.overlay', 'lazr.choiceedit', | 1595 | 'lazr.base', 'lazr.overlay', 'lazr.choiceedit', |
305 | 1737 | 'lp.picker', 'lp.client.plugins', 'lp.subscriber']}); | 1596 | 'lp.picker', 'lp.client.plugins', 'lp.subscriber', |
306 | 1597 | 'lp.errors']}); | ||
307 | 1738 | 1598 | ||
308 | 1739 | 1599 | ||
309 | === added file 'lib/canonical/launchpad/javascript/lp/comment.js' | |||
310 | --- lib/canonical/launchpad/javascript/lp/comment.js 1970-01-01 00:00:00 +0000 | |||
311 | +++ lib/canonical/launchpad/javascript/lp/comment.js 2009-10-14 13:59:13 +0000 | |||
312 | @@ -0,0 +1,349 @@ | |||
313 | 1 | YUI.add('lp.comment', function(Y) { | ||
314 | 2 | |||
315 | 3 | Y.lp = Y.namespace('lp'); | ||
316 | 4 | |||
317 | 5 | var Comment = function () { | ||
318 | 6 | Comment.superclass.constructor.apply(this, arguments); | ||
319 | 7 | }; | ||
320 | 8 | |||
321 | 9 | |||
322 | 10 | Comment.NAME = 'comment'; | ||
323 | 11 | |||
324 | 12 | Comment.ATTRS = { | ||
325 | 13 | }; | ||
326 | 14 | Y.extend(Comment, Y.Widget, { | ||
327 | 15 | |||
328 | 16 | /** | ||
329 | 17 | * Initialize the Comment | ||
330 | 18 | * | ||
331 | 19 | * @method initializer | ||
332 | 20 | */ | ||
333 | 21 | initializer: function() { | ||
334 | 22 | this.submit_button = this.get_submit(); | ||
335 | 23 | this.comment_input = Y.get('[id="field.comment"]'); | ||
336 | 24 | this.lp_client = new LP.client.Launchpad(); | ||
337 | 25 | this.error_handler = new LP.client.ErrorHandler(); | ||
338 | 26 | this.error_handler.clearProgressUI = bind(this.clearProgressUI, this); | ||
339 | 27 | this.error_handler.showError = bind(function (error_msg) { | ||
340 | 28 | Y.lp.display_error(this.submit_button, error_msg); | ||
341 | 29 | }, this); | ||
342 | 30 | this.progress_message = Y.Node.create( | ||
343 | 31 | '<span class="update-in-progress-message">Saving...</span>'); | ||
344 | 32 | }, | ||
345 | 33 | |||
346 | 34 | /** | ||
347 | 35 | * Return the Submit button. | ||
348 | 36 | * | ||
349 | 37 | * This is provided so that it can be overridden in subclasses. | ||
350 | 38 | * | ||
351 | 39 | * @method get_submit | ||
352 | 40 | */ | ||
353 | 41 | get_submit: function(){ | ||
354 | 42 | return Y.get('[id="field.actions.save"]'); | ||
355 | 43 | }, | ||
356 | 44 | /** | ||
357 | 45 | * Implementation of Widget.renderUI. | ||
358 | 46 | * | ||
359 | 47 | * This redisplays the submit button, in case it has been hidden by | ||
360 | 48 | * the web page. | ||
361 | 49 | * | ||
362 | 50 | * @method renderUI | ||
363 | 51 | */ | ||
364 | 52 | renderUI: function() { | ||
365 | 53 | this.submit_button.addClass('js-action'); | ||
366 | 54 | this.submit_button.setStyle('display', 'inline'); | ||
367 | 55 | }, | ||
368 | 56 | /** | ||
369 | 57 | * Ensure that the widget's values are suitable for submission. | ||
370 | 58 | * | ||
371 | 59 | * The contents of the comment field must contain at least one | ||
372 | 60 | * non-whitespace character. | ||
373 | 61 | * | ||
374 | 62 | * @method validate | ||
375 | 63 | */ | ||
376 | 64 | validate: function() { | ||
377 | 65 | return trim(this.comment_input.get('value')) !== ''; | ||
378 | 66 | }, | ||
379 | 67 | /** | ||
380 | 68 | * Make the widget enabled or disabled. | ||
381 | 69 | * | ||
382 | 70 | * @method set_disabled | ||
383 | 71 | * @param disabled A boolean, true if the widget is disabled. | ||
384 | 72 | */ | ||
385 | 73 | set_disabled: function(disabled){ | ||
386 | 74 | this.comment_input.set('disabled', disabled); | ||
387 | 75 | }, | ||
388 | 76 | /** | ||
389 | 77 | * Add the widget's comment as a new comment, updating the display. | ||
390 | 78 | * | ||
391 | 79 | * @method add_comment | ||
392 | 80 | * @param e An event | ||
393 | 81 | */ | ||
394 | 82 | add_comment: function(e){ | ||
395 | 83 | e.halt(); | ||
396 | 84 | /* Don't try to add an empty comment. */ | ||
397 | 85 | if (!this.validate()) { | ||
398 | 86 | return; | ||
399 | 87 | } | ||
400 | 88 | this.set_disabled(true); | ||
401 | 89 | this.submit_button.get('parentNode').replaceChild( | ||
402 | 90 | this.progress_message, this.submit_button); | ||
403 | 91 | this.post_comment(bind(function(message_entry) { | ||
404 | 92 | this.get_comment_HTML( | ||
405 | 93 | message_entry, bind(this.insert_comment_HTML, this)); | ||
406 | 94 | }, this)); | ||
407 | 95 | }, | ||
408 | 96 | /** | ||
409 | 97 | * Post the comment to the Launchpad API | ||
410 | 98 | * | ||
411 | 99 | * @method post_comment | ||
412 | 100 | * @param callback A callable to call if the post is successful. | ||
413 | 101 | */ | ||
414 | 102 | post_comment: function(callback) { | ||
415 | 103 | var config = { | ||
416 | 104 | on: { | ||
417 | 105 | success: callback, | ||
418 | 106 | failure: this.error_handler.getFailureHandler() | ||
419 | 107 | }, | ||
420 | 108 | parameters: {content: this.comment_input.get('value')} | ||
421 | 109 | }; | ||
422 | 110 | this.lp_client.named_post( | ||
423 | 111 | LP.client.cache.bug.self_link, 'newMessage', config); | ||
424 | 112 | }, | ||
425 | 113 | /** | ||
426 | 114 | * Retrieve the HTML of the specified message entry. | ||
427 | 115 | * | ||
428 | 116 | * @method get_comment_HTML | ||
429 | 117 | * @param message_entry The comment to get the HTML for. | ||
430 | 118 | * @param callback On success, call this with the HTML of the comment. | ||
431 | 119 | */ | ||
432 | 120 | get_comment_HTML: function(message_entry, callback){ | ||
433 | 121 | var config = { | ||
434 | 122 | on: { | ||
435 | 123 | success: callback | ||
436 | 124 | }, | ||
437 | 125 | accept: LP.client.XHTML | ||
438 | 126 | }; | ||
439 | 127 | this.lp_client.get(message_entry.get('self_link'), config); | ||
440 | 128 | }, | ||
441 | 129 | /** | ||
442 | 130 | * Insert the specified HTML into the page. | ||
443 | 131 | * | ||
444 | 132 | * @method insert_comment_HTML | ||
445 | 133 | * @param message_html The HTML of the comment to insert. | ||
446 | 134 | */ | ||
447 | 135 | insert_comment_HTML: function(message_html) { | ||
448 | 136 | var fieldset = Y.get('#add-comment-form'); | ||
449 | 137 | var comment = Y.Node.create(message_html); | ||
450 | 138 | fieldset.get('parentNode').insertBefore(comment, fieldset); | ||
451 | 139 | this.reset_contents(); | ||
452 | 140 | Y.lazr.anim.green_flash({node: comment}).run(); | ||
453 | 141 | }, | ||
454 | 142 | /** | ||
455 | 143 | * Reset the widget to a blank state. | ||
456 | 144 | * | ||
457 | 145 | * @method reset_contents | ||
458 | 146 | */ | ||
459 | 147 | reset_contents: function() { | ||
460 | 148 | this.clearProgressUI(); | ||
461 | 149 | this.comment_input.set('value', ''); | ||
462 | 150 | this.syncUI(); | ||
463 | 151 | }, | ||
464 | 152 | /** | ||
465 | 153 | * Stop indicating that a submission is in progress. | ||
466 | 154 | * | ||
467 | 155 | * @method clearProgressUI | ||
468 | 156 | */ | ||
469 | 157 | clearProgressUI: function(){ | ||
470 | 158 | this.progress_message.get('parentNode').replaceChild( | ||
471 | 159 | this.submit_button, this.progress_message); | ||
472 | 160 | this.set_disabled(false); | ||
473 | 161 | }, | ||
474 | 162 | /** | ||
475 | 163 | * Implementation of Widget.bindUI: Bind events to methods. | ||
476 | 164 | * | ||
477 | 165 | * Key and mouse presses (e.g. mouse paste) call syncUI, in case the submit | ||
478 | 166 | * button needs to be updated. Clicking on the submit button invokes | ||
479 | 167 | * add_comment. | ||
480 | 168 | * | ||
481 | 169 | * @method bindUI | ||
482 | 170 | */ | ||
483 | 171 | bindUI: function(){ | ||
484 | 172 | this.comment_input.on('keyup', bind(this.syncUI, this)); | ||
485 | 173 | this.comment_input.on('mouseup', bind(this.syncUI, this)); | ||
486 | 174 | this.submit_button.on('click', bind(this.add_comment, this)); | ||
487 | 175 | }, | ||
488 | 176 | /** | ||
489 | 177 | * Implementation of Widget.syncUI: Update appearance according to state. | ||
490 | 178 | * | ||
491 | 179 | * This just updates the submit button. | ||
492 | 180 | * | ||
493 | 181 | * @method syncUI | ||
494 | 182 | */ | ||
495 | 183 | syncUI: function(){ | ||
496 | 184 | this.submit_button.set('disabled', !this.validate()); | ||
497 | 185 | } | ||
498 | 186 | }); | ||
499 | 187 | |||
500 | 188 | Y.lp.Comment = Comment; | ||
501 | 189 | |||
502 | 190 | var CodeReviewComment = function(){ | ||
503 | 191 | CodeReviewComment.superclass.constructor.apply(this, arguments); | ||
504 | 192 | }; | ||
505 | 193 | CodeReviewComment.NAME = 'codereviewcomment'; | ||
506 | 194 | |||
507 | 195 | |||
508 | 196 | Y.extend(CodeReviewComment, Comment, { | ||
509 | 197 | /** | ||
510 | 198 | * Initialize the CodeReviewComment | ||
511 | 199 | * | ||
512 | 200 | * @method initializer | ||
513 | 201 | */ | ||
514 | 202 | initializer: function() { | ||
515 | 203 | this.vote_input = Y.get('[id="field.vote"]'); | ||
516 | 204 | this.review_type = Y.get('[id="field.review_type"]'); | ||
517 | 205 | }, | ||
518 | 206 | /** | ||
519 | 207 | * Return the Submit button. | ||
520 | 208 | * | ||
521 | 209 | * @method get_submit | ||
522 | 210 | */ | ||
523 | 211 | get_submit: function(){ | ||
524 | 212 | return Y.get('[id="field.actions.add"]'); | ||
525 | 213 | }, | ||
526 | 214 | /** | ||
527 | 215 | * Return the vote value selected, or null if none is selected. | ||
528 | 216 | * | ||
529 | 217 | * @method get_vote | ||
530 | 218 | */ | ||
531 | 219 | get_vote: function() { | ||
532 | 220 | var selected_idx = this.vote_input.get('selectedIndex'); | ||
533 | 221 | var selected = this.vote_input.get('options').item(selected_idx); | ||
534 | 222 | if (selected.get('value') === ''){ | ||
535 | 223 | return null; | ||
536 | 224 | } | ||
537 | 225 | return selected.get('innerHTML'); | ||
538 | 226 | }, | ||
539 | 227 | /** | ||
540 | 228 | * Ensure that the widget's values are suitable for submission. | ||
541 | 229 | * | ||
542 | 230 | * This allows the vote to be submitted, even when no text is specified | ||
543 | 231 | * for the comment. | ||
544 | 232 | * | ||
545 | 233 | * @method validate | ||
546 | 234 | */ | ||
547 | 235 | validate: function(){ | ||
548 | 236 | if (this.get_vote() !== null) { | ||
549 | 237 | return true; | ||
550 | 238 | } | ||
551 | 239 | return CodeReviewComment.superclass.validate.apply(this); | ||
552 | 240 | }, | ||
553 | 241 | /** | ||
554 | 242 | * Make the widget enabled or disabled. | ||
555 | 243 | * | ||
556 | 244 | * @method set_disabled | ||
557 | 245 | * @param disabled A boolean, true if the widget is disabled. | ||
558 | 246 | */ | ||
559 | 247 | set_disabled: function(disabled){ | ||
560 | 248 | CodeReviewComment.superclass.set_disabled.call(this, disabled); | ||
561 | 249 | this.vote_input.set('disabled', disabled); | ||
562 | 250 | this.review_type.set('disabled', disabled); | ||
563 | 251 | }, | ||
564 | 252 | /** | ||
565 | 253 | * Post the comment to the Launchpad API | ||
566 | 254 | * | ||
567 | 255 | * @method post_comment | ||
568 | 256 | * @param callback A callable to call if the post is successful. | ||
569 | 257 | */ | ||
570 | 258 | post_comment: function(callback) { | ||
571 | 259 | var config = { | ||
572 | 260 | on: { | ||
573 | 261 | success: callback, | ||
574 | 262 | failure: this.error_handler.getFailureHandler() | ||
575 | 263 | }, | ||
576 | 264 | parameters: { | ||
577 | 265 | content: this.comment_input.get('value'), | ||
578 | 266 | subject: '', | ||
579 | 267 | review_type: this.review_type.get('value'), | ||
580 | 268 | vote: this.get_vote() | ||
581 | 269 | } | ||
582 | 270 | }; | ||
583 | 271 | this.lp_client.named_post( | ||
584 | 272 | LP.client.cache.context.self_link, 'createComment', config); | ||
585 | 273 | }, | ||
586 | 274 | /** | ||
587 | 275 | * Retrieve the HTML of the specified message entry. | ||
588 | 276 | * | ||
589 | 277 | * @method get_comment_HTML | ||
590 | 278 | * @param message_entry The comment to get the HTML for. | ||
591 | 279 | * @param callback On success, call this with the HTML of the comment. | ||
592 | 280 | */ | ||
593 | 281 | get_comment_HTML: function(comment_entry, callback) { | ||
594 | 282 | fragment_url = 'comments/' + comment_entry.get('id') + '/+fragment'; | ||
595 | 283 | Y.io(fragment_url, { | ||
596 | 284 | on: { | ||
597 | 285 | success: function(id, response){ | ||
598 | 286 | callback(response.responseText); | ||
599 | 287 | }, | ||
600 | 288 | failure: this.error_handler.getFailureHandler() | ||
601 | 289 | } | ||
602 | 290 | }); | ||
603 | 291 | }, | ||
604 | 292 | /** | ||
605 | 293 | * Reset the widget to a blank state. | ||
606 | 294 | * | ||
607 | 295 | * @method reset_contents | ||
608 | 296 | */ | ||
609 | 297 | reset_contents: function() { | ||
610 | 298 | this.review_type.set('value', ''); | ||
611 | 299 | this.vote_input.set('selectedIndex', 0); | ||
612 | 300 | CodeReviewComment.superclass.reset_contents.apply(this); | ||
613 | 301 | }, | ||
614 | 302 | /** | ||
615 | 303 | * Insert the specified HTML into the page. | ||
616 | 304 | * | ||
617 | 305 | * @method insert_comment_HTML | ||
618 | 306 | * @param message_html The HTML of the comment to insert. | ||
619 | 307 | */ | ||
620 | 308 | insert_comment_HTML: function(message_html){ | ||
621 | 309 | var conversation = Y.get('[id=conversation]'); | ||
622 | 310 | var comment = Y.Node.create(message_html); | ||
623 | 311 | conversation.appendChild(comment); | ||
624 | 312 | this.reset_contents(); | ||
625 | 313 | Y.lazr.anim.green_flash({node: comment}).run(); | ||
626 | 314 | }, | ||
627 | 315 | renderUI: function() { | ||
628 | 316 | CodeReviewComment.superclass.renderUI.apply(this); | ||
629 | 317 | Y.get('#inline-add-comment').setStyle('display', 'block'); | ||
630 | 318 | }, | ||
631 | 319 | /** | ||
632 | 320 | * Implementation of Widget.bindUI: Bind events to methods. | ||
633 | 321 | * | ||
634 | 322 | * In addition to Comment behaviour, mouseups and keyups on the vote and | ||
635 | 323 | * review type cause a sync. | ||
636 | 324 | * | ||
637 | 325 | * @method bindUI | ||
638 | 326 | */ | ||
639 | 327 | bindUI: function() { | ||
640 | 328 | CodeReviewComment.superclass.bindUI.apply(this); | ||
641 | 329 | this.vote_input.on('mouseup', bind(this.syncUI, this)); | ||
642 | 330 | this.review_type.on('keyup', bind(this.syncUI, this)); | ||
643 | 331 | this.review_type.on('mouseup', bind(this.syncUI, this)); | ||
644 | 332 | }, | ||
645 | 333 | /** | ||
646 | 334 | * Implementation of Widget.syncUI: Update appearance according to state. | ||
647 | 335 | * | ||
648 | 336 | * This enables and disables the review type, in addition to Comment | ||
649 | 337 | * behaviour. | ||
650 | 338 | * | ||
651 | 339 | * @method syncUI | ||
652 | 340 | */ | ||
653 | 341 | syncUI: function() { | ||
654 | 342 | CodeReviewComment.superclass.syncUI.apply(this); | ||
655 | 343 | var review_type_disabled = (this.get_vote() === null); | ||
656 | 344 | this.review_type.set('disabled', review_type_disabled); | ||
657 | 345 | } | ||
658 | 346 | }); | ||
659 | 347 | Y.lp.CodeReviewComment = CodeReviewComment; | ||
660 | 348 | |||
661 | 349 | }, '0.1' ,{requires:['oop', 'io', 'widget', 'node', 'lp.client.plugins', 'lp.errors']}); | ||
662 | 0 | 350 | ||
663 | === added file 'lib/canonical/launchpad/javascript/lp/errors.js' | |||
664 | --- lib/canonical/launchpad/javascript/lp/errors.js 1970-01-01 00:00:00 +0000 | |||
665 | +++ lib/canonical/launchpad/javascript/lp/errors.js 2009-10-14 13:59:13 +0000 | |||
666 | @@ -0,0 +1,81 @@ | |||
667 | 1 | YUI.add('lp.errors', function(Y) { | ||
668 | 2 | |||
669 | 3 | Y.lp = Y.namespace('lp'); | ||
670 | 4 | |||
671 | 5 | /* | ||
672 | 6 | * Create a form button for canceling an error form | ||
673 | 7 | * that won't reload the page on submit. | ||
674 | 8 | * | ||
675 | 9 | * @method cancel_form_button | ||
676 | 10 | * @return button {Node} The form's cancel button. | ||
677 | 11 | */ | ||
678 | 12 | var cancel_form_button = function() { | ||
679 | 13 | var button = Y.Node.create('<button>OK</button>'); | ||
680 | 14 | button.on('click', function(e) { | ||
681 | 15 | e.preventDefault(); | ||
682 | 16 | error_overlay.hide(); | ||
683 | 17 | }); | ||
684 | 18 | return button; | ||
685 | 19 | }; | ||
686 | 20 | |||
687 | 21 | |||
688 | 22 | var error_overlay; | ||
689 | 23 | /* | ||
690 | 24 | * Create the form overlay to use when encountering errors. | ||
691 | 25 | * | ||
692 | 26 | * @method create_error_overlay | ||
693 | 27 | */ | ||
694 | 28 | var create_error_overlay = function() { | ||
695 | 29 | if (error_overlay === undefined) { | ||
696 | 30 | error_overlay = new Y.lazr.FormOverlay({ | ||
697 | 31 | headerContent: '<h2>Error</h2>', | ||
698 | 32 | form_header: '', | ||
699 | 33 | form_content: '', | ||
700 | 34 | form_submit_button: Y.Node.create( | ||
701 | 35 | '<button style="display:none"></button>'), | ||
702 | 36 | form_cancel_button: cancel_form_button(), | ||
703 | 37 | centered: true, | ||
704 | 38 | visible: false | ||
705 | 39 | }); | ||
706 | 40 | error_overlay.render(); | ||
707 | 41 | } | ||
708 | 42 | }; | ||
709 | 43 | |||
710 | 44 | /** | ||
711 | 45 | * Run a callback, optionally flashing a specified node red beforehand. | ||
712 | 46 | * | ||
713 | 47 | * If the supplied node evaluates false, the callback is invoked immediately. | ||
714 | 48 | * | ||
715 | 49 | * @method maybe_red_flash | ||
716 | 50 | * @param flash_node The node to flash red, or null for no flash. | ||
717 | 51 | * @param callback The callback to invoke. | ||
718 | 52 | */ | ||
719 | 53 | var maybe_red_flash = function(flash_node, callback) | ||
720 | 54 | { | ||
721 | 55 | if (flash_node) { | ||
722 | 56 | var anim = Y.lazr.anim.red_flash({ node: flash_node }); | ||
723 | 57 | anim.on('end', callback); | ||
724 | 58 | anim.run(); | ||
725 | 59 | } else { | ||
726 | 60 | callback(); | ||
727 | 61 | } | ||
728 | 62 | }; | ||
729 | 63 | |||
730 | 64 | |||
731 | 65 | /* | ||
732 | 66 | * Take an error message and display in an overlay (creating it if necessary). | ||
733 | 67 | * | ||
734 | 68 | * @method display_error | ||
735 | 69 | * @param flash_node {Node} The node to red flash. | ||
736 | 70 | * @param msg {String} The message to display. | ||
737 | 71 | */ | ||
738 | 72 | var display_error = function(flash_node, msg) { | ||
739 | 73 | create_error_overlay(); | ||
740 | 74 | maybe_red_flash(flash_node, function(){ | ||
741 | 75 | error_overlay.showError(msg); | ||
742 | 76 | error_overlay.show(); | ||
743 | 77 | }); | ||
744 | 78 | }; | ||
745 | 79 | |||
746 | 80 | Y.lp.display_error = display_error; | ||
747 | 81 | }, '0.1', {requires:['lazr.formoverlay']}); | ||
748 | 0 | 82 | ||
749 | === modified file 'lib/lp/bugs/templates/bugtask-index.pt' | |||
750 | --- lib/lp/bugs/templates/bugtask-index.pt 2009-09-22 18:05:41 +0000 | |||
751 | +++ lib/lp/bugs/templates/bugtask-index.pt 2009-10-14 13:59:13 +0000 | |||
752 | @@ -25,6 +25,14 @@ | |||
753 | 25 | tal:define="lp_js string:${icingroot}/build" | 25 | tal:define="lp_js string:${icingroot}/build" |
754 | 26 | tal:attributes="src string:${lp_js}/bugs/bug_tags_entry.js"> | 26 | tal:attributes="src string:${lp_js}/bugs/bug_tags_entry.js"> |
755 | 27 | </script> | 27 | </script> |
756 | 28 | <script type="text/javascript" | ||
757 | 29 | tal:define="lp_js string:${icingroot}/build" | ||
758 | 30 | tal:attributes="src string:${lp_js}/lp/comment.js"> | ||
759 | 31 | </script> | ||
760 | 32 | <script type="text/javascript" | ||
761 | 33 | tal:define="lp_js string:${icingroot}/build" | ||
762 | 34 | tal:attributes="src string:${lp_js}/lp/errors.js"> | ||
763 | 35 | </script> | ||
764 | 28 | </tal:devmode> | 36 | </tal:devmode> |
765 | 29 | <script type="text/javascript"> | 37 | <script type="text/javascript"> |
766 | 30 | YUI().use('base', 'node', 'oop', 'event', 'bugs.bugtask_index', function(Y) { | 38 | YUI().use('base', 'node', 'oop', 'event', 'bugs.bugtask_index', function(Y) { |
767 | @@ -282,6 +290,12 @@ | |||
768 | 282 | var button = document.getElementById('field.actions.save'); | 290 | var button = document.getElementById('field.actions.save'); |
769 | 283 | button.style.display = 'none'; | 291 | button.style.display = 'none'; |
770 | 284 | </script> | 292 | </script> |
771 | 293 | <script type="text/javascript"> | ||
772 | 294 | YUI().use('lp.comment', function(Y) { | ||
773 | 295 | var comment = new Y.lp.Comment(); | ||
774 | 296 | comment.render(); | ||
775 | 297 | }) | ||
776 | 298 | </script> | ||
777 | 285 | </div> | 299 | </div> |
778 | 286 | <tal:attachment-link | 300 | <tal:attachment-link |
779 | 287 | define="add_attachment_link context_menu/addcomment" | 301 | define="add_attachment_link context_menu/addcomment" |
780 | 288 | 302 | ||
781 | === modified file 'lib/lp/code/browser/configure.zcml' | |||
782 | --- lib/lp/code/browser/configure.zcml 2009-09-28 21:06:28 +0000 | |||
783 | +++ lib/lp/code/browser/configure.zcml 2009-10-14 13:59:13 +0000 | |||
784 | @@ -612,6 +612,9 @@ | |||
785 | 612 | <browser:page | 612 | <browser:page |
786 | 613 | name="+comment-footer" | 613 | name="+comment-footer" |
787 | 614 | template="../templates/codereviewcomment-footer.pt"/> | 614 | template="../templates/codereviewcomment-footer.pt"/> |
788 | 615 | <browser:page | ||
789 | 616 | name="+fragment" | ||
790 | 617 | template="../templates/codereviewcomment-fragment.pt"/> | ||
791 | 615 | </browser:pages> | 618 | </browser:pages> |
792 | 616 | <browser:page | 619 | <browser:page |
793 | 617 | name="+reply" | 620 | name="+reply" |
794 | 618 | 621 | ||
795 | === modified file 'lib/lp/code/interfaces/branchmergeproposal.py' | |||
796 | --- lib/lp/code/interfaces/branchmergeproposal.py 2009-10-01 13:25:12 +0000 | |||
797 | +++ lib/lp/code/interfaces/branchmergeproposal.py 2009-10-14 13:59:13 +0000 | |||
798 | @@ -41,9 +41,10 @@ | |||
799 | 41 | from canonical.launchpad.webapp.interfaces import ITableBatchNavigator | 41 | from canonical.launchpad.webapp.interfaces import ITableBatchNavigator |
800 | 42 | from lazr.restful.fields import CollectionField, Reference | 42 | from lazr.restful.fields import CollectionField, Reference |
801 | 43 | from lazr.restful.declarations import ( | 43 | from lazr.restful.declarations import ( |
805 | 44 | call_with, export_as_webservice_entry, export_read_operation, | 44 | call_with, export_as_webservice_entry, export_factory_operation, |
806 | 45 | export_write_operation, exported, operation_parameters, | 45 | export_read_operation, export_write_operation, exported, |
807 | 46 | operation_returns_entry, rename_parameters_as, REQUEST_USER) | 46 | operation_parameters, operation_returns_entry, rename_parameters_as, |
808 | 47 | REQUEST_USER) | ||
809 | 47 | 48 | ||
810 | 48 | 49 | ||
811 | 49 | class InvalidBranchMergeProposal(Exception): | 50 | class InvalidBranchMergeProposal(Exception): |
812 | @@ -454,7 +455,8 @@ | |||
813 | 454 | vote=Choice(vocabulary=CodeReviewVote), review_type=Text(), | 455 | vote=Choice(vocabulary=CodeReviewVote), review_type=Text(), |
814 | 455 | parent=Reference(schema=Interface)) | 456 | parent=Reference(schema=Interface)) |
815 | 456 | @call_with(owner=REQUEST_USER) | 457 | @call_with(owner=REQUEST_USER) |
817 | 457 | @export_write_operation() | 458 | # ICodeReviewComment supplied as Interface to avoid circular imports. |
818 | 459 | @export_factory_operation(Interface, []) | ||
819 | 458 | def createComment(owner, subject, content=None, vote=None, | 460 | def createComment(owner, subject, content=None, vote=None, |
820 | 459 | review_type=None, parent=None): | 461 | review_type=None, parent=None): |
821 | 460 | """Create an ICodeReviewComment associated with this merge proposal. | 462 | """Create an ICodeReviewComment associated with this merge proposal. |
822 | 461 | 463 | ||
823 | === modified file 'lib/lp/code/stories/webservice/xx-branchmergeproposal.txt' | |||
824 | --- lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-10-12 09:40:17 +0000 | |||
825 | +++ lib/lp/code/stories/webservice/xx-branchmergeproposal.txt 2009-10-14 13:59:13 +0000 | |||
826 | @@ -146,10 +146,12 @@ | |||
827 | 146 | 146 | ||
828 | 147 | Now the code review should be made. | 147 | Now the code review should be made. |
829 | 148 | 148 | ||
831 | 149 | >>> comment = reviewer_webservice.named_post( | 149 | >>> comment_result = reviewer_webservice.named_post( |
832 | 150 | ... merge_proposal['self_link'], 'createComment', | 150 | ... merge_proposal['self_link'], 'createComment', |
833 | 151 | ... subject='Great work', content='This is great work', | 151 | ... subject='Great work', content='This is great work', |
835 | 152 | ... vote=CodeReviewVote.APPROVE.title, review_type='code').jsonBody() | 152 | ... vote=CodeReviewVote.APPROVE.title, review_type='code') |
836 | 153 | >>> comment_link = comment_result.getHeader('Location') | ||
837 | 154 | >>> comment = reviewer_webservice.get(comment_link).jsonBody() | ||
838 | 153 | >>> pprint_entry(comment) | 155 | >>> pprint_entry(comment) |
839 | 154 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' | 156 | branch_merge_proposal_link: u'http://.../~source/fooix/fix-it/+merge/1' |
840 | 155 | id: 3 | 157 | id: 3 |
841 | 156 | 158 | ||
842 | === modified file 'lib/lp/code/templates/branchmergeproposal-index.pt' | |||
843 | --- lib/lp/code/templates/branchmergeproposal-index.pt 2009-09-19 05:01:40 +0000 | |||
844 | +++ lib/lp/code/templates/branchmergeproposal-index.pt 2009-10-14 13:59:13 +0000 | |||
845 | @@ -20,6 +20,14 @@ | |||
846 | 20 | tal:define="lp_js string:${icingroot}/build" | 20 | tal:define="lp_js string:${icingroot}/build" |
847 | 21 | tal:attributes="src string:${lp_js}/code/codereview.js"> | 21 | tal:attributes="src string:${lp_js}/code/codereview.js"> |
848 | 22 | </script> | 22 | </script> |
849 | 23 | <script type="text/javascript" | ||
850 | 24 | tal:define="lp_js string:${icingroot}/build" | ||
851 | 25 | tal:attributes="src string:${lp_js}/lp/comment.js"> | ||
852 | 26 | </script> | ||
853 | 27 | <script type="text/javascript" | ||
854 | 28 | tal:define="lp_js string:${icingroot}/build" | ||
855 | 29 | tal:attributes="src string:${lp_js}/lp/errors.js"> | ||
856 | 30 | </script> | ||
857 | 23 | </metal:block> | 31 | </metal:block> |
858 | 24 | 32 | ||
859 | 25 | 33 | ||
860 | @@ -65,10 +73,23 @@ | |||
861 | 65 | tal:content="structure link/render"> | 73 | tal:content="structure link/render"> |
862 | 66 | Add comment | 74 | Add comment |
863 | 67 | </div> | 75 | </div> |
866 | 68 | <tal:comments condition="view/conversation" | 76 | <div id="conversation" |
867 | 69 | replace="structure view/conversation/@@+render"/> | 77 | tal:content="structure view/conversation/@@+render"/> |
868 | 78 | </div> | ||
869 | 79 | <!-- Hide inline commenting if YUI isn't used. --> | ||
870 | 80 | <div id="inline-add-comment" style="display: none"> | ||
871 | 81 | <tal:comment replace="structure context/@@+comment/++form++" /> | ||
872 | 82 | <div class="actions" id="launchpad-form-actions"> | ||
873 | 83 | <input type="submit" id="field.actions.add" name="field.actions.add" value="Save Comment" class="button" /> | ||
874 | 84 | </div> | ||
875 | 70 | </div> | 85 | </div> |
876 | 71 | 86 | ||
877 | 87 | <script type="text/javascript"> | ||
878 | 88 | YUI().use('lp.comment', function(Y) { | ||
879 | 89 | var comment = new Y.lp.CodeReviewComment(); | ||
880 | 90 | comment.render(); | ||
881 | 91 | }) | ||
882 | 92 | </script> | ||
883 | 72 | <div class="yui-g"> | 93 | <div class="yui-g"> |
884 | 73 | <div class="yui-u first"> | 94 | <div class="yui-u first"> |
885 | 74 | <div id="source-revisions" | 95 | <div id="source-revisions" |
886 | 75 | 96 | ||
887 | === added file 'lib/lp/code/templates/codereviewcomment-fragment.pt' | |||
888 | --- lib/lp/code/templates/codereviewcomment-fragment.pt 1970-01-01 00:00:00 +0000 | |||
889 | +++ lib/lp/code/templates/codereviewcomment-fragment.pt 2009-10-14 13:59:13 +0000 | |||
890 | @@ -0,0 +1,2 @@ | |||
891 | 1 | <tal:fragment replace="structure view/comment/@@+render" | ||
892 | 2 | xmlns:tal="http://xml.zope.org/namespaces/tal" /> | ||
893 | 0 | 3 | ||
894 | === modified file 'lib/lp/code/windmill/testing.py' | |||
895 | --- lib/lp/code/windmill/testing.py 2009-09-15 08:58:30 +0000 | |||
896 | +++ lib/lp/code/windmill/testing.py 2009-10-14 13:59:13 +0000 | |||
897 | @@ -9,6 +9,9 @@ | |||
898 | 9 | ] | 9 | ] |
899 | 10 | 10 | ||
900 | 11 | 11 | ||
901 | 12 | import windmill | ||
902 | 13 | |||
903 | 14 | from canonical.launchpad.webapp import canonical_url as real_canonical_url | ||
904 | 12 | from canonical.testing.layers import BaseWindmillLayer | 15 | from canonical.testing.layers import BaseWindmillLayer |
905 | 13 | 16 | ||
906 | 14 | 17 | ||
907 | @@ -16,3 +19,16 @@ | |||
908 | 16 | """Layer for Code Windmill tests.""" | 19 | """Layer for Code Windmill tests.""" |
909 | 17 | 20 | ||
910 | 18 | base_url = 'http://code.launchpad.dev:8085/' | 21 | base_url = 'http://code.launchpad.dev:8085/' |
911 | 22 | |||
912 | 23 | |||
913 | 24 | def canonical_url(*args, **kwargs): | ||
914 | 25 | """Wrapper for canonical_url that ensures test URLs are returned. | ||
915 | 26 | |||
916 | 27 | Real canonical URLs require SSL support, but our test clients don't | ||
917 | 28 | trust the certificate. | ||
918 | 29 | """ | ||
919 | 30 | url = real_canonical_url(*args, **kwargs) | ||
920 | 31 | url = url.replace( | ||
921 | 32 | 'http://code.launchpad.dev', windmill.settings['TEST_URL']) | ||
922 | 33 | assert url.startswith(windmill.settings['TEST_URL']) | ||
923 | 34 | return url | ||
924 | 19 | 35 | ||
925 | === added file 'lib/lp/code/windmill/tests/test_merge_proposal_commenting.py' | |||
926 | --- lib/lp/code/windmill/tests/test_merge_proposal_commenting.py 1970-01-01 00:00:00 +0000 | |||
927 | +++ lib/lp/code/windmill/tests/test_merge_proposal_commenting.py 2009-10-14 13:59:13 +0000 | |||
928 | @@ -0,0 +1,55 @@ | |||
929 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
930 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
931 | 3 | |||
932 | 4 | """Test for the inline commenting UI.""" | ||
933 | 5 | |||
934 | 6 | __metaclass__ = type | ||
935 | 7 | __all__ = [] | ||
936 | 8 | |||
937 | 9 | import unittest | ||
938 | 10 | |||
939 | 11 | import transaction | ||
940 | 12 | from windmill.authoring import WindmillTestClient | ||
941 | 13 | |||
942 | 14 | from canonical.launchpad.windmill.testing import lpuser | ||
943 | 15 | from canonical.uuid import generate_uuid | ||
944 | 16 | from lp.code.windmill.testing import canonical_url, CodeWindmillLayer | ||
945 | 17 | from lp.testing import TestCaseWithFactory | ||
946 | 18 | |||
947 | 19 | WAIT_PAGELOAD = u'30000' | ||
948 | 20 | WAIT_ELEMENT_COMPLETE = u'30000' | ||
949 | 21 | WAIT_CHECK_CHANGE = u'1000' | ||
950 | 22 | ADD_COMMENT_BUTTON = ( | ||
951 | 23 | u'//input[@id="field.actions.add" and @class="button js-action"]') | ||
952 | 24 | |||
953 | 25 | |||
954 | 26 | class TestMergeProposalCommenting(TestCaseWithFactory): | ||
955 | 27 | |||
956 | 28 | layer = CodeWindmillLayer | ||
957 | 29 | |||
958 | 30 | |||
959 | 31 | def test_merge_proposal_commenting(self): | ||
960 | 32 | """Test commenting on bugs.""" | ||
961 | 33 | client = WindmillTestClient('Bug commenting') | ||
962 | 34 | lpuser.NO_PRIV.ensure_login(client) | ||
963 | 35 | |||
964 | 36 | proposal = self.factory.makeBranchMergeProposal() | ||
965 | 37 | transaction.commit() | ||
966 | 38 | client.open(url=canonical_url(proposal)) | ||
967 | 39 | client.waits.forPageLoad(timeout=WAIT_PAGELOAD) | ||
968 | 40 | client.waits.forElement(xpath=ADD_COMMENT_BUTTON) | ||
969 | 41 | |||
970 | 42 | # Generate a unique piece of text, so we can run the test multiple | ||
971 | 43 | # times, without resetting the db. | ||
972 | 44 | new_comment_text = generate_uuid() | ||
973 | 45 | client.type(text=new_comment_text, id="field.comment") | ||
974 | 46 | client.click(xpath=ADD_COMMENT_BUTTON) | ||
975 | 47 | # A PRE inside a boardCommentBody, itself somewhere in the | ||
976 | 48 | # #conversation | ||
977 | 49 | client.waits.forElement( | ||
978 | 50 | xpath='//div[@id="conversation"]//div[@class="boardCommentBody"]' | ||
979 | 51 | '/pre[contains(., "%s")]' % new_comment_text) | ||
980 | 52 | |||
981 | 53 | |||
982 | 54 | def test_suite(): | ||
983 | 55 | return unittest.TestLoader().loadTestsFromName(__name__) |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
reviewer rockstar ui
reviewer launchpad-reviewers code
= Summary =
This branch provides bug-like inline commenting for merge proposals.
== Proposed fix ==
Implement a CodeReviewComment widget that extends Comment, use it in the
page. Only show it if YUI is enabled.
Replies are not yet supported as an inline operation.
== Pre-implementation notes ==
Preimplementation was with thumper. Further discussion was with rockstar
== Implementation details ==
To ensure consistent rendering, a new url, "+fragment" is provided, so
that the HTML fragment for a particular CodeReviewComment can be retrieved.
A new canonical_url function is provided for retrieving test URLs. (The
normal urls require an SSL certificate to be accepted.)
The conversation now has a top-level div with id "conversation" to make
it easier to append to the conversation.
== Tests == CodeWindmillLay er test_merge_ proposal_ commenting
bin/test -v --layer=
== Demo and Q/A ==
Create a merge proposal and go to its page. The comment form will
appear at the bottom. Make a comment or vote.
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: code/browser/ configure. zcml code/templates/ branchmergeprop osal-index. pt code/windmill/ tests/test_ merge_proposal_ commenting. py code/windmill/ testing. py code/templates/ codereviewcomme nt-fragment. pt /launchpad/ javascript/ lp/comment. js
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/canonical
== JSLint notices == abentley/ launchpad/ inline- comment/ lib/canonical/ launchpad/ javascript/ lp/comment. js'.
jslint: No problem found in
'/home/
jslint: No problem found in abentley/ launchpad/ inline- comment/ lib/canonical/ launchpad/ javascript/ lp/errors. js'.
'/home/
jslint: No problem found in abentley/ launchpad/ inline- comment/ lib/canonical/ launchpad/ javascript/ bugs/bugtask- index.js' .
'/home/
jslint: 3 files to lint. enigmail. mozdev. org
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAkr Ut+AACgkQ0F+ nu1YWqI2gEgCcDE xr+O2PM2T4hjymy JAjNlXx 2fFYU5ieWX9tJgq MfT
R38AnjWMDltSr/
=HhFi
-----END PGP SIGNATURE-----