← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/blueprint-info-type-ui into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/blueprint-info-type-ui into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/blueprint-info-type-ui/+merge/122140

= Summary =
Partial implementation of Specification.information_type UI

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
Generalize the implementation of the Bugs information_type UI.

== Tests ==
xvfb-run bin/test --layer=YUITestLayer

== Demo and Q/A ==
Change the the type of a bug to private.  It should change, and you should be subscribed to the bug.  Reload to ensure that the change stuck.

Go to a blueprint.  You should see no information_type UI.

Set the 'blueprints.information_type.enabled' feature flag to "on".

Reload the blueprint.  You should see the information_type chooser.

Attempt to change the information type.  It should inform you you do not have the correct permission.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/templates/specification-index.pt
  lib/lp/app/javascript/information_type.js
  lib/lp/blueprints/templates/blueprint-portlet-privacy.pt
  lib/lp/testing/factory.py
  lib/lp/app/javascript/tests/test_information_type.js
  lib/lp/blueprints/model/specification.py
  lib/lp/bugs/javascript/bugtask_index.js
  lib/lp/services/features/flags.py
  lib/lp/blueprints/browser/configure.zcml
  lib/lp/blueprints/browser/specification.py
  lib/lp/blueprints/browser/tests/test_specification.py
  lib/lp/app/javascript/tests/test_information_type.html
  lib/lp/blueprints/interfaces/specification.py
-- 
https://code.launchpad.net/~abentley/launchpad/blueprint-info-type-ui/+merge/122140
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/blueprint-info-type-ui into lp:launchpad.
=== modified file 'lib/lp/app/javascript/information_type.js'
--- lib/lp/app/javascript/information_type.js	2012-08-28 23:53:32 +0000
+++ lib/lp/app/javascript/information_type.js	2012-08-30 20:24:20 +0000
@@ -8,6 +8,87 @@
 
 var namespace = Y.namespace('lp.app.information_type');
 
+// For testing.
+var skip_animation = false;
+
+/**
+ * Save the new information type. If validate_change is true, then a check
+ * will be done to ensure the bug will not become invisible. If the bug will
+ * become invisible, a confirmation popup is used to confirm the user's
+ * intention. Then this method is called again with validate_change set to
+ * false to allow the change to proceed.
+ *
+ * @param widget
+ * @param initial_value
+ * @param value
+ * @param lp_client
+ * @param validate_change
+ */
+namespace.save_information_type = function(widget, initial_value, value,
+                                           lp_client, context,
+                                           subscribers_list, validate_change) {
+    var error_handler = new Y.lp.client.FormErrorHandler();
+    error_handler.showError = function(error_msg) {
+        Y.lp.app.errors.display_error(
+            Y.one('#information-type'), error_msg);
+    };
+    error_handler.handleError = function(ioId, response) {
+        if( response.status === 400
+                && response.statusText === 'Bug Visibility') {
+            namespace._confirm_information_type_change(
+                    widget, initial_value, lp_client, context,
+                    subscribers_list);
+            return true;
+        }
+        var orig_value = namespace.information_type_value_from_key(
+            context.information_type, 'name', 'value');
+        widget.set('value', orig_value);
+        widget._showFailed();
+        namespace.update_privacy_portlet(orig_value);
+        return false;
+    };
+    var submit_url = document.URL + "/+secrecy";
+    var qs = Y.lp.client.append_qs('', 'field.actions.change', 'Change');
+    qs = Y.lp.client.append_qs(qs, 'field.information_type', value);
+    qs = Y.lp.client.append_qs(
+            qs, 'field.validate_change', validate_change?'on':'off');
+    var config = {
+        method: "POST",
+        headers: {'Accept': 'application/xhtml;application/json'},
+        data: qs,
+        on: {
+            start: function () {
+                widget._uiSetWaiting();
+                if (Y.Lang.isValue(subscribers_list)){
+                    subscribers_list.subscribers_list.startActivity(
+                        'Updating subscribers...');
+                }
+            },
+            end: function () {
+                widget._uiClearWaiting();
+                if (Y.Lang.isValue(subscribers_list)){
+                    subscribers_list.subscribers_list.stopActivity();
+                }
+            },
+            success: function (id, response) {
+                var result_data = null;
+                if (response.responseText !== '' &&
+                    response.getResponseHeader('Content-Type') ===
+                    'application/json')
+                {
+                    result_data = Y.JSON.parse(response.responseText);
+                }
+                namespace.information_type_save_success(
+                    widget, context, value, subscribers_list, result_data);
+                Y.lp.client.display_notifications(
+                    response.getResponseHeader('X-Lazr-Notifications'));
+            },
+            failure: error_handler.getFailureHandler()
+        }
+    };
+    lp_client.io_provider.io(submit_url, config);
+};
+
 var get_information_type_banner_text = function(value) {
     var text_template = "This page contains {info_type} information.";
     var info_type = namespace.information_type_value_from_key(
@@ -15,6 +96,112 @@
     return Y.Lang.sub(text_template, {'info_type': info_type});
 };
 
+namespace.information_type_save_success = function(widget, context, value,
+                                                   subscribers_list,
+                                                   subscribers_data) {
+    context.information_type =
+        namespace.information_type_value_from_key(
+                value, 'value', 'name');
+    namespace.update_privacy_banner(value);
+    widget._showSucceeded();
+    if (Y.Lang.isObject(subscribers_data)) {
+        var subscribers = subscribers_data.subscription_data;
+        subscribers_list._loadSubscribersFromList(subscribers);
+        var cache_data = subscribers_data.cache_data;
+        var item;
+        for (item in cache_data) {
+            if (cache_data.hasOwnProperty(item)) {
+                LP.cache[item] = cache_data[item];
+            }
+        }
+    }
+    if (Y.Lang.isValue(subscribers_list)){
+        var ns = Y.lp.bugs.bugtask_index.portlets.subscription;
+        ns.update_subscription_status(skip_animation);
+    }
+};
+
+/**
+ * Possibly prompt the user to confirm the change of information type.
+ * If the old value is public, and the new value is private, we want to
+ * confirm that the user really wants to make the change.
+ *
+ * @param widget
+ * @param initial_value
+ * @param lp_client
+ * @private
+ */
+namespace._confirm_information_type_change = function(widget, initial_value,
+                                                      lp_client, context,
+                                                      subscribers_list) {
+    var value = widget.get('value');
+    var do_save = function() {
+        namespace.update_privacy_portlet(value);
+        namespace.save_information_type(
+            widget, initial_value, value, lp_client, context, subscribers_list,
+            false);
+    };
+    // Reset the widget back to it's original value so the user doesn't see it
+    // change while the confirmation dialog is showing.
+    var new_value = widget.get('value');
+    widget.set('value', initial_value);
+    namespace.update_privacy_portlet(initial_value);
+    var confirm_text_template = [
+        '<p class="block-sprite large-warning">',
+        '    You are about to mark this bug as ',
+        '    <strong>{information_type}</strong>.<br>',
+        '    The bug will become invisible because there is no-one with',
+        '    permissions to see {information_type} bugs.',
+        '</p><p>',
+        '    <strong>Please confirm you really want to do this.</strong>',
+        '</p>'
+        ].join('');
+    var title = namespace.information_type_value_from_key(
+            value, 'value', 'name');
+    var confirm_text = Y.Lang.sub(confirm_text_template,
+            {information_type: title});
+    var co = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+        submit_fn: function() {
+            widget.set('value', new_value);
+            namespace.update_privacy_portlet(new_value);
+            do_save();
+        },
+        form_content: confirm_text,
+        headerContent: '<h2>Confirm information type change</h2>'
+    });
+    co.show();
+};
+
+namespace.setup_information_type_choice = function(privacy_link, lp_client,
+                                                   context, subscribers_list,
+                                                   skip_anim) {
+    skip_animation = skip_anim;
+    var initial_value = namespace.information_type_value_from_key(
+        context.information_type, 'name', 'value');
+    var information_type_value = Y.one('#information-type');
+    var information_type_edit = new Y.ChoiceSource({
+        editicon: privacy_link,
+        contentBox: Y.one('#privacy'),
+        value_location: information_type_value,
+        value: initial_value,
+        title: "Change information type",
+        items: LP.cache.information_type_data,
+        backgroundColor: '#FFFF99',
+        flashEnabled: false
+    });
+    Y.lp.app.choice.hook_up_choicesource_spinner(information_type_edit);
+    information_type_edit.render();
+    information_type_edit.on("save", function(e) {
+        var value = information_type_edit.get('value');
+        namespace.update_privacy_portlet(value);
+        namespace.save_information_type(
+            information_type_edit, initial_value, value, lp_client, context,
+            subscribers_list, true);
+    });
+    privacy_link.addClass('js-action');
+    return information_type_edit;
+};
+
 /**
  * Lookup the information_type property, keyed on the named value.
  *
@@ -80,4 +267,6 @@
     }
 };
 
-}, "0.1", {"requires": ["base", "oop", "node", "lp.app.banner.privacy"]});
+}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
+                        "lazr.choiceedit", "lp.bugs.bugtask_index",
+                        "lp.app.banner.privacy", "lp.app.choice"]});

=== renamed file 'lib/lp/bugs/javascript/tests/test_information_type_choice.html' => 'lib/lp/app/javascript/tests/test_information_type.html'
--- lib/lp/bugs/javascript/tests/test_information_type_choice.html	2012-08-29 14:02:57 +0000
+++ lib/lp/app/javascript/tests/test_information_type.html	2012-08-30 20:24:20 +0000
@@ -6,7 +6,7 @@
 
 <html>
   <head>
-      <title>lp.bugs.information_type_choice Tests</title>
+      <title>lp.app.information_type Tests</title>
 
       <!-- YUI and test setup -->
       <script type="text/javascript"
@@ -30,8 +30,6 @@
       <script type="text/javascript"
           src="../../../../../build/js/lp/app/choice.js"></script>
       <script type="text/javascript"
-          src="../../../../../build/js/lp/app/information_type.js"></script>
-      <script type="text/javascript"
           src="../../../../../build/js/lp/app/testing/mockio.js"></script>
       <script type="text/javascript"
           src="../../../../../build/js/lp/app/client.js"></script>
@@ -79,18 +77,18 @@
           src="../../../../../build/js/lp/bugs/bugtask_index.js"></script>
 
       <!-- The module under test. -->
-      <script type="text/javascript" src="../information_type_choice.js"></script>
+      <script type="text/javascript" src="../information_type.js"></script>
 
       <!-- Placeholder for any css asset for this module. -->
       <!-- <link rel="stylesheet" href="../assets/bugs.information_type_choice-core.css" /> -->
 
       <!-- The test suite -->
-      <script type="text/javascript" src="test_information_type_choice.js"></script>
+      <script type="text/javascript" src="test_information_type.js"></script>
 
     </head>
     <body class="yui3-skin-sam">
         <ul id="suites">
-            <li>lp.bugs.information_type_choice.test</li>
+            <li>lp.app.information_type.test</li>
         </ul>
         <div id="fixture"></div>
         <script type="text/x-template" id="portlet-template">

=== renamed file 'lib/lp/bugs/javascript/tests/test_information_type_choice.js' => 'lib/lp/app/javascript/tests/test_information_type.js'
--- lib/lp/bugs/javascript/tests/test_information_type_choice.js	2012-08-28 01:52:34 +0000
+++ lib/lp/app/javascript/tests/test_information_type.js	2012-08-30 20:24:20 +0000
@@ -1,13 +1,13 @@
 /* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
 
-YUI.add('lp.bugs.information_type_choice.test', function (Y) {
+YUI.add('lp.app.information_type.test', function (Y) {
 
-    var tests = Y.namespace('lp.bugs.information_type_choice.test');
-    var ns = Y.lp.bugs.information_type_choice;
-    tests.suite = new Y.Test.Suite('lp.bugs.information_type_choice Tests');
+    var tests = Y.namespace('lp.app.information_type.test');
+    var ns = Y.lp.app.information_type;
+    tests.suite = new Y.Test.Suite('lp.app.information_type Tests');
 
     tests.suite.add(new Y.Test.Case({
-        name: 'lp.bugs.information_type_choice_tests',
+        name: 'lp.app.information_type_tests',
 
         setUp: function() {
             window.LP = {
@@ -64,7 +64,7 @@
         makeWidget: function() {
             var privacy_link = Y.one('#privacy-link');
             this.widget = ns.setup_information_type_choice(
-                privacy_link, this.lp_client, true);
+                privacy_link, this.lp_client, LP.cache.bug, null, true);
         },
 
         _shim_privacy_banner: function () {
@@ -84,8 +84,8 @@
         },
 
         test_library_exists: function () {
-            Y.Assert.isObject(Y.lp.bugs.information_type_choice,
-                "Cannot locate the lp.bugs.information_type_choice module");
+            Y.Assert.isObject(Y.lp.app.information_type,
+                "Cannot locate the lp.app.information_type module");
         },
 
         // The save XHR call works as expected.
@@ -93,7 +93,7 @@
             this.makeWidget();
             var orig_save_success = ns.information_type_save_success;
             var save_success_called = false;
-            ns.information_type_save_success = function(widget, value,
+            ns.information_type_save_success = function(widget, context, value,
                                                    subscribers_list,
                                                    subscribers_data) {
                 Y.Assert.areEqual('USERDATA', value);
@@ -104,7 +104,8 @@
                 save_success_called = true;
             };
             ns.save_information_type(
-                    this.widget, 'PUBLIC', 'USERDATA', this.lp_client, true);
+                    this.widget, 'PUBLIC', 'USERDATA', this.lp_client,
+                    LP.cache.bug, null, true);
             this.mockio.success({
                 responseText: '{"subscription_data": "subscribers",' +
                     '"cache_data": {"item": "value"}}',
@@ -133,7 +134,8 @@
                 update_flag = true;
             });
 
-            ns.information_type_save_success(this.widget, 'PROPRIETARY');
+            ns.information_type_save_success(this.widget, LP.cache.bug,
+                                             'PROPRIETARY');
             var body = Y.one('body');
             Y.Assert.isTrue(body.hasClass('private'));
             Y.Assert.isTrue(hide_flag);
@@ -188,7 +190,8 @@
                 }
             };
             ns.information_type_save_success(
-                this.widget, 'PUBLIC', subscribers_list, subscribers_data);
+                this.widget, LP.cache.bug, 'PUBLIC', subscribers_list,
+                subscribers_data);
             Y.Assert.isTrue(load_subscribers_called);
             Y.Assert.areEqual('value1', window.LP.cache.item1);
             Y.Assert.areEqual('value2', window.LP.cache.item2);
@@ -227,7 +230,7 @@
             var function_called = false;
             ns.save_information_type =
                     function(widget, initial_value, value, lp_client,
-                             validate_change) {
+                             context, subscribers_list, validate_change) {
                 // We only care if the function is called with
                 // validate_change = false
                 Y.Assert.areEqual('PUBLIC', initial_value);
@@ -259,7 +262,7 @@
             var function_called = false;
             ns.save_information_type =
                     function(widget, initial_value, value, lp_client,
-                             validate_change) {
+                             context, subscribers_list, validate_change) {
                 // We only care if the function is called with
                 // validate_change = false
                 function_called = !validate_change;
@@ -285,7 +288,8 @@
             this.makeWidget();
             this.widget.set('value', 'USERDATA');
             ns.save_information_type(
-                    this.widget, 'PUBLIC', 'USERDATA', this.lp_client);
+                    this.widget, 'PUBLIC', 'USERDATA', this.lp_client,
+                    LP.cache.bug);
             this.mockio.last_request.respond({
                 status: 500,
                 statusText: 'An error occurred'
@@ -304,5 +308,5 @@
     }));
 
 }, '0.1', {'requires': ['test', 'console', 'event', 'node-event-simulate',
-        'lp.testing.mockio', 'lp.client', 'lp.app.informatin_type',
-        'lp.bugs.information_type_choice', 'lp.bugs.subscribers']});
+        'lp.testing.mockio', 'lp.client', 'lp.app.information_type',
+        'lp.bugs.subscribers']});

=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml	2012-08-01 19:02:49 +0000
+++ lib/lp/blueprints/browser/configure.zcml	2012-08-30 20:24:20 +0000
@@ -311,6 +311,9 @@
             <browser:page
                 name="+listing-detailed"
                 template="../templates/specification-listing-detailed.pt"/>
+            <browser:page
+                name="+portlet-privacy"
+                template="../templates/blueprint-portlet-privacy.pt"/>
         </browser:pages>
         <browser:page
             for="lp.blueprints.interfaces.specification.ISpecification"

=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py	2012-08-20 19:14:22 +0000
+++ lib/lp/blueprints/browser/specification.py	2012-08-30 20:24:20 +0000
@@ -78,6 +78,7 @@
     )
 
 from lp import _
+from lp.app.browser.informationtype import InformationTypePortletMixin
 from lp.app.browser.launchpad import AppFrontPageSearchView
 from lp.app.browser.launchpadform import (
     action,
@@ -111,6 +112,7 @@
 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
 from lp.blueprints.interfaces.sprintspecification import ISprintSpecification
 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
+from lp.registry.enums import PRIVATE_INFORMATION_TYPES
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.product import IProduct
 from lp.services.config import config
@@ -423,7 +425,7 @@
              'linkbug', 'unlinkbug', 'linkbranch',
              'adddependency', 'removedependency',
              'dependencytree', 'linksprint', 'supersede',
-             'retarget']
+             'retarget', 'information_type']
 
     @enabled_with_permission('launchpad.Edit')
     def milestone(self):
@@ -528,8 +530,13 @@
             text = 'Link a related branch'
         return Link('+linkbranch', text, icon='add')
 
-
-class SpecificationSimpleView(LaunchpadView):
+    def information_type(self):
+        """Return the 'Set privacy/security' Link."""
+        text = 'Change privacy/security'
+        return Link('#', text)
+
+
+class SpecificationSimpleView(InformationTypePortletMixin, LaunchpadView):
     """Used to render portlets and listing items that need browser code."""
 
     @cachedproperty
@@ -545,6 +552,17 @@
     def bug_links(self):
         return self.context.getLinkedBugTasks(self.user)
 
+    @cachedproperty
+    def privacy_portlet_css(self):
+        if self.private:
+            return 'portlet private'
+        else:
+            return 'portlet public'
+
+    @cachedproperty
+    def private(self):
+        return self.context.information_type in PRIVATE_INFORMATION_TYPES
+
 
 class SpecificationView(SpecificationSimpleView):
     """Used to render the main view of a specification."""
@@ -562,6 +580,7 @@
         return self.context.summary
 
     def initialize(self):
+        super(SpecificationView, self).initialize()
         # The review that the user requested on this spec, if any.
         self.notices = []
 

=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
--- lib/lp/blueprints/browser/tests/test_specification.py	2012-01-01 02:58:52 +0000
+++ lib/lp/blueprints/browser/tests/test_specification.py	2012-08-30 20:24:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 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).
 
 __metaclass__ = type
@@ -8,10 +8,14 @@
 
 from BeautifulSoup import BeautifulSoup
 import pytz
-from testtools.matchers import Equals
+from testtools.matchers import (
+    Equals,
+    Not,
+    )
 from zope.component import getUtility
 from zope.publisher.interfaces import NotFound
 from zope.security.proxy import removeSecurityProxy
+import soupmatchers
 
 from lp.app.browser.tales import format_link
 from lp.blueprints.browser import specification
@@ -20,7 +24,9 @@
     ISpecification,
     ISpecificationSet,
     )
+from lp.registry.enums import InformationType
 from lp.registry.interfaces.person import PersonVisibility
+from lp.services.features.testing import FeatureFixture
 from lp.services.webapp.interfaces import BrowserNotificationLevel
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
@@ -165,6 +171,41 @@
                 "... Registered by Some Person ... ago ..."))
 
 
+class TestSpecificationInformationType(BrowserTestCase):
+
+    layer = DatabaseFunctionalLayer
+
+    portlet_tag = soupmatchers.Tag('info-type-portlet', True,
+                                   attrs=dict(id='information-type-summary'))
+
+    def setUp(self):
+        super(TestSpecificationInformationType, self).setUp()
+        self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
+            'true'}))
+
+    def assertBrowserMatches(self, matcher):
+        browser = self.getViewBrowser(self.factory.makeSpecification())
+        self.assertThat(browser.contents, matcher)
+
+    def test_has_privacy_portlet(self):
+        self.assertBrowserMatches(soupmatchers.HTMLContains(self.portlet_tag))
+
+    def test_privacy_portlet_requires_flag(self):
+        self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
+            ''}))
+        self.assertBrowserMatches(
+            Not(soupmatchers.HTMLContains(self.portlet_tag)))
+
+    def test_has_privacy_banner(self):
+        spec = self.factory.makeSpecification(
+            information_type=InformationType.PROPRIETARY)
+        browser = self.getViewBrowser(spec)
+        privacy_banner = soupmatchers.Tag('privacy-banner', True,
+                attrs={'class': 'banner-text'})
+        self.assertThat(browser.contents,
+                           soupmatchers.HTMLContains(privacy_banner))
+
+
 class TestSpecificationViewPrivateArtifacts(BrowserTestCase):
     """ Tests that specifications with private team artifacts can be viewed.
 

=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py	2012-08-22 15:41:05 +0000
+++ lib/lp/blueprints/interfaces/specification.py	2012-08-30 20:24:20 +0000
@@ -565,6 +565,12 @@
         :param user: The user doing the search.
         """
 
+    def getAllowedInformationTypes(who):
+        """Get a list of acceptable `InformationType`s for this spec.
+
+        The intersection of the affected pillars' allowed types is permitted.
+        """
+
 
 class ISpecificationEditRestricted(Interface):
     """Specification's attributes and methods protected with launchpad.Edit.

=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2012-08-22 15:42:58 +0000
+++ lib/lp/blueprints/model/specification.py	2012-08-30 20:24:20 +0000
@@ -815,6 +815,9 @@
         return '<Specification %s %r for %r>' % (
             self.id, self.name, self.target.name)
 
+    def getAllowedInformationTypes(self, who):
+        return set(InformationType.items)
+
     @property
     def private(self):
         return self.information_type in PRIVATE_INFORMATION_TYPES
@@ -1080,10 +1083,12 @@
     def new(self, name, title, specurl, summary, definition_status,
         owner, approver=None, product=None, distribution=None, assignee=None,
         drafter=None, whiteboard=None, workitems_text=None,
-        priority=SpecificationPriority.UNDEFINED):
+        priority=SpecificationPriority.UNDEFINED, information_type=None):
         """See ISpecificationSet."""
         # Adapt the NewSpecificationDefinitionStatus item to a
         # SpecificationDefinitionStatus item.
+        if information_type is None:
+            information_type = InformationType.PUBLIC
         status_name = definition_status.name
         status_names = NewSpecificationDefinitionStatus.items.mapping.keys()
         if status_name not in status_names:
@@ -1095,7 +1100,8 @@
             summary=summary, priority=priority,
             definition_status=definition_status, owner=owner,
             approver=approver, product=product, distribution=distribution,
-            assignee=assignee, drafter=drafter, whiteboard=whiteboard)
+            assignee=assignee, drafter=drafter, whiteboard=whiteboard,
+            information_type=information_type)
 
     def getDependencyDict(self, specifications):
         """See `ISpecificationSet`."""

=== added file 'lib/lp/blueprints/templates/blueprint-portlet-privacy.pt'
--- lib/lp/blueprints/templates/blueprint-portlet-privacy.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/templates/blueprint-portlet-privacy.pt	2012-08-30 20:24:20 +0000
@@ -0,0 +1,16 @@
+<div
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  id="privacy"
+  tal:attributes="class view/privacy_portlet_css"
+  tal:define="link context/menu:context/information_type"
+>
+    <span id="information-type-summary"
+      tal:attributes="class view/information_type_css;">This blueprint
+      contains <strong id="information-type" tal:content="view/information_type"></strong> information</span>&nbsp;<a class="sprite edit action-icon"
+        id="privacy-link" tal:attributes="href link/path"
+        tal:condition="link/enabled" style="display: none" >Edit</a>
+
+    <div id="information-type-description" style="padding-top: 5px" tal:content="view/information_type_description"></div>
+</div>

=== modified file 'lib/lp/blueprints/templates/specification-index.pt'
--- lib/lp/blueprints/templates/specification-index.pt	2012-08-23 13:55:00 +0000
+++ lib/lp/blueprints/templates/specification-index.pt	2012-08-30 20:24:20 +0000
@@ -310,7 +310,15 @@
     </div>
 
   <script type="text/javascript">
-    LPJS.use('lp.anim', 'lp.deprecated.ui', 'node', 'widget', function(Y) {
+    LPJS.use('lp.anim', 'lp.client', 'lp.deprecated.ui',
+             'lp.app.information_type', 'node', 'widget', function(Y) {
+        Y.on('domready', function(){
+            var privacy_link = Y.one('#privacy-link');
+            Y.lp.app.information_type.setup_information_type_choice(
+              privacy_link, new Y.lp.client.Launchpad(), LP.cache.context,
+              null);
+            privacy_link.setStyle('display', 'inline');
+        });
 
         Y.on('lp:context:implementation_status:changed', function(e) {
             var icon = Y.one('#informational-icon');
@@ -345,7 +353,8 @@
         });
         Y.on('lp:context:title:changed', function(e) {
             // change the window title and breadcrumb.
-            Y.lp.deprecated.ui.update_field('ol.breadcrumbs li:last-child', e.new_value);
+            Y.lp.deprecated.ui.update_field('ol.breadcrumbs li:last-child',
+                                            e.new_value);
             var title = window.document.title;
             title = e.new_value + title.substring(e.old_value.length);
             window.document.title = title;
@@ -377,7 +386,7 @@
 
   <tal:side metal:fill-slot="side">
     <tal:menu replace="structure context/@@+global-actions" />
-
+    <tal:privacy replace="structure context/@@+portlet-privacy" condition="features/blueprints.information_type.enabled" />
     <div tal:replace="structure context/@@+portlet-subscribers" />
   </tal:side>
 </body>

=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
--- lib/lp/bugs/javascript/bugtask_index.js	2012-08-23 18:58:50 +0000
+++ lib/lp/bugs/javascript/bugtask_index.js	2012-08-30 20:24:20 +0000
@@ -49,8 +49,11 @@
 
         privacy_link = Y.one('#privacy-link');
         if (privacy_link) {
-            Y.lp.bugs.information_type_choice.setup_information_type_choice(
-                privacy_link, lp_client);
+                var sub_list_node = Y.one('#other-bug-subscribers');
+                var subscribers_list = sub_list_node.getData(
+                    'subscribers_loader');
+                Y.lp.app.information_type.setup_information_type_choice(
+                privacy_link, lp_client, LP.cache.bug, subscribers_list);
         }
         setup_add_attachment();
         setup_link_branch_picker();
@@ -1125,7 +1128,7 @@
                         "lazr.formoverlay", "lp.anim", "lazr.overlay",
                         "lazr.choiceedit", "lp.app.picker",
                         "lp.bugs.bugtask_index.portlets.subscription",
-                        "lp.bugs.information_type_choice",
+                        "lp.app.information_type",
                         "lp.app.widgets.expander", "lp.client", "escape",
                         "lp.client.plugins", "lp.app.errors",
                         "lp.app.banner.privacy",

=== removed file 'lib/lp/bugs/javascript/information_type_choice.js'
--- lib/lp/bugs/javascript/information_type_choice.js	2012-08-28 01:52:34 +0000
+++ lib/lp/bugs/javascript/information_type_choice.js	1970-01-01 00:00:00 +0000
@@ -1,189 +0,0 @@
-/* Copyright 2012 Canonical Ltd.  This software is licensed under the
- * GNU Affero General Public License version 3 (see the file LICENSE).
- *
- * Information Type choice widget for bug pages.
- */
-
-YUI.add('lp.bugs.information_type_choice', function(Y) {
-
-var namespace = Y.namespace('lp.bugs.information_type_choice');
-var information_type = Y.namespace('lp.app.information_type');
-
-// For testing.
-var skip_animation = false;
-
-/**
- * Save the new information type. If validate_change is true, then a check
- * will be done to ensure the bug will not become invisible. If the bug will
- * become invisible, a confirmation popup is used to confirm the user's
- * intention. Then this method is called again with validate_change set to
- * false to allow the change to proceed.
- *
- * @param widget
- * @param initial_value
- * @param value
- * @param lp_client
- * @param validate_change
- */
-namespace.save_information_type = function(widget, initial_value, value,
-                                           lp_client, validate_change) {
-    var error_handler = new Y.lp.client.FormErrorHandler();
-    error_handler.showError = function(error_msg) {
-        Y.lp.app.errors.display_error(
-            Y.one('#information-type'), error_msg);
-    };
-    error_handler.handleError = function(ioId, response) {
-        if( response.status === 400
-                && response.statusText === 'Bug Visibility') {
-            namespace._confirm_information_type_change(
-                    widget, initial_value, lp_client);
-            return true;
-        }
-        var orig_value = information_type.information_type_value_from_key(
-            LP.cache.bug.information_type, 'name', 'value');
-        widget.set('value', orig_value);
-        widget._showFailed();
-        information_type.update_privacy_portlet(orig_value);
-        return false;
-    };
-    var submit_url = document.URL + "/+secrecy";
-    var qs = Y.lp.client.append_qs('', 'field.actions.change', 'Change');
-    qs = Y.lp.client.append_qs(qs, 'field.information_type', value);
-    qs = Y.lp.client.append_qs(
-            qs, 'field.validate_change', validate_change?'on':'off');
-    var sub_list_node = Y.one('#other-bug-subscribers');
-    var subscribers_list = sub_list_node.getData('subscribers_loader');
-    var config = {
-        method: "POST",
-        headers: {'Accept': 'application/xhtml;application/json'},
-        data: qs,
-        on: {
-            start: function () {
-                widget._uiSetWaiting();
-                subscribers_list.subscribers_list.startActivity(
-                    'Updating subscribers...');
-            },
-            end: function () {
-                widget._uiClearWaiting();
-                subscribers_list.subscribers_list.stopActivity();
-            },
-            success: function (id, response) {
-                var result_data = null;
-                if (response.responseText !== '') {
-                    result_data = Y.JSON.parse(response.responseText);
-                }
-                namespace.information_type_save_success(
-                    widget, value, subscribers_list, result_data);
-                Y.lp.client.display_notifications(
-                    response.getResponseHeader('X-Lazr-Notifications'));
-            },
-            failure: error_handler.getFailureHandler()
-        }
-    };
-    lp_client.io_provider.io(submit_url, config);
-};
-
-namespace.information_type_save_success = function(widget, value,
-                                                   subscribers_list,
-                                                   subscribers_data) {
-    LP.cache.bug.information_type =
-        information_type.information_type_value_from_key(
-                value, 'value', 'name');
-    information_type.update_privacy_banner(value);
-    widget._showSucceeded();
-    if (Y.Lang.isObject(subscribers_data)) {
-        var subscribers = subscribers_data.subscription_data;
-        subscribers_list._loadSubscribersFromList(subscribers);
-        var cache_data = subscribers_data.cache_data;
-        var item;
-        for (item in cache_data) {
-            if (cache_data.hasOwnProperty(item)) {
-                LP.cache[item] = cache_data[item];
-            }
-        }
-    }
-    var ns = Y.lp.bugs.bugtask_index.portlets.subscription;
-    ns.update_subscription_status(skip_animation);
-};
-
-/**
- * Possibly prompt the user to confirm the change of information type.
- * If the old value is public, and the new value is private, we want to
- * confirm that the user really wants to make the change.
- *
- * @param widget
- * @param initial_value
- * @param lp_client
- * @private
- */
-namespace._confirm_information_type_change = function(widget, initial_value,
-                                                      lp_client) {
-    var value = widget.get('value');
-    var do_save = function() {
-        information_type.update_privacy_portlet(value);
-        namespace.save_information_type(
-            widget, initial_value, value, lp_client, false);
-    };
-    // Reset the widget back to it's original value so the user doesn't see it
-    // change while the confirmation dialog is showing.
-    var new_value = widget.get('value');
-    widget.set('value', initial_value);
-    information_type.update_privacy_portlet(initial_value);
-    var confirm_text_template = [
-        '<p class="block-sprite large-warning">',
-        '    You are about to mark this bug as ',
-        '    <strong>{information_type}</strong>.<br>',
-        '    The bug will become invisible because there is no-one with',
-        '    permissions to see {information_type} bugs.',
-        '</p><p>',
-        '    <strong>Please confirm you really want to do this.</strong>',
-        '</p>'
-        ].join('');
-    var title = information_type.information_type_value_from_key(
-            value, 'value', 'name');
-    var confirm_text = Y.Lang.sub(confirm_text_template,
-            {information_type: title});
-    var co = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
-        submit_fn: function() {
-            widget.set('value', new_value);
-            information_type.update_privacy_portlet(new_value);
-            do_save();
-        },
-        form_content: confirm_text,
-        headerContent: '<h2>Confirm information type change</h2>'
-    });
-    co.show();
-};
-
-namespace.setup_information_type_choice = function(privacy_link, lp_client,
-                                                   skip_anim) {
-    skip_animation = skip_anim;
-    var initial_value = information_type.information_type_value_from_key(
-        LP.cache.bug.information_type, 'name', 'value');
-    var information_type_value = Y.one('#information-type');
-    var information_type_edit = new Y.ChoiceSource({
-        editicon: privacy_link,
-        contentBox: Y.one('#privacy'),
-        value_location: information_type_value,
-        value: initial_value,
-        title: "Change information type",
-        items: LP.cache.information_type_data,
-        backgroundColor: '#FFFF99',
-        flashEnabled: false
-    });
-    Y.lp.app.choice.hook_up_choicesource_spinner(information_type_edit);
-    information_type_edit.render();
-    information_type_edit.on("save", function(e) {
-        var value = information_type_edit.get('value');
-        information_type.update_privacy_portlet(value);
-        namespace.save_information_type(
-            information_type_edit, initial_value, value, lp_client, true);
-
-    });
-    privacy_link.addClass('js-action');
-    return information_type_edit;
-};
-}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
-                        "lazr.choiceedit", "lp.bugs.bugtask_index",
-                        "lp.app.banner.privacy", "lp.app.choice",
-                        "lp.app.information_type"]});

=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py	2012-08-30 11:52:28 +0000
+++ lib/lp/services/features/flags.py	2012-08-30 20:24:20 +0000
@@ -57,6 +57,12 @@
      '',
      '',
      ''),
+    ('blueprints.information_type.enabled',
+     'boolean',
+     'Enable UI for information_type on Blueprints.',
+     'Disable UI',
+     'Blueprint information_type UI',
+     'https://dev.launchpad.net/LEP/PrivateProjects'),
     ('bugs.affected_count_includes_dupes.disabled',
      'boolean',
      ("Disable adding up affected users across all duplicate bugs."),

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2012-08-29 03:19:38 +0000
+++ lib/lp/testing/factory.py	2012-08-30 20:24:20 +0000
@@ -2065,7 +2065,8 @@
                           status=NewSpecificationDefinitionStatus.NEW,
                           implementation_status=None, goal=None, specurl=None,
                           assignee=None, drafter=None, approver=None,
-                          priority=None, whiteboard=None, milestone=None):
+                          priority=None, whiteboard=None, milestone=None,
+                          information_type=None):
         """Create and return a new, arbitrary Blueprint.
 
         :param product: The product to make the blueprint on.  If one is
@@ -2102,7 +2103,8 @@
             approver=approver,
             product=product,
             distribution=distribution,
-            priority=priority)
+            priority=priority,
+            information_type=information_type)
         naked_spec = removeSecurityProxy(spec)
         if status.name not in status_names:
             # Set the closed status after the status has a sane initial state.


Follow ups