Merge lp:~wallyworld/launchpad/filebug-choice-widgets-1003748 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15325
Proposed branch: lp:~wallyworld/launchpad/filebug-choice-widgets-1003748
Merge into: lp:launchpad
Diff against target: 355 lines (+207/-8)
8 files modified
lib/lp/app/browser/lazrjs.py (+4/-2)
lib/lp/app/javascript/choice.js (+39/-0)
lib/lp/app/widgets/tests/test_itemswidgets.py (+9/-0)
lib/lp/bugs/browser/bugtarget.py (+18/-0)
lib/lp/bugs/browser/tests/test_bugtarget_filebug.py (+54/-1)
lib/lp/bugs/javascript/filebug.js (+10/-2)
lib/lp/bugs/javascript/tests/test_filebug.html (+16/-0)
lib/lp/bugs/javascript/tests/test_filebug.js (+57/-3)
To merge this branch: bzr merge lp:~wallyworld/launchpad/filebug-choice-widgets-1003748
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+107616@code.launchpad.net

Commit message

Add progressive enhancement for status and importance popups on file bug page.

Description of the change

== Implementation ==

The branch provides functionality to enhance the +filebug form when javascript is enabled. The standard form uses simple drop down combo controls for bug status and importance. These controls are replaced with choice source popups the same as used on the bugtask form itself. So the user gets nice colours and descriptions etc.

The way it works is that the drop down combo is hidden and the choice source popup is rendered in its place. The the popup choice is made, it is saved to the hidden combo widget so that when the form is saved, the correct value is passed to the back end.

I have made an addition change for the popups. Not all of the available bug status choices are valid when filing a bug. Specifically, these choices have been excluded from selection:
- expired
- invalid
- opinion
- won't fix
- incomplete

So the user gets to choose from:
- new
- confirmed
- triaged
- in progress
- fix committed
- fix released

The latter 2 choices allow a bug to be file retrospectively for a fix that has already been completed.

== Tests ==

Add new yui tests for the additional file bug functionality.
Update the TestFileBugRequestCache test case to check the additional json request cache attributes for status and importance.
Update TestVocabularyToChoiceEditItems to check that vocabulary_to_choice_edit_items() handles the excluded_items parameter.

= Lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/browser/lazrjs.py
  lib/lp/app/javascript/choice.js
  lib/lp/app/widgets/tests/test_itemswidgets.py
  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.html
  lib/lp/bugs/javascript/tests/test_filebug.js

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

Wow. Thank you.

I do not think "Confirmed" should be in the list given your reasoning. Confirmed means someone other than the bug reporter is affected by the bug, so the UI should not let me cheat. If I am the triager, I can set "Triaged" to mean someone affiliated with the project acknowledges the bug.

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/lazrjs.py'
2--- lib/lp/app/browser/lazrjs.py 2012-03-05 09:17:17 +0000
3+++ lib/lp/app/browser/lazrjs.py 2012-05-28 22:54:19 +0000
4@@ -513,8 +513,8 @@
5
6 def vocabulary_to_choice_edit_items(
7 vocab, include_description=False, css_class_prefix=None,
8- disabled_items=None, as_json=False, name_fn=None, value_fn=None,
9- description_fn=None):
10+ disabled_items=None, excluded_items=None,
11+ as_json=False, name_fn=None, value_fn=None, description_fn=None):
12 """Convert an enumerable to JSON for a ChoiceEdit.
13
14 :vocab: The enumeration to iterate over.
15@@ -532,6 +532,8 @@
16 # SimpleTerm objects have the object itself at item.value.
17 if safe_hasattr(item, 'value'):
18 item = item.value
19+ if excluded_items and item in excluded_items:
20+ continue
21 if name_fn is not None:
22 name = name_fn(item)
23 else:
24
25=== modified file 'lib/lp/app/javascript/choice.js'
26--- lib/lp/app/javascript/choice.js 2012-05-15 16:51:06 +0000
27+++ lib/lp/app/javascript/choice.js 2012-05-28 22:54:19 +0000
28@@ -56,4 +56,43 @@
29 widget.render();
30 };
31
32+namespace.addPopupChoice = function(field_name, choices) {
33+ var legacy_field_node = Y.one('[id=field.' + field_name + ']');
34+ var initial_field_value = legacy_field_node.get('value');
35+
36+ var choice_node = Y.Node.create([
37+ '<span id="' + field_name + '-content"><span class="value"></span>',
38+ '<a href="#"><img class="sprite edit editicon"/>&nbsp;</a></span>'
39+ ].join(' '));
40+
41+ legacy_field_node.insertBefore(choice_node, legacy_field_node);
42+ legacy_field_node.addClass('unseen');
43+ var field_content = Y.one('#' + field_name + '-content');
44+
45+ var choice_edit = new Y.ChoiceSource({
46+ contentBox: field_content,
47+ value: initial_field_value,
48+ title: 'Set ' + field_name + " as",
49+ items: choices,
50+ elementToFlash: field_content
51+ });
52+ choice_edit.render();
53+
54+ var update_selected_value_css = function(selected_value) {
55+ Y.Array.each(choices, function(item) {
56+ if (item.value === selected_value) {
57+ field_content.addClass(item.css_class);
58+ } else {
59+ field_content.removeClass(item.css_class);
60+ }
61+ });
62+ };
63+ update_selected_value_css(initial_field_value);
64+ choice_edit.on('save', function(e) {
65+ var selected_value = choice_edit.get('value');
66+ update_selected_value_css(selected_value);
67+ legacy_field_node.set('value', selected_value);
68+ });
69+};
70+
71 }, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins"]});
72
73=== modified file 'lib/lp/app/widgets/tests/test_itemswidgets.py'
74--- lib/lp/app/widgets/tests/test_itemswidgets.py 2012-05-16 23:56:08 +0000
75+++ lib/lp/app/widgets/tests/test_itemswidgets.py 2012-05-28 22:54:19 +0000
76@@ -298,3 +298,12 @@
77 self.ChoiceEnum, include_description=True)
78 expected = [self._makeItemDict(e.value) for e in self.ChoiceEnum]
79 self.assertEqual(expected, items)
80+
81+ def test_vocabulary_to_choice_edit_items_excluded_items(self):
82+ # Excluded items are not included.
83+ items = vocabulary_to_choice_edit_items(
84+ self.ChoiceEnum, include_description=True,
85+ excluded_items=[self.ChoiceEnum.ITEM_B])
86+ overrides = {'description': ''}
87+ expected = [self._makeItemDict(self.ChoiceEnum.ITEM_A, overrides)]
88+ self.assertEqual(expected, items)
89
90=== modified file 'lib/lp/bugs/browser/bugtarget.py'
91--- lib/lp/bugs/browser/bugtarget.py 2012-05-25 14:38:42 +0000
92+++ lib/lp/bugs/browser/bugtarget.py 2012-05-28 22:54:19 +0000
93@@ -58,6 +58,7 @@
94 LaunchpadFormView,
95 safe_action,
96 )
97+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
98 from lp.app.browser.stringformatter import FormattersAPI
99 from lp.app.enums import ServiceUsage
100 from lp.app.errors import (
101@@ -98,6 +99,8 @@
102 IOfficialBugTagTargetRestricted,
103 )
104 from lp.bugs.interfaces.bugtask import (
105+ BugTaskImportance,
106+ BugTaskStatus,
107 IBugTaskSet,
108 UNRESOLVED_BUGTASK_STATUSES,
109 )
110@@ -389,6 +392,21 @@
111 cache.objects['enable_bugfiling_duplicate_search'] = (
112 IProjectGroup.providedBy(self.context)
113 or self.context.enable_bugfiling_duplicate_search)
114+ bugtask_status_data = vocabulary_to_choice_edit_items(
115+ BugTaskStatus, include_description=True, css_class_prefix='status',
116+ excluded_items=[
117+ BugTaskStatus.UNKNOWN,
118+ BugTaskStatus.EXPIRED,
119+ BugTaskStatus.INVALID,
120+ BugTaskStatus.OPINION,
121+ BugTaskStatus.WONTFIX,
122+ BugTaskStatus.INCOMPLETE])
123+ cache.objects['bugtask_status_data'] = bugtask_status_data
124+ bugtask_importance_data = vocabulary_to_choice_edit_items(
125+ BugTaskImportance, include_description=True,
126+ css_class_prefix='importance',
127+ excluded_items=[BugTaskImportance.UNKNOWN])
128+ cache.objects['bugtask_importance_data'] = bugtask_importance_data
129 if (self.extra_data_token is not None and
130 not self.extra_data_to_process):
131 # self.extra_data has been initialized in publishTraverse().
132
133=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
134--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-05-27 23:04:28 +0000
135+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-05-28 22:54:19 +0000
136@@ -21,6 +21,10 @@
137 IBugAddForm,
138 IBugSet,
139 )
140+from lp.bugs.interfaces.bugtask import (
141+ BugTaskImportance,
142+ BugTaskStatus,
143+ )
144 from lp.bugs.publisher import BugsLayer
145 from lp.registry.enums import (
146 InformationType,
147@@ -496,10 +500,59 @@
148
149 layer = DatabaseFunctionalLayer
150
151+ def setUp(self):
152+ super(TestFileBugRequestCache, self).setUp()
153+ self.useFixture(FeatureFixture({
154+ 'disclosure.enhanced_choice_popup.enabled': 'true'
155+ }))
156+
157 def _assert_cache_values(self, view, duplicate_search):
158 cache = IJSONRequestCache(view.request).objects
159 self.assertEqual(
160- cache['enable_bugfiling_duplicate_search'], duplicate_search)
161+ duplicate_search, cache['enable_bugfiling_duplicate_search'])
162+ excluded_statuses = [
163+ BugTaskStatus.UNKNOWN,
164+ BugTaskStatus.EXPIRED,
165+ BugTaskStatus.INVALID,
166+ BugTaskStatus.OPINION,
167+ BugTaskStatus.WONTFIX,
168+ BugTaskStatus.INCOMPLETE]
169+ bugtask_status_data = []
170+ for item in BugTaskStatus:
171+ item = item.value
172+ if item in excluded_statuses:
173+ continue
174+ name = item.title
175+ value = item.title
176+ description = item.description
177+ new_item = {'name': name, 'value': value,
178+ 'description': description,
179+ 'description_css_class': 'choice-description',
180+ 'style': '',
181+ 'help': '', 'disabled': False,
182+ 'css_class': 'status' + item.name}
183+ bugtask_status_data.append(new_item)
184+ self.assertEqual(
185+ bugtask_status_data, cache['bugtask_status_data'])
186+ excluded_importances = [
187+ BugTaskImportance.UNKNOWN]
188+ bugtask_importance_data = []
189+ for item in BugTaskImportance:
190+ item = item.value
191+ if item in excluded_importances:
192+ continue
193+ name = item.title
194+ value = item.title
195+ description = item.description
196+ new_item = {'name': name, 'value': value,
197+ 'description': description,
198+ 'description_css_class': 'choice-description',
199+ 'style': '',
200+ 'help': '', 'disabled': False,
201+ 'css_class': 'importance' + item.name}
202+ bugtask_importance_data.append(new_item)
203+ self.assertEqual(
204+ bugtask_importance_data, cache['bugtask_importance_data'])
205
206 def test_product(self):
207 project = self.factory.makeProduct(official_malone=True)
208
209=== modified file 'lib/lp/bugs/javascript/filebug.js'
210--- lib/lp/bugs/javascript/filebug.js 2012-05-25 14:38:42 +0000
211+++ lib/lp/bugs/javascript/filebug.js 2012-05-28 22:54:19 +0000
212@@ -28,6 +28,7 @@
213 } else {
214 setup_security_related();
215 }
216+ setupChoiceWidgets();
217 var filebug_privacy_text = "This report will be private. " +
218 "You can disclose it later.";
219 update_privacy_banner(
220@@ -53,6 +54,12 @@
221 }, "input[name='field.information_type']");
222 };
223
224+var setupChoiceWidgets = function() {
225+ Y.lp.app.choice.addPopupChoice('status', LP.cache.bugtask_status_data);
226+ Y.lp.app.choice.addPopupChoice(
227+ 'importance', LP.cache.bugtask_importance_data);
228+};
229+
230 var setup_security_related = function() {
231 var sec = Y.one('[id="field.security_related"]');
232 if (!Y.Lang.isValue(sec)) {
233@@ -71,5 +78,6 @@
234 namespace.setup_filebug = setup_filebug;
235
236 }, "0.1", {"requires": [
237- "base", "node", "event", "node-event-delegate",
238- "lp.app.banner.privacy", "lp.bugs.filebug_dupefinder"]});
239+ "base", "node", "event", "node-event-delegate", "lazr.choiceedit",
240+ "lp.app.banner.privacy", "lp.app.choice",
241+ "lp.bugs.filebug_dupefinder"]});
242
243=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.html'
244--- lib/lp/bugs/javascript/tests/test_filebug.html 2012-05-25 14:38:42 +0000
245+++ lib/lp/bugs/javascript/tests/test_filebug.html 2012-05-28 22:54:19 +0000
246@@ -28,6 +28,10 @@
247 <script type="text/javascript"
248 src="../../../../../build/js/lp/app/mustache.js"></script>
249 <script type="text/javascript"
250+ src="../../../../../build/js/lp/app/choice.js"></script>
251+ <script type="text/javascript"
252+ src="../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
253+ <script type="text/javascript"
254 src="../../../../../build/js/lp/app/expander.js"></script>
255 <script type="text/javascript"
256 src="../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
257@@ -98,6 +102,18 @@
258 </tr>
259 </tbody>
260 </table>
261+ <div class="value">
262+ <select size="1" name="field.status" id="field.status">
263+ <option value="New" selected="selected">New</option>
264+ <option value="Incomplete">Incomplete</option>
265+ </select>
266+ </div>
267+ <div class="value">
268+ <select size="1" name="field.status" id="field.importance">
269+ <option value="Undecided" selected="selected">Undecided</option>
270+ <option value="High">High</option>
271+ </select>
272+ </div>
273 </script>
274 </body>
275 </html>
276
277=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.js'
278--- lib/lp/bugs/javascript/tests/test_filebug.js 2012-05-25 14:38:42 +0000
279+++ lib/lp/bugs/javascript/tests/test_filebug.js 2012-05-28 22:54:19 +0000
280@@ -13,7 +13,15 @@
281 window.LP = {
282 links: {},
283 cache: {
284- private_types: ['EMBARGOEDSECURITY', 'USERDATA']
285+ private_types: ['EMBARGOEDSECURITY', 'USERDATA'],
286+ bugtask_status_data: [
287+ {name: 'New', value: 'New'},
288+ {name: 'Incomplete', value: 'Incomplete'}
289+ ],
290+ bugtask_importance_data: [
291+ {name: 'Undecided', value: 'Undecided'},
292+ {name: 'High', value: 'High'}
293+ ]
294 }
295 };
296 this.fixture = Y.one('#fixture');
297@@ -104,10 +112,56 @@
298 Y.lp.bugs.filebug_dupefinder.setup_dupes = orig_setup_dupes;
299 Y.lp.bugs.filebug_dupefinder.setup_dupe_finder
300 = orig_setup_dupe_finder;
301+ },
302+
303+ // The bugtask status choice popup is rendered.
304+ test_status_setup: function () {
305+ Y.lp.bugs.filebug.setup_filebug(true);
306+ var status_node = Y.one('#status-content .value');
307+ Y.Assert.areEqual('New', status_node.get('text'));
308+ var status_edit_node = Y.one('#status-content a img.edit');
309+ Y.Assert.isNotNull(status_edit_node);
310+ var legacy_dropdown = Y.one('[id=field.status]');
311+ Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
312+ },
313+
314+ // The bugtask importance choice popup is rendered.
315+ test_importance_setup: function () {
316+ Y.lp.bugs.filebug.setup_filebug(true);
317+ var importance_node = Y.one('#importance-content .value');
318+ Y.Assert.areEqual('Undecided', importance_node.get('text'));
319+ var importance_edit_node = Y.one('#status-content a img.edit');
320+ Y.Assert.isNotNull(importance_edit_node);
321+ var legacy_dropdown = Y.one('[id=field.importance]');
322+ Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
323+ },
324+
325+ // The bugtask status choice popup updates the form.
326+ test_status_selection: function() {
327+ Y.lp.bugs.filebug.setup_filebug(true);
328+ var status_popup = Y.one('#status-content a');
329+ status_popup.simulate('click');
330+ var status_choice = Y.one(
331+ '.yui3-ichoicelist-content a[href=#Incomplete]');
332+ status_choice.simulate('click');
333+ var legacy_dropdown = Y.one('[id=field.status]');
334+ Y.Assert.areEqual('Incomplete', legacy_dropdown.get('value'));
335+ },
336+
337+ // The bugtask importance choice popup updates the form.
338+ test_importance_selection: function() {
339+ Y.lp.bugs.filebug.setup_filebug(true);
340+ var status_popup = Y.one('#importance-content a');
341+ status_popup.simulate('click');
342+ var status_choice = Y.one(
343+ '.yui3-ichoicelist-content a[href=#High]');
344+ status_choice.simulate('click');
345+ var legacy_dropdown = Y.one('[id=field.importance]');
346+ Y.Assert.areEqual('High', legacy_dropdown.get('value'));
347 }
348 }));
349
350 }, '0.1', {'requires': ['test', 'console', 'event', 'node-event-simulate',
351- 'lp.app.banner.privacy', 'lp.bugs.filebug_dupefinder',
352- 'lp.bugs.filebug'
353+ 'lp.app.banner.privacy', 'lp.app.choice',
354+ 'lp.bugs.filebug_dupefinder', 'lp.bugs.filebug'
355 ]});