Merge lp:~wallyworld/launchpad/filebug-choice-widget-infotype-1006799 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 15353
Proposed branch: lp:~wallyworld/launchpad/filebug-choice-widget-infotype-1006799
Merge into: lp:launchpad
Diff against target: 385 lines (+203/-22)
9 files modified
lib/lp/app/browser/informationtype.py (+2/-1)
lib/lp/app/javascript/choice.js (+84/-8)
lib/lp/bugs/browser/bugtarget.py (+5/-0)
lib/lp/bugs/browser/tests/test_bugtarget_filebug.py (+20/-1)
lib/lp/bugs/browser/tests/test_bugview.py (+1/-1)
lib/lp/bugs/javascript/filebug.js (+3/-1)
lib/lp/bugs/javascript/tests/test_filebug.js (+82/-4)
lib/lp/code/browser/tests/test_branch.py (+4/-4)
lib/lp/registry/vocabularies.py (+2/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/filebug-choice-widget-infotype-1006799
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+108286@code.launchpad.net

Commit message

Enhance information type radio buttons on filebug page - replace with choice popup widget

Description of the change

== Implementation ==

Start with the js addPopupChoice() method added previously and create a version to handle radio button groups. Refactor out the common code. So now filebug will use choice popups for info type, importance, status. For info type, the description of the selected value is displayed as well.

The branch also contains a coupl eof drive bys. The main one is for the information_type_description() method in app/browser/informationtype.py. A previous branch fixed the description text but it is being generated in 2 places and I missed this one.

== Demo ==

http://people.canonical.com/~ianb/filebug-infotype.png

== Tests ==

Add new tests to file_bug yui tests
Add new tests to test_bugtarget_filebug

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/browser/informationtype.py
  lib/lp/app/javascript/choice.js
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
  lib/lp/bugs/javascript/filebug.js
  lib/lp/bugs/javascript/tests/test_filebug.js
  lib/lp/registry/vocabularies.py

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

looks good

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/app/browser/informationtype.py'
2--- lib/lp/app/browser/informationtype.py 2012-05-30 20:31:37 +0000
3+++ lib/lp/app/browser/informationtype.py 2012-06-02 13:58:21 +0000
4@@ -59,5 +59,6 @@
5 if (self.context.information_type == InformationType.USERDATA and
6 self.show_userdata_as_private):
7 description = (
8- description.replace('user data', 'private information'))
9+ 'Visible only to users with whom the project has '
10+ 'shared private information.')
11 return description
12
13=== modified file 'lib/lp/app/javascript/choice.js'
14--- lib/lp/app/javascript/choice.js 2012-05-30 21:54:13 +0000
15+++ lib/lp/app/javascript/choice.js 2012-06-02 13:58:21 +0000
16@@ -56,17 +56,36 @@
17 widget.render();
18 };
19
20-namespace.addPopupChoice = function(field_name, choices) {
21- var legacy_field_node = Y.one('[id=field.' + field_name + ']');
22- var initial_field_value = legacy_field_node.get('value');
23-
24+/**
25+ * Replace a legacy input widget with a popup choice widget.
26+ * @param legacy_node the YUI node containing the legacy widget.
27+ * @param field_name the Launchpad form field name.
28+ * @param choices the choices for the popup choice widget.
29+ * @param show_description whether to show the selected value's description.
30+ * @param get_value_fn getter for the legacy widget's value.
31+ * @param set_value_fn setter for the legacy widget's value.
32+ */
33+var wirePopupChoice = function(legacy_node, field_name, choices,
34+ show_description, get_value_fn, set_value_fn) {
35+ var choice_descriptions = {};
36+ Y.Array.forEach(choices, function(item) {
37+ choice_descriptions[item.value] = item.description;
38+ });
39+ var initial_field_value = get_value_fn(legacy_node);
40 var choice_node = Y.Node.create([
41 '<span id="' + field_name + '-content"><span class="value"></span>',
42 '&nbsp;<a class="sprite edit editicon" href="#"></a></span>'
43 ].join(' '));
44+ if (show_description) {
45+ choice_node.append(Y.Node.create('<div class="formHelp"></div>'));
46+ }
47
48- legacy_field_node.insertBefore(choice_node, legacy_field_node);
49- legacy_field_node.addClass('unseen');
50+ legacy_node.insertBefore(choice_node, legacy_node);
51+ if (show_description) {
52+ choice_node.one('.formHelp')
53+ .set('text', choice_descriptions[initial_field_value]);
54+ }
55+ legacy_node.addClass('unseen');
56 var field_content = Y.one('#' + field_name + '-content');
57
58 var choice_edit = new Y.ChoiceSource({
59@@ -91,8 +110,65 @@
60 choice_edit.on('save', function(e) {
61 var selected_value = choice_edit.get('value');
62 update_selected_value_css(selected_value);
63- legacy_field_node.set('value', selected_value);
64+ set_value_fn(legacy_node, selected_value);
65+ if (show_description) {
66+ choice_node.one('.formHelp')
67+ .set('text', choice_descriptions[selected_value]);
68+ }
69 });
70 };
71
72-}, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins"]});
73+/**
74+ * Replace a drop down combo box with a popup choice selection widget.
75+ * @param field_name
76+ * @param choices
77+ * @param show_description
78+ */
79+namespace.addPopupChoice = function(field_name, choices, show_description) {
80+ var legacy_node = Y.one('[id=field.' + field_name + ']');
81+ var get_fn = function(node) {
82+ return node.get('value');
83+ };
84+ var set_fn = function(node, value) {
85+ node.set('value', value);
86+ };
87+ wirePopupChoice(
88+ legacy_node, field_name, choices, show_description, get_fn, set_fn);
89+};
90+
91+/**
92+ * Replace a radio button group with a popup choice selection widget.
93+ * @param field_name
94+ * @param choices
95+ * @param show_description
96+ */
97+namespace.addPopupChoiceForRadioButtons = function(field_name, choices,
98+ show_description) {
99+
100+ var legacy_node = Y.one('[name=field.' + field_name + ']')
101+ .ancestor('table.radio-button-widget');
102+ var get_fn = function(node) {
103+ var field_value = choices[0].value;
104+ node.all('input[name=field.' + field_name + ']').each(function(node) {
105+ if (node.get('checked')) {
106+ field_value = node.get('value');
107+ }
108+ });
109+ return field_value;
110+ };
111+ var set_fn = function(node, value) {
112+ node.all('input[name=field.' + field_name + ']')
113+ .each(function(node) {
114+ var node_selected = node.get('value') === value;
115+ node.set('checked', node_selected);
116+ if (node_selected) {
117+ node.simulate('change');
118+ }
119+ });
120+ };
121+ wirePopupChoice(
122+ legacy_node, field_name, choices, show_description, get_fn, set_fn);
123+};
124+
125+}, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins",
126+ "node-event-simulate"]});
127
128=== modified file 'lib/lp/bugs/browser/bugtarget.py'
129--- lib/lp/bugs/browser/bugtarget.py 2012-05-30 00:51:34 +0000
130+++ lib/lp/bugs/browser/bugtarget.py 2012-06-02 13:58:21 +0000
131@@ -392,6 +392,11 @@
132 cache.objects['enable_bugfiling_duplicate_search'] = (
133 IProjectGroup.providedBy(self.context)
134 or self.context.enable_bugfiling_duplicate_search)
135+ cache.objects['information_type_data'] = [
136+ {'value': term.name, 'description': term.description,
137+ 'name': term.title,
138+ 'description_css_class': 'choice-description'}
139+ for term in InformationTypeVocabulary(self.context)]
140 bugtask_status_data = vocabulary_to_choice_edit_items(
141 BugTaskStatus, include_description=True, css_class_prefix='status',
142 excluded_items=[
143
144=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
145--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-05-30 00:51:34 +0000
146+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-06-02 13:58:21 +0000
147@@ -540,7 +540,7 @@
148 'disclosure.enhanced_choice_popup.enabled': 'true'
149 }))
150
151- def _assert_cache_values(self, view, duplicate_search):
152+ def _assert_cache_values(self, view, duplicate_search, private_only=False):
153 cache = IJSONRequestCache(view.request).objects
154 self.assertEqual(
155 duplicate_search, cache['enable_bugfiling_duplicate_search'])
156@@ -587,6 +587,17 @@
157 bugtask_importance_data.append(new_item)
158 self.assertEqual(
159 bugtask_importance_data, cache['bugtask_importance_data'])
160+ bugtask_info_type_data = []
161+ information_types = list(PRIVATE_INFORMATION_TYPES)
162+ if not private_only:
163+ information_types.extend(PUBLIC_INFORMATION_TYPES)
164+ for item in information_types:
165+ new_item = {'name': item.title, 'value': item.name,
166+ 'description': item.description,
167+ 'description_css_class': 'choice-description'}
168+ bugtask_info_type_data.append(new_item)
169+ self.assertContentEqual(
170+ bugtask_info_type_data, cache['information_type_data'])
171
172 def test_product(self):
173 project = self.factory.makeProduct(official_malone=True)
174@@ -595,6 +606,14 @@
175 view = create_initialized_view(project, '+filebug', principal=user)
176 self._assert_cache_values(view, True)
177
178+ def test_product_private_bugs(self):
179+ project = self.factory.makeProduct(
180+ official_malone=True, private_bugs=True)
181+ user = self.factory.makePerson()
182+ login_person(user)
183+ view = create_initialized_view(project, '+filebug', principal=user)
184+ self._assert_cache_values(view, True, True)
185+
186 def test_product_no_duplicate_search(self):
187 product = self.factory.makeProduct(official_malone=True)
188 removeSecurityProxy(product).enable_bugfiling_duplicate_search = False
189
190=== modified file 'lib/lp/bugs/browser/tests/test_bugview.py'
191--- lib/lp/bugs/browser/tests/test_bugview.py 2012-05-17 09:45:29 +0000
192+++ lib/lp/bugs/browser/tests/test_bugview.py 2012-06-02 13:58:21 +0000
193@@ -79,7 +79,7 @@
194 self.assertEqual('Private', view.information_type)
195 self.assertTextMatchesExpressionIgnoreWhitespace(
196 'Visible only to users with whom the project has shared '
197- 'information containing private information',
198+ 'private information.',
199 view.information_type_description)
200
201 def test_proprietary_hidden(self):
202
203=== modified file 'lib/lp/bugs/javascript/filebug.js'
204--- lib/lp/bugs/javascript/filebug.js 2012-05-28 12:34:29 +0000
205+++ lib/lp/bugs/javascript/filebug.js 2012-06-02 13:58:21 +0000
206@@ -47,7 +47,7 @@
207
208 var setup_information_type = function() {
209 var itypes_table = Y.one('.radio-button-widget');
210- itypes_table.delegate('click', function() {
211+ itypes_table.delegate('change', function() {
212 var private_type = (Y.Array.indexOf(
213 LP.cache.private_types, this.get('value')) >= 0);
214 update_privacy_banner(private_type);
215@@ -58,6 +58,8 @@
216 Y.lp.app.choice.addPopupChoice('status', LP.cache.bugtask_status_data);
217 Y.lp.app.choice.addPopupChoice(
218 'importance', LP.cache.bugtask_importance_data);
219+ Y.lp.app.choice.addPopupChoiceForRadioButtons(
220+ 'information_type', LP.cache.information_type_data, true);
221 };
222
223 var setup_security_related = function() {
224
225=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.js'
226--- lib/lp/bugs/javascript/tests/test_filebug.js 2012-05-30 21:36:01 +0000
227+++ lib/lp/bugs/javascript/tests/test_filebug.js 2012-06-02 13:58:21 +0000
228@@ -14,6 +14,15 @@
229 links: {},
230 cache: {
231 private_types: ['EMBARGOEDSECURITY', 'USERDATA'],
232+ information_type_data: [
233+ {name: 'Public', value: 'PUBLIC',
234+ description: 'Public bug'},
235+ {name: 'Embargoed Security',
236+ value: 'EMBARGOEDSECURITY',
237+ description: 'Embargoed security bug'},
238+ {name: 'User Data', value: 'USERDATA',
239+ description: 'User Data bug'}
240+ ],
241 bugtask_status_data: [
242 {name: 'New', value: 'New'},
243 {name: 'Incomplete', value: 'Incomplete'}
244@@ -64,8 +73,9 @@
245 'You can disclose it later.', banner_text);
246 },
247
248- // Selecting a private info type turns on the privacy banner.
249- test_select_private_info_type: function () {
250+ // Selecting a private info type using the legacy radio buttons
251+ // turns on the privacy banner.
252+ test_legacy_select_private_info_type: function () {
253 window.LP.cache.show_information_type_in_ui = true;
254 Y.lp.bugs.filebug.setup_filebug(true);
255 var banner_hidden = Y.one('.yui3-privacybanner-hidden');
256@@ -79,11 +89,13 @@
257 'You can disclose it later.', banner_text);
258 },
259
260- // Selecting a public info type turns off the privacy banner.
261- test_select_public_info_type: function () {
262+ // Selecting a public info type using the legacy radio buttons
263+ // turns off the privacy banner.
264+ test_legacy_select_public_info_type: function () {
265 window.LP.cache.show_information_type_in_ui = true;
266 window.LP.cache.bug_private_by_default = true;
267 Y.lp.bugs.filebug.setup_filebug(true);
268+ Y.one('[id=field.information_type.2]').simulate('click');
269 var banner_hidden = Y.one('.yui3-privacybanner-hidden');
270 Y.Assert.isNull(banner_hidden);
271 Y.one('[id=field.information_type.0]').simulate('click');
272@@ -159,6 +171,72 @@
273 status_choice.simulate('click');
274 var legacy_dropdown = Y.one('[id=field.importance]');
275 Y.Assert.areEqual('High', legacy_dropdown.get('value'));
276+ },
277+
278+ // The bugtask information_type choice popup is rendered.
279+ test_information_type_setup: function () {
280+ Y.lp.bugs.filebug.setup_filebug(true);
281+ var information_type_node =
282+ Y.one('#information_type-content .value');
283+ Y.Assert.areEqual('Public', information_type_node.get('text'));
284+ var information_type_node_edit_node =
285+ Y.one('#information_type-content a.sprite.edit');
286+ Y.Assert.isNotNull(information_type_node_edit_node);
287+ var legacy_field = Y.one('table.radio-button-widget');
288+ Y.Assert.isTrue(legacy_field.hasClass('unseen'));
289+ },
290+
291+ // The bugtask information_type choice popup updates the form.
292+ test_information_type_selection: function() {
293+ Y.lp.bugs.filebug.setup_filebug(true);
294+ var information_type_popup = Y.one('#information_type-content a');
295+ information_type_popup.simulate('click');
296+ var information_type_choice = Y.one(
297+ '.yui3-ichoicelist-content a[href=#USERDATA]');
298+ information_type_choice.simulate('click');
299+ var legacy_radio_button = Y.one('[id=field.information_type.3]');
300+ Y.Assert.isTrue(legacy_radio_button.get('checked'));
301+ },
302+
303+ // Selecting a private info type using the popup choice widget
304+ // turns on the privacy banner.
305+ test_select_private_info_type: function () {
306+ window.LP.cache.show_information_type_in_ui = true;
307+ Y.lp.bugs.filebug.setup_filebug(true);
308+ var banner_hidden = Y.one('.yui3-privacybanner-hidden');
309+ Y.Assert.isNotNull(banner_hidden);
310+ var information_type_popup = Y.one('#information_type-content a');
311+ information_type_popup.simulate('click');
312+ var information_type_choice = Y.one(
313+ '.yui3-ichoicelist-content a[href=#USERDATA]');
314+ information_type_choice.simulate('click');
315+ banner_hidden = Y.one('.yui3-privacybanner-hidden');
316+ Y.Assert.isNull(banner_hidden);
317+ var banner_text = Y.one('.banner-text').get('text');
318+ Y.Assert.areEqual(
319+ 'This report will be private. ' +
320+ 'You can disclose it later.', banner_text);
321+ },
322+
323+ // Selecting a public info type using the popup choice widget
324+ // turns off the privacy banner.
325+ test_select_public_info_type: function () {
326+ window.LP.cache.show_information_type_in_ui = true;
327+ window.LP.cache.bug_private_by_default = true;
328+ Y.lp.bugs.filebug.setup_filebug(true);
329+ var information_type_popup = Y.one('#information_type-content a');
330+ information_type_popup.simulate('click');
331+ var information_type_choice = Y.one(
332+ '.yui3-ichoicelist-content a[href=#USERDATA]');
333+ information_type_choice.simulate('click');
334+ var banner_hidden = Y.one('.yui3-privacybanner-hidden');
335+ Y.Assert.isNull(banner_hidden);
336+ information_type_popup.simulate('click');
337+ information_type_choice = Y.one(
338+ '.yui3-ichoicelist-content a[href=#PUBLIC]');
339+ information_type_choice.simulate('click');
340+ banner_hidden = Y.one('.yui3-privacybanner-hidden');
341+ Y.Assert.isNotNull(banner_hidden);
342 }
343 }));
344
345
346=== modified file 'lib/lp/code/browser/tests/test_branch.py'
347--- lib/lp/code/browser/tests/test_branch.py 2012-06-01 10:17:13 +0000
348+++ lib/lp/code/browser/tests/test_branch.py 2012-06-02 13:58:21 +0000
349@@ -989,9 +989,9 @@
350 information_type = soup.find('strong')
351 description = soup.find('div', id='information-type-description')
352 self.assertEqual('User Data', information_type.renderContents())
353- self.assertEqual(
354+ self.assertTextMatchesExpressionIgnoreWhitespace(
355 'Visible only to users with whom the project has shared '
356- 'information\ncontaining user data.\n',
357+ 'information containing user data.',
358 description.renderContents())
359
360 def test_information_type_in_ui_with_display_as_private(self):
361@@ -1011,7 +1011,7 @@
362 information_type = soup.find('strong')
363 description = soup.find('div', id='information-type-description')
364 self.assertEqual('Private', information_type.renderContents())
365- self.assertEqual(
366+ self.assertTextMatchesExpressionIgnoreWhitespace(
367 'Visible only to users with whom the project has shared '
368- 'information\ncontaining private information.\n',
369+ 'private information.',
370 description.renderContents())
371
372=== modified file 'lib/lp/registry/vocabularies.py'
373--- lib/lp/registry/vocabularies.py 2012-06-01 02:18:27 +0000
374+++ lib/lp/registry/vocabularies.py 2012-06-02 13:58:21 +0000
375@@ -2250,8 +2250,8 @@
376 if (context is None or
377 not IProduct.providedBy(context) or
378 not context.private_bugs):
379- types.insert(0, InformationType.UNEMBARGOEDSECURITY)
380- types.insert(0, InformationType.PUBLIC)
381+ types = [InformationType.PUBLIC,
382+ InformationType.UNEMBARGOEDSECURITY] + types
383
384 terms = []
385 for type in types: