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

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 15542
Proposed branch: lp:~wallyworld/launchpad/new-team-picker-enhanced-form
Merge into: lp:launchpad
Diff against target: 814 lines (+265/-67)
15 files modified
lib/lp/app/javascript/choice.js (+40/-27)
lib/lp/app/javascript/picker/team.js (+8/-1)
lib/lp/app/javascript/picker/tests/test_personpicker.html (+2/-0)
lib/lp/app/javascript/picker/tests/test_team.html (+22/-0)
lib/lp/app/javascript/picker/tests/test_team.js (+57/-4)
lib/lp/bugs/javascript/tests/test_filebug.js (+16/-12)
lib/lp/registry/browser/configure.zcml (+1/-1)
lib/lp/registry/browser/distribution.py (+4/-3)
lib/lp/registry/browser/pillar.py (+30/-7)
lib/lp/registry/browser/product.py (+5/-4)
lib/lp/registry/browser/productseries.py (+2/-2)
lib/lp/registry/browser/project.py (+4/-3)
lib/lp/registry/browser/tests/test_distribution.py (+28/-1)
lib/lp/registry/browser/tests/test_product.py (+16/-0)
lib/lp/registry/browser/tests/test_projectgroup.py (+30/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/new-team-picker-enhanced-form
Reviewer Review Type Date Requested Status
Richard Harding (community) code Approve
Review via email: mp+113020@code.launchpad.net

Commit message

Enhance the New Team form inside the person picker to use a choice popup for subscription policy, enable New Team link on maintainers and drivers for all pillars.

Description of the change

== Implementation ==

This branch enhances the New Team form inside the person picker to use a choice popup for selecting the team subscription policy. It also ensures the New Team link is enabled on maintainer/driver pickers for all pillars - product, project group, distribution.

The json request cache needed to have the team subscription policy data shoved into it for the choice popup to be wired in. A PillarViewMixin is provided to do this. There was an existing PillarView base class but this should really have been called PillarInvolvementView so I renamed it to eliminate any possible confusion.

As a driveby, fix the issue where the header text on the choice popup was displaying the field name with an underscore.

== Demo ==

See screenshot:
http://people.canonical.com/~ianb/enhanced-newteam-picker.png

Do we want to leave the public/private drop down as is or make that a choice popup too to make it all look consistent? I think it looks a bit funny now with the dropdown and choice popup widgets both being used.

== Tests ==

Add tests for [Product|Distribution|ProjectGroup]View to ensure the json cache has all the required data.
Add yui tests for the enhanced new team form.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/choice.js
  lib/lp/app/javascript/picker/team.js
  lib/lp/app/javascript/picker/tests/test_team.html
  lib/lp/app/javascript/picker/tests/test_team.js
  lib/lp/archiveuploader/nascentupload.py
  lib/lp/bugs/javascript/tests/test_filebug.js
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/distribution.py
  lib/lp/registry/browser/pillar.py
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/productseries.py
  lib/lp/registry/browser/project.py
  lib/lp/registry/browser/tests/test_distribution.py
  lib/lp/registry/browser/tests/test_product.py
  lib/lp/registry/browser/tests/test_projectgroup.py

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Thanks Ian. I'm reviewing the code, but leave the question on the select dropdown to others. It seems a bit simple to turn over into the larger UI widget, but there's something to be said for consistant look/feel.

#9
The var is already not attached to the namespace so it's forced private. I wouldn't prefix it with an underscore unless it was attached to an object instance and you wanted to indicate it should be private on that object.

#158
subscriptionpolicyedit is a bit hard on the eyes. I generally find that once a name gets past two distinct words, breaking up with _ helps. For instance, I notice you used team_subscriptionpolicy_data, and think that'd be a good standard to keep with. subscriptionpolicy_XXXXX.

#775
Typo: grupo

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

Wow, quick review, thanks.

> Thanks Ian. I'm reviewing the code, but leave the question on the select dropdown to others. It seems a bit simple to turn over into the larger UI widget, but there's something to be said for consistant look/feel.
>

I'll land as is and fix the dropdown if needed. It's behind a feature
flag anyway.

>
> #775
> Typo: grupo
>

Perhaps. I used the same project group name as was used in another test
in the same module and thought that maybe the author was using a
humorous name. I'll change both to avoid future questions :-)

>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/choice.js'
2--- lib/lp/app/javascript/choice.js 2012-06-27 14:05:07 +0000
3+++ lib/lp/app/javascript/choice.js 2012-07-03 07:13:21 +0000
4@@ -56,54 +56,65 @@
5 widget.render();
6 };
7
8+// The default configuration used for wiring choice popup widgets.
9+var default_popup_choice_config = {
10+ container: Y,
11+ render_immediately: true,
12+ show_description: false,
13+ field_title: null
14+};
15+
16 /**
17 * Replace a legacy input widget with a popup choice widget.
18 * @param legacy_node the YUI node containing the legacy widget.
19 * @param field_name the Launchpad form field name.
20 * @param choices the choices for the popup choice widget.
21- * @param show_description whether to show the selected value's description.
22+ * @param cfg configuration for the wiring action.
23 * @param get_value_fn getter for the legacy widget's value.
24 * @param set_value_fn setter for the legacy widget's value.
25 */
26-var wirePopupChoice = function(legacy_node, field_name, choices,
27- show_description, get_value_fn, set_value_fn) {
28+var wirePopupChoice = function(legacy_node, field_name, choices, cfg,
29+ get_value_fn, set_value_fn) {
30 var choice_descriptions = {};
31 Y.Array.forEach(choices, function(item) {
32 choice_descriptions[item.value] = item.description;
33 });
34 var initial_field_value = get_value_fn(legacy_node);
35 var choice_node = Y.Node.create([
36- '<span id="' + field_name + '-content"><span class="value"></span>',
37+ '<span class="' + field_name + '-content"><span class="value"></span>',
38 '<a class="sprite edit editicon action-icon"',
39 ' href="#">Edit</a></span>'
40 ].join(''));
41- if (show_description) {
42+ if (cfg.show_description) {
43 choice_node.append(Y.Node.create('<div class="formHelp"></div>'));
44 }
45-
46 legacy_node.insertBefore(choice_node, legacy_node);
47- if (show_description) {
48+ if (cfg.show_description) {
49 choice_node.one('.formHelp')
50 .set('text', choice_descriptions[initial_field_value]);
51 }
52 legacy_node.addClass('unseen');
53- var field_content = Y.one('#' + field_name + '-content');
54-
55+ if (!Y.Lang.isValue(cfg.field_title)) {
56+ cfg.field_title = field_name.replace('_', ' ');
57+ }
58 var choice_edit = new Y.ChoiceSource({
59- contentBox: field_content,
60+ contentBox: choice_node,
61 value: initial_field_value,
62- title: 'Set ' + field_name + " as",
63+ title: 'Set ' + cfg.field_title + " as",
64 items: choices,
65- elementToFlash: field_content
66+ elementToFlash: choice_node,
67+ zIndex: 1050
68 });
69- choice_edit.render();
70+ if (cfg.render_immediately) {
71+ choice_edit.render();
72+ }
73
74 var update_selected_value_css = function(selected_value) {
75 Y.Array.each(choices, function(item) {
76 if (item.value === selected_value) {
77- field_content.addClass(item.css_class);
78+ choice_node.addClass(item.css_class);
79 } else {
80- field_content.removeClass(item.css_class);
81+ choice_node.removeClass(item.css_class);
82 }
83 });
84 };
85@@ -112,21 +123,23 @@
86 var selected_value = choice_edit.get('value');
87 update_selected_value_css(selected_value);
88 set_value_fn(legacy_node, selected_value);
89- if (show_description) {
90+ if (cfg.show_description) {
91 choice_node.one('.formHelp')
92 .set('text', choice_descriptions[selected_value]);
93 }
94 });
95+ return choice_edit;
96 };
97
98 /**
99 * Replace a drop down combo box with a popup choice selection widget.
100 * @param field_name
101 * @param choices
102- * @param show_description
103+ * @param cfg
104 */
105-namespace.addPopupChoice = function(field_name, choices, show_description) {
106- var legacy_node = Y.one('[id="field.' + field_name + '"]');
107+namespace.addPopupChoice = function(field_name, choices, cfg) {
108+ cfg = Y.merge(default_popup_choice_config, cfg);
109+ var legacy_node = cfg.container.one('[id="field.' + field_name + '"]');
110 if (!Y.Lang.isValue(legacy_node)) {
111 return;
112 }
113@@ -136,19 +149,19 @@
114 var set_fn = function(node, value) {
115 node.set('value', value);
116 };
117- wirePopupChoice(
118- legacy_node, field_name, choices, show_description, get_fn, set_fn);
119+ return wirePopupChoice(
120+ legacy_node, field_name, choices, cfg, get_fn, set_fn);
121 };
122
123 /**
124 * Replace a radio button group with a popup choice selection widget.
125 * @param field_name
126 * @param choices
127- * @param show_description
128+ * @param cfg
129 */
130-namespace.addPopupChoiceForRadioButtons = function(field_name, choices,
131- show_description) {
132- var legacy_node = Y.one('[name="field.' + field_name + '"]')
133+namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {
134+ cfg = Y.merge(default_popup_choice_config, cfg);
135+ var legacy_node = cfg.container.one('[name="field.' + field_name + '"]')
136 .ancestor('table.radio-button-widget');
137 if (!Y.Lang.isValue(legacy_node)) {
138 return;
139@@ -172,8 +185,8 @@
140 }
141 });
142 };
143- wirePopupChoice(
144- legacy_node, field_name, choices, show_description, get_fn, set_fn);
145+ return wirePopupChoice(
146+ legacy_node, field_name, choices, cfg, get_fn, set_fn);
147 };
148
149 }, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins",
150
151=== modified file 'lib/lp/app/javascript/picker/team.js'
152--- lib/lp/app/javascript/picker/team.js 2012-07-02 04:16:19 +0000
153+++ lib/lp/app/javascript/picker/team.js 2012-07-03 07:13:21 +0000
154@@ -106,6 +106,12 @@
155 e.halt();
156 this.fire(ns.CANCEL_TEAM);
157 }, this);
158+ this.subscriptionpolicy_edit = Y.lp.app.choice.addPopupChoice(
159+ 'subscriptionpolicy', LP.cache.team_subscriptionpolicy_data, {
160+ container: container,
161+ render_immediately: false,
162+ field_title: 'subscription policy'
163+ });
164 container.one('.extra-form-buttons').removeClass('hidden');
165 },
166
167@@ -114,6 +120,7 @@
168 if (form_elements.size() > 0) {
169 form_elements.item(0).focus();
170 }
171+ this.subscriptionpolicy_edit.render();
172 },
173
174 hide: function() {
175@@ -214,5 +221,5 @@
176 });
177
178
179-}, "0.1", {"requires": ["base", "node"]});
180+}, "0.1", {"requires": ["base", "node", "lp.app.choice"]});
181
182
183=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
184--- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-26 03:23:20 +0000
185+++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-07-03 07:13:21 +0000
186@@ -26,6 +26,8 @@
187
188 <!-- Dependencies -->
189 <script type="text/javascript"
190+ src="../../../../../../build/js/lp/app/choice.js"></script>
191+ <script type="text/javascript"
192 src="../../../../../../build/js/lp/app/client.js"></script>
193 <script type="text/javascript"
194 src="../../../../../../build/js/lp/app/lp.js"></script>
195
196=== modified file 'lib/lp/app/javascript/picker/tests/test_team.html'
197--- lib/lp/app/javascript/picker/tests/test_team.html 2012-06-26 09:02:51 +0000
198+++ lib/lp/app/javascript/picker/tests/test_team.html 2012-07-03 07:13:21 +0000
199@@ -26,12 +26,34 @@
200
201 <!-- Dependencies -->
202 <script type="text/javascript"
203+ src="../../../../../../build/js/lp/app/choice.js"></script>
204+ <script type="text/javascript"
205 src="../../../../../../build/js/lp/app/client.js"></script>
206 <script type="text/javascript"
207+ src="../../../../../../build/js/lp/app/errors.js"></script>
208+ <script type="text/javascript"
209 src="../../../../../../build/js/lp/app/lp.js"></script>
210 <script type="text/javascript"
211 src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
212 <script type="text/javascript"
213+ src="../../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
214+ <script type="text/javascript"
215+ src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
216+ <script type="text/javascript"
217+ src="../../../../../../build/js/lp/app/anim/anim.js"></script>
218+ <script type="text/javascript"
219+ src="../../../../../../build/js/lp/app/effects/effects.js"></script>
220+ <script type="text/javascript"
221+ src="../../../../../../build/js/lp/app/expander.js"></script>
222+ <script type="text/javascript"
223+ src="../../../../../../build/js/lp/app/extras/extras.js"></script>
224+ <script type="text/javascript"
225+ src="../../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
226+ <script type="text/javascript"
227+ src="../../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
228+ <script type="text/javascript"
229+ src="../../../../../../build/js/lp/app/inlineedit/editor.js"></script>
230+ <script type="text/javascript"
231 src="../../../../../../build/js/lp/app/testing/mockio.js"></script>
232
233 <!-- The module under test. -->
234
235=== modified file 'lib/lp/app/javascript/picker/tests/test_team.js'
236--- lib/lp/app/javascript/picker/tests/test_team.js 2012-07-02 04:16:19 +0000
237+++ lib/lp/app/javascript/picker/tests/test_team.js 2012-07-03 07:13:21 +0000
238@@ -12,9 +12,19 @@
239
240
241 setUp: function() {
242+ window.LP = {
243+ links: {},
244+ cache: {
245+ team_subscriptionpolicy_data: [
246+ {name: 'Moderated', value: 'MODERATED'},
247+ {name: 'Restricted', value: 'RESTRICTED'}
248+ ]
249+ }
250+ };
251 },
252
253 tearDown: function() {
254+ delete window.LP;
255 delete this.mockio;
256 if (this.fixture !== undefined) {
257 this.fixture.empty(true);
258@@ -26,10 +36,21 @@
259 },
260
261 _simple_team_form: function() {
262- return '<table><tr><td>' +
263- '<input id="field.name" name="field.name"/>' +
264- '<input id="field.displayname" ' +
265- 'name="field.displayname"/></td></tr></table>';
266+ return [
267+ '<table><tr><td>',
268+ '<input id="field.name" name="field.name"/>',
269+ '<input id="field.displayname" ',
270+ 'name="field.displayname"/>',
271+ '<div class="value">',
272+ '<select size="1" name="field.subscriptionpolicy" ',
273+ 'id="field.subscriptionpolicy">',
274+ '<option value="RESTRICTED" ',
275+ 'selected="selected">Restricted</option>',
276+ '<option value="MODERATED">Moderated</option>',
277+ '</select>',
278+ '</div>',
279+ '</td></tr></table>'
280+ ].join('');
281 },
282
283 create_widget: function() {
284@@ -45,6 +66,7 @@
285 responseHeaders: {'Content-Type': 'text/html'}});
286 this.fixture = Y.one('#fixture');
287 this.fixture.appendChild(this.widget.get('container'));
288+ this.widget.show();
289 },
290
291 test_library_exists: function () {
292@@ -101,6 +123,37 @@
293 this.widget._save_team_success('', team_data);
294 Y.Assert.isTrue(event_publishd);
295 Y.Assert.areEqual('test', Y.one('form p').get('text'));
296+ },
297+
298+ test_subscriptionpolicy_setup: function() {
299+ // The subscription policy choice popup is rendered.
300+ this.create_widget();
301+ var subscriptionpolicy_node =
302+ Y.one('.subscriptionpolicy-content .value');
303+ Y.Assert.areEqual(
304+ 'Restricted', subscriptionpolicy_node.get('text'));
305+ var subscriptionpolicy_edit_node =
306+ Y.one('.subscriptionpolicy-content a.sprite.edit');
307+ Y.Assert.isNotNull(subscriptionpolicy_edit_node);
308+ var legacy_dropdown = Y.one('[id="field.subscriptionpolicy"]');
309+ Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
310+ },
311+
312+ test_subscriptionpolicy_selection: function() {
313+ // The subscriptionpolicy choice popup updates the form.
314+ this.create_widget();
315+ var subscriptionpolicy_popup =
316+ Y.one('.subscriptionpolicy-content a');
317+ subscriptionpolicy_popup.simulate('click');
318+ var header_text =
319+ Y.one('.yui3-ichoicelist-focused .yui3-widget-hd h2')
320+ .get('text');
321+ Y.Assert.areEqual('Set subscription policy as', header_text);
322+ var subscriptionpolicy_choice = Y.one(
323+ '.yui3-ichoicelist-content a[href="#MODERATED"]');
324+ subscriptionpolicy_choice.simulate('click');
325+ var legacy_dropdown = Y.one('[id="field.subscriptionpolicy"]');
326+ Y.Assert.areEqual('MODERATED', legacy_dropdown.get('value'));
327 }
328 }));
329
330
331=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.js'
332--- lib/lp/bugs/javascript/tests/test_filebug.js 2012-06-27 14:05:07 +0000
333+++ lib/lp/bugs/javascript/tests/test_filebug.js 2012-07-03 07:13:21 +0000
334@@ -127,9 +127,9 @@
335 // The bugtask status choice popup is rendered.
336 test_status_setup: function () {
337 Y.lp.bugs.filebug.setup_filebug(true);
338- var status_node = Y.one('#status-content .value');
339+ var status_node = Y.one('.status-content .value');
340 Y.Assert.areEqual('New', status_node.get('text'));
341- var status_edit_node = Y.one('#status-content a.sprite.edit');
342+ var status_edit_node = Y.one('.status-content a.sprite.edit');
343 Y.Assert.isNotNull(status_edit_node);
344 var legacy_dropdown = Y.one('[id="field.status"]');
345 Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
346@@ -138,10 +138,10 @@
347 // The bugtask importance choice popup is rendered.
348 test_importance_setup: function () {
349 Y.lp.bugs.filebug.setup_filebug(true);
350- var importance_node = Y.one('#importance-content .value');
351+ var importance_node = Y.one('.importance-content .value');
352 Y.Assert.areEqual('Undecided', importance_node.get('text'));
353 var importance_edit_node =
354- Y.one('#importance-content a.sprite.edit');
355+ Y.one('.importance-content a.sprite.edit');
356 Y.Assert.isNotNull(importance_edit_node);
357 var legacy_dropdown = Y.one('[id="field.importance"]');
358 Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
359@@ -158,7 +158,7 @@
360 // The bugtask status choice popup updates the form.
361 test_status_selection: function() {
362 Y.lp.bugs.filebug.setup_filebug(true);
363- var status_popup = Y.one('#status-content a');
364+ var status_popup = Y.one('.status-content a');
365 status_popup.simulate('click');
366 var status_choice = Y.one(
367 '.yui3-ichoicelist-content a[href="#Incomplete"]');
368@@ -170,7 +170,7 @@
369 // The bugtask importance choice popup updates the form.
370 test_importance_selection: function() {
371 Y.lp.bugs.filebug.setup_filebug(true);
372- var status_popup = Y.one('#importance-content a');
373+ var status_popup = Y.one('.importance-content a');
374 status_popup.simulate('click');
375 var status_choice = Y.one(
376 '.yui3-ichoicelist-content a[href="#High"]');
377@@ -183,10 +183,10 @@
378 test_information_type_setup: function () {
379 Y.lp.bugs.filebug.setup_filebug(true);
380 var information_type_node =
381- Y.one('#information_type-content .value');
382+ Y.one('.information_type-content .value');
383 Y.Assert.areEqual('Public', information_type_node.get('text'));
384 var information_type_node_edit_node =
385- Y.one('#information_type-content a.sprite.edit');
386+ Y.one('.information_type-content a.sprite.edit');
387 Y.Assert.isNotNull(information_type_node_edit_node);
388 var legacy_field = Y.one('table.radio-button-widget');
389 Y.Assert.isTrue(legacy_field.hasClass('unseen'));
390@@ -195,8 +195,12 @@
391 // The bugtask information_type choice popup updates the form.
392 test_information_type_selection: function() {
393 Y.lp.bugs.filebug.setup_filebug(true);
394- var information_type_popup = Y.one('#information_type-content a');
395+ var information_type_popup = Y.one('.information_type-content a');
396 information_type_popup.simulate('click');
397+ var header_text =
398+ Y.one('.yui3-ichoicelist-focused .yui3-widget-hd h2')
399+ .get('text');
400+ Y.Assert.areEqual('Set information type as', header_text);
401 var information_type_choice = Y.one(
402 '.yui3-ichoicelist-content a[href="#USERDATA"]');
403 information_type_choice.simulate('click');
404@@ -210,7 +214,7 @@
405 Y.lp.bugs.filebug.setup_filebug(true);
406 var banner_hidden = Y.one('.yui3-privacybanner-hidden');
407 Y.Assert.isNotNull(banner_hidden);
408- var information_type_popup = Y.one('#information_type-content a');
409+ var information_type_popup = Y.one('.information_type-content a');
410 information_type_popup.simulate('click');
411 var information_type_choice = Y.one(
412 '.yui3-ichoicelist-content a[href="#USERDATA"]');
413@@ -228,7 +232,7 @@
414 Y.lp.bugs.filebug.setup_filebug(true);
415 var banner_hidden = Y.one('.yui3-privacybanner-hidden');
416 Y.Assert.isNotNull(banner_hidden);
417- var information_type_popup = Y.one('#information_type-content a');
418+ var information_type_popup = Y.one('.information_type-content a');
419 information_type_popup.simulate('click');
420 var information_type_choice = Y.one(
421 '.yui3-ichoicelist-content a[href="#USERDATA"]');
422@@ -245,7 +249,7 @@
423 test_select_public_info_type: function () {
424 window.LP.cache.bug_private_by_default = true;
425 Y.lp.bugs.filebug.setup_filebug(true);
426- var information_type_popup = Y.one('#information_type-content a');
427+ var information_type_popup = Y.one('.information_type-content a');
428 information_type_popup.simulate('click');
429 var information_type_choice = Y.one(
430 '.yui3-ichoicelist-content a[href="#USERDATA"]');
431
432=== modified file 'lib/lp/registry/browser/configure.zcml'
433--- lib/lp/registry/browser/configure.zcml 2012-06-25 06:13:53 +0000
434+++ lib/lp/registry/browser/configure.zcml 2012-07-03 07:13:21 +0000
435@@ -567,7 +567,7 @@
436 <browser:page
437 name="+get-involved"
438 for="*"
439- class="lp.registry.browser.pillar.PillarView"
440+ class="lp.registry.browser.pillar.PillarInvolvementView"
441 permission="zope.Public"
442 template="../templates/pillar-involvement-portlet.pt"/>
443 <browser:url
444
445=== modified file 'lib/lp/registry/browser/distribution.py'
446--- lib/lp/registry/browser/distribution.py 2012-06-14 05:18:22 +0000
447+++ lib/lp/registry/browser/distribution.py 2012-07-03 07:13:21 +0000
448@@ -93,6 +93,7 @@
449 from lp.registry.browser.pillar import (
450 PillarBugsMenu,
451 PillarNavigationMixin,
452+ PillarViewMixin,
453 )
454 from lp.registry.interfaces.distribution import (
455 IDerivativeDistribution,
456@@ -648,7 +649,7 @@
457 return self.has_exact_matches
458
459
460-class DistributionView(HasAnnouncementsView, FeedsMixin):
461+class DistributionView(PillarViewMixin, HasAnnouncementsView, FeedsMixin):
462 """Default Distribution view class."""
463
464 def initialize(self):
465@@ -666,7 +667,7 @@
466 self.context, IDistribution['owner'],
467 format_link(self.context.owner),
468 header='Change maintainer', edit_view='+reassign',
469- step_title='Select a new maintainer')
470+ step_title='Select a new maintainer', show_create_team=True)
471
472 @property
473 def driver_widget(self):
474@@ -679,7 +680,7 @@
475 format_link(self.context.driver, empty_value=empty_value),
476 header='Change driver', edit_view='+driver',
477 null_display_value=empty_value,
478- step_title='Select a new driver')
479+ step_title='Select a new driver', show_create_team=True)
480
481 @property
482 def members_widget(self):
483
484=== modified file 'lib/lp/registry/browser/pillar.py'
485--- lib/lp/registry/browser/pillar.py 2012-05-15 08:16:09 +0000
486+++ lib/lp/registry/browser/pillar.py 2012-07-03 07:13:21 +0000
487@@ -1,4 +1,4 @@
488-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
489+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
490 # GNU Affero General Public License version 3 (see the file LICENSE).
491
492 """Common views for objects that implement `IPillar`."""
493@@ -8,7 +8,8 @@
494 __all__ = [
495 'InvolvedMenu',
496 'PillarBugsMenu',
497- 'PillarView',
498+ 'PillarInvolvementView',
499+ 'PillarViewMixin',
500 'PillarNavigationMixin',
501 'PillarPersonSharingView',
502 'PillarSharingView',
503@@ -26,11 +27,15 @@
504 implements,
505 Interface,
506 )
507-from zope.schema.vocabulary import getVocabularyRegistry
508+from zope.schema.vocabulary import (
509+ getVocabularyRegistry,
510+ SimpleVocabulary,
511+ )
512 from zope.security.interfaces import Unauthorized
513 from zope.traversing.browser.absoluteurl import absoluteURL
514
515 from lp.app.browser.launchpad import iter_view_registrations
516+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
517 from lp.app.browser.tales import MenuAPI
518 from lp.app.browser.vocabulary import vocabulary_filters
519 from lp.app.enums import (
520@@ -47,7 +52,10 @@
521 IDistributionSourcePackage,
522 )
523 from lp.registry.interfaces.distroseries import IDistroSeries
524-from lp.registry.interfaces.person import IPersonSet
525+from lp.registry.interfaces.person import (
526+ CLOSED_TEAM_POLICY,
527+ IPersonSet,
528+ )
529 from lp.registry.interfaces.pillar import IPillar
530 from lp.registry.interfaces.projectgroup import IProjectGroup
531 from lp.registry.model.pillar import PillarPerson
532@@ -131,15 +139,15 @@
533 enabled=service_uses_launchpad(self.pillar.blueprints_usage))
534
535
536-class PillarView(LaunchpadView):
537- """A view for any `IPillar`."""
538+class PillarInvolvementView(LaunchpadView):
539+ """A view for any `IPillar` implementing the IInvolved interface."""
540 implements(IInvolved)
541
542 configuration_links = []
543 visible_disabled_link_names = []
544
545 def __init__(self, context, request):
546- super(PillarView, self).__init__(context, request)
547+ super(PillarInvolvementView, self).__init__(context, request)
548 self.official_malone = False
549 self.answers_usage = ServiceUsage.UNKNOWN
550 self.blueprints_usage = ServiceUsage.UNKNOWN
551@@ -252,6 +260,21 @@
552 return Link('+securitycontact', text, icon='edit')
553
554
555+class PillarViewMixin():
556+ """A mixin for pillar views to populate the json request cache."""
557+
558+ def initialize(self):
559+ # Insert close team subscription policy data into the json cache.
560+ # This data is used for the maintainer and driver pickers.
561+ cache = IJSONRequestCache(self.request)
562+ policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
563+ team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
564+ SimpleVocabulary.fromItems(policy_items),
565+ value_fn=lambda item: item.name)
566+ cache.objects['team_subscriptionpolicy_data'] = (
567+ team_subscriptionpolicy_data)
568+
569+
570 class PillarSharingView(LaunchpadView):
571
572 page_title = "Sharing"
573
574=== modified file 'lib/lp/registry/browser/product.py'
575--- lib/lp/registry/browser/product.py 2012-06-21 06:50:10 +0000
576+++ lib/lp/registry/browser/product.py 2012-07-03 07:13:21 +0000
577@@ -149,8 +149,9 @@
578 )
579 from lp.registry.browser.pillar import (
580 PillarBugsMenu,
581+ PillarInvolvementView,
582 PillarNavigationMixin,
583- PillarView,
584+ PillarViewMixin,
585 )
586 from lp.registry.browser.productseries import get_series_branch_error
587 from lp.registry.interfaces.pillar import IPillarNameSet
588@@ -339,7 +340,7 @@
589 return Link('', text, summary)
590
591
592-class ProductInvolvementView(PillarView):
593+class ProductInvolvementView(PillarInvolvementView):
594 """Encourage configuration of involvement links for projects."""
595
596 has_involvement = True
597@@ -927,8 +928,8 @@
598 return None
599
600
601-class ProductView(HasAnnouncementsView, SortSeriesMixin, FeedsMixin,
602- ProductDownloadFileMixin):
603+class ProductView(PillarViewMixin, HasAnnouncementsView, SortSeriesMixin,
604+ FeedsMixin, ProductDownloadFileMixin):
605
606 implements(IProductActionMenu, IEditableContextTitle)
607
608
609=== modified file 'lib/lp/registry/browser/productseries.py'
610--- lib/lp/registry/browser/productseries.py 2012-06-19 18:29:44 +0000
611+++ lib/lp/registry/browser/productseries.py 2012-07-03 07:13:21 +0000
612@@ -110,7 +110,7 @@
613 )
614 from lp.registry.browser.pillar import (
615 InvolvedMenu,
616- PillarView,
617+ PillarInvolvementView,
618 )
619 from lp.registry.interfaces.packaging import (
620 IPackaging,
621@@ -236,7 +236,7 @@
622 return self.view.context.product
623
624
625-class ProductSeriesInvolvementView(PillarView):
626+class ProductSeriesInvolvementView(PillarInvolvementView):
627 """Encourage configuration of involvement links for project series."""
628
629 implements(IProductSeriesInvolved)
630
631=== modified file 'lib/lp/registry/browser/project.py'
632--- lib/lp/registry/browser/project.py 2012-01-04 12:08:24 +0000
633+++ lib/lp/registry/browser/project.py 2012-07-03 07:13:21 +0000
634@@ -2,6 +2,7 @@
635 # GNU Affero General Public License version 3 (see the file LICENSE).
636
637 """Project-related View Classes"""
638+from lp.registry.browser.pillar import PillarViewMixin
639
640 __metaclass__ = type
641
642@@ -356,7 +357,7 @@
643 return Link('+filebug', text, icon='add')
644
645
646-class ProjectView(HasAnnouncementsView, FeedsMixin):
647+class ProjectView(PillarViewMixin, HasAnnouncementsView, FeedsMixin):
648
649 implements(IProjectGroupActionMenu)
650
651@@ -367,7 +368,7 @@
652 format_link(self.context.owner, empty_value="Not yet selected"),
653 header='Change maintainer', edit_view='+reassign',
654 step_title='Select a new maintainer',
655- null_display_value="Not yet selected")
656+ null_display_value="Not yet selected", show_create_team=True)
657
658 @property
659 def driver_widget(self):
660@@ -377,7 +378,7 @@
661 header='Change driver', edit_view='+driver',
662 step_title='Select a new driver',
663 null_display_value="Not yet selected",
664- help_link="/+help-registry/driver.html")
665+ help_link="/+help-registry/driver.html", show_create_team=True)
666
667 def initialize(self):
668 super(ProjectView, self).initialize()
669
670=== modified file 'lib/lp/registry/browser/tests/test_distribution.py'
671--- lib/lp/registry/browser/tests/test_distribution.py 2012-06-14 05:18:22 +0000
672+++ lib/lp/registry/browser/tests/test_distribution.py 2012-07-03 07:13:21 +0000
673@@ -12,6 +12,10 @@
674 Not,
675 )
676
677+from zope.schema.vocabulary import SimpleVocabulary
678+from lazr.restful.interfaces import IJSONRequestCache
679+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
680+from lp.registry.interfaces.person import CLOSED_TEAM_POLICY
681 from lp.registry.interfaces.series import SeriesStatus
682 from lp.services.webapp import canonical_url
683 from lp.testing import (
684@@ -76,7 +80,7 @@
685
686 def test_distributionpage_series_list_noadmin(self):
687 # A non-admin does see the series list when there is a series.
688- series = self.factory.makeDistroSeries(distribution=self.distro,
689+ self.factory.makeDistroSeries(distribution=self.distro,
690 status=SeriesStatus.CURRENT)
691 login_person(self.simple_user)
692 view = create_initialized_view(
693@@ -93,3 +97,26 @@
694 text='Active series and milestones'))
695 self.assertThat(view.render(), series_header_match)
696 self.assertThat(view.render(), Not(add_series_match))
697+
698+
699+class TestDistributionView(TestCaseWithFactory):
700+ """Tests the DistributionView."""
701+
702+ layer = DatabaseFunctionalLayer
703+
704+ def setUp(self):
705+ super(TestDistributionView, self).setUp()
706+ self.distro = self.factory.makeDistribution(
707+ name="distro", displayname=u'distro')
708+
709+ def test_view_data_model(self):
710+ # The view's json request cache contains the expected data.
711+ view = create_initialized_view(self.distro, '+index')
712+ cache = IJSONRequestCache(view.request)
713+ policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
714+ team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
715+ SimpleVocabulary.fromItems(policy_items),
716+ value_fn=lambda item: item.name)
717+ self.assertContentEqual(
718+ team_subscriptionpolicy_data,
719+ cache.objects['team_subscriptionpolicy_data'])
720
721=== modified file 'lib/lp/registry/browser/tests/test_product.py'
722--- lib/lp/registry/browser/tests/test_product.py 2012-05-25 21:16:11 +0000
723+++ lib/lp/registry/browser/tests/test_product.py 2012-07-03 07:13:21 +0000
724@@ -6,8 +6,12 @@
725 __metaclass__ = type
726
727 from zope.component import getUtility
728+from zope.schema.vocabulary import SimpleVocabulary
729
730+from lazr.restful.interfaces import IJSONRequestCache
731+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
732 from lp.app.enums import ServiceUsage
733+from lp.registry.interfaces.person import CLOSED_TEAM_POLICY
734 from lp.registry.interfaces.product import (
735 IProductSet,
736 License,
737@@ -208,6 +212,18 @@
738 'fnord-dom-edit-license-approved',
739 view.license_approved_widget.content_box_id)
740
741+ def test_view_data_model(self):
742+ # The view's json request cache contains the expected data.
743+ view = create_initialized_view(self.product, '+index')
744+ cache = IJSONRequestCache(view.request)
745+ policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
746+ team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
747+ SimpleVocabulary.fromItems(policy_items),
748+ value_fn=lambda item: item.name)
749+ self.assertContentEqual(
750+ team_subscriptionpolicy_data,
751+ cache.objects['team_subscriptionpolicy_data'])
752+
753
754 class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory):
755 """Tests the ProductSetReviewLicensesView."""
756
757=== modified file 'lib/lp/registry/browser/tests/test_projectgroup.py'
758--- lib/lp/registry/browser/tests/test_projectgroup.py 2012-06-04 16:13:51 +0000
759+++ lib/lp/registry/browser/tests/test_projectgroup.py 2012-07-03 07:13:21 +0000
760@@ -8,9 +8,15 @@
761 from fixtures import FakeLogger
762 from testtools.matchers import Not
763 from zope.component import getUtility
764+from zope.schema.vocabulary import SimpleVocabulary
765 from zope.security.interfaces import Unauthorized
766
767-from lp.registry.interfaces.person import IPersonSet
768+from lazr.restful.interfaces import IJSONRequestCache
769+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
770+from lp.registry.interfaces.person import (
771+ CLOSED_TEAM_POLICY,
772+ IPersonSet,
773+ )
774 from lp.services.webapp import canonical_url
775 from lp.services.webapp.interfaces import ILaunchBag
776 from lp.testing import (
777@@ -24,6 +30,28 @@
778 from lp.testing.views import create_initialized_view
779
780
781+class TestProjectGroupView(TestCaseWithFactory):
782+ """Tests the +index view."""
783+
784+ layer = DatabaseFunctionalLayer
785+
786+ def setUp(self):
787+ super(TestProjectGroupView, self).setUp()
788+ self.project_group = self.factory.makeProject(name='group')
789+
790+ def test_view_data_model(self):
791+ # The view's json request cache contains the expected data.
792+ view = create_initialized_view(self.project_group, '+index')
793+ cache = IJSONRequestCache(view.request)
794+ policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
795+ team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
796+ SimpleVocabulary.fromItems(policy_items),
797+ value_fn=lambda item: item.name)
798+ self.assertContentEqual(
799+ team_subscriptionpolicy_data,
800+ cache.objects['team_subscriptionpolicy_data'])
801+
802+
803 class TestProjectGroupEditView(TestCaseWithFactory):
804 """Tests the edit view."""
805
806@@ -31,7 +59,7 @@
807
808 def setUp(self):
809 super(TestProjectGroupEditView, self).setUp()
810- self.project_group = self.factory.makeProject(name='grupo')
811+ self.project_group = self.factory.makeProject(name='group')
812 # Use a FakeLogger fixture to prevent Memcached warnings to be
813 # printed to stdout while browsing pages.
814 self.useFixture(FakeLogger())