Merge lp:~wallyworld/launchpad/private-dupe-bug-warning3 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15719
Proposed branch: lp:~wallyworld/launchpad/private-dupe-bug-warning3
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/private-dupe-bug-warning2-943497
Diff against target: 494 lines (+253/-24)
8 files modified
lib/canonical/launchpad/icing/css/forms.css (+1/-1)
lib/lp/bugs/browser/tests/test_bugs.py (+27/-0)
lib/lp/bugs/interfaces/malone.py (+18/-0)
lib/lp/bugs/javascript/bugtask_index.js (+6/-1)
lib/lp/bugs/javascript/duplicates.js (+63/-14)
lib/lp/bugs/javascript/tests/test_duplicates.js (+79/-8)
lib/lp/bugs/tests/test_searchtasks_webservice.py (+33/-0)
lib/lp/systemhomes.py (+26/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/private-dupe-bug-warning3
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+116997@code.launchpad.net

Description of the change

== Implementation ==

Thus branch completes the first round of work to improve the bug dupe form. Main new features compared with previous branch:

- Short circuit some checks
If the same bug number as the current bug is entered, of the same bug number as the current dupe, no search is done and a suitable error message is displayed iommediately.

- Display privacy warning
If the current bug is public and the proposed dupe bug is private, display a warning
(improvements to message text welcome)

- All correct bug data retrieved from server

The implementation adds a new method getBugData() to IMaloneApplication and exposes it as a named ws op on the /bugs web service interface. The method currently takes just the one search criteria (bug_id) but can be extended if we want to allow searching on pillar and key words. The getBugData method returns a list of json bug data.

==Demo and QA ==

Here's a screenshot of the privacy warning.

http://people.canonical.com/~ianb/bug-dupe-privacy-warning.png

== Tests ==

Extend yui tests
Add tests for getBugData (for direct call and via web service)

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/systemhomes.py
  lib/lp/bugs/browser/tests/test_bugs.py
  lib/lp/bugs/interfaces/malone.py
  lib/lp/bugs/javascript/bugtask_index.js
  lib/lp/bugs/javascript/duplicates.js
  lib/lp/bugs/javascript/tests/test_duplicates.js
  lib/lp/bugs/tests/test_searchtasks_webserv

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

Hi Ian.

The code looks fine, but I have some concerns about the layout and messages. I think we want to decide what we want to do about these issues, and decide which branch we want to address them

1. The buttons in the screenshot are centred. No design guidelines for any os/web site I have seen suggest centred is correct. Maybe the centreing is an illusion created by the very large content of the bug listing extending beyond the width of the other elements. Lp's rules state the forms action buttons are to bottom left. lazr would place them at the bottom right. We don't have many example align right with real button -- The create milestone overlay is the only one I recall.

2. More bother. The Lp's overlays prefer to make the data the action. So in the screenshot, The cancel action would be the cancel button in the upper right corner, The search field would always be visible so search again is not needed. Plus, taking the search away implies a two step picker and the green progress bar says there is only one step. So instead of Save Duplicate, the user would choose the bug in the listing...but this is not a list. We don't want a listing either. The link to the found bug will reload my page and I loose state. Like the person picker if we need a link we need to be clear that the user can "view details" with a icon telling the user that the link opens a new window.

3. I do not think the privacy warning will deter people from being rude. Maybe
   Marking this bug a duplicate of a private bug prevents contributors from helping,
   and encourages the reporting of more duplicate bugs.
   Is there a public version of this bug that can be used instead?

4. Is it really an error to mark a bug a duplicate of a bug that is already the duplicate? I think this is a no-op where the overlay just closes with a success.

review: Needs Information (code + ui)
Revision history for this message
Ian Booth (wallyworld) wrote :

> Hi Ian.
>
> The code looks fine, but I have some concerns about the layout and messages. I
> think we want to decide what we want to do about these issues, and decide
> which branch we want to address them
>
> 1. The buttons in the screenshot are centred. No design guidelines for any
> os/web site I have seen suggest centred is correct. Maybe the centreing is an
> illusion created by the very large content of the bug listing extending beyond
> the width of the other elements. Lp's rules state the forms action buttons are
> to bottom left. lazr would place them at the bottom right. We don't have many
> example align right with real button -- The create milestone overlay is the
> only one I recall.
>

I struggled to make this look nice due to the width of the overlay and the small width of the initial lp form to enter the bug number, as laid out by the formoverlay module and the standard lp form html. The initial Save Duplicate and Cancel buttons appeared centred but were in fact laid out to the right edge of the form I think. But since the form itself was narrow and centred, it gave that illusion. Then when the bug details are rendered after the search, the buttons, if right aligned, you have jumped far right and it looked bad. I could find a way to align everything left but the same problem would remain - the 2 "forms" have different widths. And when the overlay expands left and right to accommodate the wider bug details, the buttons would jump left also. Maybe it doesn;t matter.

> 2. More bother. The Lp's overlays prefer to make the data the action. So in
> the screenshot, The cancel action would be the cancel button in the upper
> right corner, The search field would always be visible so search again is not
> needed. Plus, taking the search away implies a two step picker and the green
> progress bar says there is only one step. So instead of Save Duplicate, the
> user would choose the bug in the listing...but this is not a list. We don't
> want a listing either. The link to the found bug will reload my page and I
> loose state. Like the person picker if we need a link we need to be clear that
> the user can "view details" with a icon telling the user that the link opens a
> new window.
>

The current formoverlay usage used in most places is to add the 2 buttons (tick/cross, yes/no, save/cancel etc). But I can look at removing the "cancel" button and rely on the cancel in the top right corner or click outside the form to close it.
I can look at making the search form always visible.
I can add the details icon as per the picker.

> 3. I do not think the privacy warning will deter people from being rude. Maybe
> Marking this bug a duplicate of a private bug prevents contributors from
> helping,
> and encourages the reporting of more duplicate bugs.
> Is there a public version of this bug that can be used instead?
>

Thanks, I didn't like my wording.

> 4. Is it really an error to mark a bug a duplicate of a bug that is already
> the duplicate? I think this is a no-op where the overlay just closes with a
> success.

I believe (from memory) that the server call, if made, will result in an error hence I short circuited it.

Revision history for this message
Curtis Hovey (sinzui) wrote :

1. Yes, that expansion behaviour is why Lp likes left aligned buttons.
2. We can do this work in another branch. The button placing is from production and your previous branch.
3. I cannot say I like my wording either.
4. I think this is a bug in the view/model. I think we just want to say done without doing any work.

Revision history for this message
Ian Booth (wallyworld) wrote :

> 1. Yes, that expansion behaviour is why Lp likes left aligned buttons.

I have made the buttons for the second confirmation form left aligned. This also means that the buttons on the person picker validation forms are also left aligned eg the form asking if you really want to assign someone to a bug. It looks reasonable I think.

> 2. We can do this work in another branch. The button placing is from
> production and your previous branch.

Ok. One point to note is that none of our formoverlay forms (that I know of) provide the (x) close/cancel button in the top right corner. They all use the little lazr tick/cross buttons, right aligned. I will change how the bug dupe form works but I should also see how feasible it would be to make everything consistent for the other forms as well.

> 3. I cannot say I like my wording either.

I used this wording:

Marking this bug as a duplicate of a private bug means that it won't be visible to contributors and encourages the reporting of more duplicate bugs.
Perhaps there is a public bug that can be used instead.

> 4. I think this is a bug in the view/model. I think we just want to say done
> without doing any work.

I disagree here. It may be that the user did a typo and really intended to type a different bug number but we would not be alerting them to that and they may click away from the page thinking that they have done what they intended when in fact nothing had changed from what it was. So I think the message alerting them to their mistake is better. If you still feel otherwise, I can change in the next branch.

Revision history for this message
Curtis Hovey (sinzui) wrote :

2. All overlays should use the right aligned lazr tick/cross buttons. We do not want to change them. We want to add the same art an behaviour to the duplicate overlay.

3. I like your text.

4. ok.

Revision history for this message
Curtis Hovey (sinzui) wrote :

I don't want to hold up this branch any further. I have some concerns that I would like addressed in a subsequent branch.

1. Search should use the search icon and be to the right of the text field. the spinner replaces it when active. I think this is the same position and behaviour as the person picker. I don't think we need the Search or Search Again buttons in the bottom.

2. I like your close button, but I want one kind of art used for all overlay close buttons. I think I prefer yours...I did not know that the choice picker had a [x] until I fixed the keyboard behavour -- The icon looks bad when it is selected, and it was always selected until I fixed it.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/css/forms.css'
2--- lib/canonical/launchpad/icing/css/forms.css 2012-07-29 23:33:20 +0000
3+++ lib/canonical/launchpad/icing/css/forms.css 2012-07-29 23:33:20 +0000
4@@ -88,7 +88,7 @@
5 display: block;
6 }
7 .extra-form-buttons {
8- text-align: center;
9+ text-align: left;
10 padding-top: 1em;
11 }
12 .extra-form-buttons button {
13
14=== modified file 'lib/lp/bugs/browser/tests/test_bugs.py'
15--- lib/lp/bugs/browser/tests/test_bugs.py 2012-02-28 04:24:19 +0000
16+++ lib/lp/bugs/browser/tests/test_bugs.py 2012-07-29 23:33:20 +0000
17@@ -9,12 +9,15 @@
18
19 from zope.component import getUtility
20
21+from lp.bugs.interfaces.bugtask import BugTaskStatus
22 from lp.bugs.interfaces.malone import IMaloneApplication
23 from lp.bugs.publisher import BugsLayer
24+from lp.registry.enums import InformationType
25 from lp.services.webapp.publisher import canonical_url
26 from lp.testing import (
27 celebrity_logged_in,
28 feature_flags,
29+ person_logged_in,
30 set_feature_flag,
31 TestCaseWithFactory,
32 )
33@@ -113,3 +116,27 @@
34
35 # we should get some valid content out of this
36 self.assertIn('Search all bugs', content)
37+
38+ def test_getBugData(self):
39+ # The getBugData method works as expected.
40+ owner = self.factory.makePerson()
41+ bug = self.factory.makeBug(
42+ owner=owner,
43+ status=BugTaskStatus.INPROGRESS,
44+ title='title', description='description',
45+ information_type=InformationType.PRIVATESECURITY)
46+ with person_logged_in(owner):
47+ bug_data = getUtility(IMaloneApplication).getBugData(owner, bug.id)
48+ expected_bug_data = {
49+ 'id': bug.id,
50+ 'information_type': 'Private Security',
51+ 'is_private': True,
52+ 'importance': 'Undecided',
53+ 'importance_class': 'importanceUNDECIDED',
54+ 'status': 'In Progress',
55+ 'status_class': 'statusINPROGRESS',
56+ 'bug_summary': 'title',
57+ 'description': 'description',
58+ 'bug_url': canonical_url(bug.default_bugtask)
59+ }
60+ self.assertEqual([expected_bug_data], bug_data)
61
62=== modified file 'lib/lp/bugs/interfaces/malone.py'
63--- lib/lp/bugs/interfaces/malone.py 2012-05-11 06:29:42 +0000
64+++ lib/lp/bugs/interfaces/malone.py 2012-07-29 23:33:20 +0000
65@@ -12,10 +12,13 @@
66 collection_default_content,
67 export_as_webservice_collection,
68 export_factory_operation,
69+ export_read_operation,
70+ operation_for_version,
71 operation_parameters,
72 REQUEST_USER,
73 )
74 from lazr.restful.fields import Reference
75+from lazr.restful.interface import copy_field
76 from zope.interface import Attribute
77
78 from lp.bugs.interfaces.bug import IBug
79@@ -36,6 +39,21 @@
80 def searchTasks(search_params):
81 """Search IBugTasks with the given search parameters."""
82
83+ @call_with(user=REQUEST_USER)
84+ @operation_parameters(
85+ bug_id=copy_field(IBug['id'])
86+ )
87+ @export_read_operation()
88+ @operation_for_version('devel')
89+ def getBugData(user, bug_id):
90+ """Search bugtasks matching the specified criteria.
91+
92+ The only criteria currently supported is to search for a bugtask with
93+ the specified bug id.
94+
95+ :return: a list of matching bugs represented as json data
96+ """
97+
98 bug_count = Attribute("The number of bugs recorded in Launchpad")
99 bugwatch_count = Attribute("The number of links to external bug trackers")
100 bugtask_count = Attribute("The number of bug tasks in Launchpad")
101
102=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
103--- lib/lp/bugs/javascript/bugtask_index.js 2012-07-25 00:31:04 +0000
104+++ lib/lp/bugs/javascript/bugtask_index.js 2012-07-29 23:33:20 +0000
105@@ -41,7 +41,12 @@
106 setup_client_and_bug();
107 var dup_widget = new Y.lp.bugs.duplicates.MarkBugDuplicate({
108 srcNode: '#duplicate-actions',
109- lp_bug_entry: lp_bug_entry
110+ lp_bug_entry: lp_bug_entry,
111+ private_warning_message:
112+ 'Marking this bug as a duplicate of a private bug means '+
113+ 'that it won\'t be visible to contributors and encourages '+
114+ 'the reporting of more duplicate bugs.<br>' +
115+ 'Perhaps there is a public bug that can be used instead.'
116 });
117 dup_widget.render();
118
119
120=== modified file 'lib/lp/bugs/javascript/duplicates.js'
121--- lib/lp/bugs/javascript/duplicates.js 2012-07-29 23:33:20 +0000
122+++ lib/lp/bugs/javascript/duplicates.js 2012-07-29 23:33:20 +0000
123@@ -138,16 +138,35 @@
124 * @private
125 */
126 _find_duplicate_bug: function(data) {
127- var new_dup_id = Y.Lang.trim(data['field.duplicateof'][0]);
128+ var new_dup_id = Y.Lang.trim(data['field.duplicateof'][0]).trim();
129 // If there's no bug data entered then we are deleting the duplicate
130 // link.
131 if (new_dup_id === '') {
132 this._update_bug_duplicate(new_dup_id);
133 return;
134 }
135+
136+ // Do some quick checks before we submit.
137+ if (new_dup_id === LP.cache.bug.id.toString()) {
138+ this.duplicate_form.showError(
139+ 'A bug cannot be marked as a duplicate of itself.');
140+ return;
141+ }
142+ var duplicate_of_link = LP.cache.bug.duplicate_of_link;
143+ var new_dupe_link
144+ = Y.lp.client.get_absolute_uri("/api/devel/bugs/" + new_dup_id);
145+ if (new_dupe_link === duplicate_of_link) {
146+ this.duplicate_form.showError(
147+ 'This bug is already marked as a duplicate of bug ' +
148+ new_dup_id + '.');
149+ return;
150+ }
151+
152 var that = this;
153 var qs_data
154 = Y.lp.client.append_qs("", "ws.accept", "application.json");
155+ qs_data = Y.lp.client.append_qs(qs_data, "ws.op", "getBugData");
156+ qs_data = Y.lp.client.append_qs(qs_data, "bug_id", new_dup_id);
157
158 var bug_field = this.duplicate_form.form_node
159 .one('[id="field.duplicateof"]');
160@@ -165,7 +184,12 @@
161 return;
162 }
163 var bug_data = Y.JSON.parse(response.responseText);
164- that._confirm_selected_bug(bug_data);
165+ if (!Y.Lang.isArray(bug_data) || bug_data.length === 0) {
166+ return;
167+ }
168+ // The server may return multiple bugs but for now we only
169+ // support displaying one of them.
170+ that._confirm_selected_bug(bug_data[0]);
171 },
172 failure: function(id, response) {
173 var error_msg;
174@@ -181,13 +205,14 @@
175 data: qs_data
176 };
177 var uri
178- = Y.lp.client.get_absolute_uri("/api/devel/bugs/" + new_dup_id);
179+ = Y.lp.client.get_absolute_uri("/api/devel/bugs");
180 this.io_provider.io(uri, config);
181 },
182
183 // Template for rendering the bug details.
184 _bug_details_template: function() {
185 return [
186+ '<table><tbody><tr><td>',
187 '<div id="client-listing">',
188 ' <div class="buglisting-col1">',
189 ' <div class="importance {{importance_class}}">',
190@@ -210,8 +235,22 @@
191 ' {{description}}</p>',
192 ' </div>',
193 ' </div>',
194- '</div>'
195- ].join(' ');
196+ '</div></td></tr>',
197+ '{{> private_warning}}',
198+ '</tbody></table>'
199+ ].join(' ');
200+ },
201+
202+ _private_warning_template: function(message) {
203+ var template = [
204+ '{{#private_warning}}',
205+ '<tr><td><p id="privacy-warning" ',
206+ 'class="block-sprite large-warning">',
207+ '{message}',
208+ '</p></td></tr>',
209+ '{{/private_warning}}'
210+ ].join(' ');
211+ return Y.Lang.substitute(template, {message: message});
212 },
213
214 // Template for the bug confirmation form.
215@@ -236,18 +275,18 @@
216 * @private
217 */
218 _confirm_selected_bug: function(bug_data) {
219- // TODO - get real data from the server
220- bug_data.importance = 'High';
221- bug_data.importance_class = 'importanceHIGH';
222- bug_data.status = 'Triaged';
223- bug_data.status_class = 'statusTRIAGED';
224- bug_data.bug_summary = bug_data.title;
225- bug_data.bug_url = bug_data.web_link;
226-
227 var bug_id = bug_data.id;
228+ bug_data.private_warning
229+ = this.get('public_context') && bug_data.is_private;
230+ var private_warning_message
231+ = this.get('private_warning_message');
232 var html = Y.lp.mustache.to_html(
233 this._bug_confirmation_form_template(), bug_data,
234- {bug_details: this._bug_details_template()});
235+ {
236+ bug_details: this._bug_details_template(),
237+ private_warning:
238+ this._private_warning_template(private_warning_message)
239+ });
240 var confirm_node = Y.Node.create(html);
241 this._show_confirm_node(confirm_node);
242 var that = this;
243@@ -521,6 +560,16 @@
244 dupe_span: '#mark-duplicate-text'
245 },
246 ATTRS: {
247+ // Is the context in which this form being used public.
248+ public_context: {
249+ getter: function() {
250+ return !Y.one(document.body).hasClass('private');
251+ }
252+ },
253+ // Warning to display if we select a private bug from a public context.
254+ private_warning_message: {
255+ value: 'You are selecting a private bug.'
256+ },
257 // The launchpad client entry for the current bug.
258 lp_bug_entry: {
259 value: null
260
261=== modified file 'lib/lp/bugs/javascript/tests/test_duplicates.js'
262--- lib/lp/bugs/javascript/tests/test_duplicates.js 2012-07-29 23:33:20 +0000
263+++ lib/lp/bugs/javascript/tests/test_duplicates.js 2012-07-29 23:33:20 +0000
264@@ -13,8 +13,9 @@
265 links: {},
266 cache: {
267 bug: {
268+ id: 1,
269 self_link: 'api/devel/bugs/1',
270- duplicate_of_link: null
271+ duplicate_of_link: ''
272 }
273 }
274 };
275@@ -52,7 +53,8 @@
276 srcNode: '#duplicate-actions',
277 lp_bug_entry: this.lp_bug_entry,
278 use_animation: false,
279- io_provider: this.mockio
280+ io_provider: this.mockio,
281+ private_warning_message: 'Privacy warning'
282 });
283 widget.render();
284 Y.Assert.areEqual(
285@@ -109,11 +111,18 @@
286 .hasClass('yui3-lazr-formoverlay-hidden'));
287 Y.DOM.byId('field.duplicateof').value = bug_id;
288 form.one('[name="field.actions.change"]').simulate('click');
289- var expected_url = '/api/devel/bugs/1';
290 if (bug_id !== '') {
291- expected_url = 'file:///api/devel/bugs/' + bug_id;
292+ Y.Assert.areEqual(
293+ 'file:///api/devel/bugs',
294+ this.mockio.last_request.url);
295+ Y.Assert.areEqual(
296+ this.mockio.last_request.config.data,
297+ 'ws.accept=application.json&ws.op=getBugData&' +
298+ 'bug_id=' + bug_id);
299+ } else {
300+ Y.Assert.areEqual(
301+ '/api/devel/bugs/1', this.mockio.last_request.url);
302 }
303- Y.Assert.areEqual(expected_url, this.mockio.last_request.url);
304 },
305
306 // The bug entry form is visible and the confirmation form is invisible
307@@ -134,15 +143,49 @@
308
309 // Invoke a successful search operation and check the form state.
310 _assert_search_form_success: function(bug_id) {
311- var expected_updated_entry = {
312+ var is_private = bug_id === 4;
313+ var expected_updated_entry = [{
314 id: bug_id,
315 uri: 'api/devel/bugs/' + bug_id,
316+ is_private: is_private,
317 duplicate_of_link: 'api/devel/bugs/' + bug_id,
318- self_link: 'api/devel/bugs/' + bug_id};
319+ self_link: 'api/devel/bugs/' + bug_id}];
320 this.mockio.last_request.successJSON(expected_updated_entry);
321 this._assert_form_state(true);
322 },
323
324+ // Attempt to make a bug as a duplicate of itself is detected and an
325+ // error is displayed immediately.
326+ test_mark_bug_as_dupe_of_self: function() {
327+ this.widget = this._createWidget(false);
328+ this.widget.get('update_dupe_link').simulate('click');
329+ this.mockio.last_request = null;
330+ Y.DOM.byId('field.duplicateof').value = 1;
331+ var form = Y.one('#duplicate-form-container');
332+ form.one('[name="field.actions.change"]').simulate('click');
333+ Y.Assert.isNull(this.mockio.last_request);
334+ this._assert_error_display(
335+ 'A bug cannot be marked as a duplicate of itself.');
336+ this._assert_form_state(false);
337+ },
338+
339+ // Attempt to make a bug as a duplicate of it's existing dupe is
340+ // detected and an error is displayed immediately.
341+ test_mark_bug_as_dupe_of_existing_dupe: function() {
342+ this.widget = this._createWidget(false);
343+ this.widget.get('update_dupe_link').simulate('click');
344+ this.mockio.last_request = null;
345+ window.LP.cache.bug.duplicate_of_link
346+ = 'file:///api/devel/bugs/4';
347+ Y.DOM.byId('field.duplicateof').value = 4;
348+ var form = Y.one('#duplicate-form-container');
349+ form.one('[name="field.actions.change"]').simulate('click');
350+ Y.Assert.isNull(this.mockio.last_request);
351+ this._assert_error_display(
352+ 'This bug is already marked as a duplicate of bug 4.');
353+ this._assert_form_state(false);
354+ },
355+
356 // A successful search for a bug displays the confirmation form.
357 test_initial_bug_search_success: function() {
358 this.widget = this._createWidget(false);
359@@ -150,6 +193,34 @@
360 this._assert_search_form_success(3);
361 },
362
363+ // No privacy warning when marking a bug as a dupe a public one.
364+ test_public_dupe: function() {
365+ this.widget = this._createWidget(false);
366+ this._assert_search_form_submission(3);
367+ this._assert_search_form_success(3);
368+ Y.Assert.isNull(Y.one('#privacy-warning'));
369+ },
370+
371+ // Privacy warning when marking a public bug as a dupe of private one.
372+ test_public_bug_private_dupe: function() {
373+ this.widget = this._createWidget(false);
374+ this._assert_search_form_submission(4);
375+ this._assert_search_form_success(4);
376+ var privacy_message = Y.one('#privacy-warning');
377+ Y.Assert.areEqual(
378+ 'Privacy warning', privacy_message.get('text').trim());
379+ },
380+
381+ // No privacy warning when marking a private bug as a dupe of another
382+ // private bug.
383+ test_private_bug_private_dupe: function() {
384+ Y.one(document.body).addClass('private');
385+ this.widget = this._createWidget(false);
386+ this._assert_search_form_submission(4);
387+ this._assert_search_form_success(4);
388+ Y.Assert.isNull(Y.one('#privacy-warning'));
389+ },
390+
391 // After a successful search, hitting the Search Again button takes us
392 // back to the bug details entry form.
393 test_initial_bug_search_try_again: function() {
394@@ -260,7 +331,7 @@
395 function(response, old_dup_url, new_dup_id) {
396 Y.Assert.areEqual(
397 'There was an error', response.responseText);
398- Y.Assert.areEqual(null, old_dup_url);
399+ Y.Assert.areEqual('', old_dup_url);
400 Y.Assert.areEqual(3, new_dup_id);
401 failure_called = true;
402 };
403
404=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
405--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-07-17 14:13:55 +0000
406+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-07-29 23:33:20 +0000
407@@ -115,3 +115,36 @@
408 # A non-matching search returns no results.
409 response = self.search("devel", information_type="Private")
410 self.assertEqual(response['total_size'], 0)
411+
412+
413+class TestGetBugData(TestCaseWithFactory):
414+ """Tests for the /bugs getBugData operation."""
415+
416+ layer = DatabaseFunctionalLayer
417+
418+ def setUp(self):
419+ super(TestGetBugData, self).setUp()
420+ self.owner = self.factory.makePerson()
421+ with person_logged_in(self.owner):
422+ self.product = self.factory.makeProduct()
423+ self.bug = self.factory.makeBug(
424+ product=self.product,
425+ information_type=InformationType.PRIVATESECURITY)
426+ self.webservice = LaunchpadWebServiceCaller(
427+ 'launchpad-library', 'salgado-change-anything')
428+
429+ def search(self, api_version, **kwargs):
430+ return self.webservice.named_get(
431+ '/bugs', 'getBugData',
432+ api_version=api_version, **kwargs).jsonBody()
433+
434+ def test_search_returns_results(self):
435+ # A matching search returns results.
436+ response = self.search(
437+ "devel", bug_id=self.bug.id)
438+ self.assertEqual(self.bug.id, response[0]['id'])
439+
440+ def test_search_returns_no_results(self):
441+ # A non-matching search returns no results.
442+ response = self.search("devel", bug_id=0)
443+ self.assertEqual(len(response), 0)
444
445=== modified file 'lib/lp/systemhomes.py'
446--- lib/lp/systemhomes.py 2012-05-11 06:36:01 +0000
447+++ lib/lp/systemhomes.py 2012-07-29 23:33:20 +0000
448@@ -58,6 +58,7 @@
449 IHWVendorIDSet,
450 ParameterError,
451 )
452+from lp.registry.enums import PRIVATE_INFORMATION_TYPES
453 from lp.registry.interfaces.distribution import IDistribution
454 from lp.registry.interfaces.distributionsourcepackage import (
455 IDistributionSourcePackage,
456@@ -77,6 +78,7 @@
457 ICanonicalUrlData,
458 ILaunchBag,
459 )
460+from lp.services.webapp.publisher import canonical_url
461 from lp.services.webservice.interfaces import IWebServiceApplication
462 from lp.services.worlddata.interfaces.language import ILanguageSet
463 from lp.testopenid.interfaces.server import ITestOpenIDApplication
464@@ -126,6 +128,30 @@
465 """See `IMaloneApplication`."""
466 return getUtility(IBugTaskSet).search(search_params)
467
468+ def getBugData(self, user, bug_id):
469+ """See `IMaloneApplication`."""
470+ search_params = BugTaskSearchParams(user, bug=bug_id)
471+ bugtasks = getUtility(IBugTaskSet).search(search_params)
472+ if not bugtasks:
473+ return []
474+ bugs = [task.bug for task in bugtasks]
475+ data = []
476+ for bug in bugs:
477+ bugtask = bug.default_bugtask
478+ data.append({
479+ 'id': bug_id,
480+ 'information_type': bug.information_type.title,
481+ 'is_private':
482+ bug.information_type in PRIVATE_INFORMATION_TYPES,
483+ 'importance': bugtask.importance.title,
484+ 'importance_class': 'importance' + bugtask.importance.name,
485+ 'status': bugtask.status.title,
486+ 'status_class': 'status' + bugtask.status.name,
487+ 'bug_summary': bug.title,
488+ 'description': bug.description,
489+ 'bug_url': canonical_url(bugtask)})
490+ return data
491+
492 def createBug(self, owner, title, description, target,
493 security_related=False, private=False, tags=None):
494 """See `IMaloneApplication`."""