← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/add_new_banner into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/add_new_banner into lp:launchpad.

Commit message:
Add a new set of JS code for handling Banner UI and a new global Y.View as a controller for the logic.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rharding/launchpad/add_new_banner/+merge/133736

= Summary =

We need to add banner support to projects and blueprints now that they support
privacy. As these features are also in beta we really wanted to enable the
display of dual banners. Currently the banners are handled through an array of
stray modules that make working on them difficult and they do several evil
things, such as manually set style: attributes on html elements.

This branch introduces a new YUI widget for banners. It also introduces a new
JS View object that is loaded on every page to handle the interaction between
events on the page and the control of banners.

== Pre Implementation ==

Lots and lots of going through current code, discussing goals with Deryck, and
a sanity check mini-code look over from jcsackett that did much of the current
banner work.

== Implementation Notes ==

This code is not wired up at all. It's meant to introduce new modules that old
code needs to interact with. The goal is to move to all code merely firing an
event that the global view handles and deals with banners. The banners
themselves are reusable widgets and could be used to create banner nodes
anywhere in the dom as many times as you want.

The entirity of the work is viewable in the temp MP:

https://code.launchpad.net/~rharding/launchpad/new_banner_stage1/+merge/132957

However it's huge and so I'm breaking this into several parts. The next step
will alter the interaction points of the old code followed by a final branch
to remove all the old banner code once everything else is updated and working.

Since all of this involved the information type, we're relying on it's events
to be the command center. The idea is that global.js sets up event listeners.
Any code that changes information type should fire the
information_type.EV_CHANGE event with the new value and the global.js will
handle processing any banner updates/changes.

The beta banner will be setup and provided from python view as it currently
is. However, the .pt will only place an empty div on the page and the JS will
handle all rendering so that all rendered html is in one place, the Widget.

The CSS is updated so that there is no need to manually set styles. Currently
it relies on the view code adding public/private to the <body> however,
however this is ONLY required because of the way the locationbar works and
when bug #1076074 this can be cleaned up.

Just to give an idea here's a screenshot of a stacked banner we're working on 
making possible with this code.

http://uploads.mitechie.com/lp/stacked_banners.png

== Tests ==

New tests for each module.

xvfb-run ./bin/test -x -cvv --layer=YUITestLayer

== QA ==

None at this point. It's only adding in code that's not wired into use yet.
-- 
https://code.launchpad.net/~rharding/launchpad/add_new_banner/+merge/133736
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/add_new_banner into lp:launchpad.
=== added file 'lib/lp/app/javascript/ui/assets/skins/sam/banner.css'
--- lib/lp/app/javascript/ui/assets/skins/sam/banner.css	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/ui/assets/skins/sam/banner.css	2012-11-09 19:09:19 +0000
@@ -0,0 +1,109 @@
+/* JS Banner styling */
+.yui3-banner {
+    /* Default to not visible so we can fade in */
+    opacity: 1;
+
+    /* Animations for fade-in/out */
+    -webkit-transition: opacity 0.3s ease-in;
+    -moz-transition: opacity 0.3s ease-in;
+    transition: opacity 0.3s ease-in;
+
+    width: 100%;
+}
+
+.yui3-banner.yui3-banner-hidden {
+    opacity: 0;
+}
+
+/* Nasty hack to get the bar moved since it's absolutely positioned
+ * This also needs to be updated
+ */
+body.beta #locationbar, body.private #locationbar {
+    top: 47px;
+}
+
+/* If we have both classes make room for two banners height */
+body.beta.private #locationbar {
+    top: 94px;
+}
+
+/* If the container exists make sure we start out with the rest of the page
+ * bumped down the starting distance to reduce flash effect
+ */
+.beta_banner_container, .private_banner_container {
+    min-height: 45px;
+}
+
+.yui3-banner-content {
+    box-shadow: 0 0 5px #333;
+    background-color: #666;
+    color: #fff;
+    display: block;
+    font-size: 14px;
+    font-weight: bold;
+    line-height: 21px;
+    padding: 8px 20px;
+    text-align: left;
+    text-shadow: 0 -1px 0 #631616;
+    z-index: 10;
+}
+
+.yui3-banner-content .badge {
+    display: inline-block;
+    height: 21px;
+    margin-right: 10px;
+    padding: 0;
+    vertical-align: middle;
+    width: 20px;
+}
+.yui3-banner-content .banner-content {}
+
+.yui3-banner-content.beta {
+    /* Some of these are required to override .beta CSS */
+
+    /* Defined for browsers that don't support transparency */
+    background-color: #606060;
+    /* Transparent background for browsers that support it */
+    background-color: rgba(64, 64, 64, 0.9);
+    height: auto;
+    margin-top: 0px;
+    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
+    width: auto;
+}
+.yui3-banner-content.beta .yui3-banner-content-content {}
+.yui3-banner-content.beta .badge {
+    /* sprite-ref: icon-sprites */
+    background-color: #c10000;
+    background: linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
+    background: -moz-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
+    background: -ms-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
+    background: -o-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
+    background: -webkit-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
+    border-radius: 5px;
+    border-top: 1px solid #e20000;
+    font-size: 12px;
+    font-weight: bold;
+    margin-right: 12px;
+    padding: 3px 6px 4px 6px;
+    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+    width: auto;
+}
+
+.yui3-banner-content.beta .beta-feature {
+    font-weight: bold;
+}
+.yui3-banner-content.beta .info-link {
+    color: #4884ef;
+}
+
+.yui3-banner-content.private {
+    /* Define colour for browsers that don't support transparency */
+    background: #8d1f1f;
+    /* Set transparent background for browsers that support it */
+    background: rgba(125,0,0,0.9);
+}
+.yui3-banner-content.private .banner-content {}
+.yui3-banner-content.private .badge {
+    background: url(/@@/notification-private.png);
+    background-repeat: no-repeat;
+}

=== added file 'lib/lp/app/javascript/ui/banner.js'
--- lib/lp/app/javascript/ui/banner.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/ui/banner.js	2012-11-09 19:09:19 +0000
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Notification banner widget
+ *
+ * @module lp.ui.banner
+ * @namespace lp.ui
+ * @module banner
+ */
+YUI.add('lp.ui.banner', function (Y) {
+
+    var ns = Y.namespace('lp.ui.banner');
+
+    // GLOBALS
+    ns.PRIVATE = 'private';
+    ns.BETA = 'beta';
+
+    /**
+     * Banner widget base class
+     *
+     * This is the base Banner, you're supposed to supply some message data to
+     * generate the banner in the proper method.
+     *
+     * This banner provides all shared functionality between the Privacy and
+     * Beta banners.
+     *
+     * @class Banner
+     * @extends Y.Widget
+     *
+     */
+    ns.Banner = Y.Base.create('banner', Y.Widget, [], {
+        template: [
+            '<div class="banner">',
+                '<span class="badge {{ banner_type }}">{{ badge_text }}</span>',
+                '<span class="banner-content">{{{ content }}}</span>',
+            '</div>'
+        ].join(''),
+
+
+        /**
+         * Bind events that our widget supports such as closing the banner.
+         *
+         * We also watch the destroy event to clean up side effect css we
+         * created.
+         *
+         * @method bindUI
+         */
+        bindUI: function () {
+            this.on('destroy', function (ev) {
+                // XXX: Bug #1076074
+                var body = Y.one('body');
+                var banner_type = this.get('banner_type');
+                body.removeClass(banner_type);
+
+                // Remove any container the page might have provided for us to
+                // start out with.
+                var container_class = '.' + banner_type + '_banner_container';
+                var container = Y.one(container_class);
+                if (container) {
+                    Y.one(container_class).remove();
+                }
+            });
+
+            this.after('contentChange', function () {
+                this.renderUI();
+            });
+        },
+
+        /**
+         * Default initialize method.
+         *
+         * @method initialize
+         * @param {Object} cfg
+         */
+        initialize: function (cfg) {
+        },
+
+        /**
+         * Widget render method to generate the html of the widget.
+         *
+         * @method renderUI
+         */
+        renderUI: function () {
+            var contentBox = this.get('contentBox');
+            contentBox.addClass(this.get('banner_type'));
+            var html = Y.lp.mustache.to_html(this.template, this.getAttrs());
+            contentBox.setHTML(html);
+
+            // XXX: Bug #1076074
+            // Needs to get cleaned up. Only applies to the global
+            // banners and not to other ones which we're working to allow.
+            // This is currently required because the #locationbar is
+            // absolutely located and needs to be moved as banners change.
+            var body = Y.one('body');
+            body.addClass(this.get('banner_type'));
+
+            if (this.get('visible')) {
+                this.show();
+            }
+        },
+
+        /**
+         * We need to override show so that we force a browser repaint which
+         * allows our CSS3 animation to run. Otherwise the browser sees we
+         * added new DOM elements and jumps straight to the finished animation
+         * point.
+         *
+         * @method show
+         */
+        show: function () {
+            var _node = this.get('boundingBox')._node;
+            var getComputedStyle = document.defaultView.getComputedStyle;
+            _node.style.display = getComputedStyle(_node).display;
+            return this.set('visible', true);
+        }
+
+    }, {
+        ATTRS: {
+            /**
+             * Instead of a sprite we might have text such as the Beta banner.
+             *
+             * @attribute badge_text
+             * @default undefined
+             * @type {String}
+             */
+            badge_text: {},
+
+            /**
+             * The Banner is meant to house some message to the user provided
+             * by this content. It can be html and is not escaped for that
+             * reason.
+             *
+             * @attribute content
+             * @default undefined
+             * @type {String}
+             */
+            content: {},
+
+            /**
+             * This is listed to help aid in discovery of how the container
+             * node for the widget is determined. It's passed into the
+             * render() method and the Widget constructs itself inside of
+             * there.
+             *
+             * @attribute boundingBox
+             * @default undefined
+             * @type {Node}
+             */
+            boundingBox: {
+
+            },
+
+            /**
+             * Much of the Widget is determined by the type of banner it is.
+             * See the constants defined PRIVATE and BETA for two known types.
+             * If you set this manually you'll be able to provide custom
+             * styling as required because the type is used as a css class
+             * property.
+             *
+             * @attribute banner_type
+             * @default undefined
+             * @type {String}
+             */
+            banner_type: {},
+
+            /**
+             * Start out as not visible which should render as opacity 0, then
+             * we update it and it animates due to our css3.
+             *
+             * @attribute visible
+             * @default false
+             * @type {Bool}
+             */
+            visible: {
+                value: false
+            }
+        }
+    });
+
+    /**
+     * Beta Banner widget
+     *
+     * This is the Beta feature banner which needs to know about the title and
+     * url of the feature to construct the content correctly. Features are
+     * meant to be matched to the current LP.cache.related_features data
+     * available.
+     *
+     * @class BetaBanner
+     * @extends Banner
+     *
+     */
+    ns.BetaBanner = Y.Base.create('banner', ns.Banner, [], {
+
+    }, {
+        ATTRS: {
+            /**
+             * @attribute badge_text
+             * @default "BETA!"
+             * @type {String}
+             */
+            badge_text: {
+                value: 'BETA!'
+            },
+
+            /**
+             * The content for the beta banner is constructed from hard coded
+             * content and the list of enabled beta features currently
+             * relevant to the page.
+             *
+             * @attribute content
+             * @default {generated}
+             * @type {String}
+             */
+            content: {
+                getter: function () {
+                    var content = "Some parts of this page are in beta:&nbsp;";
+                    var key;
+                    // We need to process the features to build the features
+                    // that apply.
+                    var features = this.get('features');
+                    for (key in features) {
+                       if (features.hasOwnProperty(key)) {
+                           var obj = features[key];
+                           if (obj.is_beta) {
+                               content = content + [
+                                 '<span class="beta-feature">',
+                                 obj.title,
+                                 '&nbsp;<a class="info-link" href="',
+                                 obj.url + '">(read more)</a>',
+                                 '</span>'
+                               ].join('');
+                           }
+                        }
+                    }
+                    return content;
+                }
+            },
+
+            /**
+             * features is a nested object of the beta features going. See
+             * LP.cache.related_features for the list of features. We only
+             * want those related features that are in beta.
+             * Ex: {
+             *     disclosure.private_projects.enabled: {
+             *         is_beta: true,
+             *         title: "",
+             *         url: "http://blog.ld.net/general/private-projects-beta";,
+             *         value: "true"
+             *     }
+             * }
+             * @attribute features
+             * @default {}
+             * @type {Object}
+             */
+            features: {},
+
+            /**
+             * Manually force the banner type so users don't need to set it.
+             * This is a beta banner class.
+             *
+             * @attribute banner_type
+             * @default BETA
+             * @type {String}
+             */
+            banner_type: {
+                value: ns.BETA
+            }
+
+        }
+    });
+
+    /**
+     * Private Banner widget
+     *
+     * This is the Private feature banner which is pretty basic.
+     *
+     * Note that this doesn't automatically follow the information type code.
+     * Nor does it listen to the choice widgets and try to update. It's purely
+     * meant to function as told to do so. Most of the work around making sure
+     * the banner shows and works properly is in the View code in global.js.
+     *
+     * @class PrivateBanner
+     * @extends Banner
+     *
+     */
+    ns.PrivateBanner = Y.Base.create('banner', ns.Banner, [], {
+
+    }, {
+        ATTRS: {
+            badge_text: {
+                value: ''
+            },
+
+            content: {
+                value: 'The information on this page is private.'
+            },
+
+            /**
+             * Manually force the banner type so users don't need to set it.
+             * This is a beta banner class.
+             *
+             * @attribute banner_type
+             * @default BETA
+             * @type {String}
+             */
+            banner_type: {
+                value: ns.PRIVATE
+            }
+        }
+    });
+
+}, '0.1', {
+    requires: ['base', 'node', 'anim', 'widget', 'lp.mustache', 'yui-log']
+});

=== added directory 'lib/lp/app/javascript/ui/tests'
=== added file 'lib/lp/app/javascript/ui/tests/test_banner.html'
--- lib/lp/app/javascript/ui/tests/test_banner.html	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/ui/tests/test_banner.html	2012-11-09 19:09:19 +0000
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+  <head>
+      <title>lp.ui.banner Tests</title>
+
+      <!-- YUI and test setup -->
+      <script type="text/javascript"
+              src="../../../../../../build/js/yui/yui/yui.js">
+      </script>
+      <link rel="stylesheet"
+      href="../../../../../../build/js/yui/console/assets/console-core.css" />
+      <link rel="stylesheet"
+      href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
+      <link rel="stylesheet"
+      href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+      <script type="text/javascript"
+              src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
+      <script type="text/javascript"
+              src="../../../../../build/js/lp/app/testing/helpers.js"></script>
+
+      <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
+
+      <!-- Dependencies -->
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/mustache.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../banner.js"></script>
+
+      <!-- Placeholder for any css asset for this module. -->
+      <link rel="stylesheet" href="../assets/skins/sam/banner.css" />
+
+      <!-- The test suite -->
+      <script type="text/javascript" src="test_banner.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <li>lp.ui.banner.test</li>
+        </ul>
+        <div id="fixture"></div>
+    </body>
+</html>

=== added file 'lib/lp/app/javascript/ui/tests/test_banner.js'
--- lib/lp/app/javascript/ui/tests/test_banner.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/ui/tests/test_banner.js	2012-11-09 19:09:19 +0000
@@ -0,0 +1,287 @@
+/* Copyright (c) 2012 Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.ui.banner.test', function (Y) {
+
+    var tests = Y.namespace('lp.ui.banner.test');
+    tests.suite = new Y.Test.Suite('ui.banner Tests');
+
+    var ns = Y.lp.ui.banner;
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'ui.banner_tests',
+
+        setUp: function () {
+            this.container = Y.one('#fixture');
+        },
+
+        tearDown: function () {
+            this.container.empty();
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lp.ui.banner,
+                "Could not locate the lp.ui.banner module");
+        },
+
+        test_render: function () {
+            var b = new ns.Banner();
+            b.render(this.container);
+
+            var banners = Y.all('.banner');
+            Y.Assert.areEqual(
+                1,
+                banners._nodes.length,
+                'We have one banner node');
+
+            // The banner should make sure it's in the container as well.
+            var contained_banners = Y.all('#fixture .banner');
+            Y.Assert.areEqual(
+                1,
+                contained_banners._nodes.length,
+                'Banner node is placed.');
+        },
+
+        test_render_content: function () {
+            var msg = 'This is a banner message. Fear me.',
+                b = new ns.Banner({
+                    content: msg
+                });
+
+            b.render(this.container);
+
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                msg,
+                banner.one('.banner-content').get('text')
+            );
+        },
+
+        test_render_private_type: function () {
+            var msg = 'Private!',
+                b = new ns.Banner({
+                    content: msg,
+                    banner_type: ns.PRIVATE
+                });
+
+            b.render(this.container);
+
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                msg,
+                banner.one('.banner-content').get('text'),
+                'The banner should have the private message.'
+            );
+
+            var badge = banner.one('.badge');
+
+            Y.Assert.isTrue(
+                badge.hasClass('private'),
+                'The badge should have a private class on it.');
+
+        },
+
+        test_render_beta_type: function () {
+            var msg = 'BETA!',
+                b = new ns.Banner({
+                    content: msg,
+                    banner_type: ns.BETA
+                });
+
+            b.render(this.container);
+
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                msg,
+                banner.one('.banner-content').get('text'),
+                'The banner should have the beta message.'
+            );
+
+            var badge = banner.one('.badge');
+
+            Y.Assert.isTrue(
+                badge.hasClass('beta'),
+                'The badge should have a beta class on it.');
+        },
+
+        test_render_badge_text: function () {
+            // We can set the badge to contain text.
+            var badge = 'BETA!',
+                b = new ns.Banner({
+                    badge_text: badge,
+                    banner_type: ns.BETA
+                });
+
+            b.render(this.container);
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                badge,
+                banner.one('.badge').get('text'),
+                'The badge should have the beta message.'
+            );
+        },
+
+        test_banner_text_update: function () {
+            // The banner should update the rendered text when the content
+            // ATTR is changed.
+            var msg = 'This is a banner message. Fear me.',
+                b = new ns.Banner({
+                    content: msg
+                });
+
+            b.render(this.container);
+
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                msg,
+                banner.one('.banner-content').get('text')
+            );
+
+            // Now change the content on the widget and check again.
+            var new_msg = 'Updated me!';
+            b.set('content', new_msg);
+            banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                new_msg,
+                banner.one('.banner-content').get('text')
+            );
+        }
+    }));
+
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'ui.beta_banner_tests',
+
+        setUp: function () {
+            this.container = Y.one('#fixture');
+        },
+
+        tearDown: function () {
+            this.container.empty();
+        },
+
+        test_base_beta_banner: function () {
+            // The beta banner is auto set to the right type, has the right
+            // badge text.
+            var badge = 'BETA!',
+                msg = 'are in beta:',
+                b = new ns.BetaBanner({
+                });
+
+            b.render(this.container);
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                badge,
+                banner.one('.badge').get('text'),
+                'The badge should have the beta message.'
+            );
+
+            Y.Assert.areEqual(
+                ns.BETA,
+                b.get('banner_type'),
+                'The banner should be the right type.'
+            );
+
+            Y.Assert.areNotEqual(
+                -1,
+                banner.one('.banner-content').get('text').indexOf(msg),
+                'The badge should have beta content.'
+            );
+        },
+
+        test_beta_features: function () {
+            // The features fed to the banner effect display of the messages.
+            var features = {
+                private_projects: {
+                    is_beta: true,
+                    title: "Private Projects",
+                    url: "http://blog.ld.net/general/private-projects-beta";,
+                    value: "true"
+                },
+                test_projects: {
+                    is_beta: true,
+                    title: "Test Projects",
+                    url: "http://blog.ld.net/general/private-projects-beta";,
+                    value: "true"
+                },
+                no_beta: {
+                    is_beta: false,
+                    title: "Better not see me",
+                    url: "http://blog.ld.net/general/private-projects-beta";,
+                    value: "true"
+                }
+            };
+
+            var b = new ns.BetaBanner({
+                    features: features
+            });
+
+            b.render(this.container);
+
+            var banner = Y.one('.banner'),
+                banner_content = banner.one('.banner-content').get('text');
+
+            Y.Assert.areNotEqual(
+                -1,
+                banner_content.indexOf(features.private_projects.title),
+                'The private projects feature should be displayed.'
+            );
+
+            Y.Assert.areNotEqual(
+                -1,
+                banner_content.indexOf(features.test_projects.title),
+                'Also test projects since we support multiple features.'
+            );
+
+            Y.Assert.areEqual(
+                -1,
+                banner_content.indexOf(features.no_beta.title),
+                'But not no beta since we only support beta features.'
+            );
+        }
+    }));
+
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'ui.private_banner_tests',
+
+        setUp: function () {
+            this.container = Y.one('#fixture');
+        },
+
+        tearDown: function () {
+            this.container.empty();
+        },
+
+        test_base_private_banner: function () {
+            // The private banner is auto set to the right type, has the right
+            // badge text.
+            var badge = '',
+                msg = 'page is private',
+                b = new ns.PrivateBanner({
+                });
+
+            b.render(this.container);
+            var banner = Y.one('.banner');
+            Y.Assert.areEqual(
+                badge,
+                banner.one('.badge').get('text'),
+                'The badge should be empty'
+            );
+
+            Y.Assert.areEqual(
+                ns.PRIVATE,
+                b.get('banner_type'),
+                'The banner should be the right type.'
+            );
+
+            Y.Assert.areNotEqual(
+                -1,
+                banner.one('.banner-content').get('text').indexOf(msg),
+                'The badge should have a private warning.'
+            );
+        }
+    }));
+
+}, '0.1', {
+    requires: ['test', 'lp.testing.helpers', 'lp.ui.banner']
+});

=== added directory 'lib/lp/app/javascript/views'
=== added file 'lib/lp/app/javascript/views/global.js'
--- lib/lp/app/javascript/views/global.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/views/global.js	2012-11-09 19:09:19 +0000
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Global View handler for Launchpad
+ *
+ * @module lp.views.Global
+ * @namespace lp.views
+ * @module global
+ */
+YUI.add('lp.views.global', function (Y) {
+
+    var ns = Y.namespace('lp.views'),
+        ui = Y.namespace('lp.ui'),
+        info_type = Y.namespace('lp.app.information_type');
+
+    /**
+     * Provides a Y.View that controls all of the things that need handling on
+     * every single request. All code currently in the base-layout-macros
+     * should eventually moved into here to be loaded via the render() method.
+     * Events bound as required, etc.
+     *
+     * @class Global
+     * @extends Y.View
+     */
+    ns.Global = Y.Base.create('lp-views-global', Y.View, [], {
+        _events: [],
+
+        /**
+         * Watch for page level events in all pages.
+         *
+         * @method _bind_events
+         */
+        _bind_events: function () {
+            var that = this;
+
+            // Watch for any changes in information type.
+            this._events.push(Y.on(info_type.EV_ISPUBLIC, function (ev) {
+                // Remove the banner if there is one.
+                if (that._private_banner) {
+                    that._private_banner.hide();
+                    that._private_banner.destroy(true);
+                    that._private_banner = null;
+                    // XXX: Bug #1076074
+                    var body = Y.one('body');
+                    body.addClass('public');
+                }
+            }));
+
+            // If the information type is changed to private, and we don't
+            // currently have a privacy banner, then create a new one and set
+            // it up.
+            this._events.push(Y.on(info_type.EV_ISPRIVATE, function (ev) {
+                // Create a Private banner if there is not currently one.
+                if (!that._private_banner) {
+                    that._private_banner = new ui.banner.PrivateBanner({
+                        content: ev.text
+                    });
+
+                    // There is no current container for the banner since
+                    // we're creating it on the fly.
+                    var container = Y.Node.create('<div>');
+
+                    // XXX: Bug #1076074
+                    var body = Y.one('body');
+                    body.removeClass('public');
+                    that._private_banner.render(container);
+                    // Only append the content to the DOM after the rest is
+                    // one to avoid any repaints on the browser end.
+                    body.prepend(container);
+                    that._private_banner.show();
+                } else {
+                    // The banner is there but we need to update text.
+                    that._private_banner.set('content', ev.text);
+                }
+            }));
+        },
+
+        /**
+         * Clean up the view and its event bindings when destroyed.
+         *
+         * @method _destroy
+         * @param {Event} ev
+         * @private
+         */
+        _destroy: function (ev) {
+            var index;
+            for (index in this._events) {
+                event = this._events[index];
+                event.detach();
+            }
+        },
+
+        _init_banners: function () {
+            var that = this;
+
+            // On page load the banner container already exists. This is so
+            // that the height of the page is already determined.
+            var is_beta = Y.one('.beta_banner_container');
+            if (is_beta) {
+                that._beta_banner = new ui.banner.BetaBanner({
+                    features: LP.cache.related_features
+                });
+                that._beta_banner.render(is_beta);
+                // We delay the show until the page is ready so we get our
+                // pretty css3 animation that distracts the user a bit.
+                Y.after('load', function (ev) {
+                    that._beta_banner.show();
+                });
+            }
+
+            // On page load the banner container already exists. This is so
+            // that the height of the page is already determined.
+            var is_private = Y.one('.private_banner_container');
+            if (is_private) {
+                that._private_banner = new ui.banner.PrivateBanner();
+                that._private_banner.render(is_private);
+                // We delay the show until the page is ready so we get our
+                // pretty css3 animation that distracts the user a bit.
+                Y.on('load', function (ev) {
+                    that._private_banner.show();
+                });
+            }
+        },
+
+        initialize: function (cfg) {},
+
+        render: function () {
+            this._bind_events();
+            this.on('destroy', this._destroy, this);
+            this._init_banners();
+        }
+
+    }, {
+        ATTRS: {
+
+        }
+    });
+
+}, '0.1', {
+    requires: ['base', 'view', 'lp.ui.banner', 'lp.app.information_type']
+});

=== added directory 'lib/lp/app/javascript/views/tests'
=== added file 'lib/lp/app/javascript/views/tests/test_global.html'
--- lib/lp/app/javascript/views/tests/test_global.html	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/views/tests/test_global.html	2012-11-09 19:09:19 +0000
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+  <head>
+      <title>Product New Tests</title>
+
+      <!-- YUI and test setup -->
+      <script type="text/javascript"
+              src="../../../../../../build/js/yui/yui/yui.js">
+      </script>
+      <link rel="stylesheet"
+      href="../../../../../../build/js/yui/console/assets/console-core.css" />
+      <link rel="stylesheet"
+      href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
+      <link rel="stylesheet"
+      href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+      <script type="text/javascript"
+              src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
+
+      <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
+
+      <!-- Dependencies -->
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/client.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/choice.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/ellipsis.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/expander.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/errors.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/information_type.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/lp.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/mustache.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/effects/effects.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/helpers.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/ui/ui.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/ui/banner.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../global.js"></script>
+
+      <!-- The test suite -->
+      <script type="text/javascript" src="test_global.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <li>lp.views.global.test</li>
+        </ul>
+        <div id="fixture"></div>
+    </body>
+</html>

=== added file 'lib/lp/app/javascript/views/tests/test_global.js'
--- lib/lp/app/javascript/views/tests/test_global.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/views/tests/test_global.js	2012-11-09 19:09:19 +0000
@@ -0,0 +1,164 @@
+/* Copyright (c) 2012 Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.views.global.test', function (Y) {
+    var tests = Y.namespace('lp.views.global.test');
+    var info_type = Y.namespace('lp.app.information_type');
+    var ns = Y.lp.views;
+
+    tests.suite = new Y.Test.Suite('lp.views.global test');
+    tests.suite.add(new Y.Test.Case({
+        name: 'lp.views.global',
+
+        setUp: function () {
+            this.container = Y.one('#fixture');
+            window.LP = {
+                cache: {
+                    related_features: {
+                        private_projects: {
+                            is_beta: true,
+                            title: "Private Projects",
+                            url: "http://blog.ld.net/general/beta";,
+                            value: "true"
+                        }
+                    },
+                    information_type_data: {
+                        PUBLIC: {
+                            value: 'PUBLIC', name: 'Public',
+                            is_private: false, order: 1,
+                            description: 'Public Description'
+                        },
+                        EMBARGOED: {
+                            value: 'EMBARGOED', name: 'Embargoed',
+                            is_private: true, order: 2,
+                            description: 'Something embargoed'
+                        },
+                        PROPRIETARY: {
+                            value: 'PROPRIETARY', name: 'Proprietary',
+                            is_private: true, order: 3,
+                            description: 'Private Description'
+                        }
+                    }
+                }
+            };
+        },
+
+        tearDown: function () {
+            this.container.empty();
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(ns.Global,
+                "Could not locate the lp.views.global module");
+        },
+
+        test_basic_render: function () {
+            // Nothing is currently rendered out by default.
+            var view = new ns.Global();
+            view.render();
+
+            Y.Assert.areEqual(
+                '',
+                this.container.get('innerHTML'),
+                'The container is still empty.');
+            view.destroy();
+        },
+
+        test_beta_banner: function () {
+            // If we've prepped on load a beta banner will auto appear.
+            var banner_container = Y.Node.create('<div/>');
+            banner_container.addClass('beta_banner_container');
+            this.container.append(banner_container);
+            var view = new ns.Global();
+            view.render();
+
+            // We have to wait until after page load event fires to test
+            // things out.
+            var banner_node = Y.one('.banner');
+            Y.Assert.isObject(
+                banner_node,
+                'The container has a new banner node in there.');
+
+            view.destroy();
+        },
+
+        test_privacy: function () {
+            var beta_container = Y.Node.create('<div/>');
+            var private_container = Y.Node.create('<div/>');
+
+            beta_container.addClass('beta_banner_container');
+            private_container.addClass('private_banner_container');
+
+            this.container.append(beta_container);
+            this.container.append(private_container);
+            var view = new ns.Global();
+            view.render();
+
+            // We have to wait until after page load event fires to test
+            // things out.
+            var banner_nodes = Y.all('.banner');
+            Y.Assert.areEqual(
+                2,
+                banner_nodes._nodes.length,
+                'We should have two banners rendered.');
+
+            view.destroy();
+        },
+
+        test_privacy_banner_from_event: function () {
+            // We can also get a privacy banner via a fired event.
+            // This is hard coded to the <body> tag so we have to do some
+            // manual clean up here.
+            var view = new ns.Global();
+            view.render();
+
+            var msg = 'Testing Global';
+            Y.fire(info_type.EV_ISPRIVATE, {
+                text: msg
+            });
+
+            var banner = Y.one('.banner');
+            var banner_text = banner.one('.banner-content').get('text');
+            Y.Assert.areNotEqual(
+                -1,
+                banner_text.indexOf(msg),
+                'The event text is turned into the banner content');
+
+           // Manually clean up.
+           Y.one('.yui3-banner').remove(true);
+           view.destroy();
+        },
+
+        test_banner_updates_content: function () {
+            // If we change our privacy information type the banner needs to
+            // update the content to the new text value from the event.
+            var view = new ns.Global();
+            view.render();
+
+            var msg = 'Testing Global';
+            Y.fire(info_type.EV_ISPRIVATE, {
+                text: msg
+            });
+
+            var updated_msg = 'Updated content';
+            Y.fire(info_type.EV_ISPRIVATE, {
+                text: updated_msg
+            });
+
+            var banner = Y.one('.banner');
+            var banner_text = banner.one('.banner-content').get('text');
+            Y.Assert.areNotEqual(
+                -1,
+                banner_text.indexOf(updated_msg),
+                'The banner updated content to the second event message.');
+
+           // Manually clean up.
+           Y.one('.yui3-banner').remove(true);
+           view.destroy();
+
+        }
+    }));
+
+}, '0.1', {
+    requires: ['test', 'event-simulate', 'node-event-simulate',
+               'lp.app.information_type', 'lp.views.global']
+});


Follow ups