Merge lp:~sinzui/launchpad/adapt-subscription-to-pillar into lp:launchpad

Proposed by Curtis Hovey on 2012-07-20
Status: Merged
Approved by: Curtis Hovey on 2012-07-23
Approved revision: no longer in the source branch.
Merged at revision: 15669
Proposed branch: lp:~sinzui/launchpad/adapt-subscription-to-pillar
Merge into: lp:launchpad
Diff against target: 338 lines (+161/-16)
10 files modified
lib/lp/app/javascript/autocomplete/autocomplete.js (+4/-2)
lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js (+10/-0)
lib/lp/bugs/adapters/bugsubscriptionfilter.py (+26/-0)
lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py (+64/-0)
lib/lp/bugs/configure.zcml (+8/-0)
lib/lp/bugs/javascript/bug_tags_entry.js (+5/-5)
lib/lp/bugs/javascript/tests/test_bug_tags_entry.js (+4/-3)
lib/lp/registry/javascript/structural-subscription.js (+17/-3)
lib/lp/registry/javascript/tests/test_structural_subscription.html (+5/-1)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+18/-2)
To merge this branch: bzr merge lp:~sinzui/launchpad/adapt-subscription-to-pillar
Reviewer Review Type Date Requested Status
Benji York (community) code 2012-07-20 Approve on 2012-07-23
Review via email: mp+116095@code.launchpad.net

Commit Message

enable bug tag autocompletion in subscription filters.

Description of the Change

bug tags do not complete because the IBugSubscriptionFilter
class cannot be adapted to an IProduct or IDistribution to provide
the official list of bug tags. Adding autocomplete to the filter's
input field is two lines of code.

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

RULES

    Pre-implementation: no one
    * Create an adapter for IProduct and IDistribution
    * Verify autocompletion works in the HTML forms
    * Call bug_tags_entry() when the input is created and pass in the
      official list of bug tags.

    ADDENDUM
    * OMG! This was so must harder than is should be.
    * We have never used autocomplete in an overlay before...It runs but
      you cannot see it because it is 1000 z-index's below the overlay!
    * The bug tag completer setup code can only be called once because
      it uses ids!
    * The structural subscription module is just a namespace of procedural
      code with few entry points to call, return, or instrument to test!

QA

    * https://bugs.qastaging.launchpad.net/~/+structural-subscriptions
    * Choose Create new filter
    * Verify that tags autocomplete
    * https://bugs.qastaging.launchpad.net/launchpad
    * Choose Subscribe to bug mail and expand the tag section
    * Verify the tag editor completes.
    * Save the subscription
    * Choose Edit bug mail
    * Edit a subscription, expand the tags
    * Enter text to see the suggestions, press Esc
    * Edit a different subscription, expand the tags
    * Enter text, verify you do not see the previous list of suggests...
      you only see the list for what you are typing.

LINT

    lib/lp/app/javascript/autocomplete/autocomplete.js
    lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js
    lib/lp/bugs/configure.zcml
    lib/lp/bugs/adapters/bugsubscriptionfilter.py
    lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py
    lib/lp/bugs/javascript/bug_tags_entry.js
    lib/lp/bugs/javascript/tests/test_bug_tags_entry.js
    lib/lp/registry/javascript/structural-subscription.js
    lib/lp/registry/javascript/tests/test_structural_subscription.html
    lib/lp/registry/javascript/tests/test_structural_subscription.js

TEST

    ./bin/test -vv lp.bugs.adapters.tests.test_bugsubscriptionfilter
    ./bin/test -vvc --layer=YUITest lp

IMPLEMENTATION

Let me see the autocomplete list that decorates the input field. This is
set at 31000 because something might need to be higher, but in general
user input is always the highest thing on the stack, so autocomplete should
be the highest.
    lib/lp/app/javascript/autocomplete/autocomplete.js
    lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js

Created and registered adapters that convert the bugsubscriptionfilter to
their product or distribution. The bug tag completer code knows how to
generate the list of official tags when the pillar is available.
    lib/lp/bugs/configure.zcml
    lib/lp/bugs/adapters/bugsubscriptionfilter.py
    lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py

I discovered that the +subscriptions view created multiple overlays, each
with its own autocomplete. Editing failed after a few uses because the
bug_tags_entry() assumes it is used once and can use an id.
    lib/lp/bugs/javascript/bug_tags_entry.js
    lib/lp/bugs/javascript/tests/test_bug_tags_entry.js

I added setup_tag_complete() with the input and official bug tags. The
completer has to be cleaned up to ensure there is only one list of
suggestions in existence at any one time. I made the tag split() code
more robust by stripping leading and trailing white-space.
    lib/lp/registry/javascript/structural-subscription.js
    lib/lp/registry/javascript/tests/test_structural_subscription.html
    lib/lp/registry/javascript/tests/test_structural_subscription.js

To post a comment you must log in.
Benji York (benji) 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/javascript/autocomplete/autocomplete.js'
2--- lib/lp/app/javascript/autocomplete/autocomplete.js 2012-06-29 03:38:38 +0000
3+++ lib/lp/app/javascript/autocomplete/autocomplete.js 2012-07-20 23:27:21 +0000
4@@ -234,7 +234,8 @@
5
6 var cbox = this.get(CONTENT_BOX);
7
8- this.get(BOUNDING_BOX).unplug(Y.Plugin.NodeMenuNav);
9+ var box = this.get(BOUNDING_BOX);
10+ box.unplug(Y.Plugin.NodeMenuNav);
11
12 if (this._completions) {
13 cbox.replaceChild(list, this._completions);
14@@ -243,7 +244,8 @@
15 }
16
17 // Re-plug the MenuNav, so it updates the menu options.
18- this.get(BOUNDING_BOX).plug(Y.Plugin.NodeMenuNav);
19+ box.plug(Y.Plugin.NodeMenuNav);
20+ box.setStyle('z-index', '31000');
21
22 // Highlight the first item.
23 this._selectItem(0, false);
24
25=== modified file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js'
26--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 2012-06-29 04:36:14 +0000
27+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 2012-07-20 23:27:21 +0000
28@@ -448,6 +448,16 @@
29 Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
30 },
31
32+ test_pressing_matching_key_raises_menu: function() {
33+ this.autocomp.set('data', ['aaaa', 'aabb']);
34+ this.complete_input('aa');
35+ var box = this.autocomp.get('boundingBox');
36+ Assert.areEqual(
37+ '31000',
38+ box.getStyle('z-index'),
39+ "The menu z-index should be 31000; above it's overlay.");
40+ },
41+
42 test_pressing_enter_completes_current_input: function() {
43 this.autocomp.set('data', ['aaaa', 'aabb']);
44
45
46=== added file 'lib/lp/bugs/adapters/bugsubscriptionfilter.py'
47--- lib/lp/bugs/adapters/bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000
48+++ lib/lp/bugs/adapters/bugsubscriptionfilter.py 2012-07-20 23:27:21 +0000
49@@ -0,0 +1,26 @@
50+# Copyright 2012 Canonical Ltd. This software is licensed under the
51+# GNU Affero General Public License version 3 (see the file LICENSE).
52+
53+"""Adapt IStructuralSubscription to other types."""
54+
55+__metaclass__ = type
56+__all__ = [
57+ 'bugsubscriptionfilter_to_distribution',
58+ 'bugsubscriptionfilter_to_product',
59+ ]
60+
61+
62+def bugsubscriptionfilter_to_distribution(bug_subscription_filter):
63+ """Adapt the `IBugSubscriptionFilter` to an `IDistribution`."""
64+ subscription = bug_subscription_filter.structural_subscription
65+ if subscription.distroseries is not None:
66+ return subscription.distroseries.distribution
67+ return subscription.distribution
68+
69+
70+def bugsubscriptionfilter_to_product(bug_subscription_filter):
71+ """Adapt the `IBugSubscriptionFilter` to an `IProduct`."""
72+ subscription = bug_subscription_filter.structural_subscription
73+ if subscription.productseries is not None:
74+ return subscription.productseries.product
75+ return subscription.product
76
77=== added file 'lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py'
78--- lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000
79+++ lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py 2012-07-20 23:27:21 +0000
80@@ -0,0 +1,64 @@
81+# Copyright 2012 Canonical Ltd. This software is licensed under the
82+# GNU Affero General Public License version 3 (see the file LICENSE).
83+
84+"""Test IBugSubscriptionFilter adapters"""
85+
86+__metaclass__ = type
87+
88+from lp.bugs.adapters.bugsubscriptionfilter import (
89+ bugsubscriptionfilter_to_distribution,
90+ bugsubscriptionfilter_to_product,
91+ )
92+from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter
93+from lp.registry.interfaces.distribution import IDistribution
94+from lp.registry.interfaces.product import IProduct
95+from lp.testing import (
96+ login_person,
97+ TestCaseWithFactory,
98+ )
99+from lp.testing.layers import DatabaseFunctionalLayer
100+
101+
102+class BugSubscriptionFilterTestCase(TestCaseWithFactory):
103+
104+ layer = DatabaseFunctionalLayer
105+
106+ def makeBugSubscriptionFilter(self, target):
107+ subscriber = self.factory.makePerson()
108+ login_person(subscriber)
109+ subscription = target.addBugSubscription(subscriber, subscriber)
110+ subscription_filter = BugSubscriptionFilter()
111+ subscription_filter.structural_subscription = subscription
112+ return subscription_filter
113+
114+ def test_bugsubscriptionfilter_to_product_with_product(self):
115+ product = self.factory.makeProduct()
116+ subscription_filter = self.makeBugSubscriptionFilter(product)
117+ self.assertEqual(
118+ product, bugsubscriptionfilter_to_product(subscription_filter))
119+ self.assertEqual(product, IProduct(subscription_filter))
120+
121+ def test_bugsubscriptionfilter_to_product_with_productseries(self):
122+ product = self.factory.makeProduct()
123+ series = product.development_focus
124+ subscription_filter = self.makeBugSubscriptionFilter(series)
125+ self.assertEqual(
126+ product, bugsubscriptionfilter_to_product(subscription_filter))
127+ self.assertEqual(product, IProduct(subscription_filter))
128+
129+ def test_bugsubscriptionfilter_to_distribution_with_distribution(self):
130+ distribution = self.factory.makeDistribution()
131+ subscription_filter = self.makeBugSubscriptionFilter(distribution)
132+ self.assertEqual(
133+ distribution,
134+ bugsubscriptionfilter_to_distribution(subscription_filter))
135+ self.assertEqual(distribution, IDistribution(subscription_filter))
136+
137+ def test_bugsubscriptionfilter_to_distroseries_with_distribution(self):
138+ distribution = self.factory.makeDistribution()
139+ series = self.factory.makeDistroSeries(distribution=distribution)
140+ subscription_filter = self.makeBugSubscriptionFilter(series)
141+ self.assertEqual(
142+ distribution,
143+ bugsubscriptionfilter_to_distribution(subscription_filter))
144+ self.assertEqual(distribution, IDistribution(subscription_filter))
145
146=== modified file 'lib/lp/bugs/configure.zcml'
147--- lib/lp/bugs/configure.zcml 2012-03-08 11:51:59 +0000
148+++ lib/lp/bugs/configure.zcml 2012-07-20 23:27:21 +0000
149@@ -623,6 +623,14 @@
150 interface=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterMethodsProtected"
151 set_schema=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterAttributes" />
152 </class>
153+ <adapter
154+ for=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilter"
155+ provides="lp.registry.interfaces.distribution.IDistribution"
156+ factory=".adapters.bugsubscriptionfilter.bugsubscriptionfilter_to_distribution"/>
157+ <adapter
158+ for=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilter"
159+ provides="lp.registry.interfaces.product.IProduct"
160+ factory=".adapters.bugsubscriptionfilter.bugsubscriptionfilter_to_product"/>
161
162 <!-- BugMute -->
163 <class
164
165=== modified file 'lib/lp/bugs/javascript/bug_tags_entry.js'
166--- lib/lp/bugs/javascript/bug_tags_entry.js 2012-06-28 16:00:11 +0000
167+++ lib/lp/bugs/javascript/bug_tags_entry.js 2012-07-20 23:27:21 +0000
168@@ -262,14 +262,14 @@
169 * @method setup_tag_complete
170 */
171 namespace.setup_tag_complete = function(input, official_tags) {
172- Y.one('body').appendChild(
173- '<div id="tags-autocomplete"><div id="tags-autocomplete-content">' +
174- '</div></div>');
175+ var bounding_box = Y.Node.create(
176+ '<div class="bug-tag-complete"><div></div></div>');
177+ Y.one('body').appendChild(bounding_box);
178 var autocomplete = new Y.lazr.AutoComplete({
179 input: input,
180 data: official_tags,
181- boundingBox: '#tags-autocomplete',
182- contentBox: '#tags-autocomplete-content'
183+ boundingBox: bounding_box,
184+ contentBox: bounding_box.one('div')
185 });
186 autocomplete.get('input').on('focus', function(e) {
187 autocomplete.render();
188
189=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.js'
190--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2012-06-28 16:15:31 +0000
191+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2012-07-20 23:27:21 +0000
192@@ -87,7 +87,7 @@
193 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tags-edit-spinner'));
194 Y.Assert.isInstanceOf(Y.Node, form_node.one('#edit-tags-cancel'));
195 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tag-input'));
196- Y.Assert.isInstanceOf(Y.Node, Y.one('#tags-autocomplete'));
197+ Y.Assert.isInstanceOf(Y.Node, Y.one('.bug-tag-complete'));
198 },
199
200 test_show_activity: function() {
201@@ -221,10 +221,11 @@
202 // The Autocompleter nodes are provided.
203 module.setup_tag_complete(
204 'input[id="field.tag"]',['project-tag']);
205- var completer_node = Y.one('#tags-autocomplete');
206+ var completer_node = Y.one('.yui3-autocomplete');
207 Y.Assert.isInstanceOf(Y.Node, completer_node);
208+ Y.Assert.isTrue(completer_node.hasClass('bug-tag-complete'));
209 var completer_content = completer_node.one(
210- '#tags-autocomplete-content');
211+ '.yui3-autocomplete-content');
212 Y.Assert.isInstanceOf(Y.Node, completer_content);
213 var input = Y.one('input[id="field.tag"]');
214 },
215
216=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
217--- lib/lp/registry/javascript/structural-subscription.js 2012-07-12 13:16:29 +0000
218+++ lib/lp/registry/javascript/structural-subscription.js 2012-07-20 23:27:21 +0000
219@@ -37,6 +37,14 @@
220 Y.augment(PortletTarget, Y.Event.Target);
221 namespace.portlet = new PortletTarget();
222
223+
224+/*
225+ * There can be only one instance of bug_tag_completer because it is watching
226+ * keyboard input.
227+ */
228+namespace.bug_tag_completer = null;
229+
230+
231 /**
232 * This helper function is used to cleanly close down the overlay.
233 */
234@@ -45,6 +53,9 @@
235 var filter_wrapper = Y.one('#' + FILTER_WRAPPER);
236 filter_wrapper.hide();
237 collapse_node(filter_wrapper);
238+ if (Y.Lang.isObject(namespace.bug_tag_completer)) {
239+ namespace.bug_tag_completer.destroy();
240+ }
241 }
242
243 function subscription_success() {
244@@ -116,7 +127,7 @@
245 // Tags are a list with one element being a space-separated string.
246 var tags = form_data.tags[0];
247 if (Y.Lang.isValue(tags) && tags !== '') {
248- patch_data.tags = tags.toLowerCase().split(' ');
249+ patch_data.tags = Y.Lang.trim(tags).toLowerCase().split(' ');
250 }
251 patch_data.find_all_tags =
252 list_contains(form_data.tag_match, MATCH_ALL);
253@@ -403,6 +414,7 @@
254 namespace._add_subscription_overlay.on('cancel', clean_up);
255 }
256
257+
258 /**
259 * Reset the overlay form to initial values.
260 */
261@@ -629,7 +641,9 @@
262 make_select_handler(node, statuses, true));
263 selectors.none_link.on('click',
264 make_select_handler(node, statuses, false));
265-
266+ var official_bug_tags = LP.cache.context.official_bug_tags || [];
267+ namespace.bug_tag_completer = Y.lp.bugs.tags_entry.setup_tag_complete(
268+ 'input[name="tags"]', official_bug_tags);
269 return accordion;
270 }
271
272@@ -1863,5 +1877,5 @@
273 }, '0.1', {requires: [
274 'dom', 'node', 'lp.anim', 'lazr.formoverlay', 'lazr.overlay',
275 'lazr.effects', 'lp.app.errors', 'lp.client', 'gallery-accordion',
276- 'lp.app.inlinehelp'
277+ 'lp.app.inlinehelp', 'lp.bugs.tags_entry'
278 ]});
279
280=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.html'
281--- lib/lp/registry/javascript/tests/test_structural_subscription.html 2012-07-07 14:00:30 +0000
282+++ lib/lp/registry/javascript/tests/test_structural_subscription.html 2012-07-20 23:27:21 +0000
283@@ -44,7 +44,11 @@
284 <script type="text/javascript"
285 src="../../../../../build/js/lp/app/inlinehelp/inlinehelp.js"></script>
286 <script type="text/javascript"
287- src="../../../../../build/js/lp/app//gallery-accordion/gallery-accordion.js"></script>
288+ src="../../../../../build/js/lp/app/gallery-accordion/gallery-accordion.js"></script>
289+ <script type="text/javascript"
290+ src="../../../../../build/js/lp/app/autocomplete/autocomplete.js"></script>
291+ <script type="text/javascript"
292+ src="../../../../../build/js/lp/bugs/bug_tags_entry.js"></script>
293
294 <!-- The module under test. -->
295 <script type="text/javascript" src="../structural-subscription.js"></script>
296
297=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
298--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-06-28 15:30:15 +0000
299+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2012-07-20 23:27:21 +0000
300@@ -453,6 +453,21 @@
301 Assert.areEqual(
302 'Add a mail subscription for Test bugs',
303 header.get('text'));
304+ var bug_tags_node = Y.one(".bug-tag-complete");
305+ Assert.isInstanceOf(Y.Node, bug_tags_node);
306+ },
307+
308+ test_clean_up_overlay: function() {
309+ // When the overlay is hidden, resources are cleaned up..
310+ module.setup(this.configuration);
311+ module._show_add_overlay(this.configuration);
312+ var cancel_button = Y.one('.lazr-neg.lazr-btn');
313+ var called = false;
314+ module.bug_tag_completer.after('destroy', function() {
315+ called = true;
316+ });
317+ cancel_button.simulate('click');
318+ Assert.isTrue(called);
319 },
320
321 test_setup_overlay_missing_content_box: function() {
322@@ -997,13 +1012,14 @@
323 name: [],
324 events: [],
325 filters: ['advanced-filter'],
326- tags: ['one two THREE'],
327+ tags: [' one two THREE '],
328 tag_match: [''],
329 importances: [],
330 statuses: []
331 };
332 var patch_data = module._extract_form_data(form_data);
333- // Note that the tags are converted to lower case.
334+ // Note that the tags are converted to lower case
335+ // and outer white-space is stripped.
336 ArrayAssert.itemsAreEqual(
337 patch_data.tags, ['one', 'two', 'three']);
338 },