← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/new-team-picker-enhanced-form into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #250955 in Launchpad itself: "easily create team when needed"
  https://bugs.launchpad.net/launchpad/+bug/250955
  Bug #1019584 in Launchpad itself: "The "information_type" type picker on +filebug has an underscore"
  https://bugs.launchpad.net/launchpad/+bug/1019584

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/new-team-picker-enhanced-form/+merge/113020

== 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

-- 
https://code.launchpad.net/~wallyworld/launchpad/new-team-picker-enhanced-form/+merge/113020
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/new-team-picker-enhanced-form into lp:launchpad.
=== modified file 'lib/lp/app/javascript/choice.js'
--- lib/lp/app/javascript/choice.js	2012-06-27 14:05:07 +0000
+++ lib/lp/app/javascript/choice.js	2012-07-03 01:38:21 +0000
@@ -56,54 +56,65 @@
   widget.render();
 };
 
+// The default configuration used for wiring choice popup widgets.
+var _default_popup_choice_config = {
+    container: Y,
+    render_immediately: true,
+    show_description: false,
+    field_title: null
+};
+
 /**
  * Replace a legacy input widget with a popup choice widget.
  * @param legacy_node the YUI node containing the legacy widget.
  * @param field_name the Launchpad form field name.
  * @param choices the choices for the popup choice widget.
- * @param show_description whether to show the selected value's description.
+ * @param cfg configuration for the wiring action.
  * @param get_value_fn getter for the legacy widget's value.
  * @param set_value_fn setter for the legacy widget's value.
  */
-var wirePopupChoice = function(legacy_node, field_name, choices,
-                               show_description, get_value_fn, set_value_fn) {
+var wirePopupChoice = function(legacy_node, field_name, choices, cfg,
+                               get_value_fn, set_value_fn) {
     var choice_descriptions = {};
     Y.Array.forEach(choices, function(item) {
         choice_descriptions[item.value] = item.description;
     });
     var initial_field_value = get_value_fn(legacy_node);
     var choice_node = Y.Node.create([
-        '<span id="' + field_name + '-content"><span class="value"></span>',
+        '<span class="' + field_name + '-content"><span class="value"></span>',
         '<a class="sprite edit editicon action-icon"',
         ' href="#">Edit</a></span>'
         ].join(''));
-    if (show_description) {
+    if (cfg.show_description) {
         choice_node.append(Y.Node.create('<div class="formHelp"></div>'));
     }
-
     legacy_node.insertBefore(choice_node, legacy_node);
-    if (show_description) {
+    if (cfg.show_description) {
         choice_node.one('.formHelp')
             .set('text', choice_descriptions[initial_field_value]);
     }
     legacy_node.addClass('unseen');
-    var field_content = Y.one('#' + field_name + '-content');
-
+    if (!Y.Lang.isValue(cfg.field_title)) {
+        cfg.field_title = field_name.replace('_', ' ');
+    }
     var choice_edit = new Y.ChoiceSource({
-        contentBox: field_content,
+        contentBox: choice_node,
         value: initial_field_value,
-        title: 'Set ' + field_name + " as",
+        title: 'Set ' + cfg.field_title + " as",
         items: choices,
-        elementToFlash: field_content
+        elementToFlash: choice_node,
+        zIndex: 1050
     });
-    choice_edit.render();
+    if (cfg.render_immediately) {
+        choice_edit.render();
+    }
 
     var update_selected_value_css = function(selected_value) {
         Y.Array.each(choices, function(item) {
             if (item.value === selected_value) {
-                field_content.addClass(item.css_class);
+                choice_node.addClass(item.css_class);
             } else {
-                field_content.removeClass(item.css_class);
+                choice_node.removeClass(item.css_class);
             }
         });
     };
@@ -112,21 +123,23 @@
         var selected_value = choice_edit.get('value');
         update_selected_value_css(selected_value);
         set_value_fn(legacy_node, selected_value);
-        if (show_description) {
+        if (cfg.show_description) {
             choice_node.one('.formHelp')
                 .set('text', choice_descriptions[selected_value]);
         }
     });
+    return choice_edit;
 };
 
 /**
  * Replace a drop down combo box with a popup choice selection widget.
  * @param field_name
  * @param choices
- * @param show_description
+ * @param cfg
  */
-namespace.addPopupChoice = function(field_name, choices, show_description) {
-    var legacy_node = Y.one('[id="field.' + field_name + '"]');
+namespace.addPopupChoice = function(field_name, choices, cfg) {
+    cfg = Y.merge(_default_popup_choice_config, cfg);
+    var legacy_node = cfg.container.one('[id="field.' + field_name + '"]');
     if (!Y.Lang.isValue(legacy_node)) {
         return;
     }
@@ -136,19 +149,19 @@
     var set_fn = function(node, value) {
         node.set('value', value);
     };
-    wirePopupChoice(
-        legacy_node, field_name, choices, show_description, get_fn, set_fn);
+    return wirePopupChoice(
+        legacy_node, field_name, choices, cfg, get_fn, set_fn);
 };
 
 /**
  * Replace a radio button group with a popup choice selection widget.
  * @param field_name
  * @param choices
- * @param show_description
+ * @param cfg
  */
-namespace.addPopupChoiceForRadioButtons = function(field_name, choices,
-                                                   show_description) {
-    var legacy_node = Y.one('[name="field.' + field_name + '"]')
+namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {
+    cfg = Y.merge(_default_popup_choice_config, cfg);
+    var legacy_node = cfg.container.one('[name="field.' + field_name + '"]')
         .ancestor('table.radio-button-widget');
     if (!Y.Lang.isValue(legacy_node)) {
         return;
@@ -172,8 +185,8 @@
             }
         });
     };
-    wirePopupChoice(
-        legacy_node, field_name, choices, show_description, get_fn, set_fn);
+    return wirePopupChoice(
+        legacy_node, field_name, choices, cfg, get_fn, set_fn);
 };
 
 }, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins",

=== modified file 'lib/lp/app/javascript/picker/team.js'
--- lib/lp/app/javascript/picker/team.js	2012-07-02 04:16:19 +0000
+++ lib/lp/app/javascript/picker/team.js	2012-07-03 01:38:21 +0000
@@ -106,6 +106,12 @@
                 e.halt();
                 this.fire(ns.CANCEL_TEAM);
             }, this);
+        this.subscriptionpolicyedit = Y.lp.app.choice.addPopupChoice(
+            'subscriptionpolicy', LP.cache.team_subscriptionpolicy_data, {
+                container: container,
+                render_immediately: false,
+                field_title: 'subscription policy'
+            });
         container.one('.extra-form-buttons').removeClass('hidden');
     },
 
@@ -114,6 +120,7 @@
         if (form_elements.size() > 0) {
             form_elements.item(0).focus();
         }
+        this.subscriptionpolicyedit.render();
     },
 
     hide: function() {
@@ -214,5 +221,5 @@
 });
 
 
-}, "0.1", {"requires": ["base", "node"]});
+}, "0.1", {"requires": ["base", "node", "lp.app.choice"]});
 

=== modified file 'lib/lp/app/javascript/picker/tests/test_team.html'
--- lib/lp/app/javascript/picker/tests/test_team.html	2012-06-26 09:02:51 +0000
+++ lib/lp/app/javascript/picker/tests/test_team.html	2012-07-03 01:38:21 +0000
@@ -26,12 +26,34 @@
 
       <!-- Dependencies -->
       <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/choice.js"></script>
+      <script type="text/javascript"
           src="../../../../../../build/js/lp/app/client.js"></script>
       <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/errors.js"></script>
+      <script type="text/javascript"
           src="../../../../../../build/js/lp/app/lp.js"></script>
       <script type="text/javascript"
           src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
       <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/effects/effects.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/expander.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/inlineedit/editor.js"></script>
+      <script type="text/javascript"
           src="../../../../../../build/js/lp/app/testing/mockio.js"></script>
 
       <!-- The module under test. -->

=== modified file 'lib/lp/app/javascript/picker/tests/test_team.js'
--- lib/lp/app/javascript/picker/tests/test_team.js	2012-07-02 04:16:19 +0000
+++ lib/lp/app/javascript/picker/tests/test_team.js	2012-07-03 01:38:21 +0000
@@ -12,9 +12,19 @@
 
 
         setUp: function() {
+            window.LP = {
+                links: {},
+                cache: {
+                    team_subscriptionpolicy_data: [
+                        {name: 'Moderated', value: 'MODERATED'},
+                        {name: 'Restricted', value: 'RESTRICTED'}
+                    ]
+                }
+            };
         },
 
         tearDown: function() {
+            delete window.LP;
             delete this.mockio;
             if (this.fixture !== undefined) {
                 this.fixture.empty(true);
@@ -26,10 +36,21 @@
         },
 
         _simple_team_form: function() {
-            return '<table><tr><td>' +
-                '<input id="field.name" name="field.name"/>' +
-                '<input id="field.displayname" ' +
-                'name="field.displayname"/></td></tr></table>';
+            return [
+                '<table><tr><td>',
+                '<input id="field.name" name="field.name"/>',
+                '<input id="field.displayname" ',
+                'name="field.displayname"/>',
+                '<div class="value">',
+                '<select size="1" name="field.subscriptionpolicy" ',
+                'id="field.subscriptionpolicy">',
+                '<option value="RESTRICTED" ',
+                'selected="selected">Restricted</option>',
+                '<option value="MODERATED">Moderated</option>',
+                '</select>',
+                '</div>',
+                '</td></tr></table>'
+            ].join('');
         },
 
         create_widget: function() {
@@ -45,6 +66,7 @@
                 responseHeaders: {'Content-Type': 'text/html'}});
             this.fixture = Y.one('#fixture');
             this.fixture.appendChild(this.widget.get('container'));
+            this.widget.show();
         },
 
         test_library_exists: function () {
@@ -101,6 +123,37 @@
             this.widget._save_team_success('', team_data);
             Y.Assert.isTrue(event_publishd);
             Y.Assert.areEqual('test', Y.one('form p').get('text'));
+        },
+
+        test_subscriptionpolicy_setup: function() {
+            // The subscription policy choice popup is rendered.
+            this.create_widget();
+            var subscriptionpolicy_node =
+                Y.one('.subscriptionpolicy-content .value');
+            Y.Assert.areEqual(
+                'Restricted', subscriptionpolicy_node.get('text'));
+            var subscriptionpolicy_edit_node =
+                Y.one('.subscriptionpolicy-content a.sprite.edit');
+            Y.Assert.isNotNull(subscriptionpolicy_edit_node);
+            var legacy_dropdown = Y.one('[id="field.subscriptionpolicy"]');
+            Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
+        },
+
+        test_subscriptionpolicy_selection: function() {
+            // The subscriptionpolicy choice popup updates the form.
+            this.create_widget();
+            var subscriptionpolicy_popup =
+                Y.one('.subscriptionpolicy-content a');
+            subscriptionpolicy_popup.simulate('click');
+            var header_text =
+                Y.one('.yui3-ichoicelist-focused .yui3-widget-hd h2')
+                    .get('text');
+            Y.Assert.areEqual('Set subscription policy as', header_text);
+            var subscriptionpolicy_choice = Y.one(
+                '.yui3-ichoicelist-content a[href="#MODERATED"]');
+            subscriptionpolicy_choice.simulate('click');
+            var legacy_dropdown = Y.one('[id="field.subscriptionpolicy"]');
+            Y.Assert.areEqual('MODERATED', legacy_dropdown.get('value'));
         }
     }));
 

=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.js'
--- lib/lp/bugs/javascript/tests/test_filebug.js	2012-06-27 14:05:07 +0000
+++ lib/lp/bugs/javascript/tests/test_filebug.js	2012-07-03 01:38:21 +0000
@@ -127,9 +127,9 @@
         // The bugtask status choice popup is rendered.
         test_status_setup: function () {
             Y.lp.bugs.filebug.setup_filebug(true);
-            var status_node = Y.one('#status-content .value');
+            var status_node = Y.one('.status-content .value');
             Y.Assert.areEqual('New', status_node.get('text'));
-            var status_edit_node = Y.one('#status-content a.sprite.edit');
+            var status_edit_node = Y.one('.status-content a.sprite.edit');
             Y.Assert.isNotNull(status_edit_node);
             var legacy_dropdown = Y.one('[id="field.status"]');
             Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
@@ -138,10 +138,10 @@
         // The bugtask importance choice popup is rendered.
         test_importance_setup: function () {
             Y.lp.bugs.filebug.setup_filebug(true);
-            var importance_node = Y.one('#importance-content .value');
+            var importance_node = Y.one('.importance-content .value');
             Y.Assert.areEqual('Undecided', importance_node.get('text'));
             var importance_edit_node =
-                Y.one('#importance-content a.sprite.edit');
+                Y.one('.importance-content a.sprite.edit');
             Y.Assert.isNotNull(importance_edit_node);
             var legacy_dropdown = Y.one('[id="field.importance"]');
             Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
@@ -158,7 +158,7 @@
         // The bugtask status choice popup updates the form.
         test_status_selection: function() {
             Y.lp.bugs.filebug.setup_filebug(true);
-            var status_popup = Y.one('#status-content a');
+            var status_popup = Y.one('.status-content a');
             status_popup.simulate('click');
             var status_choice = Y.one(
                 '.yui3-ichoicelist-content a[href="#Incomplete"]');
@@ -170,7 +170,7 @@
         // The bugtask importance choice popup updates the form.
         test_importance_selection: function() {
             Y.lp.bugs.filebug.setup_filebug(true);
-            var status_popup = Y.one('#importance-content a');
+            var status_popup = Y.one('.importance-content a');
             status_popup.simulate('click');
             var status_choice = Y.one(
                 '.yui3-ichoicelist-content a[href="#High"]');
@@ -183,10 +183,10 @@
         test_information_type_setup: function () {
             Y.lp.bugs.filebug.setup_filebug(true);
             var information_type_node =
-                Y.one('#information_type-content .value');
+                Y.one('.information_type-content .value');
             Y.Assert.areEqual('Public', information_type_node.get('text'));
             var information_type_node_edit_node =
-                Y.one('#information_type-content a.sprite.edit');
+                Y.one('.information_type-content a.sprite.edit');
             Y.Assert.isNotNull(information_type_node_edit_node);
             var legacy_field = Y.one('table.radio-button-widget');
             Y.Assert.isTrue(legacy_field.hasClass('unseen'));
@@ -195,8 +195,12 @@
         // The bugtask information_type choice popup updates the form.
         test_information_type_selection: function() {
             Y.lp.bugs.filebug.setup_filebug(true);
-            var information_type_popup = Y.one('#information_type-content a');
+            var information_type_popup = Y.one('.information_type-content a');
             information_type_popup.simulate('click');
+            var header_text =
+                Y.one('.yui3-ichoicelist-focused .yui3-widget-hd h2')
+                    .get('text');
+            Y.Assert.areEqual('Set information type as', header_text);
             var information_type_choice = Y.one(
                 '.yui3-ichoicelist-content a[href="#USERDATA"]');
             information_type_choice.simulate('click');
@@ -210,7 +214,7 @@
             Y.lp.bugs.filebug.setup_filebug(true);
             var banner_hidden = Y.one('.yui3-privacybanner-hidden');
             Y.Assert.isNotNull(banner_hidden);
-            var information_type_popup = Y.one('#information_type-content a');
+            var information_type_popup = Y.one('.information_type-content a');
             information_type_popup.simulate('click');
             var information_type_choice = Y.one(
                 '.yui3-ichoicelist-content a[href="#USERDATA"]');
@@ -228,7 +232,7 @@
             Y.lp.bugs.filebug.setup_filebug(true);
             var banner_hidden = Y.one('.yui3-privacybanner-hidden');
             Y.Assert.isNotNull(banner_hidden);
-            var information_type_popup = Y.one('#information_type-content a');
+            var information_type_popup = Y.one('.information_type-content a');
             information_type_popup.simulate('click');
             var information_type_choice = Y.one(
                 '.yui3-ichoicelist-content a[href="#USERDATA"]');
@@ -245,7 +249,7 @@
         test_select_public_info_type: function () {
             window.LP.cache.bug_private_by_default = true;
             Y.lp.bugs.filebug.setup_filebug(true);
-            var information_type_popup = Y.one('#information_type-content a');
+            var information_type_popup = Y.one('.information_type-content a');
             information_type_popup.simulate('click');
             var information_type_choice = Y.one(
                 '.yui3-ichoicelist-content a[href="#USERDATA"]');

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2012-06-25 06:13:53 +0000
+++ lib/lp/registry/browser/configure.zcml	2012-07-03 01:38:21 +0000
@@ -567,7 +567,7 @@
     <browser:page
         name="+get-involved"
         for="*"
-        class="lp.registry.browser.pillar.PillarView"
+        class="lp.registry.browser.pillar.PillarInvolvementView"
         permission="zope.Public"
         template="../templates/pillar-involvement-portlet.pt"/>
         <browser:url

=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py	2012-06-14 05:18:22 +0000
+++ lib/lp/registry/browser/distribution.py	2012-07-03 01:38:21 +0000
@@ -93,6 +93,7 @@
 from lp.registry.browser.pillar import (
     PillarBugsMenu,
     PillarNavigationMixin,
+    PillarViewMixin,
     )
 from lp.registry.interfaces.distribution import (
     IDerivativeDistribution,
@@ -648,7 +649,7 @@
         return self.has_exact_matches
 
 
-class DistributionView(HasAnnouncementsView, FeedsMixin):
+class DistributionView(PillarViewMixin, HasAnnouncementsView, FeedsMixin):
     """Default Distribution view class."""
 
     def initialize(self):
@@ -666,7 +667,7 @@
             self.context, IDistribution['owner'],
             format_link(self.context.owner),
             header='Change maintainer', edit_view='+reassign',
-            step_title='Select a new maintainer')
+            step_title='Select a new maintainer', show_create_team=True)
 
     @property
     def driver_widget(self):
@@ -679,7 +680,7 @@
             format_link(self.context.driver, empty_value=empty_value),
             header='Change driver', edit_view='+driver',
             null_display_value=empty_value,
-            step_title='Select a new driver')
+            step_title='Select a new driver', show_create_team=True)
 
     @property
     def members_widget(self):

=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2012-05-15 08:16:09 +0000
+++ lib/lp/registry/browser/pillar.py	2012-07-03 01:38:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Common views for objects that implement `IPillar`."""
@@ -8,7 +8,8 @@
 __all__ = [
     'InvolvedMenu',
     'PillarBugsMenu',
-    'PillarView',
+    'PillarInvolvementView',
+    'PillarViewMixin',
     'PillarNavigationMixin',
     'PillarPersonSharingView',
     'PillarSharingView',
@@ -26,11 +27,15 @@
     implements,
     Interface,
     )
-from zope.schema.vocabulary import getVocabularyRegistry
+from zope.schema.vocabulary import (
+    getVocabularyRegistry,
+    SimpleVocabulary,
+    )
 from zope.security.interfaces import Unauthorized
 from zope.traversing.browser.absoluteurl import absoluteURL
 
 from lp.app.browser.launchpad import iter_view_registrations
+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
 from lp.app.browser.tales import MenuAPI
 from lp.app.browser.vocabulary import vocabulary_filters
 from lp.app.enums import (
@@ -47,7 +52,10 @@
     IDistributionSourcePackage,
     )
 from lp.registry.interfaces.distroseries import IDistroSeries
-from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.person import (
+    CLOSED_TEAM_POLICY,
+    IPersonSet,
+    )
 from lp.registry.interfaces.pillar import IPillar
 from lp.registry.interfaces.projectgroup import IProjectGroup
 from lp.registry.model.pillar import PillarPerson
@@ -131,15 +139,15 @@
             enabled=service_uses_launchpad(self.pillar.blueprints_usage))
 
 
-class PillarView(LaunchpadView):
-    """A view for any `IPillar`."""
+class PillarInvolvementView(LaunchpadView):
+    """A view for any `IPillar` implementing the IInvolved interface."""
     implements(IInvolved)
 
     configuration_links = []
     visible_disabled_link_names = []
 
     def __init__(self, context, request):
-        super(PillarView, self).__init__(context, request)
+        super(PillarInvolvementView, self).__init__(context, request)
         self.official_malone = False
         self.answers_usage = ServiceUsage.UNKNOWN
         self.blueprints_usage = ServiceUsage.UNKNOWN
@@ -252,6 +260,21 @@
         return Link('+securitycontact', text, icon='edit')
 
 
+class PillarViewMixin():
+    """A mixin for pillar views to populate the json request cache."""
+
+    def initialize(self):
+        # Insert close team subscription policy data into the json cache.
+        # This data is used for the maintainer and driver pickers.
+        cache = IJSONRequestCache(self.request)
+        policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
+        team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
+            SimpleVocabulary.fromItems(policy_items),
+            value_fn=lambda item: item.name)
+        cache.objects['team_subscriptionpolicy_data'] = (
+            team_subscriptionpolicy_data)
+
+
 class PillarSharingView(LaunchpadView):
 
     page_title = "Sharing"

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2012-06-21 06:50:10 +0000
+++ lib/lp/registry/browser/product.py	2012-07-03 01:38:21 +0000
@@ -149,8 +149,9 @@
     )
 from lp.registry.browser.pillar import (
     PillarBugsMenu,
+    PillarInvolvementView,
     PillarNavigationMixin,
-    PillarView,
+    PillarViewMixin,
     )
 from lp.registry.browser.productseries import get_series_branch_error
 from lp.registry.interfaces.pillar import IPillarNameSet
@@ -339,7 +340,7 @@
         return Link('', text, summary)
 
 
-class ProductInvolvementView(PillarView):
+class ProductInvolvementView(PillarInvolvementView):
     """Encourage configuration of involvement links for projects."""
 
     has_involvement = True
@@ -927,8 +928,8 @@
         return None
 
 
-class ProductView(HasAnnouncementsView, SortSeriesMixin, FeedsMixin,
-                  ProductDownloadFileMixin):
+class ProductView(PillarViewMixin, HasAnnouncementsView, SortSeriesMixin,
+                  FeedsMixin, ProductDownloadFileMixin):
 
     implements(IProductActionMenu, IEditableContextTitle)
 

=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py	2012-06-19 18:29:44 +0000
+++ lib/lp/registry/browser/productseries.py	2012-07-03 01:38:21 +0000
@@ -110,7 +110,7 @@
     )
 from lp.registry.browser.pillar import (
     InvolvedMenu,
-    PillarView,
+    PillarInvolvementView,
     )
 from lp.registry.interfaces.packaging import (
     IPackaging,
@@ -236,7 +236,7 @@
         return self.view.context.product
 
 
-class ProductSeriesInvolvementView(PillarView):
+class ProductSeriesInvolvementView(PillarInvolvementView):
     """Encourage configuration of involvement links for project series."""
 
     implements(IProductSeriesInvolved)

=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py	2012-01-04 12:08:24 +0000
+++ lib/lp/registry/browser/project.py	2012-07-03 01:38:21 +0000
@@ -2,6 +2,7 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Project-related View Classes"""
+from lp.registry.browser.pillar import PillarViewMixin
 
 __metaclass__ = type
 
@@ -356,7 +357,7 @@
         return Link('+filebug', text, icon='add')
 
 
-class ProjectView(HasAnnouncementsView, FeedsMixin):
+class ProjectView(PillarViewMixin, HasAnnouncementsView, FeedsMixin):
 
     implements(IProjectGroupActionMenu)
 
@@ -367,7 +368,7 @@
             format_link(self.context.owner, empty_value="Not yet selected"),
             header='Change maintainer', edit_view='+reassign',
             step_title='Select a new maintainer',
-            null_display_value="Not yet selected")
+            null_display_value="Not yet selected", show_create_team=True)
 
     @property
     def driver_widget(self):
@@ -377,7 +378,7 @@
             header='Change driver', edit_view='+driver',
             step_title='Select a new driver',
             null_display_value="Not yet selected",
-            help_link="/+help-registry/driver.html")
+            help_link="/+help-registry/driver.html", show_create_team=True)
 
     def initialize(self):
         super(ProjectView, self).initialize()

=== modified file 'lib/lp/registry/browser/tests/test_distribution.py'
--- lib/lp/registry/browser/tests/test_distribution.py	2012-06-14 05:18:22 +0000
+++ lib/lp/registry/browser/tests/test_distribution.py	2012-07-03 01:38:21 +0000
@@ -12,6 +12,10 @@
     Not,
     )
 
+from zope.schema.vocabulary import SimpleVocabulary
+from lazr.restful.interfaces import IJSONRequestCache
+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
+from lp.registry.interfaces.person import CLOSED_TEAM_POLICY
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.webapp import canonical_url
 from lp.testing import (
@@ -76,7 +80,7 @@
 
     def test_distributionpage_series_list_noadmin(self):
         # A non-admin does see the series list when there is a series.
-        series = self.factory.makeDistroSeries(distribution=self.distro,
+        self.factory.makeDistroSeries(distribution=self.distro,
             status=SeriesStatus.CURRENT)
         login_person(self.simple_user)
         view = create_initialized_view(
@@ -93,3 +97,26 @@
                 text='Active series and milestones'))
         self.assertThat(view.render(), series_header_match)
         self.assertThat(view.render(), Not(add_series_match))
+
+
+class TestDistributionView(TestCaseWithFactory):
+    """Tests the DistributionView."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestDistributionView, self).setUp()
+        self.distro = self.factory.makeDistribution(
+            name="distro", displayname=u'distro')
+
+    def test_view_data_model(self):
+        # The view's json request cache contains the expected data.
+        view = create_initialized_view(self.distro, '+index')
+        cache = IJSONRequestCache(view.request)
+        policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
+        team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
+            SimpleVocabulary.fromItems(policy_items),
+            value_fn=lambda item: item.name)
+        self.assertContentEqual(
+            team_subscriptionpolicy_data,
+            cache.objects['team_subscriptionpolicy_data'])

=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py	2012-05-25 21:16:11 +0000
+++ lib/lp/registry/browser/tests/test_product.py	2012-07-03 01:38:21 +0000
@@ -6,8 +6,12 @@
 __metaclass__ = type
 
 from zope.component import getUtility
+from zope.schema.vocabulary import SimpleVocabulary
 
+from lazr.restful.interfaces import IJSONRequestCache
+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
 from lp.app.enums import ServiceUsage
+from lp.registry.interfaces.person import CLOSED_TEAM_POLICY
 from lp.registry.interfaces.product import (
     IProductSet,
     License,
@@ -208,6 +212,18 @@
             'fnord-dom-edit-license-approved',
             view.license_approved_widget.content_box_id)
 
+    def test_view_data_model(self):
+        # The view's json request cache contains the expected data.
+        view = create_initialized_view(self.product, '+index')
+        cache = IJSONRequestCache(view.request)
+        policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
+        team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
+            SimpleVocabulary.fromItems(policy_items),
+            value_fn=lambda item: item.name)
+        self.assertContentEqual(
+            team_subscriptionpolicy_data,
+            cache.objects['team_subscriptionpolicy_data'])
+
 
 class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory):
     """Tests the ProductSetReviewLicensesView."""

=== modified file 'lib/lp/registry/browser/tests/test_projectgroup.py'
--- lib/lp/registry/browser/tests/test_projectgroup.py	2012-06-04 16:13:51 +0000
+++ lib/lp/registry/browser/tests/test_projectgroup.py	2012-07-03 01:38:21 +0000
@@ -8,9 +8,15 @@
 from fixtures import FakeLogger
 from testtools.matchers import Not
 from zope.component import getUtility
+from zope.schema.vocabulary import SimpleVocabulary
 from zope.security.interfaces import Unauthorized
 
-from lp.registry.interfaces.person import IPersonSet
+from lazr.restful.interfaces import IJSONRequestCache
+from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
+from lp.registry.interfaces.person import (
+    CLOSED_TEAM_POLICY,
+    IPersonSet,
+    )
 from lp.services.webapp import canonical_url
 from lp.services.webapp.interfaces import ILaunchBag
 from lp.testing import (
@@ -24,6 +30,28 @@
 from lp.testing.views import create_initialized_view
 
 
+class TestProjectGroupView(TestCaseWithFactory):
+    """Tests the +index view."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestProjectGroupView, self).setUp()
+        self.project_group = self.factory.makeProject(name='grupo')
+
+    def test_view_data_model(self):
+        # The view's json request cache contains the expected data.
+        view = create_initialized_view(self.project_group, '+index')
+        cache = IJSONRequestCache(view.request)
+        policy_items = [(item.name, item) for item in CLOSED_TEAM_POLICY]
+        team_subscriptionpolicy_data = vocabulary_to_choice_edit_items(
+            SimpleVocabulary.fromItems(policy_items),
+            value_fn=lambda item: item.name)
+        self.assertContentEqual(
+            team_subscriptionpolicy_data,
+            cache.objects['team_subscriptionpolicy_data'])
+
+
 class TestProjectGroupEditView(TestCaseWithFactory):
     """Tests the edit view."""
 


Follow ups