Merge lp:~wallyworld/launchpad/new-team-picker into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 15475
Proposed branch: lp:~wallyworld/launchpad/new-team-picker
Merge into: lp:launchpad
Prerequisite: lp:~jcsackett/launchpad/cleanup-pickers-with-base-create
Diff against target: 1171 lines (+491/-226)
11 files modified
lib/lp/app/browser/lazrjs.py (+10/-2)
lib/lp/app/browser/tests/test_inlineeditpickerwidget.py (+20/-2)
lib/lp/app/javascript/picker/person_picker.js (+148/-18)
lib/lp/app/javascript/picker/picker.js (+82/-16)
lib/lp/app/javascript/picker/picker_patcher.js (+17/-36)
lib/lp/app/javascript/picker/tests/test_personpicker.html (+2/-0)
lib/lp/app/javascript/picker/tests/test_personpicker.js (+158/-135)
lib/lp/app/widgets/popup.py (+23/-13)
lib/lp/app/widgets/tests/test_popup.py (+21/-0)
lib/lp/registry/browser/product.py (+4/-4)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/new-team-picker
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Jelmer Vernooij Pending
Review via email: mp+111346@code.launchpad.net

Commit message

Initial infrastructure to provide a "New Team" link on person pickers.

Description of the change

== Implementation ==

The LOC is a little high, but much of this is a necessary cut and paste to remove duplicate code from existing person picker tests. Also, there is some hardwired html which will be replaced by a generated form.

This branch is the first in a series which will add the ability to create a new team directly from within the person picker. There is one view in Launchpad which offers the user a chance to create a new team alongside providing a person picker. This is the product +edit-people view, and what is rendered is a text input field for typing the field value (eg maintainer) and next to that two links: a "Choose..." link and a "or create new team" link. However, with inline pickers being used on the project overview page for setting maintainer and driver, only people without javascript actually see this view.

So what is done is that a "New Team" link is provided for the person picker. This link is rendered in the "extra buttons" div alongside the "Pick Me" and "Remove Person" links. The new team link is part of the picker and so is available regardless of how the picker is created, inline popup or Choose... link. So part of the work therefore is to have the picker javascript convert the "Choose... or Create new team" pair of links into a single, simple Choose... link.

As is currently the case, only person pickers configured to "show_create_team_link" have the create team functionality. This is used on product maintainer and driver fields.

The feature is controlled by a feature flag: "disclosure.add-team-person-picker.enabled". Without the flag, everything renders as before.

This is only an initial implementation to get the base infrastructure in place. Current unfinished things include:

- The create team form html is hardwired into the picker as a mustache template. It needs to be fetched from the backend using a ++form++ call.
- The form doesn't allow subscription policy to be set.
- The team is not actually created. A 'save' event is published by the picker as if it were created allowing the feature to be demoed.

Some code in the picker stuff was moved. There was code in picker_patcher to show and hide the validation forms used to confirm a user's selection. These methods were moved to Picker itself and made into generic functions to show/hide arbitrary forms (eg the team creation form).

== Demo ==

http://people.canonical.com/~ianb/picker-new-team-demo.ogv

== Tests ==

Add tests for the back end widgets: test_popup and test_inlineeditpickerwidget:
- test_show_create_team_link_with_feature_flag
- test_show_create_team_link
- test_create_team_link

Move duplicated tests in test_personpicker to a common base class.

Add new yui tests for the create team functionality:
- test_picker_no_team_button_unless_configured
- test_picker_new_team_button_click_shows_form
- test_picker_new_team_cancel
- test_picker_new_team_save

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/browser/lazrjs.py
  lib/lp/app/browser/tests/test_inlineeditpickerwidget.py
  lib/lp/app/javascript/picker/person_picker.js
  lib/lp/app/javascript/picker/picker.js
  lib/lp/app/javascript/picker/picker_patcher.js
  lib/lp/app/javascript/picker/tests/test_personpicker.html
  lib/lp/app/javascript/picker/tests/test_personpicker.js
  lib/lp/app/widgets/popup.py
  lib/lp/app/widgets/tests/test_popup.py
  lib/lp/registry/browser/product.py
  lib/lp/services/features/flags.py

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

Thank you for this improvement. I have a few questions, but they wont block you because you have put this behind a feature flag (and this is an incremental branch.)

lines 121 and 122: <buttons> must have text for so users with screen readers can know what they do. Oh. I see the script sets the text later? Why? The buttons will only ever have one value, so you could inline the text in the string.

Why do you need a native DOM node. I see this pattern starting about line 190?
     Y.Node.getDOMNode(node.one('[id=field.name]')).value;
I expected
     node.one('[id=field.name]').get('value');

On 237 I see an image being loaded when the sprite is already loaded and the padding does not match the image:
    'style="background-image: url(/@@/person); ',
could be
    class="sprite person"

And in lines 247, maybe this should be
    class="sprite remove"

^ In both the preceding cases there is a class, but it probably duplicates sprite rules. Can we just use sprites? That is to say, this is no longer lazr.js. I see you did exactly as I expect for a new team.

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

>
> lines 121 and 122: <buttons> must have text for so users with screen readers can know what they do. Oh. I see the script sets the text later? Why? The buttons will only ever have one value, so you could inline the text in the string.
>
I can do that. The pattern I used mimicked the approach used in the
picker validation forms where the button did have different text and so
a generic form skeleton was created and the button text set later. But
here it is fixed, you are right.

> Why do you need a native DOM node. I see this pattern starting about line 190?
> Y.Node.getDOMNode(node.one('[id=field.name]')).value;
> I expected
> node.one('[id=field.name]').get('value');
>

I did indeed try the approach you suggest but get('value') kept
returning ''. I have no idea why. I'll try it again.

> On 237 I see an image being loaded when the sprite is already loaded and the padding does not match the image:
> 'style="background-image: url(/@@/person); ',
> could be
> class="sprite person"
>
> And in lines 247, maybe this should be
> class="sprite remove"
>
> ^ In both the preceding cases there is a class, but it probably duplicates sprite rules. Can we just use sprites? That is to say, this is no longer lazr.js. I see you did exactly as I expect for a new team.

That code was cut and pasted from elsewhere and put into a method for
convenience. Yes, I should have noticed the old pattern and replaced
with sprites. I'll fix.

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-06-04 14:06:41 +0000
3+++ lib/lp/app/browser/lazrjs.py 2012-06-22 14:19:22 +0000
4@@ -346,7 +346,8 @@
5 class InlinePersonEditPickerWidget(InlineEditPickerWidget):
6 def __init__(self, context, exported_field, default_html,
7 content_box_id=None, header='Select an item',
8- step_title='Search', assign_me_text='Pick me',
9+ step_title='Search', show_create_team=False,
10+ assign_me_text='Pick me',
11 remove_person_text='Remove person',
12 remove_team_text='Remove team',
13 null_display_value='None',
14@@ -372,13 +373,13 @@
15 in and when JS is off. Defaults to the edit_view on the context.
16 :param edit_title: Used to set the title attribute of the anchor.
17 :param help_link: Used to set a link for help for the widget.
18- :param target_context: The target the person is being set for.
19 """
20 super(InlinePersonEditPickerWidget, self).__init__(
21 context, exported_field, default_html, content_box_id, header,
22 step_title, null_display_value,
23 edit_view, edit_url, edit_title, help_link)
24
25+ self._show_create_team = show_create_team
26 self.assign_me_text = assign_me_text
27 self.remove_person_text = remove_person_text
28 self.remove_team_text = remove_team_text
29@@ -399,11 +400,18 @@
30 user = getUtility(ILaunchBag).user
31 return user and user in vocabulary
32
33+ @property
34+ def show_create_team(self):
35+ return (self._show_create_team
36+ and getFeatureFlag(
37+ "disclosure.add-team-person-picker.enabled"))
38+
39 def getConfig(self):
40 config = super(InlinePersonEditPickerWidget, self).getConfig()
41 config.update(dict(
42 show_remove_button=self.optional_field,
43 show_assign_me_button=self.show_assign_me_button,
44+ show_create_team=self.show_create_team,
45 assign_me_text=self.assign_me_text,
46 remove_person_text=self.remove_person_text,
47 remove_team_text=self.remove_team_text))
48
49=== modified file 'lib/lp/app/browser/tests/test_inlineeditpickerwidget.py'
50--- lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2012-01-01 02:58:52 +0000
51+++ lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2012-06-22 14:19:22 +0000
52@@ -15,6 +15,7 @@
53 InlineEditPickerWidget,
54 InlinePersonEditPickerWidget,
55 )
56+from lp.services.features.testing import FeatureFixture
57 from lp.testing import (
58 login_person,
59 TestCaseWithFactory,
60@@ -68,7 +69,7 @@
61
62 layer = DatabaseFunctionalLayer
63
64- def getWidget(self, widget_value, **kwargs):
65+ def getWidget(self, widget_value, show_create_team=False, **kwargs):
66 class ITest(Interface):
67 test_field = Choice(**kwargs)
68
69@@ -80,7 +81,8 @@
70
71 context = Test()
72 return InlinePersonEditPickerWidget(
73- context, ITest['test_field'], None, edit_url='fake')
74+ context, ITest['test_field'], None, edit_url='fake',
75+ show_create_team=show_create_team)
76
77 def test_person_selected_value_meta(self):
78 # The widget has the correct meta value for a person value.
79@@ -115,3 +117,19 @@
80 None, vocabulary='TargetPPAs', required=True)
81 login_person(self.factory.makePerson())
82 self.assertFalse(widget.config['show_assign_me_button'])
83+
84+ def test_show_create_team_link_with_feature_flag(self):
85+ with FeatureFixture(
86+ {'disclosure.add-team-person-picker.enabled': 'true'}):
87+ widget = self.getWidget(
88+ None, vocabulary='ValidPersonOrTeam', required=True,
89+ show_create_team=True)
90+ login_person(self.factory.makePerson())
91+ self.assertTrue(widget.config['show_create_team'])
92+
93+ def test_show_create_team_link(self):
94+ widget = self.getWidget(
95+ None, vocabulary='ValidPersonOrTeam', required=True,
96+ show_create_team=True)
97+ login_person(self.factory.makePerson())
98+ self.assertFalse(widget.config['show_create_team'])
99
100=== modified file 'lib/lp/app/javascript/picker/person_picker.js'
101--- lib/lp/app/javascript/picker/person_picker.js 2012-06-19 16:21:02 +0000
102+++ lib/lp/app/javascript/picker/person_picker.js 2012-06-22 14:19:22 +0000
103@@ -16,8 +16,60 @@
104 initializer: function(cfg) {
105 // If the user isn't logged in, override the show_assign_me value.
106 if (!Y.Lang.isValue(LP.links.me)) {
107- this.set('show_assign_me_button', false);
108+ this.set('show_assign_me_button', false);
109 }
110+ this.set('new_team_template', this._new_team_template());
111+ this.set('new_team_form', this._new_team_form());
112+ },
113+
114+ _new_team_template: function() {
115+ return [
116+ '<div class="new-team-node">',
117+ '<div class="step-on" style="width: 100%;"></div>',
118+ '<div class="transparent important-notice-popup">',
119+ '{{> new_team_form}}',
120+ '<div class="extra-form-buttons">',
121+ '<button class="yes_button" type="button"></button>',
122+ '<button class="no_button" type="button"></button>',
123+ '</div>',
124+ '</div>',
125+ '</div>'].join('');
126+ },
127+
128+ _new_team_form: function() {
129+ // TODO - get the form using ++form++
130+ return [
131+ "<table id='launchpad-form-widgets' class='form'>",
132+ "<tbody><tr><td colspan='2'><div>",
133+ "<label for='field.name'>Name:</label><div>",
134+ "<input type='text' value='' size='20'",
135+ " name='field.name' id='field.name'",
136+ " class='lowerCaseText textType'></div>",
137+ "<p class='formHelp'>",
138+ " A short unique name, beginning with a lower-case letter",
139+ " or number, and containing only letters, numbers, dots,",
140+ " hyphens, or plus signs.</p>",
141+ "</div></td></tr><tr><td colspan='2'><div>",
142+ "<label for='field.displayname'>Display Name:</label><div>",
143+ "<input type='text' value='' size='20'",
144+ " name='field.displayname' id='field.displayname'",
145+ " class='textType'></div>",
146+ "<p class='formHelp'>",
147+ " This team's name as you would like it displayed",
148+ " throughout Launchpad.</p>",
149+ "</div></td></tr><tr><td colspan='2'><div>",
150+ "<label for='field.visibility'>Visibility:</label>",
151+ "<div><div><div class='value'>",
152+ "<select size='1'",
153+ " name='field.visibility' id='field.visibility'>",
154+ "<option value='PUBLIC' selected='selected'>Public</option>",
155+ "<option value='PRIVATE'>Private</option></select></div>",
156+ "</div></div><p class='formHelp'>",
157+ " Anyone can see a public team's data. Only team members",
158+ " and Launchpad admins can see private team data.",
159+ " Private teams cannot become public.</p>",
160+ "</div></td></tr></tbody></table>"
161+ ].join('');
162 },
163
164 hide: function() {
165@@ -31,10 +83,11 @@
166 },
167
168 _update_button_text: function() {
169+ var link_text;
170 if (this.get('selected_value_metadata') === 'team') {
171- var link_text = this.get('remove_team_text');
172+ link_text = this.get('remove_team_text');
173 } else {
174- var link_text = this.get('remove_person_text');
175+ link_text = this.get('remove_person_text');
176 }
177 this.remove_button.set('text', link_text);
178 },
179@@ -75,32 +128,106 @@
180 });
181 },
182
183+ _cancel_new_team: function(picker) {
184+ var node = picker.get('contentBox').one('.new-team-node');
185+ picker.hide_extra_content(node);
186+ },
187+
188+ _save_new_team: function(picker) {
189+ var node = picker.get('contentBox').one('.new-team-node');
190+ var team_name = Y.Node.getDOMNode(node.one('[id=field.name]')).value;
191+ var team_display_name =
192+ Y.Node.getDOMNode(node.one('[id=field.displayname]')).value;
193+ picker.hide_extra_content(node);
194+ // TODO - make back end call to save team
195+ var value = {
196+ "api_uri": "/~" + team_name,
197+ "title": team_display_name,
198+ "value": team_name,
199+ "metadata": "team"};
200+ picker.fire('validate', value);
201+ },
202+
203+ show_new_team_form: function () {
204+ var partials = {new_team_form: this.get('new_team_form')};
205+ var html = Y.lp.mustache.to_html(
206+ this.get('new_team_template'), {}, partials);
207+ var self = this;
208+ var button_callback = function(e, callback_fn) {
209+ e.halt();
210+ if (Y.Lang.isFunction(callback_fn) ) {
211+ callback_fn(self);
212+ }
213+ };
214+ var team_form_node = Y.Node.create(html);
215+ team_form_node.one(".yes_button")
216+ .set('text', 'Create Team')
217+ .on('click', function(e) {
218+ button_callback(e, self._save_new_team);
219+ });
220+
221+ team_form_node.one(".no_button")
222+ .set('text', 'Cancel')
223+ .on('click', function(e) {
224+ button_callback(e, self._cancel_new_team);
225+ });
226+ this.get('contentBox').one('.yui3-widget-bd')
227+ .insert(team_form_node, 'before');
228+ this.show_extra_content(
229+ team_form_node.one(".important-notice-popup"),
230+ "Enter new team details");
231+ },
232+
233+ _assign_me_button_html: function() {
234+ return [
235+ '<a class="yui-picker-assign-me-button bg-image ',
236+ 'js-action" href="javascript:void(0)" ',
237+ 'style="background-image: url(/@@/person); ',
238+ 'padding-right: 1em">',
239+ this.get('assign_me_text'),
240+ '</a>'].join('');
241+ },
242+
243+ _remove_button_html: function() {
244+ return [
245+ '<a class="yui-picker-remove-button bg-image js-action" ',
246+ 'href="javascript:void(0)" ',
247+ 'style="background-image: url(/@@/remove); ',
248+ 'padding-right: 1em">',
249+ this.get('remove_person_text'),
250+ '</a>'].join('');
251+ },
252+
253+ _new_team_button_html: function() {
254+ return [
255+ '<a class="yui-picker-new-team-button sprite add ',
256+ 'js-action" href="javascript:void(0)">',
257+ 'New Team',
258+ '</a>'].join('');
259+ },
260 renderUI: function() {
261 Y.lazr.picker.Picker.prototype.renderUI.apply(this, arguments);
262 var extra_buttons = this.get('extra_buttons');
263- var remove_button, assign_me_button;
264+ var remove_button, assign_me_button, new_team_button;
265
266 if (this.get('show_remove_button')) {
267- remove_button = Y.Node.create(
268- '<a class="yui-picker-remove-button bg-image" ' +
269- 'href="javascript:void(0)" ' +
270- 'style="background-image: url(/@@/remove); padding-right: ' +
271- '1em">' + this.get('remove_person_text') + '</a>');
272+ remove_button = Y.Node.create(this._remove_button_html());
273 remove_button.on('click', this.remove, this);
274 extra_buttons.appendChild(remove_button);
275 this.remove_button = remove_button;
276 }
277
278 if (this.get('show_assign_me_button')) {
279- assign_me_button = Y.Node.create(
280- '<a class="yui-picker-assign-me-button bg-image" ' +
281- 'href="javascript:void(0)" ' +
282- 'style="background-image: url(/@@/person)">' +
283- this.get('assign_me_text') + '</a>');
284+ assign_me_button = Y.Node.create(this._assign_me_button_html());
285 assign_me_button.on('click', this.assign_me, this);
286 extra_buttons.appendChild(assign_me_button);
287 this.assign_me_button = assign_me_button;
288 }
289+ if (this.get('show_create_team')) {
290+ new_team_button = Y.Node.create(this._new_team_button_html());
291+ new_team_button.on('click', this.show_new_team_form, this);
292+ extra_buttons.appendChild(new_team_button);
293+ }
294 this._search_input.insert(
295 extra_buttons, this._search_input.get('parentNode'));
296 this._show_hide_buttons();
297@@ -112,15 +239,18 @@
298 ATTRS: {
299 extra_buttons: {
300 valueFn: function () {
301- return Y.Node.create('<div class="extra-form-buttons"/>')
302- }
303+ return Y.Node.create('<div class="extra-form-buttons"/>');
304+ }
305 },
306 show_assign_me_button: { value: true },
307 show_remove_button: {value: true },
308 assign_me_text: {value: 'Pick me'},
309 remove_person_text: {value: 'Remove person'},
310 remove_team_text: {value: 'Remove team'},
311- min_search_chars: {value: 2}
312+ min_search_chars: {value: 2},
313+ show_create_team: {value: false},
314+ new_team_template: {value: null},
315+ new_team_form: {value: null}
316 }
317 });
318-}, "0.1", {"requires": ["base", "node", "lazr.picker"]});
319+}, "0.1", {"requires": ["base", "node", "lazr.picker", "lp.mustache"]});
320
321=== modified file 'lib/lp/app/javascript/picker/picker.js'
322--- lib/lp/app/javascript/picker/picker.js 2012-06-20 16:38:28 +0000
323+++ lib/lp/app/javascript/picker/picker.js 2012-06-22 14:19:22 +0000
324@@ -20,7 +20,7 @@
325 */
326 ns.Picker = Y.Base.create('picker', Y.lazr.PrettyOverlay, [], {
327
328- /**
329+ /**
330 * The search input node.
331 *
332 * @property _search_button
333@@ -823,6 +823,61 @@
334 },
335
336 /*
337+ * Insert the extra content into the form and animate its appearance.
338+ */
339+ show_extra_content: function(extra_content, header) {
340+ if (Y.Lang.isValue(header)) {
341+ this.set('picker_header', this.get('headerContent'));
342+ this.set(
343+ 'headerContent',
344+ Y.Node.create("<h2></h2>").set('text', header));
345+ }
346+ this.get('contentBox').one('.yui3-widget-bd').hide();
347+ this.get('contentBox').all('.steps').hide();
348+ var duration = 0;
349+ if (this.get('use_animation')) {
350+ duration = 0.9;
351+ }
352+ var fade_in = new Y.Anim({
353+ node: extra_content,
354+ to: {opacity: 1},
355+ duration: duration
356+ });
357+ fade_in.run();
358+ },
359+
360+ hide_extra_content: function(extra_content_node) {
361+ var saved_header = this.get('picker_header');
362+ if (Y.Lang.isValue(saved_header)) {
363+ this.set('headerContent', saved_header);
364+ this.set('picker_header', null);
365+ }
366+ this.get('contentBox').all('.steps').show();
367+ var content_node = this.get('contentBox').one('.yui3-widget-bd');
368+ if (extra_content_node !== null) {
369+ extra_content_node.get('parentNode')
370+ .removeChild(extra_content_node);
371+ content_node.addClass('transparent');
372+ content_node.setStyle('opacity', 0);
373+ content_node.show();
374+ var duration = 0;
375+ if (this.get('use_animation')) {
376+ duration = 0.6;
377+ }
378+ var content_fade_in = new Y.Anim({
379+ node: content_node,
380+ to: {opacity: 1},
381+ duration: duration
382+ });
383+ content_fade_in.run();
384+ } else {
385+ content_node.removeClass('transparent');
386+ content_node.setStyle('opacity', 1);
387+ content_node.show();
388+ }
389+ },
390+
391+ /*
392 * Clear all elements of the picker, resetting it to its original state.
393 *
394 * @method _clear
395@@ -970,8 +1025,8 @@
396 clear_on_cancel: { value: false },
397
398 /**
399- * A CSS selector for the DOM element that will activate (show) the picker
400- * once clicked.
401+ * A CSS selector for the DOM element that will activate (show) the
402+ * picker once clicked.
403 *
404 * @attribute picker_activator
405 * @type String
406@@ -979,8 +1034,8 @@
407 picker_activator: {},
408
409 /**
410- * An extra CSS class to be added to the picker_activator, generally used
411- * to distinguish regular links from js-triggering ones.
412+ * An extra CSS class to be added to the picker_activator, generally
413+ * used to distinguish regular links from js-triggering ones.
414 *
415 * @attribute picker_activator_css_class
416 * @type String
417@@ -998,8 +1053,8 @@
418 min_search_chars: { value: 3 },
419
420 /**
421- * The current search string, which is needed when clicking on a different
422- * batch if the search input has been modified.
423+ * The current search string, which is needed when clicking on a
424+ * different batch if the search input has been modified.
425 *
426 * @attribute current_search_string
427 * @type String
428@@ -1015,8 +1070,8 @@
429 current_filter_value: {value: null},
430
431 /**
432- * A list of attribute name values used to construct the filtering options
433- * for this picker..
434+ * A list of attribute name values used to construct the filtering
435+ * options for this picker.
436 *
437 * @attribute filter_options
438 * @type Object
439@@ -1079,7 +1134,7 @@
440 * this value automatically updates the display.
441 *
442 * This an array of object containing the two keys, name (used as
443- * the batch label) and value (used as additional details to 'search'
444+ * the batch label) and value (used as additional details to 'search'
445 * event).
446 *
447 * @attribute batches
448@@ -1091,9 +1146,9 @@
449 * For simplified batch creation, you can set this to the number of
450 * batches in the search results. In this case, the batch labels
451 * and values are automatically calculated. The batch name (used as the
452- * batch label) will be the batch number starting from 1. The batch value
453- * (used as additional details to the 'search' event) will be the batch
454- * number, starting from zero.
455+ * batch label) will be the batch number starting from 1. The batch
456+ * value (used as additional details to the 'search' event) will be the
457+ * batch number, starting from zero.
458 *
459 * If 'batches' is set (see above), batch_count is ignored.
460 *
461@@ -1139,8 +1194,8 @@
462 error: { value: null },
463
464 /**
465- * The message to display when the search returned no results. This string
466- * can contain a 'query' placeholder
467+ * The message to display when the search returned no results.
468+ * This string can contain a 'query' placeholder
469 *
470 * @attribute no_results_search_message
471 * @type String
472@@ -1148,6 +1203,17 @@
473 */
474 no_results_search_message: {
475 value: 'No items matched "{query}".'
476+ },
477+
478+ /**
479+ * Whether to use animations (fade in/out) for content rendering.
480+ *
481+ * @attribute use_animation
482+ * @type Boolean
483+ * @default true
484+ */
485+ use_animation: {
486+ value: true
487 }
488 }
489 });
490@@ -1186,7 +1252,7 @@
491 this.get('host').setAttrs({
492 selected_value_metadata: result.metadata,
493 selected_value: result.value
494- })
495+ });
496 input.set("value", result.value || '');
497 // If the search input isn't blurred before it is focused,
498 // then the I-beam disappears.
499
500=== modified file 'lib/lp/app/javascript/picker/picker_patcher.js'
501--- lib/lp/app/javascript/picker/picker_patcher.js 2012-06-18 21:52:25 +0000
502+++ lib/lp/app/javascript/picker/picker_patcher.js 2012-06-22 14:19:22 +0000
503@@ -24,9 +24,21 @@
504 if (show_widget_node.hasClass('js-action')) {
505 return;
506 }
507- show_widget_node.set('innerHTML', 'Choose&hellip;');
508- show_widget_node.addClass('js-action');
509- show_widget_node.get('parentNode').removeClass('unseen');
510+ var picker_span = show_widget_node.get('parentNode');
511+ if (config.enhanced_picker) {
512+ var new_node = Y.Node.create('<span>(<a href="#"></a>)</span>');
513+ show_widget_node = new_node.one('a');
514+ show_widget_node
515+ .set('id', show_widget_id)
516+ .addClass('js-action')
517+ .set('text', 'Choose\u2026');
518+ picker_span.empty();
519+ picker_span.appendChild(new_node);
520+ } else {
521+ show_widget_node.set('text', 'Choose\u2026');
522+ show_widget_node.addClass('js-action');
523+ }
524+ picker_span.removeClass('unseen');
525 show_widget_node.on('click', function (e) {
526 if (picker === null) {
527 picker = namespace.create(
528@@ -261,7 +273,7 @@
529
530 node.one(".validation-content-placeholder").replace(content);
531 picker.get('contentBox').one('.yui3-widget-bd').insert(node, 'before');
532- animate_validation_content(picker, node.one(".important-notice-popup"));
533+ picker.show_extra_content(node.one(".important-notice-popup"));
534 };
535
536 /*
537@@ -298,42 +310,11 @@
538 };
539
540 /*
541- * Insert the validation content into the form and animate its appearance.
542- */
543-function animate_validation_content(picker, validation_content) {
544- picker.get('contentBox').one('.yui3-widget-bd').hide();
545- picker.get('contentBox').all('.steps').hide();
546- var validation_fade_in = new Y.Anim({
547- node: validation_content,
548- to: {opacity: 1},
549- duration: 0.9
550- });
551- validation_fade_in.run();
552-}
553-
554-/*
555 * Restore a picker to its functional state after a validation operation.
556 */
557 function reset_form(picker) {
558- picker.get('contentBox').all('.steps').show();
559 var validation_node = picker.get('contentBox').one('.validation-node');
560- var content_node = picker.get('contentBox').one('.yui3-widget-bd');
561- if (validation_node !== null) {
562- validation_node.get('parentNode').removeChild(validation_node);
563- content_node.addClass('transparent');
564- content_node.setStyle('opacity', 0);
565- content_node.show();
566- var content_fade_in = new Y.Anim({
567- node: content_node,
568- to: {opacity: 1},
569- duration: 0.6
570- });
571- content_fade_in.run();
572- } else {
573- content_node.removeClass('transparent');
574- content_node.setStyle('opacity', 1);
575- content_node.show();
576- }
577+ picker.hide_extra_content(validation_node);
578 }
579
580
581
582=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
583--- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-18 21:52:51 +0000
584+++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-22 14:19:22 +0000
585@@ -36,6 +36,8 @@
586 <script type="text/javascript"
587 src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
588 <script type="text/javascript"
589+ src="../../../../../../build/js/lp/app/mustache.js"></script>
590+ <script type="text/javascript"
591 src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
592 <script type="text/javascript"
593 src="../../../../../../build/js/lp/app/effects/effects.js"></script>
594
595=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
596--- lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-20 16:38:28 +0000
597+++ lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-22 14:19:22 +0000
598@@ -3,7 +3,7 @@
599
600 YUI().use('test', 'console', 'plugin',
601 'lazr.picker', 'lazr.person-picker', 'lp.app.picker',
602- 'node-event-simulate', function(Y) {
603+ 'lp.app.mustache', 'node-event-simulate', function(Y) {
604
605 var Assert = Y.Assert;
606
607@@ -92,11 +92,12 @@
608 },
609
610 _picker_params: function(
611- show_assign_me_button, show_remove_button,
612+ show_assign_me_button, show_remove_button, show_create_team,
613 selected_value, selected_value_metadata) {
614 return {
615 "show_assign_me_button": show_assign_me_button,
616 "show_remove_button": show_remove_button,
617+ "show_create_team": show_create_team,
618 "selected_value": selected_value,
619 "selected_value_metadata": selected_value_metadata
620 };
621@@ -185,10 +186,8 @@
622
623 test_picker_remove_person_button_text: function() {
624 // The remove button text is correct.
625- this.create_picker(this._picker_params(true,
626- true,
627- "fred",
628- "person"));
629+ this.create_picker(this._picker_params(
630+ true, true, false, "fred", "person"));
631 this.picker.render();
632 var remove_button = Y.one('.yui-picker-remove-button');
633 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
634@@ -196,7 +195,8 @@
635
636 test_picker_remove_team_button_text: function() {
637 // The remove button text is correct.
638- this.create_picker(this._picker_params(true, true, "cats", "team"));
639+ this.create_picker(this._picker_params(
640+ true, true, false, "cats", "team"));
641 this.picker.render();
642 var remove_button = Y.one('.yui-picker-remove-button');
643 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
644@@ -220,7 +220,8 @@
645 test_picker_no_assign_me_button_if_value_is_me: function() {
646 // The assign me button is not shown if the picker is created for a
647 // field where the value is "me".
648- this.create_picker(this._picker_params(true, true, "me"), this.ME);
649+ this.create_picker(this._picker_params(
650+ true, true, false, "me"), this.ME);
651 this.picker.render();
652 this._check_assign_me_button_state(false);
653 },
654@@ -236,7 +237,8 @@
655 test_picker_has_remove_button_if_value: function() {
656 // The remove button is shown if the picker is created for a field
657 // which has a value.
658- this.create_picker(this._picker_params(true, true, "me"), this.ME);
659+ this.create_picker(this._picker_params(
660+ true, true, false, "me"), this.ME);
661 this.picker.render();
662 this._check_remove_button_state(true);
663 },
664@@ -244,9 +246,151 @@
665 test_picker_no_remove_button_unless_configured: function() {
666 // The remove button is only rendered if show_remove_button
667 // setting is true.
668- this.create_picker(this._picker_params(true, false, "me"), this.ME);
669+ this.create_picker(this._picker_params(
670+ true, false, false, "me"), this.ME);
671 this.picker.render();
672 Assert.isNull(Y.one('.yui-picker-remove-button'));
673+ },
674+
675+ test_picker_assign_me_button_hide_on_save: function() {
676+ // The assign me button is shown initially but hidden if the picker
677+ // saves a value equal to 'me'.
678+ this.create_picker(this._picker_params(true, true));
679+ this._check_assign_me_button_state(true);
680+ this.picker.set('results', this.vocabulary);
681+ this.picker.render();
682+ simulate(
683+ this.picker.get('boundingBox').one('.yui3-picker-results'),
684+ 'li:nth-child(1)', 'click');
685+ this._check_assign_me_button_state(false);
686+ },
687+
688+ test_picker_remove_button_clicked: function() {
689+ // The remove button is hidden once a picker value has been removed.
690+ // And the assign me button is shown.
691+ this.create_picker(this._picker_params(
692+ true, true, false, "me"), this.ME);
693+ this.picker.render();
694+ this._check_assign_me_button_state(false);
695+ var remove = Y.one('.yui-picker-remove-button');
696+ remove.simulate('click');
697+ this._check_remove_button_state(false);
698+ this._check_assign_me_button_state(true);
699+ },
700+
701+ test_picker_assign_me_button_clicked: function() {
702+ // The assign me button is hidden once it is clicked.
703+ // And the remove button is shown.
704+ this.create_picker(this._picker_params(true, true));
705+ this.picker.render();
706+ var assign_me = Y.one('.yui-picker-assign-me-button');
707+ assign_me.simulate('click');
708+ this._check_remove_button_state(true);
709+ this._check_assign_me_button_state(false);
710+ },
711+
712+ test_picker_assign_me_updates_remove_text: function() {
713+ // When Assign me is used, the Remove button text is updated from
714+ // the team removal text to the person removal text.
715+ this.create_picker(this._picker_params(
716+ true, true, false, "cats", "team"));
717+ this.picker.render();
718+ var remove_button = Y.one('.yui-picker-remove-button');
719+ Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
720+ var assign_me = Y.one('.yui-picker-assign-me-button');
721+ assign_me.simulate('click');
722+ Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
723+ },
724+
725+ test_picker_save_updates_remove_text: function() {
726+ // When save is called, the Remove button text is updated
727+ // according to the newly saved value.
728+ this.create_picker(this._picker_params(
729+ true, true, false, "me"), this.ME);
730+ var remove_button = Y.one('.yui-picker-remove-button');
731+ Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
732+ this.picker.set('results', this.vocabulary);
733+ this.picker.render();
734+ simulate(
735+ this.picker.get('boundingBox').one('.yui3-picker-results'),
736+ 'li:nth-child(2)', 'click');
737+ Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
738+ },
739+
740+ test_picker_no_team_button_unless_configured: function() {
741+ // The new team button is only rendered if show_create_team
742+ // setting is true.
743+ this.create_picker(this._picker_params(true, false, false));
744+ this.picker.render();
745+ Assert.isNull(Y.one('.yui-picker-new-team-button'));
746+ },
747+
748+ test_picker_new_team_button_click_shows_form: function() {
749+ // Clicking the new team button displays the new team form.
750+ this.create_picker(this._picker_params(true, true, true));
751+ this.picker.render();
752+ var new_team = this.picker.get('boundingBox')
753+ .one('.yui-picker-new-team-button');
754+ new_team.simulate('click');
755+ Y.Assert.areEqual(
756+ 'Enter new team details',
757+ this.picker.get('headerContent').get('text'));
758+ Y.Assert.isNotNull(
759+ this.picker.get('contentBox').one('[id=field.name]'));
760+ Y.Assert.areEqual('none',
761+ this.picker.get('contentBox').one('.yui3-widget-bd')
762+ .getStyle('display'));
763+ },
764+
765+ test_picker_new_team_cancel: function() {
766+ // Clicking the cancel button on the new team form reverts back to
767+ // the normal picker.
768+ this.create_picker(this._picker_params(true, true, true));
769+ this.picker.render();
770+ var new_team = this.picker.get('boundingBox')
771+ .one('.yui-picker-new-team-button');
772+ new_team.simulate('click');
773+ Y.Assert.areEqual(
774+ 'Enter new team details',
775+ this.picker.get('headerContent').get('text'));
776+ var form_buttons = this.picker.get('contentBox')
777+ .one('.extra-form-buttons');
778+ simulate(
779+ form_buttons, 'button:nth-child(2)', 'click');
780+ Y.Assert.areEqual(
781+ 'Pick Someone',
782+ this.picker.get('headerContent').get('text'));
783+ Y.Assert.isNull(
784+ this.picker.get('contentBox').one('[id=field.name]'));
785+ Y.Assert.isNotNull(
786+ this.picker.get('contentBox').one('.yui3-picker-search'));
787+ },
788+
789+ test_picker_new_team_save: function() {
790+ // Clicking the save button on the new team form fires a 'save'
791+ // event with the expected data.
792+ this.create_picker(this._picker_params(true, true, true));
793+ this.picker.render();
794+
795+ var result_published = false;
796+ this.picker.subscribe('save', function(e) {
797+ var saved_value =
798+ e.details[Y.lazr.picker.Picker.SAVE_RESULT];
799+ Y.Assert.areEqual('/~fred', saved_value.api_uri);
800+ Y.Assert.areEqual('fred', saved_value.value);
801+ result_published = true;
802+ });
803+
804+ var picker_content = this.picker.get('boundingBox');
805+ var new_team =
806+ picker_content.one('.yui-picker-new-team-button');
807+ new_team.simulate('click');
808+ var team_name = picker_content.one('[id=field.name]');
809+ Y.Node.getDOMNode(team_name).value = 'fred';
810+ var form_buttons = picker_content.one('.extra-form-buttons');
811+ simulate(
812+ form_buttons, 'button:nth-child(1)', 'click');
813+ Y.Assert.isTrue(result_published);
814 }
815 };
816
817@@ -266,6 +410,7 @@
818 }
819
820 var config = {
821+ "use_animation": false,
822 "picker_type": "person",
823 "step_title": "Choose someone",
824 "header": "Pick Someone",
825@@ -275,6 +420,7 @@
826 "show_remove_button": params.show_remove_button,
827 "selected_value": params.selected_value,
828 "selected_value_metadata": params.selected_value_metadata,
829+ "show_create_team": params.show_create_team,
830 "assign_me_text": "Assign Moi",
831 "remove_person_text": "Remove someone",
832 "remove_team_text": "Remove some team"
833@@ -285,69 +431,6 @@
834 "test_link",
835 "picker_id",
836 config);
837- console.log(this.picker);
838- },
839-
840- test_picker_assign_me_button_hide_on_save: function() {
841- // The assign me button is shown initially but hidden if the picker
842- // saves a value equal to 'me'.
843- this.create_picker(this._picker_params(true, true));
844- this._check_assign_me_button_state(true);
845- this.picker.set('results', this.vocabulary);
846- this.picker.render();
847- simulate(
848- this.picker.get('boundingBox').one('.yui3-picker-results'),
849- 'li:nth-child(1)', 'click');
850- this._check_assign_me_button_state(false);
851- },
852-
853- test_picker_remove_button_clicked: function() {
854- // The remove button is hidden once a picker value has been removed.
855- // And the assign me button is shown.
856- this.create_picker(this._picker_params(true, true, "me"), this.ME);
857- this.picker.render();
858- this._check_assign_me_button_state(false);
859- var remove = Y.one('.yui-picker-remove-button');
860- remove.simulate('click');
861- this._check_remove_button_state(false);
862- this._check_assign_me_button_state(true);
863- },
864-
865- test_picker_assign_me_button_clicked: function() {
866- // The assign me button is hidden once it is clicked.
867- // And the remove button is shown.
868- this.create_picker(this._picker_params(true, true));
869- this.picker.render();
870- var assign_me = Y.one('.yui-picker-assign-me-button');
871- assign_me.simulate('click');
872- this._check_remove_button_state(true);
873- this._check_assign_me_button_state(false);
874- },
875-
876- test_picker_assign_me_updates_remove_text: function() {
877- // When Assign me is used, the Remove button text is updated from
878- // the team removal text to the person removal text.
879- this.create_picker(this._picker_params(true, true, "cats", "team"));
880- this.picker.render();
881- var remove_button = Y.one('.yui-picker-remove-button');
882- Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
883- var assign_me = Y.one('.yui-picker-assign-me-button');
884- assign_me.simulate('click');
885- Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
886- },
887-
888- test_picker_save_updates_remove_text: function() {
889- // When save is called, the Remove button text is updated
890- // according to the newly saved value.
891- this.create_picker(this._picker_params(true, true, "me"), this.ME);
892- var remove_button = Y.one('.yui-picker-remove-button');
893- Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
894- this.picker.set('results', this.vocabulary);
895- this.picker.render();
896- simulate(
897- this.picker.get('boundingBox').one('.yui3-picker-results'),
898- 'li:nth-child(2)', 'click');
899- Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
900 }
901 };
902
903@@ -377,9 +460,11 @@
904 }
905 var config = {
906 "picker_type": "person",
907+ "header": "Pick Someone",
908 "associated_field_id": associated_field_id,
909 "show_assign_me_button": params.show_assign_me_button,
910 "show_remove_button": params.show_remove_button,
911+ "show_create_team": params.show_create_team,
912 "selected_value": params.selected_value,
913 "selected_value_metadata": params.selected_value_metadata,
914 "assign_me_text": "Assign Moi",
915@@ -388,68 +473,6 @@
916 };
917 this.picker = Y.lp.app.picker.create(
918 this.vocabulary, config, associated_field_id);
919- },
920-
921- test_picker_assign_me_button_hide_on_save: function() {
922- // The assign me button is shown initially but hidden if the picker
923- // saves a value equal to 'me'.
924- this.create_picker(this._picker_params(true, true));
925- this._check_assign_me_button_state(true);
926- this.picker.set('results', this.vocabulary);
927- this.picker.render();
928- simulate(
929- this.picker.get('boundingBox').one('.yui3-picker-results'),
930- 'li:nth-child(1)', 'click');
931- this._check_assign_me_button_state(false);
932- },
933-
934- test_picker_remove_button_clicked: function() {
935- // The remove button is hidden once a picker value has been removed.
936- // And the assign me button is shown.
937- this.create_picker(this._picker_params(true, true, "me"), this.ME);
938- this.picker.render();
939- this._check_assign_me_button_state(false);
940- var remove = Y.one('.yui-picker-remove-button');
941- remove.simulate('click');
942- this._check_remove_button_state(false);
943- this._check_assign_me_button_state(true);
944- },
945-
946- test_picker_assign_me_button_clicked: function() {
947- // The assign me button is hidden once it is clicked.
948- // And the remove button is shown.
949- this.create_picker(this._picker_params(true, true));
950- this.picker.render();
951- var assign_me = Y.one('.yui-picker-assign-me-button');
952- assign_me.simulate('click');
953- this._check_remove_button_state(true);
954- this._check_assign_me_button_state(false);
955- },
956-
957- test_picker_assign_me_updates_remove_text: function() {
958- // When Assign me is used, the Remove button text is updated from
959- // the team removal text to the person removal text.
960- this.create_picker(this._picker_params(true, true, "cats", "team"));
961- this.picker.render();
962- var remove_button = Y.one('.yui-picker-remove-button');
963- Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
964- var assign_me = Y.one('.yui-picker-assign-me-button');
965- assign_me.simulate('click');
966- Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
967- },
968-
969- test_picker_save_updates_remove_text: function() {
970- // When save is called, the Remove button text is updated
971- // according to the newly saved value.
972- this.create_picker(this._picker_params(true, true, "me"), this.ME);
973- var remove_button = Y.one('.yui-picker-remove-button');
974- Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
975- this.picker.set('results', this.vocabulary);
976- this.picker.render();
977- simulate(
978- this.picker.get('boundingBox').one('.yui3-picker-results'),
979- 'li:nth-child(2)', 'click');
980- Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
981 }
982 };
983
984
985=== modified file 'lib/lp/app/widgets/popup.py'
986--- lib/lp/app/widgets/popup.py 2012-02-22 05:22:13 +0000
987+++ lib/lp/app/widgets/popup.py 2012-06-22 14:19:22 +0000
988@@ -23,6 +23,7 @@
989 get_person_picker_entry_metadata,
990 vocabulary_filters,
991 )
992+from lp.services.features import getFeatureFlag
993 from lp.services.propertycache import cachedproperty
994 from lp.services.webapp import canonical_url
995
996@@ -41,6 +42,7 @@
997 assign_me_text = 'Pick me'
998 remove_person_text = 'Remove person'
999 remove_team_text = 'Remove team'
1000+ show_create_team_link = False
1001
1002 popup_name = 'popup-vocabulary-picker'
1003
1004@@ -56,6 +58,12 @@
1005 # Defaults to self.vocabulary.displayname.
1006 header = None
1007
1008+ @property
1009+ def enhanced_picker(self):
1010+ flag = getFeatureFlag(
1011+ "disclosure.add-team-person-picker.enabled")
1012+ return flag and self.show_create_team_link
1013+
1014 @cachedproperty
1015 def matches(self):
1016 """Return a list of matches (as ITokenizedTerm) to whatever the
1017@@ -143,7 +151,9 @@
1018 vocabulary_name=self.vocabulary_name,
1019 vocabulary_filters=self.vocabulary_filters,
1020 input_element=self.input_id,
1021- show_widget_id=self.show_widget_id)
1022+ show_widget_id=self.show_widget_id,
1023+ enhanced_picker=self.enhanced_picker,
1024+ show_create_team=self.enhanced_picker)
1025
1026 @property
1027 def json_config(self):
1028@@ -206,8 +216,12 @@
1029 else:
1030 css = ''
1031 return ('<span class="%s">(<a id="%s" href="%s">'
1032- 'Find&hellip;</a>)</span>') % (
1033- css, self.show_widget_id, self.nonajax_uri or '#')
1034+ 'Find&hellip;</a>)%s</span>') % (
1035+ css, self.show_widget_id, self.nonajax_uri or '#',
1036+ self.extraChooseLink() or '')
1037+
1038+ def extraChooseLink(self):
1039+ return None
1040
1041 @property
1042 def nonajax_uri(self):
1043@@ -220,7 +234,6 @@
1044
1045 class PersonPickerWidget(VocabularyPickerWidget):
1046
1047- include_create_team_link = False
1048 show_assign_me_button = True
1049 show_remove_button = False
1050 picker_type = 'person'
1051@@ -230,12 +243,11 @@
1052 val = self._getFormValue()
1053 return get_person_picker_entry_metadata(val)
1054
1055- def chooseLink(self):
1056- link = super(PersonPickerWidget, self).chooseLink()
1057- if self.include_create_team_link:
1058- link += ('or (<a href="/people/+newteam">'
1059+ def extraChooseLink(self):
1060+ if self.show_create_team_link:
1061+ return ('or (<a href="/people/+newteam">'
1062 'Create a new team&hellip;</a>)')
1063- return link
1064+ return None
1065
1066 @property
1067 def nonajax_uri(self):
1068@@ -251,10 +263,8 @@
1069 >Register an external bug tracker&hellip;</a>)
1070 """
1071
1072- def chooseLink(self):
1073- link = super(BugTrackerPickerWidget, self).chooseLink()
1074- link += self.link_template
1075- return link
1076+ def extraChooseLink(self):
1077+ return self.link_template
1078
1079 @property
1080 def nonajax_uri(self):
1081
1082=== modified file 'lib/lp/app/widgets/tests/test_popup.py'
1083--- lib/lp/app/widgets/tests/test_popup.py 2012-02-03 06:27:17 +0000
1084+++ lib/lp/app/widgets/tests/test_popup.py 2012-06-22 14:19:22 +0000
1085@@ -1,5 +1,6 @@
1086 # Copyright 2010-2011 Canonical Ltd. This software is licensed under the
1087 # GNU Affero General Public License version 3 (see the file LICENSE).
1088+from lp.services.features.testing import FeatureFixture
1089
1090 __metaclass__ = type
1091
1092@@ -168,6 +169,26 @@
1093 # But not the remove button.
1094 self.assertFalse(person_picker_widget.config['show_remove_button'])
1095
1096+ def test_create_team_link(self):
1097+ # The person picker widget shows a create team link if the feature flag
1098+ # is on.
1099+ field = ITest['test_valid.item']
1100+ bound_field = field.bind(self.context)
1101+
1102+ with FeatureFixture(
1103+ {'disclosure.add-team-person-picker.enabled': 'true'}):
1104+ picker_widget = PersonPickerWidget(
1105+ bound_field, self.vocabulary, self.request)
1106+ picker_widget.show_create_team_link = True
1107+ self.assertTrue(picker_widget.config['show_create_team'])
1108+ self.assertTrue(picker_widget.config['enhanced_picker'])
1109+
1110+ picker_widget = PersonPickerWidget(
1111+ bound_field, self.vocabulary, self.request)
1112+ picker_widget.show_create_team_link = True
1113+ self.assertFalse(picker_widget.config['show_create_team'])
1114+ self.assertFalse(picker_widget.config['enhanced_picker'])
1115+
1116 def test_widget_personvalue_meta(self):
1117 # The person picker has the correct meta value for a person value.
1118 person = self.factory.makePerson()
1119
1120=== modified file 'lib/lp/registry/browser/product.py'
1121--- lib/lp/registry/browser/product.py 2012-06-04 11:41:47 +0000
1122+++ lib/lp/registry/browser/product.py 2012-06-22 14:19:22 +0000
1123@@ -938,7 +938,7 @@
1124 self.context, IProduct['owner'],
1125 format_link(self.context.owner),
1126 header='Change maintainer', edit_view='+edit-people',
1127- step_title='Select a new maintainer')
1128+ step_title='Select a new maintainer', show_create_team=True)
1129
1130 @property
1131 def driver_widget(self):
1132@@ -946,7 +946,7 @@
1133 self.context, IProduct['driver'],
1134 format_link(self.context.driver, empty_value="Not yet selected"),
1135 header='Change driver', edit_view='+edit-people',
1136- step_title='Select a new driver',
1137+ step_title='Select a new driver', show_create_team=True,
1138 null_display_value="Not yet selected",
1139 help_link="/+help-registry/driver.html")
1140
1141@@ -2269,11 +2269,11 @@
1142 initial_values = {'transfer_to_registry': False}
1143
1144 custom_widget('owner', PersonPickerWidget, header="Select the maintainer",
1145- include_create_team_link=True)
1146+ show_create_team_link=True)
1147 custom_widget('transfer_to_registry', CheckBoxWidget,
1148 widget_class='field subordinate')
1149 custom_widget('driver', PersonPickerWidget, header="Select the driver",
1150- include_create_team_link=True)
1151+ show_create_team_link=True)
1152
1153 @property
1154 def page_title(self):
1155
1156=== modified file 'lib/lp/services/features/flags.py'
1157--- lib/lp/services/features/flags.py 2012-06-08 02:16:58 +0000
1158+++ lib/lp/services/features/flags.py 2012-06-22 14:19:22 +0000
1159@@ -223,6 +223,12 @@
1160 '',
1161 '',
1162 ''),
1163+ ('disclosure.add-team-person-picker.enabled',
1164+ 'boolean',
1165+ 'Allows users to add a new team directly from the person picker.',
1166+ '',
1167+ '',
1168+ ''),
1169 ('bugs.autoconfirm.enabled_distribution_names',
1170 'space delimited',
1171 ('Enables auto-confirming bugtasks for distributions (and their '