Merge lp:~sinzui/launchpad/bug-tag-completions into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15522
Proposed branch: lp:~sinzui/launchpad/bug-tag-completions
Merge into: lp:launchpad
Diff against target: 381 lines (+198/-29)
10 files modified
lib/lp/app/javascript/autocomplete/autocomplete.js (+2/-0)
lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js (+3/-3)
lib/lp/bugs/browser/widgets/bug.py (+34/-1)
lib/lp/bugs/browser/widgets/tests/test_bug.py (+81/-0)
lib/lp/bugs/javascript/bug_tags_entry.js (+20/-11)
lib/lp/bugs/javascript/tests/test_bug_tags_entry.html (+9/-0)
lib/lp/bugs/javascript/tests/test_bug_tags_entry.js (+45/-8)
lib/lp/bugs/templates/bugtask-index.pt (+2/-2)
lib/lp/bugs/templates/bugtask-macros-tableview.pt (+0/-2)
lib/lp/registry/javascript/milestoneoverlay.js (+2/-2)
To merge this branch: bzr merge lp:~sinzui/launchpad/bug-tag-completions
Reviewer Review Type Date Requested Status
Richard Harding (community) Approve
j.c.sackett (community) Approve
Review via email: mp+112580@code.launchpad.net

Commit message

Enable bug tag completions on +filebug and advanced bug search.

Description of the change

Recent refactorings of the bug tag entry code make it possible to
reuse the autocompletion part on +filebug and advanced bug search.

--------------------------------------------------------------------

RULES

    Pre-implementation: jcsackett
    * Extract the autocompletion from setup_tag_entry() so that
      other use cases can reuse it when the tag input field already
      exists.
    * Update +filebug to provide available_official_tags.
    * setup autocomplete in the template.
    * Update advanced search to provide available_official_tags.
    * setup autocomplete in the template.

    Addendum
    * I tried to extend the Autocomplete widget, but I discovered that
      I would also need to write a lot of duplicate css rules. The
      subclass effectively renames all the presentation rules :(
    * I tried a plugin, but it did not generate the markup before
      the widget was initialised.
    * I settled on a simple setup function.

QA

    * Visit https://bugs.qastaging.launchpad.net/gdp/+bugs?advanced
    * Verify the bug tags input will complete tags.
    * Choose Report a bug.
    * Enter "testing", Continue, expand Extra options.
    * Verify the bug tags input will complete tags.

LINT

    lib/lp/app/javascript/autocomplete/autocomplete.js
    lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js
    lib/lp/bugs/browser/widgets/bug.py
    lib/lp/bugs/browser/widgets/tests/test_bug.py
    lib/lp/bugs/javascript/bug_tags_entry.js
    lib/lp/bugs/javascript/tests/test_bug_tags_entry.html
    lib/lp/bugs/javascript/tests/test_bug_tags_entry.js
    lib/lp/bugs/templates/bugtask-macros-tableview.pt

TEST

    ./bin/test -vvc lp.bugs.browser.widgets.tests.test_bug
    ./bin/test -vvc --layer=YUITest

IMPLEMENTATION

I turn of the browser's autocomplete when the widget is run. The bug tag
completions were hidden under Chromium's completions. The browser must
be turned off for every input we add completions to.
    lib/lp/app/javascript/autocomplete/autocomplete.js
    lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js

I extracted the setup the autocomplete widget to a separate function
that could be called without the editor. I removed the "on('queryChange'"
because I discovered it did nothing when I wrote a test for it...it
was probably used to debug key input when the widget was created.
    lib/lp/bugs/javascript/bug_tags_entry.js
    lib/lp/bugs/javascript/tests/test_bug_tags_entry.html
    lib/lp/bugs/javascript/tests/test_bug_tags_entry.js

I Updated the BugTagsWidget to include the script to progressively
enhance any bug tag input to support autocomplete. This widget is used
both on advanced search and on +filebug. I moved the help link into the
widget to ensure it is always rendered and that it is rendered
consistently. I discovered that both +filebug and advanced bug had
duplicate ids in their crafted forms. A simple selector was not enough;
the selector must find the bug tag input field that is type="text".
    lib/lp/bugs/browser/widgets/bug.py
    lib/lp/bugs/browser/widgets/tests/test_bug.py
    lib/lp/bugs/templates/bugtask-macros-tableview.pt

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Curtis--

This looks good. Just one style note.

In a few places (line 226 or 311, for example): It looks like you're doing
pythonic brace/paren closure in places. I think for javascript we have the
closing brace at the same tab level as the opening call, rather than at the
tab level of the enclosed lines.

In other words, I believe we prefer:

  some_call(function () {
    stuff;
  });

Rather than:

  some_call(function () {
    stuff;
    });

review: Approve
Revision history for this message
Richard Harding (rharding) wrote :

Looks good. In addition to the alignment of the closing braces JC brought up, I've got a couple other suggestions.

As we discussed in IRC, widgets have a CSS_PREFIX value you can set to manually override the generated CSS prefix based on NAME. This might let you continue the idea of extending the Autocomplete widget.

http://yuilibrary.com/yui/docs/api/files/widget_js_Widget.js.html#l95

Small naming question/suggestion. Now that we're starting to properly build and namespace our JS, I'm trying to see if I can start to simplify naming. So could the module just be Y.lp.bugs.tags_entry [sans bug_]?

#197 is the autocomplete declared var?

#294/300/306/307 please surround attribute selectors in quotes: id="field.tag"

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

Hi. Jon, Rick.

I outdented the close of the methods calls that contain anonymous functions

I removed the redundant "bug_" from the namespace.

autocomplete is a private namespace var declared on line 24 of the module. The module has lots of hidden, untestible parts because of the structure of the entire module.

I quoted the attribute value in the selectors. I was inconsistent.

I will play with the widget and CSS in another branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/javascript/autocomplete/autocomplete.js'
--- lib/lp/app/javascript/autocomplete/autocomplete.js 2012-05-18 22:56:13 +0000
+++ lib/lp/app/javascript/autocomplete/autocomplete.js 2012-06-29 04:38:24 +0000
@@ -198,6 +198,8 @@
198 'left': iregion.left + 'px',198 'left': iregion.left + 'px',
199 'top': iregion.bottom + 'px'199 'top': iregion.bottom + 'px'
200 });200 });
201 // Disable the browser autocomplete so that it does not conflict.
202 input.setAttribute('autocomplete', 'off');
201 },203 },
202204
203 /**205 /**
204206
=== modified file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js'
--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 2012-04-06 17:28:25 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 2012-06-29 04:38:24 +0000
@@ -329,13 +329,13 @@
329329
330 /* A helper function to determine if two match result items are equal */330 /* A helper function to determine if two match result items are equal */
331 matches_are_equal: function(a, b) {331 matches_are_equal: function(a, b) {
332 if (typeof a == 'undefined') {332 if (Y.Lang.isUndefined(a)) {
333 Assert.fail("Match set 'a' is of type 'undefined'!");333 Assert.fail("Match set 'a' is of type 'undefined'!");
334 }334 }
335 if (typeof b == 'undefined') {335 if (Y.Lang.isUndefined(b)) {
336 Assert.fail("Match set 'b' is of type 'undefined'!");336 Assert.fail("Match set 'b' is of type 'undefined'!");
337 }337 }
338 return (a.text == b.text) && (a.offset == b.offset);338 return (a.text === b.text) && (a.offset === b.offset);
339 },339 },
340340
341 test_no_matches_returns_an_empty_array: function() {341 test_no_matches_returns_an_empty_array: function() {
342342
=== modified file 'lib/lp/bugs/browser/widgets/bug.py'
--- lib/lp/bugs/browser/widgets/bug.py 2011-02-24 15:30:54 +0000
+++ lib/lp/bugs/browser/widgets/bug.py 2012-06-29 04:38:24 +0000
@@ -10,7 +10,7 @@
10 ]10 ]
1111
12import re12import re
1313from simplejson import dumps
14from zope.app.form.browser.textwidgets import (14from zope.app.form.browser.textwidgets import (
15 IntWidget,15 IntWidget,
16 TextAreaWidget,16 TextAreaWidget,
@@ -26,6 +26,8 @@
26from lp.app.errors import NotFoundError26from lp.app.errors import NotFoundError
27from lp.app.validators import LaunchpadValidationError27from lp.app.validators import LaunchpadValidationError
28from lp.bugs.interfaces.bug import IBugSet28from lp.bugs.interfaces.bug import IBugSet
29from lp.registry.interfaces.distribution import IDistribution
30from lp.registry.interfaces.product import IProduct
2931
3032
31class BugWidget(IntWidget):33class BugWidget(IntWidget):
@@ -135,6 +137,37 @@
135 def _getInputValue(self):137 def _getInputValue(self):
136 return TextWidget.getInputValue(self)138 return TextWidget.getInputValue(self)
137139
140 def __call__(self):
141 """Return the input with a script."""
142 input_markup = super(BugTagsWidget, self).__call__()
143 script_markup = """
144 <a href="/+help-bugs/tag-search.html"
145 class="sprite maybe action-icon"
146 target="help">Tag search help</a>
147 <script type="text/javascript">
148 LPJS.use('event', 'lp.bugs.tags_entry', function(Y) {
149 %s
150 Y.on('domready', function(e) {
151 Y.lp.bugs.tags_entry.setup_tag_complete(
152 'input[id="field.%s"][type="text"]', official_tags);
153 });
154 });
155 </script>
156 """ % (self.official_tags_js, self.context.__name__)
157 return input_markup + script_markup
158
159 @property
160 def official_tags_js(self):
161 """Return the JavaScript of bug tags used by the bug tag completer."""
162 bug_target = self.context.context
163 pillar_target = (
164 IProduct(bug_target, None) or IDistribution(bug_target, None))
165 if pillar_target is not None:
166 official_tags = list(pillar_target.official_bug_tags)
167 else:
168 official_tags = []
169 return 'var official_tags = %s;' % dumps(official_tags)
170
138171
139class BugTagsFrozenSetWidget(BugTagsWidget):172class BugTagsFrozenSetWidget(BugTagsWidget):
140 """A widget for editing bug tags in a `FrozenSet` field."""173 """A widget for editing bug tags in a `FrozenSet` field."""
141174
=== added file 'lib/lp/bugs/browser/widgets/tests/test_bug.py'
--- lib/lp/bugs/browser/widgets/tests/test_bug.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/widgets/tests/test_bug.py 2012-06-29 04:38:24 +0000
@@ -0,0 +1,81 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from lp.bugs.browser.widgets.bug import BugTagsWidget
7from lp.bugs.interfaces.bugtarget import IHasOfficialBugTags
8from lp.services.webapp.servers import LaunchpadTestRequest
9from lp.testing import (
10 person_logged_in,
11 TestCaseWithFactory,
12 )
13from lp.testing.layers import DatabaseFunctionalLayer
14
15
16class BugTagsWidgetTestCase(TestCaseWithFactory):
17
18 layer = DatabaseFunctionalLayer
19
20 def get_widget(self, bug_target):
21 field = IHasOfficialBugTags['official_bug_tags']
22 bound_field = field.bind(bug_target)
23 request = LaunchpadTestRequest()
24 return BugTagsWidget(bound_field, None, request)
25
26 def test_official_tags_js_not_adaptable_to_product_or_distro(self):
27 # project groups are not full bug targets so they have no tags.
28 project_group = self.factory.makeProject()
29 widget = self.get_widget(project_group)
30 js = widget.official_tags_js
31 self.assertEqual('var official_tags = [];', js)
32
33 def test_official_tags_js_product_without_tags(self):
34 # Products without tags have an empty list.
35 product = self.factory.makeProduct()
36 widget = self.get_widget(product)
37 js = widget.official_tags_js
38 self.assertEqual('var official_tags = [];', js)
39
40 def test_official_tags_js_product_with_tags(self):
41 # Products with tags have a list of tags.
42 product = self.factory.makeProduct()
43 with person_logged_in(product.owner):
44 product.official_bug_tags = [u'cows', u'pigs', u'sheep']
45 widget = self.get_widget(product)
46 js = widget.official_tags_js
47 self.assertEqual('var official_tags = ["cows", "pigs", "sheep"];', js)
48
49 def test_official_tags_js_distribution_without_tags(self):
50 # Distributions without tags have an empty list.
51 distribution = self.factory.makeDistribution()
52 widget = self.get_widget(distribution)
53 js = widget.official_tags_js
54 self.assertEqual('var official_tags = [];', js)
55
56 def test_official_tags_js_distribution_with_tags(self):
57 # Distributions with tags have a list of tags.
58 distribution = self.factory.makeDistribution()
59 with person_logged_in(distribution.owner):
60 distribution.official_bug_tags = [u'cows', u'pigs', u'sheep']
61 widget = self.get_widget(distribution)
62 js = widget.official_tags_js
63 self.assertEqual('var official_tags = ["cows", "pigs", "sheep"];', js)
64
65 def test_call(self):
66 # __call__ renders the input, help link, and script with official tags.
67 # Products with tags have a list of tags.
68 product = self.factory.makeProduct()
69 with person_logged_in(product.owner):
70 product.official_bug_tags = [u'cows', u'pigs', u'sheep']
71 widget = self.get_widget(product)
72 markup = widget()
73 self.assertIn(
74 '<input class="textType" id="field.official_bug_tags"', markup)
75 self.assertIn('<a href="/+help-bugs/tag-search.html"', markup)
76 self.assertIn('var official_tags = ["cows", "pigs", "sheep"];', markup)
77 self.assertIn(
78 'Y.lp.bugs.tags_entry.setup_tag_complete(', markup)
79 self.assertIn(
80 """'input[id="field.official_bug_tags"][type="text"]',""", markup)
81 self.assertIn("official_tags)", markup)
082
=== modified file 'lib/lp/bugs/javascript/bug_tags_entry.js'
--- lib/lp/bugs/javascript/bug_tags_entry.js 2012-06-22 19:49:29 +0000
+++ lib/lp/bugs/javascript/bug_tags_entry.js 2012-06-29 04:38:24 +0000
@@ -7,9 +7,9 @@
7 * @submodule bug_tags_entry7 * @submodule bug_tags_entry
8 */8 */
99
10YUI.add('lp.bugs.bug_tags_entry', function(Y) {10YUI.add('lp.bugs.tags_entry', function(Y) {
1111
12var namespace = Y.namespace('lp.bugs.bug_tags_entry');12var namespace = Y.namespace('lp.bugs.tags_entry');
1313
14var bug_tags_div;14var bug_tags_div;
15var tags_heading;15var tags_heading;
@@ -251,21 +251,30 @@
251 });251 });
252 tags_trigger.addClass('js-action');252 tags_trigger.addClass('js-action');
253253
254 bug_tags_div.append(254 autocomplete = namespace.setup_tag_complete(
255 '#tag-input', available_official_tags);
256};
257
258
259/**
260 * Set up bug tag autocompletion on a text input.
261 *
262 * @method setup_tag_complete
263 */
264namespace.setup_tag_complete = function(input, official_tags) {
265 Y.one('body').appendChild(
255 '<div id="tags-autocomplete"><div id="tags-autocomplete-content">' +266 '<div id="tags-autocomplete"><div id="tags-autocomplete-content">' +
256 '</div></div>');267 '</div></div>');
257 autocomplete = new Y.lazr.AutoComplete({268 var autocomplete = new Y.lazr.AutoComplete({
258 input: '#tag-input',269 input: input,
259 data: available_official_tags,270 data: official_tags,
260 boundingBox: '#tags-autocomplete',271 boundingBox: '#tags-autocomplete',
261 contentBox: '#tags-autocomplete-content'272 contentBox: '#tags-autocomplete-content'
262 });273 });
263 autocomplete.on('queryChange', function(e) {274 autocomplete.get('input').on('focus', function(e) {
264 var val = "null";275 autocomplete.render();
265 if (e.newVal !== null) {
266 val = "'" + e.newVal.text + "', " + e.newVal.offset;
267 }
268 });276 });
277 return autocomplete;
269};278};
270}, "0.1", {279}, "0.1", {
271 "requires": [280 "requires": [
272281
=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.html'
--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.html 2012-06-22 14:58:26 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.html 2012-06-29 04:38:24 +0000
@@ -82,5 +82,14 @@
82 class="sprite maybe action-icon">Tag help</a>82 class="sprite maybe action-icon">Tag help</a>
83 </div>83 </div>
84 </script>84 </script>
85
86 <script type="text/x-template" id="form-with-bug-tags">
87 <form>
88 <div>
89 <input type="text" name="field.tag" id="field.tag" />
90 <a href="/help/tags.html" target="help" class="help">(?)</a>
91 </div>
92 </form>
93 </script>
85 </body>94 </body>
86</html>95</html>
8796
=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.js'
--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2012-06-22 19:49:29 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2012-06-29 04:38:24 +0000
@@ -1,8 +1,8 @@
1YUI().use('lp.testing.runner', 'lp.testing.mockio', 'test', 'console',1YUI().use('lp.testing.runner', 'lp.testing.mockio', 'test', 'console',
2 'lp.client', 'node-event-simulate', 'lp.bugs.bug_tags_entry',2 'lp.client', 'node-event-simulate', 'lp.bugs.tags_entry',
3 function(Y) {3 function(Y) {
44
5 var module = Y.lp.bugs.bug_tags_entry;5 var module = Y.lp.bugs.tags_entry;
6 var suite = new Y.Test.Suite("Bug tags entry Tests");6 var suite = new Y.Test.Suite("Bug tags entry Tests");
77
8 suite.add(new Y.Test.Case({8 suite.add(new Y.Test.Case({
@@ -87,12 +87,7 @@
87 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tags-edit-spinner'));87 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tags-edit-spinner'));
88 Y.Assert.isInstanceOf(Y.Node, form_node.one('#edit-tags-cancel'));88 Y.Assert.isInstanceOf(Y.Node, form_node.one('#edit-tags-cancel'));
89 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tag-input'));89 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tag-input'));
90 // The Autocompleter nodes are provided.90 Y.Assert.isInstanceOf(Y.Node, Y.one('#tags-autocomplete'));
91 var completer_node = this.bug_tags_div.one('#tags-autocomplete');
92 Y.Assert.isInstanceOf(Y.Node, completer_node);
93 var completer_content = completer_node.one(
94 '#tags-autocomplete-content');
95 Y.Assert.isInstanceOf(Y.Node, completer_content);
96 },91 },
9792
98 test_show_activity: function() {93 test_show_activity: function() {
@@ -206,5 +201,47 @@
206 }201 }
207 }));202 }));
208203
204 suite.add(new Y.Test.Case({
205 name: 'Completer',
206
207 setUp: function() {
208 this.fixture = Y.one("#fixture");
209 var template = Y.one('#form-with-bug-tags').getContent();
210 this.fixture.append(template);
211 },
212
213 tearDown: function() {
214 if (this.fixture !== null) {
215 this.fixture.empty();
216 }
217 delete this.fixture;
218 },
219
220 test_setup_tag_complete: function() {
221 // The Autocompleter nodes are provided.
222 module.setup_tag_complete(
223 'input[id="field.tag"]',['project-tag']);
224 var completer_node = Y.one('#tags-autocomplete');
225 Y.Assert.isInstanceOf(Y.Node, completer_node);
226 var completer_content = completer_node.one(
227 '#tags-autocomplete-content');
228 Y.Assert.isInstanceOf(Y.Node, completer_content);
229 var input = Y.one('input[id="field.tag"]');
230 },
231
232 test_render_on_focus: function() {
233 // The Autocompleter nodes are provided.
234 var completer = module.setup_tag_complete(
235 'input[id="field.tag"]',['project-tag']);
236 var input = Y.one('input[id="field.tag"]');
237 var called = false;
238 completer.render = function() {
239 called = true;
240 };
241 input.simulate('focus');
242 Y.Assert.isTrue(called);
243 }
244 }));
245
209 Y.lp.testing.Runner.run(suite);246 Y.lp.testing.Runner.run(suite);
210});247});
211248
=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
--- lib/lp/bugs/templates/bugtask-index.pt 2012-06-22 20:57:42 +0000
+++ lib/lp/bugs/templates/bugtask-index.pt 2012-06-29 04:38:24 +0000
@@ -153,10 +153,10 @@
153 </div>153 </div>
154154
155 <script type="text/javascript">155 <script type="text/javascript">
156 LPJS.use('event', 'node', 'lp.bugs.bug_tags_entry', function(Y) {156 LPJS.use('event', 'node', 'lp.bugs.tags_entry', function(Y) {
157 Y.on('domready',157 Y.on('domready',
158 function(e) {158 function(e) {
159 Y.lp.bugs.bug_tags_entry.setup_tag_entry(159 Y.lp.bugs.tags_entry.setup_tag_entry(
160 available_official_tags);160 available_official_tags);
161 },161 },
162 window);162 window);
163163
=== modified file 'lib/lp/bugs/templates/bugtask-macros-tableview.pt'
--- lib/lp/bugs/templates/bugtask-macros-tableview.pt 2012-03-01 19:12:34 +0000
+++ lib/lp/bugs/templates/bugtask-macros-tableview.pt 2012-06-29 04:38:24 +0000
@@ -516,8 +516,6 @@
516 tal:content="structure view/widgets/tag/label">516 tal:content="structure view/widgets/tag/label">
517 Tag517 Tag
518 </label>: <input tal:replace="structure view/widgets/tag" />518 </label>: <input tal:replace="structure view/widgets/tag" />
519 <a href="/+help-bugs/tag-search.html"
520 target="help">Tag search help</a>
521 <div519 <div
522 tal:condition="error"520 tal:condition="error"
523 class="message"521 class="message"
524522
=== modified file 'lib/lp/registry/javascript/milestoneoverlay.js'
--- lib/lp/registry/javascript/milestoneoverlay.js 2012-05-12 01:58:54 +0000
+++ lib/lp/registry/javascript/milestoneoverlay.js 2012-06-29 04:38:24 +0000
@@ -78,7 +78,7 @@
7878
79 var set_milestone_tags = function(milestone) {79 var set_milestone_tags = function(milestone) {
80 var tagstring = data['field.tags'][0].toLowerCase();80 var tagstring = data['field.tags'][0].toLowerCase();
81 var tags = Y.lp.bugs.bug_tags_entry.parse_tags(tagstring);81 var tags = Y.lp.bugs.tags_entry.parse_tags(tagstring);
8282
83 // Since we are creating a new milestone it cannot already83 // Since we are creating a new milestone it cannot already
84 // have pre-existing tags, so if none are given we can84 // have pre-existing tags, so if none are given we can
@@ -201,5 +201,5 @@
201 "lazr.formoverlay",201 "lazr.formoverlay",
202 "lp.app.calendar",202 "lp.app.calendar",
203 "lp.client",203 "lp.client",
204 "lp.bugs.bug_tags_entry"204 "lp.bugs.tags_entry"
205 ]});205 ]});