Merge lp:~wallyworld/launchpad/inconsistent-bugtask-table-346531 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15764
Proposed branch: lp:~wallyworld/launchpad/inconsistent-bugtask-table-346531
Merge into: lp:launchpad
Diff against target: 667 lines (+233/-128)
8 files modified
lib/lp/bugs/browser/bug.py (+27/-2)
lib/lp/bugs/browser/tests/test_bug_views.py (+49/-0)
lib/lp/bugs/javascript/bug_picker.js (+12/-0)
lib/lp/bugs/javascript/bugtask_index.js (+1/-2)
lib/lp/bugs/javascript/duplicates.js (+75/-67)
lib/lp/bugs/javascript/tests/test_duplicates.html (+7/-0)
lib/lp/bugs/javascript/tests/test_duplicates.js (+46/-45)
lib/lp/code/javascript/branch.bugspeclinks.js (+16/-12)
To merge this branch: bzr merge lp:~wallyworld/launchpad/inconsistent-bugtask-table-346531
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+118518@code.launchpad.net

Commit message

Refactor bug dupe XHR calls to use a post to the view instead of web services patch operation to allow bug tasks table to be updated

Description of the change

== Implementation ==

The XHR call to mark a bug as a dupe used a web service patch operation which simply returned an updated copy of the resource entry. This branch changes that to instead make an XHR POST call to the MarkBugAsDuplicateView, the same as the HTML dupe form would. The view has been modified to detect the ajax request and return the HTML for the new bug task table which is then rendered in place of the original table. This same technique is used to process bug task deletes.

As a drive by, the error handling for the bug picker widgets was improved to use an ErrorHandler instance to process XHR failures. This means that the error text displayed in the picker correctly has the ooops id etc extracted and displayed.

== QA ==

Mark and unmark a bug as a duplicate and ensure the bug tasks table turns grey when there is a dupe and is enabled otherwise.

== Tests ==

The yui tests for the various bug picker instances had to be updated to test the new XHR call sequences as well as checking that the new bug task table is rendered.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/browser/bug.py
  lib/lp/bugs/browser/tests/test_bug_views.py
  lib/lp/bugs/javascript/bug_picker.js
  lib/lp/bugs/javascript/bugtask_index.js
  lib/lp/bugs/javascript/duplicates.js
  lib/lp/bugs/javascript/tests/test_duplicates.html
  lib/lp/bugs/javascript/tests/test_duplicates.js
  lib/lp/code/javascript/branch.bugspeclinks.js

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bug.py'
2--- lib/lp/bugs/browser/bug.py 2012-08-07 02:31:56 +0000
3+++ lib/lp/bugs/browser/bug.py 2012-08-07 10:12:43 +0000
4@@ -45,7 +45,10 @@
5 from simplejson import dumps
6 from zope import formlib
7 from zope.app.form.browser import TextWidget
8-from zope.component import getUtility
9+from zope.component import (
10+ getMultiAdapter,
11+ getUtility,
12+ )
13 from zope.event import notify
14 from zope.interface import (
15 implements,
16@@ -746,12 +749,20 @@
17 """See `LaunchpadFormView.`"""
18 return {'duplicateof': self.context.bug.duplicateof}
19
20+ @property
21+ def next_url(self):
22+ """Return the next URL to call when this call completes."""
23+ if not self.request.is_ajax:
24+ return canonical_url(self.context)
25+ return None
26+
27 def _validate(self, action, data):
28 if action.name != 'remove':
29 return super(BugMarkAsDuplicateView, self)._validate(action, data)
30 return []
31
32- @action('Set Duplicate', name='change')
33+ @action('Set Duplicate', name='change',
34+ failure=LaunchpadFormView.ajax_failure_handler)
35 def change_action(self, action, data):
36 """Update the bug."""
37 data = dict(data)
38@@ -765,6 +776,7 @@
39 ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
40 # Apply other changes.
41 self.updateBugFromData(data)
42+ return self._duplicate_action_result()
43
44 def shouldShowRemoveButton(self, action):
45 return self.context.bug.duplicateof is not None
46@@ -778,6 +790,19 @@
47 bug.markAsDuplicate(None)
48 notify(
49 ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
50+ return self._duplicate_action_result()
51+
52+ def _duplicate_action_result(self):
53+ if self.request.is_ajax:
54+ bug = self.context.bug
55+ launchbag = getUtility(ILaunchBag)
56+ launchbag.add(bug.default_bugtask)
57+ view = getMultiAdapter(
58+ (bug, self.request),
59+ name='+bugtasks-and-nominations-table')
60+ view.initialize()
61+ return view.render()
62+ return None
63
64
65 class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
66
67=== modified file 'lib/lp/bugs/browser/tests/test_bug_views.py'
68--- lib/lp/bugs/browser/tests/test_bug_views.py 2012-07-31 09:41:51 +0000
69+++ lib/lp/bugs/browser/tests/test_bug_views.py 2012-08-07 10:12:43 +0000
70@@ -567,3 +567,52 @@
71 self.bug.default_bugtask, name="+duplicate",
72 principal=self.bug_owner, form=form)
73 self.assertIsNone(self.bug.duplicateof)
74+
75+ def test_ajax_create_duplicate(self):
76+ # An ajax request to create a duplicate returns the new bugtask table.
77+ with person_logged_in(self.bug_owner):
78+ extra = {
79+ 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
80+ }
81+ form = {
82+ 'field.actions.change': u'Set Duplicate',
83+ 'field.duplicateof': u'%s' % self.duplicate_bug.id
84+ }
85+ view = create_initialized_view(
86+ self.bug.default_bugtask, name="+duplicate",
87+ principal=self.bug_owner, form=form, **extra)
88+ result_html = view.render()
89+
90+ self.assertEqual(self.duplicate_bug, self.bug.duplicateof)
91+ self.assertEqual(
92+ view.request.response.getHeader('content-type'), 'text/html')
93+ soup = BeautifulSoup(result_html)
94+ table = soup.find(
95+ 'table',
96+ {'id': 'affected-software', 'class': 'duplicate listing'})
97+ self.assertIsNotNone(table)
98+
99+ def test_ajax_remove_duplicate(self):
100+ # An ajax request to remove a duplicate returns the new bugtask table.
101+ with person_logged_in(self.bug_owner):
102+ self.bug.markAsDuplicate(self.duplicate_bug)
103+ extra = {
104+ 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
105+ }
106+ form = {
107+ 'field.actions.remove': u'Remove Duplicate',
108+ }
109+
110+ view = create_initialized_view(
111+ self.bug.default_bugtask, name="+duplicate",
112+ principal=self.bug_owner, form=form, **extra)
113+ result_html = view.render()
114+
115+ self.assertIsNone(self.bug.duplicateof)
116+ self.assertEqual(
117+ view.request.response.getHeader('content-type'), 'text/html')
118+ soup = BeautifulSoup(result_html)
119+ table = soup.find(
120+ 'table',
121+ {'id': 'affected-software', 'class': 'listing'})
122+ self.assertIsNotNone(table)
123
124=== modified file 'lib/lp/bugs/javascript/bug_picker.js'
125--- lib/lp/bugs/javascript/bug_picker.js 2012-08-07 05:00:15 +0000
126+++ lib/lp/bugs/javascript/bug_picker.js 2012-08-07 10:12:43 +0000
127@@ -91,6 +91,7 @@
128 * Show a spinner next to the specified node.
129 *
130 * @method _show_bug_spinner
131+ * @param node
132 * @protected
133 */
134 _show_bug_spinner: function(node) {
135@@ -104,6 +105,17 @@
136 },
137
138 /**
139+ * Hide the specified spinner.
140+ * @param spinner
141+ * @protected
142+ */
143+ _hide_bug_spinner: function(spinner) {
144+ if( Y.Lang.isValue(spinner)) {
145+ spinner.remove(true);
146+ }
147+ },
148+
149+ /**
150 * Look up the selected bug and get the user to confirm that it is the one
151 * they want.
152 *
153
154=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
155--- lib/lp/bugs/javascript/bugtask_index.js 2012-08-02 09:04:27 +0000
156+++ lib/lp/bugs/javascript/bugtask_index.js 2012-08-07 10:12:43 +0000
157@@ -41,8 +41,7 @@
158 setup_client_and_bug();
159
160 var config = {
161- picker_activator: '.menu-link-mark-dupe, #change_duplicate_bug',
162- lp_bug_entry: lp_bug_entry
163+ picker_activator: '.menu-link-mark-dupe, #change_duplicate_bug'
164 };
165 var dup_widget = new Y.lp.bugs.duplicates.DuplicateBugPicker(config);
166 dup_widget.render();
167
168=== modified file 'lib/lp/bugs/javascript/duplicates.js'
169--- lib/lp/bugs/javascript/duplicates.js 2012-08-06 11:19:31 +0000
170+++ lib/lp/bugs/javascript/duplicates.js 2012-08-07 10:12:43 +0000
171@@ -120,23 +120,60 @@
172 Y.lp.bugs.bug_picker.BugPicker.prototype._find_bug.call(this, data);
173 },
174
175+ // A common error handler for XHR operations.
176+ _error_handler: function() {
177+ var that = this;
178+ var error_handler = new Y.lp.client.ErrorHandler();
179+ error_handler.handleError = function(id, response) {
180+ var error_msg = response.responseText;
181+ if (response.status === 400) {
182+ var response_info = Y.JSON.parse(response.responseText);
183+ var dup_error = response_info.errors['field.duplicateof'];
184+ if (Y.Lang.isString(dup_error)) {
185+ var error_info = dup_error.split('\n');
186+ if (error_info.length === 1) {
187+ error_msg = error_info;
188+ } else {
189+ error_msg = error_info.slice(1).join(' ');
190+ }
191+ that.set('error', error_msg);
192+ return true;
193+ }
194+ }
195+ return false;
196+ };
197+ error_handler.showError = function(error_msg) {
198+ that.set('error', error_msg);
199+ };
200+ return error_handler;
201+ },
202+
203+ // Render the new bug task table.
204+ _render_bugtask_table: function(new_table) {
205+ var bugtask_table = Y.one('#affected-software');
206+ bugtask_table.replace(new_table);
207+ Y.lp.bugs.bugtask_index.setup_bugtask_table();
208+ },
209+
210 /**
211 * Bug was successfully marked as a duplicate, update the UI.
212 *
213 * @method _submit_bug_success
214- * @param updated_entry
215+ * @param response
216 * @param new_dup_url
217 * @param new_dup_id
218 * @param new_dupe_title
219 * @private
220 */
221- _submit_bug_success: function(updated_entry, new_dup_url,
222- new_dup_id, new_dup_title) {
223+ _submit_bug_success: function(response, new_dup_url,
224+ new_dup_id, new_dupe_title) {
225 this._performDefaultSave();
226- updated_entry.set('duplicate_of_link', new_dup_url);
227- LP.cache.bug.duplicate_of_link = updated_entry.duplicate_of_link;
228- this.set('lp_bug_entry', updated_entry);
229-
230+
231+ // Render the new bug tasks table.
232+ LP.cache.bug.duplicate_of_link = new_dup_url;
233+ this._render_bugtask_table(response.responseText);
234+
235+ // Render the new dupe portlet.
236 var dupe_span = this.get('dupe_span').ancestor('li');
237 var update_dup_url = dupe_span.one('a').get('href');
238 var edit_link;
239@@ -158,7 +195,8 @@
240 if (Y.Lang.isValue(duplicatesNode)) {
241 duplicatesNode.remove(true);
242 }
243- this._show_comment_on_duplicate_warning(new_dup_id, new_dup_title);
244+ this._show_comment_on_duplicate_warning(
245+ new_dup_id, new_dupe_title);
246 } else {
247 dupe_span.addClass('sprite bug-dupe');
248 dupe_span.setContent([
249@@ -189,61 +227,38 @@
250 },
251
252 /**
253- * There was an error marking a bug as a duplicate.
254- *
255- * @method _submit_bug_failure
256- * @param response
257- * @param old_dup_url
258- * @private
259- */
260- _submit_bug_failure: function(response, old_dup_url) {
261- // Reset the lp_bug_entry.duplicate_of_link as it wasn't
262- // updated.
263- this.get('lp_bug_entry').set('duplicate_of_link', old_dup_url);
264- var error_msg = response.responseText;
265- if (response.status === 400) {
266- var error_info = response.responseText.split('\n');
267- error_msg = error_info.slice(1).join(' ');
268- }
269- this.set('error', error_msg);
270- },
271-
272- /**
273 * Update the bug duplicate via the LP API
274 *
275 * @method _submit_bug
276 * @param new_dup_id
277- * @param new_dup_title
278+ * @param new_dupe_title
279 * @param widget
280 * @private
281 */
282 _submit_bug: function(new_dup_id, new_dupe_title, widget) {
283- // XXX noodles 2009-03-17 bug=336866 It seems the etag
284- // returned by lp_save() is incorrect. Remove it for now
285- // so that the second save does not result in a '412
286- // precondition failed' error.
287- //
288- // XXX deryck 2009-04-29 bug=369293 Also, this has to
289- // happen before *any* call to lp_save now that bug
290- // subscribing can be done inline. Named operations
291- // don't return new objects, making the cached bug's
292- // etag invalid as well.
293- var lp_bug_entry = this.get('lp_bug_entry');
294- lp_bug_entry.removeAttr('http_etag');
295-
296+ var dupe_span = this.get('dupe_span');
297+ var that = this;
298 var new_dup_url = null;
299+
300+ var qs;
301 if (new_dup_id !== '') {
302- var self_link = lp_bug_entry.get('self_link');
303- var last_slash_index = self_link.lastIndexOf('/');
304- new_dup_url = self_link.slice(0, last_slash_index+1) + new_dup_id;
305+ var bug_link = LP.cache.bug.self_link;
306+ var last_slash_index = bug_link.lastIndexOf('/');
307+ new_dup_url = bug_link.slice(0, last_slash_index+1) + new_dup_id;
308+ qs = Y.lp.client.append_qs(
309+ '', 'field.actions.change', 'Set Duplicate');
310+ qs = Y.lp.client.append_qs(qs, "field.duplicateof", new_dup_id);
311+ } else {
312+ qs = Y.lp.client.append_qs(
313+ '', 'field.actions.remove', 'Remove Duplicate');
314 }
315- var old_dup_url = lp_bug_entry.get('duplicate_of_link');
316- lp_bug_entry.set('duplicate_of_link', new_dup_url);
317
318- var dupe_span = this.get('dupe_span');
319- var that = this;
320 var spinner = null;
321- var config = {
322+ var error_handler = this._error_handler();
323+ var submit_url = LP.cache.context.web_link + '/+duplicate';
324+ var y_config = {
325+ method: "POST",
326+ headers: {'Accept': 'application/json; application/xhtml'},
327 on: {
328 start: function() {
329 dupe_span.removeClass('sprite bug-dupe');
330@@ -253,21 +268,18 @@
331 },
332 end: function() {
333 dupe_span.removeClass('update-in-progress-message');
334- if (spinner !== null) {
335- spinner.remove(true);
336- }
337+ that._hide_bug_spinner(spinner);
338 },
339- success: function(updated_entry) {
340+ success: function(id, response) {
341 that._submit_bug_success(
342- updated_entry, new_dup_url, new_dup_id, new_dupe_title);
343+ response, new_dup_url, new_dup_id, new_dupe_title);
344 },
345- failure: function(id, response) {
346- that._submit_bug_failure(response, old_dup_url);
347- }
348- }
349+ failure: error_handler.getFailureHandler()
350+ },
351+ data: qs
352 };
353- // And save the updated entry.
354- lp_bug_entry.lp_save(config);
355+ var io_provider = this.lp_client.io_provider;
356+ io_provider.io(submit_url, y_config);
357 },
358
359 /*
360@@ -315,10 +327,6 @@
361 }
362 }, {
363 ATTRS: {
364- // The launchpad client entry for the current bug.
365- lp_bug_entry: {
366- value: null
367- },
368 // The rendered duplicate information.
369 dupe_span: {
370 getter: function() {
371@@ -362,5 +370,5 @@
372 });
373
374 }, "0.1", {"requires": [
375- "base", "io", "oop", "node", "event", "json",
376- "lp.bugs.bug_picker"]});
377+ "base", "io", "oop", "node", "event", "json", "lp.app.errors",
378+ "lp.bugs.bug_picker", "lp.bugs.bugtask_index"]});
379
380=== modified file 'lib/lp/bugs/javascript/tests/test_duplicates.html'
381--- lib/lp/bugs/javascript/tests/test_duplicates.html 2012-08-03 12:47:11 +0000
382+++ lib/lp/bugs/javascript/tests/test_duplicates.html 2012-08-07 10:12:43 +0000
383@@ -44,6 +44,11 @@
384 <script type="text/javascript" src="../../../../../build/js/lp/app/overlay/overlay.js"></script>
385 <script type="text/javascript" src="../../../../../build/js/lp/app/picker/picker.js"></script>
386 <script type="text/javascript" src="../../../../../build/js/lp/bugs/bug_picker.js"></script>
387+ <script type="text/javascript" src="../../../../../build/js/lp/bugs/bugtask_index.js"></script>
388+ <script type="text/javascript" src="../../../../../build/js/lp/app/banners/banner.js"></script>
389+ <script type="text/javascript" src="../../../../../build/js/lp/app/banners/privacy.js"></script>
390+ <script type="text/javascript" src="../../../../../build/js/lp/app/choice.js"></script>
391+ <script type="text/javascript" src="../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
392
393 <!-- The module under test. -->
394 <script type="text/javascript" src="../duplicates.js"></script>
395@@ -63,6 +68,7 @@
396 <div id="fixture">
397 </div>
398 <script type="text/x-template" id="no-existing-duplicate">
399+ <table id="affected-software"></table>
400 <a href="#" class="pick-bug js-action">Select a bug</a>
401 <div><ul id="duplicate-actions">
402 <li class="sprite bug-dupe">
403@@ -78,6 +84,7 @@
404 <div id="portlet-duplicates"></div>
405 </script>
406 <script type="text/x-template" id="existing-duplicate">
407+ <table id="affected-software"></table>
408 <a href="#" class="pick-bug js-action">Select a bug</a>
409 <div><ul id="duplicate-actions">
410 <li>
411
412=== modified file 'lib/lp/bugs/javascript/tests/test_duplicates.js'
413--- lib/lp/bugs/javascript/tests/test_duplicates.js 2012-08-06 02:54:11 +0000
414+++ lib/lp/bugs/javascript/tests/test_duplicates.js 2012-08-07 10:12:43 +0000
415@@ -18,6 +18,9 @@
416 id: 1,
417 self_link: 'api/devel/bugs/1',
418 duplicate_of_link: ''
419+ },
420+ context: {
421+ web_link: '/foobar/bug/1'
422 }
423 }
424 };
425@@ -25,9 +28,6 @@
426 this.lp_client = new Y.lp.client.Launchpad({
427 io_provider: this.mockio
428 });
429- var bug_repr = window.LP.cache.bug;
430- this.lp_bug_entry = new Y.lp.client.Entry(
431- this.lp_client, bug_repr, bug_repr.self_link);
432 },
433
434 tearDown: function () {
435@@ -55,7 +55,6 @@
436 Y.Node.create(Y.one('#' + fixture_id).getContent()));
437 var widget = new Y.lp.bugs.duplicates.DuplicateBugPicker({
438 picker_activator: '.pick-bug',
439- lp_bug_entry: this.lp_bug_entry,
440 use_animation: false,
441 io_provider: this.mockio,
442 private_warning_message:
443@@ -151,9 +150,11 @@
444 .simulate('click');
445 this._assert_form_state(true);
446 Y.Assert.areEqual(
447- '/api/devel/bugs/1', this.mockio.last_request.url);
448+ '/foobar/bug/1/+duplicate',
449+ this.mockio.last_request.url);
450 var expected_link =
451- '{"duplicate_of_link":"api/devel/bugs/' + bug_id + '"}';
452+ 'field.actions.change=Set%20Duplicate' +
453+ '&field.duplicateof=3';
454 Y.Assert.areEqual(
455 expected_link, this.mockio.last_request.config.data);
456 },
457@@ -164,21 +165,19 @@
458 this._assert_dupe_submission(3);
459 var success_called = false;
460 this.widget._submit_bug_success =
461- function(updated_entry, new_dup_url, new_dup_id,
462+ function(response, new_dup_url, new_dup_id,
463 new_dup_title) {
464 Y.Assert.areEqual(
465- expected_updated_entry.duplicate_of_link,
466- updated_entry.duplicate_of_link);
467+ '<table>New Table</table>', response.responseText);
468 Y.Assert.areEqual('api/devel/bugs/3', new_dup_url);
469 Y.Assert.areEqual(3, new_dup_id);
470 Y.Assert.areEqual('dupe title', new_dup_title);
471 success_called = true;
472 };
473- var expected_updated_entry = {
474- uri: 'api/devel/bugs/1',
475- duplicate_of_link: 'api/devel/bugs/3',
476- self_link: 'api/devel/bugs/1'};
477- this.mockio.last_request.successJSON(expected_updated_entry);
478+ this.mockio.success({
479+ responseText: '<table>New Table</table>',
480+ responseHeaders: {'Content-Type': 'text/html'}
481+ });
482 Y.Assert.isTrue(success_called);
483 },
484
485@@ -187,18 +186,16 @@
486 this.widget = this._createWidget(false);
487 this._assert_dupe_submission(3);
488 var failure_called = false;
489- this.widget._submit_bug_failure =
490- function(response, old_dup_url) {
491- Y.Assert.areEqual(
492- 'There was an error', response.responseText);
493- Y.Assert.areEqual('', old_dup_url);
494- failure_called = true;
495- };
496 this.mockio.respond({
497 status: 400,
498- responseText: 'There was an error',
499- responseHeaders: {'Content-Type': 'text/html'}});
500- Y.Assert.isTrue(failure_called);
501+ responseText:
502+ '{"error_summary": "There is 1 error.",' +
503+ '"errors":' +
504+ '{"field.duplicateof": "There was an error"}, ' +
505+ '"form_wide_errors": []}',
506+ responseHeaders: {'Content-Type': 'application/json'}});
507+ var error = this.widget.get('error');
508+ Y.Assert.areEqual('There was an error', error);
509 },
510
511 // Submitting a dupe removal request works as expected.
512@@ -206,59 +203,63 @@
513 this.widget = this._createWidget(false);
514 var success_called = false;
515 this.widget._submit_bug_success =
516- function(updated_entry, new_dup_url, new_dup_id,
517+ function(response, new_dup_url, new_dup_id,
518 new_dupe_title) {
519- Y.Assert.areEqual(expected_updated_entry, updated_entry);
520+ Y.Assert.areEqual(
521+ response.responseText, '<table>New Table</table>');
522 Y.Assert.areEqual(null, new_dup_url);
523 Y.Assert.areEqual('', new_dup_id);
524 success_called = true;
525 };
526 Y.one('.yui3-bugpickerwidget a.remove').simulate('click');
527- var expected_updated_entry =
528- '{"duplicate_of_link":""}';
529 this.mockio.success({
530- responseText: expected_updated_entry,
531- responseHeaders: {'Content-Type': 'text/html'}});
532+ responseText: '<table>New Table</table>',
533+ responseHeaders: {'Content-Type': 'text/html'}
534+ });
535 Y.Assert.isTrue(success_called);
536 },
537
538 // The mark bug duplicate success function works as expected.
539 test_submit_bug_success: function() {
540 this.widget = this._createWidget(false);
541- var data = {
542- self_link: 'api/devel/bugs/1'};
543- var new_bug_entry = new Y.lp.client.Entry(
544- this.lp_client, data, data.self_link);
545+ var response = {
546+ responseText: '<table id="affected-software">' +
547+ '<tbody><tr><td>Bug tasks</td></tr></tbody></table>',
548+ responseHeaders: {'Content-Type': 'text/html'}};
549 this.widget._submit_bug_success(
550- new_bug_entry, 'api/devel/bugs/3', 3, 'dupe title');
551+ response, 'api/devel/bugs/3', 3, 'dupe title');
552 // Test the updated bug entry.
553 Y.Assert.areEqual(
554- 'api/devel/bugs/3',
555- this.widget.get('lp_bug_entry').get('duplicate_of_link'));
556+ 'api/devel/bugs/3', LP.cache.bug.duplicate_of_link);
557 // Test the Change Duplicate link.
558 Y.Assert.isNotNull(Y.one('#mark-duplicate-text a'));
559 // Test the duplicate warning message.
560 Y.Assert.isNotNull(Y.one('#warning-comment-on-duplicate'));
561 // Any previously listed duplicates are removed.
562 Y.Assert.isNull(Y.one('#portlet-duplicates'));
563+ // The bug dupe table is updated.
564+ Y.Assert.areEqual(
565+ 'Bug tasks', Y.one('#affected-software').get('text'));
566 },
567
568 // The remove bug duplicate success function works as expected.
569 test_remove_bug_duplicate_success: function() {
570 this.widget = this._createWidget(true);
571- var data = {
572- self_link: 'api/devel/bugs/1'};
573- var new_bug_entry = new Y.lp.client.Entry(
574- this.lp_client, data, data.self_link);
575- this.widget._submit_bug_success(new_bug_entry, null, '');
576+ var response = {
577+ responseText: '<table id="affected-software">' +
578+ '<tbody><tr><td>Bug tasks</td></tr></tbody></table>',
579+ responseHeaders: {'Content-Type': 'text/html'}};
580+ this.widget._submit_bug_success(response, null, '');
581 // Test the updated bug entry.
582- Y.Assert.isNull(
583- this.widget.get('lp_bug_entry').get('duplicate_of_link'));
584+ Y.Assert.isNull(LP.cache.bug.duplicate_of_link);
585 // Test the Mark as Duplicate link.
586 Y.Assert.isNotNull(
587 Y.one('#mark-duplicate-text .menu-link-mark-dupe'));
588 // Test the duplicate warning message is gone.
589 Y.Assert.isNull(Y.one('#warning-comment-on-duplicate'));
590+ // The bug dupe table is updated.
591+ Y.Assert.areEqual(
592+ 'Bug tasks', Y.one('#affected-software').get('text'));
593 }
594 })));
595
596@@ -267,5 +268,5 @@
597 'test', 'lp.testing.helpers', 'event', 'node-event-simulate',
598 'console', 'lp.client', 'lp.testing.mockio', 'lp.anim',
599 'lp.bugs.bug_picker', 'lp.bugs.duplicates',
600- 'lp.bugs.bug_picker.test']
601+ 'lp.bugs.bug_picker.test', 'lp.bugs.bugtask_index']
602 });
603
604=== modified file 'lib/lp/code/javascript/branch.bugspeclinks.js'
605--- lib/lp/code/javascript/branch.bugspeclinks.js 2012-08-05 10:51:39 +0000
606+++ lib/lp/code/javascript/branch.bugspeclinks.js 2012-08-07 10:12:43 +0000
607@@ -120,6 +120,14 @@
608 Y.lp.client.get_absolute_uri("/api/devel/bugs/" + bug_id);
609 var spinner;
610 var that = this;
611+ var error_handler = new Y.lp.client.ErrorHandler();
612+ error_handler.clearProgressUI = function() {
613+ that._hide_bug_spinner(spinner);
614+ that._hide_temporary_spinner();
615+ };
616+ error_handler.showError = function(error_msg) {
617+ that.set('error', error_msg);
618+ };
619 var config = {
620 on: {
621 start: function() {
622@@ -127,16 +135,10 @@
623 spinner = that._show_bug_spinner(widget);
624 that._show_temporary_spinner();
625 },
626- success: function(entry) {
627+ success: function() {
628 that._update_bug_links(bug_id, spinner);
629 },
630- failure: function(id, response) {
631- if (spinner !== null) {
632- spinner.remove(true);
633- }
634- that._hide_temporary_spinner();
635- that.set('error', response.responseText);
636- }
637+ failure: error_handler.getFailureHandler()
638 },
639 parameters: {
640 bug: bug_link
641@@ -149,10 +151,14 @@
642 /**
643 * Update the list of bug links.
644 * @param bug_id
645- * @param spinner
646+ * @param widget
647 * @private
648 */
649 _update_bug_links: function(bug_id, spinner) {
650+ var error_handler = new Y.lp.client.ErrorHandler();
651+ error_handler.showError = function(error_msg) {
652+ that.set('error', error_msg);
653+ };
654 var that = this;
655 this.lp_client.io_provider.io('++bug-links', {
656 on: {
657@@ -165,9 +171,7 @@
658 success: function(id, response) {
659 that._link_bug_success(bug_id, response.responseText);
660 },
661- failure: function(id, response) {
662- that.set('error', response.responseText);
663- }
664+ failure: error_handler.getFailureHandler()
665 }
666 });
667 },