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

Proposed by Richard Harding
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 16256
Proposed branch: lp:~rharding/launchpad/add_new_banner
Merge into: lp:launchpad
Diff against target: 1165 lines (+1128/-0)
7 files modified
lib/lp/app/javascript/ui/assets/skins/sam/banner.css (+109/-0)
lib/lp/app/javascript/ui/banner.js (+315/-0)
lib/lp/app/javascript/ui/tests/test_banner.html (+49/-0)
lib/lp/app/javascript/ui/tests/test_banner.js (+287/-0)
lib/lp/app/javascript/views/global.js (+142/-0)
lib/lp/app/javascript/views/tests/test_global.html (+62/-0)
lib/lp/app/javascript/views/tests/test_global.js (+164/-0)
To merge this branch: bzr merge lp:~rharding/launchpad/add_new_banner
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+133736@code.launchpad.net

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.

Description of the change

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

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This code looks good. As we discussed in IRC though, the wiring up will need to be careful to leave a banner present on no-js browsers, which still need to be supported for some time.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/app/javascript/ui/assets/skins/sam/banner.css'
2--- lib/lp/app/javascript/ui/assets/skins/sam/banner.css 1970-01-01 00:00:00 +0000
3+++ lib/lp/app/javascript/ui/assets/skins/sam/banner.css 2012-11-09 19:09:19 +0000
4@@ -0,0 +1,109 @@
5+/* JS Banner styling */
6+.yui3-banner {
7+ /* Default to not visible so we can fade in */
8+ opacity: 1;
9+
10+ /* Animations for fade-in/out */
11+ -webkit-transition: opacity 0.3s ease-in;
12+ -moz-transition: opacity 0.3s ease-in;
13+ transition: opacity 0.3s ease-in;
14+
15+ width: 100%;
16+}
17+
18+.yui3-banner.yui3-banner-hidden {
19+ opacity: 0;
20+}
21+
22+/* Nasty hack to get the bar moved since it's absolutely positioned
23+ * This also needs to be updated
24+ */
25+body.beta #locationbar, body.private #locationbar {
26+ top: 47px;
27+}
28+
29+/* If we have both classes make room for two banners height */
30+body.beta.private #locationbar {
31+ top: 94px;
32+}
33+
34+/* If the container exists make sure we start out with the rest of the page
35+ * bumped down the starting distance to reduce flash effect
36+ */
37+.beta_banner_container, .private_banner_container {
38+ min-height: 45px;
39+}
40+
41+.yui3-banner-content {
42+ box-shadow: 0 0 5px #333;
43+ background-color: #666;
44+ color: #fff;
45+ display: block;
46+ font-size: 14px;
47+ font-weight: bold;
48+ line-height: 21px;
49+ padding: 8px 20px;
50+ text-align: left;
51+ text-shadow: 0 -1px 0 #631616;
52+ z-index: 10;
53+}
54+
55+.yui3-banner-content .badge {
56+ display: inline-block;
57+ height: 21px;
58+ margin-right: 10px;
59+ padding: 0;
60+ vertical-align: middle;
61+ width: 20px;
62+}
63+.yui3-banner-content .banner-content {}
64+
65+.yui3-banner-content.beta {
66+ /* Some of these are required to override .beta CSS */
67+
68+ /* Defined for browsers that don't support transparency */
69+ background-color: #606060;
70+ /* Transparent background for browsers that support it */
71+ background-color: rgba(64, 64, 64, 0.9);
72+ height: auto;
73+ margin-top: 0px;
74+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
75+ width: auto;
76+}
77+.yui3-banner-content.beta .yui3-banner-content-content {}
78+.yui3-banner-content.beta .badge {
79+ /* sprite-ref: icon-sprites */
80+ background-color: #c10000;
81+ background: linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
82+ background: -moz-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
83+ background: -ms-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
84+ background: -o-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
85+ background: -webkit-linear-gradient(bottom, rgb(158,0,0) 0%, rgb(193,0,0) 70%);
86+ border-radius: 5px;
87+ border-top: 1px solid #e20000;
88+ font-size: 12px;
89+ font-weight: bold;
90+ margin-right: 12px;
91+ padding: 3px 6px 4px 6px;
92+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
93+ width: auto;
94+}
95+
96+.yui3-banner-content.beta .beta-feature {
97+ font-weight: bold;
98+}
99+.yui3-banner-content.beta .info-link {
100+ color: #4884ef;
101+}
102+
103+.yui3-banner-content.private {
104+ /* Define colour for browsers that don't support transparency */
105+ background: #8d1f1f;
106+ /* Set transparent background for browsers that support it */
107+ background: rgba(125,0,0,0.9);
108+}
109+.yui3-banner-content.private .banner-content {}
110+.yui3-banner-content.private .badge {
111+ background: url(/@@/notification-private.png);
112+ background-repeat: no-repeat;
113+}
114
115=== added file 'lib/lp/app/javascript/ui/banner.js'
116--- lib/lp/app/javascript/ui/banner.js 1970-01-01 00:00:00 +0000
117+++ lib/lp/app/javascript/ui/banner.js 2012-11-09 19:09:19 +0000
118@@ -0,0 +1,315 @@
119+/*
120+ * Copyright 2012 Canonical Ltd. This software is licensed under the
121+ * GNU Affero General Public License version 3 (see the file LICENSE).
122+ *
123+ * Notification banner widget
124+ *
125+ * @module lp.ui.banner
126+ * @namespace lp.ui
127+ * @module banner
128+ */
129+YUI.add('lp.ui.banner', function (Y) {
130+
131+ var ns = Y.namespace('lp.ui.banner');
132+
133+ // GLOBALS
134+ ns.PRIVATE = 'private';
135+ ns.BETA = 'beta';
136+
137+ /**
138+ * Banner widget base class
139+ *
140+ * This is the base Banner, you're supposed to supply some message data to
141+ * generate the banner in the proper method.
142+ *
143+ * This banner provides all shared functionality between the Privacy and
144+ * Beta banners.
145+ *
146+ * @class Banner
147+ * @extends Y.Widget
148+ *
149+ */
150+ ns.Banner = Y.Base.create('banner', Y.Widget, [], {
151+ template: [
152+ '<div class="banner">',
153+ '<span class="badge {{ banner_type }}">{{ badge_text }}</span>',
154+ '<span class="banner-content">{{{ content }}}</span>',
155+ '</div>'
156+ ].join(''),
157+
158+
159+ /**
160+ * Bind events that our widget supports such as closing the banner.
161+ *
162+ * We also watch the destroy event to clean up side effect css we
163+ * created.
164+ *
165+ * @method bindUI
166+ */
167+ bindUI: function () {
168+ this.on('destroy', function (ev) {
169+ // XXX: Bug #1076074
170+ var body = Y.one('body');
171+ var banner_type = this.get('banner_type');
172+ body.removeClass(banner_type);
173+
174+ // Remove any container the page might have provided for us to
175+ // start out with.
176+ var container_class = '.' + banner_type + '_banner_container';
177+ var container = Y.one(container_class);
178+ if (container) {
179+ Y.one(container_class).remove();
180+ }
181+ });
182+
183+ this.after('contentChange', function () {
184+ this.renderUI();
185+ });
186+ },
187+
188+ /**
189+ * Default initialize method.
190+ *
191+ * @method initialize
192+ * @param {Object} cfg
193+ */
194+ initialize: function (cfg) {
195+ },
196+
197+ /**
198+ * Widget render method to generate the html of the widget.
199+ *
200+ * @method renderUI
201+ */
202+ renderUI: function () {
203+ var contentBox = this.get('contentBox');
204+ contentBox.addClass(this.get('banner_type'));
205+ var html = Y.lp.mustache.to_html(this.template, this.getAttrs());
206+ contentBox.setHTML(html);
207+
208+ // XXX: Bug #1076074
209+ // Needs to get cleaned up. Only applies to the global
210+ // banners and not to other ones which we're working to allow.
211+ // This is currently required because the #locationbar is
212+ // absolutely located and needs to be moved as banners change.
213+ var body = Y.one('body');
214+ body.addClass(this.get('banner_type'));
215+
216+ if (this.get('visible')) {
217+ this.show();
218+ }
219+ },
220+
221+ /**
222+ * We need to override show so that we force a browser repaint which
223+ * allows our CSS3 animation to run. Otherwise the browser sees we
224+ * added new DOM elements and jumps straight to the finished animation
225+ * point.
226+ *
227+ * @method show
228+ */
229+ show: function () {
230+ var _node = this.get('boundingBox')._node;
231+ var getComputedStyle = document.defaultView.getComputedStyle;
232+ _node.style.display = getComputedStyle(_node).display;
233+ return this.set('visible', true);
234+ }
235+
236+ }, {
237+ ATTRS: {
238+ /**
239+ * Instead of a sprite we might have text such as the Beta banner.
240+ *
241+ * @attribute badge_text
242+ * @default undefined
243+ * @type {String}
244+ */
245+ badge_text: {},
246+
247+ /**
248+ * The Banner is meant to house some message to the user provided
249+ * by this content. It can be html and is not escaped for that
250+ * reason.
251+ *
252+ * @attribute content
253+ * @default undefined
254+ * @type {String}
255+ */
256+ content: {},
257+
258+ /**
259+ * This is listed to help aid in discovery of how the container
260+ * node for the widget is determined. It's passed into the
261+ * render() method and the Widget constructs itself inside of
262+ * there.
263+ *
264+ * @attribute boundingBox
265+ * @default undefined
266+ * @type {Node}
267+ */
268+ boundingBox: {
269+
270+ },
271+
272+ /**
273+ * Much of the Widget is determined by the type of banner it is.
274+ * See the constants defined PRIVATE and BETA for two known types.
275+ * If you set this manually you'll be able to provide custom
276+ * styling as required because the type is used as a css class
277+ * property.
278+ *
279+ * @attribute banner_type
280+ * @default undefined
281+ * @type {String}
282+ */
283+ banner_type: {},
284+
285+ /**
286+ * Start out as not visible which should render as opacity 0, then
287+ * we update it and it animates due to our css3.
288+ *
289+ * @attribute visible
290+ * @default false
291+ * @type {Bool}
292+ */
293+ visible: {
294+ value: false
295+ }
296+ }
297+ });
298+
299+ /**
300+ * Beta Banner widget
301+ *
302+ * This is the Beta feature banner which needs to know about the title and
303+ * url of the feature to construct the content correctly. Features are
304+ * meant to be matched to the current LP.cache.related_features data
305+ * available.
306+ *
307+ * @class BetaBanner
308+ * @extends Banner
309+ *
310+ */
311+ ns.BetaBanner = Y.Base.create('banner', ns.Banner, [], {
312+
313+ }, {
314+ ATTRS: {
315+ /**
316+ * @attribute badge_text
317+ * @default "BETA!"
318+ * @type {String}
319+ */
320+ badge_text: {
321+ value: 'BETA!'
322+ },
323+
324+ /**
325+ * The content for the beta banner is constructed from hard coded
326+ * content and the list of enabled beta features currently
327+ * relevant to the page.
328+ *
329+ * @attribute content
330+ * @default {generated}
331+ * @type {String}
332+ */
333+ content: {
334+ getter: function () {
335+ var content = "Some parts of this page are in beta:&nbsp;";
336+ var key;
337+ // We need to process the features to build the features
338+ // that apply.
339+ var features = this.get('features');
340+ for (key in features) {
341+ if (features.hasOwnProperty(key)) {
342+ var obj = features[key];
343+ if (obj.is_beta) {
344+ content = content + [
345+ '<span class="beta-feature">',
346+ obj.title,
347+ '&nbsp;<a class="info-link" href="',
348+ obj.url + '">(read more)</a>',
349+ '</span>'
350+ ].join('');
351+ }
352+ }
353+ }
354+ return content;
355+ }
356+ },
357+
358+ /**
359+ * features is a nested object of the beta features going. See
360+ * LP.cache.related_features for the list of features. We only
361+ * want those related features that are in beta.
362+ * Ex: {
363+ * disclosure.private_projects.enabled: {
364+ * is_beta: true,
365+ * title: "",
366+ * url: "http://blog.ld.net/general/private-projects-beta",
367+ * value: "true"
368+ * }
369+ * }
370+ * @attribute features
371+ * @default {}
372+ * @type {Object}
373+ */
374+ features: {},
375+
376+ /**
377+ * Manually force the banner type so users don't need to set it.
378+ * This is a beta banner class.
379+ *
380+ * @attribute banner_type
381+ * @default BETA
382+ * @type {String}
383+ */
384+ banner_type: {
385+ value: ns.BETA
386+ }
387+
388+ }
389+ });
390+
391+ /**
392+ * Private Banner widget
393+ *
394+ * This is the Private feature banner which is pretty basic.
395+ *
396+ * Note that this doesn't automatically follow the information type code.
397+ * Nor does it listen to the choice widgets and try to update. It's purely
398+ * meant to function as told to do so. Most of the work around making sure
399+ * the banner shows and works properly is in the View code in global.js.
400+ *
401+ * @class PrivateBanner
402+ * @extends Banner
403+ *
404+ */
405+ ns.PrivateBanner = Y.Base.create('banner', ns.Banner, [], {
406+
407+ }, {
408+ ATTRS: {
409+ badge_text: {
410+ value: ''
411+ },
412+
413+ content: {
414+ value: 'The information on this page is private.'
415+ },
416+
417+ /**
418+ * Manually force the banner type so users don't need to set it.
419+ * This is a beta banner class.
420+ *
421+ * @attribute banner_type
422+ * @default BETA
423+ * @type {String}
424+ */
425+ banner_type: {
426+ value: ns.PRIVATE
427+ }
428+ }
429+ });
430+
431+}, '0.1', {
432+ requires: ['base', 'node', 'anim', 'widget', 'lp.mustache', 'yui-log']
433+});
434
435=== added directory 'lib/lp/app/javascript/ui/tests'
436=== added file 'lib/lp/app/javascript/ui/tests/test_banner.html'
437--- lib/lp/app/javascript/ui/tests/test_banner.html 1970-01-01 00:00:00 +0000
438+++ lib/lp/app/javascript/ui/tests/test_banner.html 2012-11-09 19:09:19 +0000
439@@ -0,0 +1,49 @@
440+<!DOCTYPE html>
441+<!--
442+Copyright 2012 Canonical Ltd. This software is licensed under the
443+GNU Affero General Public License version 3 (see the file LICENSE).
444+-->
445+
446+<html>
447+ <head>
448+ <title>lp.ui.banner Tests</title>
449+
450+ <!-- YUI and test setup -->
451+ <script type="text/javascript"
452+ src="../../../../../../build/js/yui/yui/yui.js">
453+ </script>
454+ <link rel="stylesheet"
455+ href="../../../../../../build/js/yui/console/assets/console-core.css" />
456+ <link rel="stylesheet"
457+ href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
458+ <link rel="stylesheet"
459+ href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
460+
461+ <script type="text/javascript"
462+ src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
463+ <script type="text/javascript"
464+ src="../../../../../build/js/lp/app/testing/helpers.js"></script>
465+
466+ <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
467+
468+ <!-- Dependencies -->
469+ <script type="text/javascript"
470+ src="../../../../../../build/js/lp/app/mustache.js"></script>
471+
472+ <!-- The module under test. -->
473+ <script type="text/javascript" src="../banner.js"></script>
474+
475+ <!-- Placeholder for any css asset for this module. -->
476+ <link rel="stylesheet" href="../assets/skins/sam/banner.css" />
477+
478+ <!-- The test suite -->
479+ <script type="text/javascript" src="test_banner.js"></script>
480+
481+ </head>
482+ <body class="yui3-skin-sam">
483+ <ul id="suites">
484+ <li>lp.ui.banner.test</li>
485+ </ul>
486+ <div id="fixture"></div>
487+ </body>
488+</html>
489
490=== added file 'lib/lp/app/javascript/ui/tests/test_banner.js'
491--- lib/lp/app/javascript/ui/tests/test_banner.js 1970-01-01 00:00:00 +0000
492+++ lib/lp/app/javascript/ui/tests/test_banner.js 2012-11-09 19:09:19 +0000
493@@ -0,0 +1,287 @@
494+/* Copyright (c) 2012 Canonical Ltd. All rights reserved. */
495+
496+YUI.add('lp.ui.banner.test', function (Y) {
497+
498+ var tests = Y.namespace('lp.ui.banner.test');
499+ tests.suite = new Y.Test.Suite('ui.banner Tests');
500+
501+ var ns = Y.lp.ui.banner;
502+
503+ tests.suite.add(new Y.Test.Case({
504+ name: 'ui.banner_tests',
505+
506+ setUp: function () {
507+ this.container = Y.one('#fixture');
508+ },
509+
510+ tearDown: function () {
511+ this.container.empty();
512+ },
513+
514+ test_library_exists: function () {
515+ Y.Assert.isObject(Y.lp.ui.banner,
516+ "Could not locate the lp.ui.banner module");
517+ },
518+
519+ test_render: function () {
520+ var b = new ns.Banner();
521+ b.render(this.container);
522+
523+ var banners = Y.all('.banner');
524+ Y.Assert.areEqual(
525+ 1,
526+ banners._nodes.length,
527+ 'We have one banner node');
528+
529+ // The banner should make sure it's in the container as well.
530+ var contained_banners = Y.all('#fixture .banner');
531+ Y.Assert.areEqual(
532+ 1,
533+ contained_banners._nodes.length,
534+ 'Banner node is placed.');
535+ },
536+
537+ test_render_content: function () {
538+ var msg = 'This is a banner message. Fear me.',
539+ b = new ns.Banner({
540+ content: msg
541+ });
542+
543+ b.render(this.container);
544+
545+ var banner = Y.one('.banner');
546+ Y.Assert.areEqual(
547+ msg,
548+ banner.one('.banner-content').get('text')
549+ );
550+ },
551+
552+ test_render_private_type: function () {
553+ var msg = 'Private!',
554+ b = new ns.Banner({
555+ content: msg,
556+ banner_type: ns.PRIVATE
557+ });
558+
559+ b.render(this.container);
560+
561+ var banner = Y.one('.banner');
562+ Y.Assert.areEqual(
563+ msg,
564+ banner.one('.banner-content').get('text'),
565+ 'The banner should have the private message.'
566+ );
567+
568+ var badge = banner.one('.badge');
569+
570+ Y.Assert.isTrue(
571+ badge.hasClass('private'),
572+ 'The badge should have a private class on it.');
573+
574+ },
575+
576+ test_render_beta_type: function () {
577+ var msg = 'BETA!',
578+ b = new ns.Banner({
579+ content: msg,
580+ banner_type: ns.BETA
581+ });
582+
583+ b.render(this.container);
584+
585+ var banner = Y.one('.banner');
586+ Y.Assert.areEqual(
587+ msg,
588+ banner.one('.banner-content').get('text'),
589+ 'The banner should have the beta message.'
590+ );
591+
592+ var badge = banner.one('.badge');
593+
594+ Y.Assert.isTrue(
595+ badge.hasClass('beta'),
596+ 'The badge should have a beta class on it.');
597+ },
598+
599+ test_render_badge_text: function () {
600+ // We can set the badge to contain text.
601+ var badge = 'BETA!',
602+ b = new ns.Banner({
603+ badge_text: badge,
604+ banner_type: ns.BETA
605+ });
606+
607+ b.render(this.container);
608+ var banner = Y.one('.banner');
609+ Y.Assert.areEqual(
610+ badge,
611+ banner.one('.badge').get('text'),
612+ 'The badge should have the beta message.'
613+ );
614+ },
615+
616+ test_banner_text_update: function () {
617+ // The banner should update the rendered text when the content
618+ // ATTR is changed.
619+ var msg = 'This is a banner message. Fear me.',
620+ b = new ns.Banner({
621+ content: msg
622+ });
623+
624+ b.render(this.container);
625+
626+ var banner = Y.one('.banner');
627+ Y.Assert.areEqual(
628+ msg,
629+ banner.one('.banner-content').get('text')
630+ );
631+
632+ // Now change the content on the widget and check again.
633+ var new_msg = 'Updated me!';
634+ b.set('content', new_msg);
635+ banner = Y.one('.banner');
636+ Y.Assert.areEqual(
637+ new_msg,
638+ banner.one('.banner-content').get('text')
639+ );
640+ }
641+ }));
642+
643+
644+ tests.suite.add(new Y.Test.Case({
645+ name: 'ui.beta_banner_tests',
646+
647+ setUp: function () {
648+ this.container = Y.one('#fixture');
649+ },
650+
651+ tearDown: function () {
652+ this.container.empty();
653+ },
654+
655+ test_base_beta_banner: function () {
656+ // The beta banner is auto set to the right type, has the right
657+ // badge text.
658+ var badge = 'BETA!',
659+ msg = 'are in beta:',
660+ b = new ns.BetaBanner({
661+ });
662+
663+ b.render(this.container);
664+ var banner = Y.one('.banner');
665+ Y.Assert.areEqual(
666+ badge,
667+ banner.one('.badge').get('text'),
668+ 'The badge should have the beta message.'
669+ );
670+
671+ Y.Assert.areEqual(
672+ ns.BETA,
673+ b.get('banner_type'),
674+ 'The banner should be the right type.'
675+ );
676+
677+ Y.Assert.areNotEqual(
678+ -1,
679+ banner.one('.banner-content').get('text').indexOf(msg),
680+ 'The badge should have beta content.'
681+ );
682+ },
683+
684+ test_beta_features: function () {
685+ // The features fed to the banner effect display of the messages.
686+ var features = {
687+ private_projects: {
688+ is_beta: true,
689+ title: "Private Projects",
690+ url: "http://blog.ld.net/general/private-projects-beta",
691+ value: "true"
692+ },
693+ test_projects: {
694+ is_beta: true,
695+ title: "Test Projects",
696+ url: "http://blog.ld.net/general/private-projects-beta",
697+ value: "true"
698+ },
699+ no_beta: {
700+ is_beta: false,
701+ title: "Better not see me",
702+ url: "http://blog.ld.net/general/private-projects-beta",
703+ value: "true"
704+ }
705+ };
706+
707+ var b = new ns.BetaBanner({
708+ features: features
709+ });
710+
711+ b.render(this.container);
712+
713+ var banner = Y.one('.banner'),
714+ banner_content = banner.one('.banner-content').get('text');
715+
716+ Y.Assert.areNotEqual(
717+ -1,
718+ banner_content.indexOf(features.private_projects.title),
719+ 'The private projects feature should be displayed.'
720+ );
721+
722+ Y.Assert.areNotEqual(
723+ -1,
724+ banner_content.indexOf(features.test_projects.title),
725+ 'Also test projects since we support multiple features.'
726+ );
727+
728+ Y.Assert.areEqual(
729+ -1,
730+ banner_content.indexOf(features.no_beta.title),
731+ 'But not no beta since we only support beta features.'
732+ );
733+ }
734+ }));
735+
736+
737+ tests.suite.add(new Y.Test.Case({
738+ name: 'ui.private_banner_tests',
739+
740+ setUp: function () {
741+ this.container = Y.one('#fixture');
742+ },
743+
744+ tearDown: function () {
745+ this.container.empty();
746+ },
747+
748+ test_base_private_banner: function () {
749+ // The private banner is auto set to the right type, has the right
750+ // badge text.
751+ var badge = '',
752+ msg = 'page is private',
753+ b = new ns.PrivateBanner({
754+ });
755+
756+ b.render(this.container);
757+ var banner = Y.one('.banner');
758+ Y.Assert.areEqual(
759+ badge,
760+ banner.one('.badge').get('text'),
761+ 'The badge should be empty'
762+ );
763+
764+ Y.Assert.areEqual(
765+ ns.PRIVATE,
766+ b.get('banner_type'),
767+ 'The banner should be the right type.'
768+ );
769+
770+ Y.Assert.areNotEqual(
771+ -1,
772+ banner.one('.banner-content').get('text').indexOf(msg),
773+ 'The badge should have a private warning.'
774+ );
775+ }
776+ }));
777+
778+}, '0.1', {
779+ requires: ['test', 'lp.testing.helpers', 'lp.ui.banner']
780+});
781
782=== added directory 'lib/lp/app/javascript/views'
783=== added file 'lib/lp/app/javascript/views/global.js'
784--- lib/lp/app/javascript/views/global.js 1970-01-01 00:00:00 +0000
785+++ lib/lp/app/javascript/views/global.js 2012-11-09 19:09:19 +0000
786@@ -0,0 +1,142 @@
787+/*
788+ * Copyright 2012 Canonical Ltd. This software is licensed under the
789+ * GNU Affero General Public License version 3 (see the file LICENSE).
790+ *
791+ * Global View handler for Launchpad
792+ *
793+ * @module lp.views.Global
794+ * @namespace lp.views
795+ * @module global
796+ */
797+YUI.add('lp.views.global', function (Y) {
798+
799+ var ns = Y.namespace('lp.views'),
800+ ui = Y.namespace('lp.ui'),
801+ info_type = Y.namespace('lp.app.information_type');
802+
803+ /**
804+ * Provides a Y.View that controls all of the things that need handling on
805+ * every single request. All code currently in the base-layout-macros
806+ * should eventually moved into here to be loaded via the render() method.
807+ * Events bound as required, etc.
808+ *
809+ * @class Global
810+ * @extends Y.View
811+ */
812+ ns.Global = Y.Base.create('lp-views-global', Y.View, [], {
813+ _events: [],
814+
815+ /**
816+ * Watch for page level events in all pages.
817+ *
818+ * @method _bind_events
819+ */
820+ _bind_events: function () {
821+ var that = this;
822+
823+ // Watch for any changes in information type.
824+ this._events.push(Y.on(info_type.EV_ISPUBLIC, function (ev) {
825+ // Remove the banner if there is one.
826+ if (that._private_banner) {
827+ that._private_banner.hide();
828+ that._private_banner.destroy(true);
829+ that._private_banner = null;
830+ // XXX: Bug #1076074
831+ var body = Y.one('body');
832+ body.addClass('public');
833+ }
834+ }));
835+
836+ // If the information type is changed to private, and we don't
837+ // currently have a privacy banner, then create a new one and set
838+ // it up.
839+ this._events.push(Y.on(info_type.EV_ISPRIVATE, function (ev) {
840+ // Create a Private banner if there is not currently one.
841+ if (!that._private_banner) {
842+ that._private_banner = new ui.banner.PrivateBanner({
843+ content: ev.text
844+ });
845+
846+ // There is no current container for the banner since
847+ // we're creating it on the fly.
848+ var container = Y.Node.create('<div>');
849+
850+ // XXX: Bug #1076074
851+ var body = Y.one('body');
852+ body.removeClass('public');
853+ that._private_banner.render(container);
854+ // Only append the content to the DOM after the rest is
855+ // one to avoid any repaints on the browser end.
856+ body.prepend(container);
857+ that._private_banner.show();
858+ } else {
859+ // The banner is there but we need to update text.
860+ that._private_banner.set('content', ev.text);
861+ }
862+ }));
863+ },
864+
865+ /**
866+ * Clean up the view and its event bindings when destroyed.
867+ *
868+ * @method _destroy
869+ * @param {Event} ev
870+ * @private
871+ */
872+ _destroy: function (ev) {
873+ var index;
874+ for (index in this._events) {
875+ event = this._events[index];
876+ event.detach();
877+ }
878+ },
879+
880+ _init_banners: function () {
881+ var that = this;
882+
883+ // On page load the banner container already exists. This is so
884+ // that the height of the page is already determined.
885+ var is_beta = Y.one('.beta_banner_container');
886+ if (is_beta) {
887+ that._beta_banner = new ui.banner.BetaBanner({
888+ features: LP.cache.related_features
889+ });
890+ that._beta_banner.render(is_beta);
891+ // We delay the show until the page is ready so we get our
892+ // pretty css3 animation that distracts the user a bit.
893+ Y.after('load', function (ev) {
894+ that._beta_banner.show();
895+ });
896+ }
897+
898+ // On page load the banner container already exists. This is so
899+ // that the height of the page is already determined.
900+ var is_private = Y.one('.private_banner_container');
901+ if (is_private) {
902+ that._private_banner = new ui.banner.PrivateBanner();
903+ that._private_banner.render(is_private);
904+ // We delay the show until the page is ready so we get our
905+ // pretty css3 animation that distracts the user a bit.
906+ Y.on('load', function (ev) {
907+ that._private_banner.show();
908+ });
909+ }
910+ },
911+
912+ initialize: function (cfg) {},
913+
914+ render: function () {
915+ this._bind_events();
916+ this.on('destroy', this._destroy, this);
917+ this._init_banners();
918+ }
919+
920+ }, {
921+ ATTRS: {
922+
923+ }
924+ });
925+
926+}, '0.1', {
927+ requires: ['base', 'view', 'lp.ui.banner', 'lp.app.information_type']
928+});
929
930=== added directory 'lib/lp/app/javascript/views/tests'
931=== added file 'lib/lp/app/javascript/views/tests/test_global.html'
932--- lib/lp/app/javascript/views/tests/test_global.html 1970-01-01 00:00:00 +0000
933+++ lib/lp/app/javascript/views/tests/test_global.html 2012-11-09 19:09:19 +0000
934@@ -0,0 +1,62 @@
935+<!DOCTYPE html>
936+
937+<!--
938+Copyright 2012 Canonical Ltd. This software is licensed under the
939+GNU Affero General Public License version 3 (see the file LICENSE).
940+-->
941+
942+<html>
943+ <head>
944+ <title>Product New Tests</title>
945+
946+ <!-- YUI and test setup -->
947+ <script type="text/javascript"
948+ src="../../../../../../build/js/yui/yui/yui.js">
949+ </script>
950+ <link rel="stylesheet"
951+ href="../../../../../../build/js/yui/console/assets/console-core.css" />
952+ <link rel="stylesheet"
953+ href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
954+ <link rel="stylesheet"
955+ href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
956+
957+ <script type="text/javascript"
958+ src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
959+
960+ <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
961+
962+ <!-- Dependencies -->
963+ <script type="text/javascript" src="../../../../../../build/js/lp/app/client.js"></script>
964+ <script type="text/javascript" src="../../../../../../build/js/lp/app/choice.js"></script>
965+ <script type="text/javascript" src="../../../../../../build/js/lp/app/ellipsis.js"></script>
966+ <script type="text/javascript" src="../../../../../../build/js/lp/app/expander.js"></script>
967+ <script type="text/javascript" src="../../../../../../build/js/lp/app/errors.js"></script>
968+ <script type="text/javascript" src="../../../../../../build/js/lp/app/information_type.js"></script>
969+ <script type="text/javascript" src="../../../../../../build/js/lp/app/lp.js"></script>
970+ <script type="text/javascript" src="../../../../../../build/js/lp/app/mustache.js"></script>
971+ <script type="text/javascript" src="../../../../../../build/js/lp/app/anim/anim.js"></script>
972+ <script type="text/javascript" src="../../../../../../build/js/lp/app/extras/extras.js"></script>
973+ <script type="text/javascript" src="../../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
974+ <script type="text/javascript" src="../../../../../../build/js/lp/app/effects/effects.js"></script>
975+ <script type="text/javascript" src="../../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
976+ <script type="text/javascript" src="../../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
977+ <script type="text/javascript" src="../../../../../../build/js/lp/app/inlineedit/editor.js"></script>
978+ <script type="text/javascript" src="../../../../../../build/js/lp/app/testing/helpers.js"></script>
979+ <script type="text/javascript" src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
980+ <script type="text/javascript" src="../../../../../../build/js/lp/app/ui/ui.js"></script>
981+ <script type="text/javascript" src="../../../../../../build/js/lp/app/ui/banner.js"></script>
982+
983+ <!-- The module under test. -->
984+ <script type="text/javascript" src="../global.js"></script>
985+
986+ <!-- The test suite -->
987+ <script type="text/javascript" src="test_global.js"></script>
988+
989+ </head>
990+ <body class="yui3-skin-sam">
991+ <ul id="suites">
992+ <li>lp.views.global.test</li>
993+ </ul>
994+ <div id="fixture"></div>
995+ </body>
996+</html>
997
998=== added file 'lib/lp/app/javascript/views/tests/test_global.js'
999--- lib/lp/app/javascript/views/tests/test_global.js 1970-01-01 00:00:00 +0000
1000+++ lib/lp/app/javascript/views/tests/test_global.js 2012-11-09 19:09:19 +0000
1001@@ -0,0 +1,164 @@
1002+/* Copyright (c) 2012 Canonical Ltd. All rights reserved. */
1003+
1004+YUI.add('lp.views.global.test', function (Y) {
1005+ var tests = Y.namespace('lp.views.global.test');
1006+ var info_type = Y.namespace('lp.app.information_type');
1007+ var ns = Y.lp.views;
1008+
1009+ tests.suite = new Y.Test.Suite('lp.views.global test');
1010+ tests.suite.add(new Y.Test.Case({
1011+ name: 'lp.views.global',
1012+
1013+ setUp: function () {
1014+ this.container = Y.one('#fixture');
1015+ window.LP = {
1016+ cache: {
1017+ related_features: {
1018+ private_projects: {
1019+ is_beta: true,
1020+ title: "Private Projects",
1021+ url: "http://blog.ld.net/general/beta",
1022+ value: "true"
1023+ }
1024+ },
1025+ information_type_data: {
1026+ PUBLIC: {
1027+ value: 'PUBLIC', name: 'Public',
1028+ is_private: false, order: 1,
1029+ description: 'Public Description'
1030+ },
1031+ EMBARGOED: {
1032+ value: 'EMBARGOED', name: 'Embargoed',
1033+ is_private: true, order: 2,
1034+ description: 'Something embargoed'
1035+ },
1036+ PROPRIETARY: {
1037+ value: 'PROPRIETARY', name: 'Proprietary',
1038+ is_private: true, order: 3,
1039+ description: 'Private Description'
1040+ }
1041+ }
1042+ }
1043+ };
1044+ },
1045+
1046+ tearDown: function () {
1047+ this.container.empty();
1048+ },
1049+
1050+ test_library_exists: function () {
1051+ Y.Assert.isObject(ns.Global,
1052+ "Could not locate the lp.views.global module");
1053+ },
1054+
1055+ test_basic_render: function () {
1056+ // Nothing is currently rendered out by default.
1057+ var view = new ns.Global();
1058+ view.render();
1059+
1060+ Y.Assert.areEqual(
1061+ '',
1062+ this.container.get('innerHTML'),
1063+ 'The container is still empty.');
1064+ view.destroy();
1065+ },
1066+
1067+ test_beta_banner: function () {
1068+ // If we've prepped on load a beta banner will auto appear.
1069+ var banner_container = Y.Node.create('<div/>');
1070+ banner_container.addClass('beta_banner_container');
1071+ this.container.append(banner_container);
1072+ var view = new ns.Global();
1073+ view.render();
1074+
1075+ // We have to wait until after page load event fires to test
1076+ // things out.
1077+ var banner_node = Y.one('.banner');
1078+ Y.Assert.isObject(
1079+ banner_node,
1080+ 'The container has a new banner node in there.');
1081+
1082+ view.destroy();
1083+ },
1084+
1085+ test_privacy: function () {
1086+ var beta_container = Y.Node.create('<div/>');
1087+ var private_container = Y.Node.create('<div/>');
1088+
1089+ beta_container.addClass('beta_banner_container');
1090+ private_container.addClass('private_banner_container');
1091+
1092+ this.container.append(beta_container);
1093+ this.container.append(private_container);
1094+ var view = new ns.Global();
1095+ view.render();
1096+
1097+ // We have to wait until after page load event fires to test
1098+ // things out.
1099+ var banner_nodes = Y.all('.banner');
1100+ Y.Assert.areEqual(
1101+ 2,
1102+ banner_nodes._nodes.length,
1103+ 'We should have two banners rendered.');
1104+
1105+ view.destroy();
1106+ },
1107+
1108+ test_privacy_banner_from_event: function () {
1109+ // We can also get a privacy banner via a fired event.
1110+ // This is hard coded to the <body> tag so we have to do some
1111+ // manual clean up here.
1112+ var view = new ns.Global();
1113+ view.render();
1114+
1115+ var msg = 'Testing Global';
1116+ Y.fire(info_type.EV_ISPRIVATE, {
1117+ text: msg
1118+ });
1119+
1120+ var banner = Y.one('.banner');
1121+ var banner_text = banner.one('.banner-content').get('text');
1122+ Y.Assert.areNotEqual(
1123+ -1,
1124+ banner_text.indexOf(msg),
1125+ 'The event text is turned into the banner content');
1126+
1127+ // Manually clean up.
1128+ Y.one('.yui3-banner').remove(true);
1129+ view.destroy();
1130+ },
1131+
1132+ test_banner_updates_content: function () {
1133+ // If we change our privacy information type the banner needs to
1134+ // update the content to the new text value from the event.
1135+ var view = new ns.Global();
1136+ view.render();
1137+
1138+ var msg = 'Testing Global';
1139+ Y.fire(info_type.EV_ISPRIVATE, {
1140+ text: msg
1141+ });
1142+
1143+ var updated_msg = 'Updated content';
1144+ Y.fire(info_type.EV_ISPRIVATE, {
1145+ text: updated_msg
1146+ });
1147+
1148+ var banner = Y.one('.banner');
1149+ var banner_text = banner.one('.banner-content').get('text');
1150+ Y.Assert.areNotEqual(
1151+ -1,
1152+ banner_text.indexOf(updated_msg),
1153+ 'The banner updated content to the second event message.');
1154+
1155+ // Manually clean up.
1156+ Y.one('.yui3-banner').remove(true);
1157+ view.destroy();
1158+
1159+ }
1160+ }));
1161+
1162+}, '0.1', {
1163+ requires: ['test', 'event-simulate', 'node-event-simulate',
1164+ 'lp.app.information_type', 'lp.views.global']
1165+});