Merge lp:~jcsackett/launchpad/progressively-enhanced-banners into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15287
Proposed branch: lp:~jcsackett/launchpad/progressively-enhanced-banners
Merge into: lp:launchpad
Diff against target: 650 lines (+184/-97)
19 files modified
lib/canonical/launchpad/icing/css/base.css (+0/-4)
lib/canonical/launchpad/icing/css/components/beta_banner.css (+5/-4)
lib/canonical/launchpad/icing/css/components/global_notification.css (+2/-3)
lib/canonical/launchpad/icing/css/components/sidebar_components.css (+0/-10)
lib/lp/app/browser/configure.zcml (+7/-0)
lib/lp/app/browser/tales.py (+13/-15)
lib/lp/app/browser/tests/base-layout.txt (+8/-4)
lib/lp/app/browser/tests/test_formatters.py (+5/-3)
lib/lp/app/doc/tales.txt (+4/-4)
lib/lp/app/javascript/banners/banner.js (+2/-11)
lib/lp/app/javascript/banners/beta-notification.js (+3/-4)
lib/lp/app/javascript/banners/privacy.js (+1/-10)
lib/lp/app/javascript/banners/tests/test_banner.js (+1/-2)
lib/lp/app/javascript/banners/tests/test_beta_notification.js (+3/-4)
lib/lp/app/javascript/banners/tests/test_privacy.js (+1/-11)
lib/lp/app/templates/banner-macros.pt (+43/-0)
lib/lp/app/templates/base-layout.pt (+5/-1)
lib/lp/services/webapp/publisher.py (+32/-7)
lib/lp/services/webapp/tests/test_publisher.py (+49/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/progressively-enhanced-banners
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+106487@code.launchpad.net

Commit message

Updates the banners to use progressive enhancmenet, so they are still shown without JS.

Description of the change

Summary
=======
The privacy banner (and it's close friend, the beta banner) don't work without
JS. This introduces basic html banners into base-layout that are enhanced by
the JS banners.

Preimp
======
Spoke with Curtis Hovey

Implementation
==============
* CSS rules for the banners have been updated to use the YUI css classes.
* The public private css formatter has been renamed to the global css
  formatter, and now adds the global-notification-visible class as well when
  it's needed.
* The `private` attribute on the LaunchpadView has been changed to a property
  method, encapsulating most of the special work in the
  public_private_formatter (e.g. checking if the context is private).
* A `beta_features` method has been added that returns the related_features
  that are in beta.
* Banner-macros has been added; it encapsulates the privacy banner and beta
  banner html, and uses the private and beta_features attributes on
  LaunchpadView to determine if the html is presented.
* Some minor js tweaks were made to work with the new banner html.

The close button doesn't get rendered on static beta banners; making this work
would require some extra investment that didn't seem worth it given the
relatively low usage of non-js browsers with beta features. If it's needed, a
followup branch can always be landed.

Tests
=====
bin/test -vvct base-layout -t test_formatters -t tales
bin/test -vvct beta -t privacy --layer=YUI

QA
==
Disable js in your browser. Check the privacy banner and the beta banner,
everything should be fine.

Enable js in your browser. Check it all again. It should all be fine.

LoC
===
This branch is part of the disclosure arc of work.

Lint
====
Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/banners/beta-notification.js
  lib/canonical/launchpad/icing/css/components/sidebar_components.css
  lib/canonical/launchpad/icing/css/components/global_notification.css
  lib/lp/app/javascript/banners/banner.js
  lib/lp/services/webapp/publisher.py
  lib/lp/app/templates/banner-macros.pt
  lib/lp/app/templates/base-layout.pt
  lib/lp/app/javascript/banners/privacy.js
  lib/lp/app/browser/configure.zcml
  lib/lp/app/browser/tales.py
  lib/canonical/launchpad/icing/css/components/beta_banner.css
  lib/canonical/launchpad/icing/css/base.css

Lint has been elided; there are some things I will fix before landing, but
most of this was a massive set of css line issues which were introduced by
more css-competent people than me that I'm leery to touch.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

The changes look good, but this branch is missing tests.
    lib/lp/app/browsers/tests/test_formatters.py
was not updated. The renaming of public_private_css => global_css should have broken the existing tests. I expect to see tests for the new rules.

I am surprised that the change of .beta-banner =>.global-notification in
    lib/lp/app/javascript/banners/beta-notification.js
did not break a js test because this is a contract between the js and css.

The two new properties in lib/lp/services/webapp/publisher.py need tests.

review: Needs Fixing (code)
Revision history for this message
j.c.sackett (jcsackett) wrote :

> The changes look good, but this branch is missing tests.
> lib/lp/app/browsers/tests/test_formatters.py
> was not updated. The renaming of public_private_css => global_css should have
> broken the existing tests. I expect to see tests for the new rules.
>
> I am surprised that the change of .beta-banner =>.global-notification in
> lib/lp/app/javascript/banners/beta-notification.js
> did not break a js test because this is a contract between the js and css.

This is the peril of making coffee after invoking bzr lp-propose; I never noticed my test changes hadn't been committed. They address the points above.

> The two new properties in lib/lp/services/webapp/publisher.py need tests.

Yes they do, good call. I have created tests for the privacy and beta_features attribute.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/icing/css/base.css'
--- lib/canonical/launchpad/icing/css/base.css 2012-03-10 15:33:33 +0000
+++ lib/canonical/launchpad/icing/css/base.css 2012-05-22 22:30:27 +0000
@@ -6,10 +6,6 @@
6 }6 }
7body.private {7body.private {
8 /* It must be obvious to the user that the context is private */8 /* It must be obvious to the user that the context is private */
9 background: url("/@@/private-y-bg") top left repeat-y;
10 }
11/* Override for when the feature flag is active */
12body.feature-flag-bugs-private-notification-enabled.private {
13 background-image: none;9 background-image: none;
14 }10 }
15body.private .private-disallow {11body.private .private-disallow {
1612
=== modified file 'lib/canonical/launchpad/icing/css/components/beta_banner.css'
--- lib/canonical/launchpad/icing/css/components/beta_banner.css 2012-03-10 15:43:21 +0000
+++ lib/canonical/launchpad/icing/css/components/beta_banner.css 2012-05-22 22:30:27 +0000
@@ -1,7 +1,7 @@
1/* ===========1/* ===========
2 Beta banner2 Beta banner
3*/3*/
4.beta-banner {4.yui3-betabanner-content .global-notification {
5 position: fixed;5 position: fixed;
6 z-index: 9;6 z-index: 9;
7 top: 0;7 top: 0;
@@ -17,6 +17,7 @@
17 text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);17 text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
18 font-size: 14px;18 font-size: 14px;
19 line-height: 21px;19 line-height: 21px;
20 text-align: left;
20 }21 }
21.beta-banner .info-link {22.beta-banner .info-link {
22 color: #4884ef;23 color: #4884ef;
@@ -40,8 +41,8 @@
40.beta-feature {41.beta-feature {
41 font-weight: bold;42 font-weight: bold;
42 }43 }
43.beta-banner .global-notification-close,44.yui3-betabanner-content .global-notification-close,
44.beta-banner .global-notification-close:active,45.yui3-betabanner-content .global-notification-close:active,
45.beta-banner .global-notification-close:visited {46.yui3-betabanner-content .global-notification-close:visited {
46 color: #fff;47 color: #fff;
47 }48 }
4849
=== modified file 'lib/canonical/launchpad/icing/css/components/global_notification.css'
--- lib/canonical/launchpad/icing/css/components/global_notification.css 2012-05-15 17:17:34 +0000
+++ lib/canonical/launchpad/icing/css/components/global_notification.css 2012-05-22 22:30:27 +0000
@@ -21,8 +21,6 @@
21 font-weight: bold;21 font-weight: bold;
22 box-shadow: 0 0 5px #333;22 box-shadow: 0 0 5px #333;
23 }23 }
24
25
26.global-notification .sprite.notification-private {24.global-notification .sprite.notification-private {
27 float: left;25 float: left;
28 display: inline-block;26 display: inline-block;
@@ -58,10 +56,11 @@
58 Privacy56 Privacy
59*/57*/
6058
61.privacy-banner {59.yui3-privacybanner-content .global-notification{
62 /* Define colour for browsers that don't support transparency */60 /* Define colour for browsers that don't support transparency */
63 background-color: #8d1f1f;61 background-color: #8d1f1f;
64 /* Set transparent background for browsers that support it */62 /* Set transparent background for browsers that support it */
65 background-color: rgba(125,0,0,0.9);63 background-color: rgba(125,0,0,0.9);
66 color: #fff;64 color: #fff;
65 text-align: left;
67}66}
6867
=== modified file 'lib/canonical/launchpad/icing/css/components/sidebar_components.css'
--- lib/canonical/launchpad/icing/css/components/sidebar_components.css 2012-03-10 15:33:33 +0000
+++ lib/canonical/launchpad/icing/css/components/sidebar_components.css 2012-05-22 22:30:27 +0000
@@ -24,16 +24,6 @@
24.side ul {24.side ul {
25 background: #fbfbfb;25 background: #fbfbfb;
26 }26 }
27#privacy.private {
28 background: url(/@@/private-bg) top left repeat-x; /* 8px high */
29 padding-top: 12px; /* = 8px + the usual 4px top padding */
30 }
31/* Override for when the feature flag is active */
32.feature-flag-bugs-private-notification-enabled #privacy.private {
33 background-image: none;
34 background-color: #FBFBFB;
35 padding-top: 0.5em;
36 }
37.downloads li {27.downloads li {
38 margin: 0;28 margin: 0;
39 padding: 2px 0 0;29 padding: 2px 0 0;
4030
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2012-04-19 20:37:47 +0000
+++ lib/lp/app/browser/configure.zcml 2012-05-22 22:30:27 +0000
@@ -33,6 +33,13 @@
33 />33 />
34 <browser:page34 <browser:page
35 for="*"35 for="*"
36 name="+banner-macros"
37 template="../templates/banner-macros.pt"
38 permission="zope.Public"
39 class="lp.app.browser.launchpad.Macro"
40 />
41 <browser:page
42 for="*"
36 name="launchpad_form"43 name="launchpad_form"
37 layer="lp.layers.LaunchpadLayer"44 layer="lp.layers.LaunchpadLayer"
38 permission="zope.Public"45 permission="zope.Public"
3946
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py 2012-04-20 07:01:18 +0000
+++ lib/lp/app/browser/tales.py 2012-05-22 22:30:27 +0000
@@ -559,7 +559,7 @@
559 # Names which are allowed but can't be traversed further.559 # Names which are allowed but can't be traversed further.
560 final_traversable_names = {560 final_traversable_names = {
561 'pagetitle': 'pagetitle',561 'pagetitle': 'pagetitle',
562 'public-private-css': 'public_private_css',562 'global-css': 'global_css',
563 }563 }
564564
565 def __init__(self, context):565 def __init__(self, context):
@@ -658,22 +658,20 @@
658 "No link implementation for %r, IPathAdapter implementation "658 "No link implementation for %r, IPathAdapter implementation "
659 "for %r." % (self, self._context))659 "for %r." % (self, self._context))
660660
661 def public_private_css(self):661 def global_css(self):
662 """Return the CSS class that represents the object's privacy."""662 css_classes = set([])
663 # If a view is marked as private, the context doesn't matter. It will
664 # always be displayed as private.
665 view = self._context663 view = self._context
666 private_view = getattr(view, 'private', False)664 private = getattr(view, 'private', False)
667 if private_view:665 if private:
668 return 'private'666 css_classes.add('private')
669667 css_classes.add('global-notification-visible')
670 # If the view is not marked as private, privacy is determined by the
671 # view's context.
672 privacy = IPrivacy(getattr(view, 'context', None), None)
673 if privacy is not None and privacy.private:
674 return 'private'
675 else:668 else:
676 return 'public'669 css_classes.add('public')
670 beta = getattr(view, 'beta_features', [])
671 if beta != []:
672 css_classes.add('global-notification-visible')
673 return ' '.join(list(css_classes))
674
677675
678 def _getSaneBreadcrumbDetail(self, breadcrumb):676 def _getSaneBreadcrumbDetail(self, breadcrumb):
679 text = breadcrumb.detail677 text = breadcrumb.detail
680678
=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
--- lib/lp/app/browser/tests/base-layout.txt 2012-03-14 04:41:36 +0000
+++ lib/lp/app/browser/tests/base-layout.txt 2012-05-22 22:30:27 +0000
@@ -157,9 +157,13 @@
157Public and private presentation157Public and private presentation
158-------------------------------158-------------------------------
159159
160The base-layout master templates uses the fmt:public-private-css formatter to160The base-layout master templates uses the fmt:global-css formatter to
161add the 'public' or 'private' CSS class to the body tag. When the context is161add the 'public' or 'private' CSS class to the body tag as well as the
162private, the 'private' class is added to the body's class attribute.162'global-notification-visible' tag, if needed.
163
164When the context is private, the 'private' class is added to the body's class
165attribute. If the context is private or there are beta features,
166'global-notification-visible' is added to the body's class attribute.
163167
164 >>> from lp.registry.interfaces.person import PersonVisibility168 >>> from lp.registry.interfaces.person import PersonVisibility
165169
@@ -170,7 +174,7 @@
170 >>> view = MainOnlyView(team, request)174 >>> view = MainOnlyView(team, request)
171 >>> body = find_tag_by_id(view.render(), 'document')175 >>> body = find_tag_by_id(view.render(), 'document')
172 >>> print body['class']176 >>> print body['class']
173 tab-overview main_only private yui3-skin-sam177 tab-overview main_only global-notification-visible private yui3-skin-sam
174178
175When the context is public, the 'public' class is in the class attribute.179When the context is public, the 'public' class is in the class attribute.
176180
177181
=== modified file 'lib/lp/app/browser/tests/test_formatters.py'
--- lib/lp/app/browser/tests/test_formatters.py 2012-04-12 19:42:55 +0000
+++ lib/lp/app/browser/tests/test_formatters.py 2012-05-22 22:30:27 +0000
@@ -82,15 +82,17 @@
82 expected_title = u'%s...\u201d : Bugs : Fnord' % detail[0:64]82 expected_title = u'%s...\u201d : Bugs : Fnord' % detail[0:64]
83 self.assertEqual(expected_title, formatter.pagetitle())83 self.assertEqual(expected_title, formatter.pagetitle())
8484
85 def test_public_private_css(self):85 def test_global_css(self):
86 person = self.factory.makePerson()86 person = self.factory.makePerson()
87 view = create_view(person, name="+index")87 view = create_view(person, name="+index")
88 formatter = ObjectFormatterAPI(view)88 formatter = ObjectFormatterAPI(view)
89 self.assertEqual('public', formatter.public_private_css())89 self.assertEqual('public', formatter.global_css())
9090
91 view = create_view(person, name="+archivesubscriptions")91 view = create_view(person, name="+archivesubscriptions")
92 formatter = ObjectFormatterAPI(view)92 formatter = ObjectFormatterAPI(view)
93 self.assertEqual('private', formatter.public_private_css())93 self.assertEqual(
94 'global-notification-visible private',
95 formatter.global_css())
9496
95class TestPillarFormatterAPI(TestCaseWithFactory):97class TestPillarFormatterAPI(TestCaseWithFactory):
9698
9799
=== modified file 'lib/lp/app/doc/tales.txt'
--- lib/lp/app/doc/tales.txt 2012-04-10 14:01:17 +0000
+++ lib/lp/app/doc/tales.txt 2012-05-22 22:30:27 +0000
@@ -1533,7 +1533,7 @@
1533not need to implement IPrivacy.1533not need to implement IPrivacy.
15341534
1535 >>> thing = object()1535 >>> thing = object()
1536 >>> print test_tales('context/fmt:public-private-css', context=thing)1536 >>> print test_tales('context/fmt:global-css', context=thing)
1537 public1537 public
15381538
1539The CSS class honors the state of the object's privacy if the object1539The CSS class honors the state of the object's privacy if the object
@@ -1544,7 +1544,7 @@
1544 >>> print bug.private1544 >>> print bug.private
1545 False1545 False
15461546
1547 >>> print test_tales('context/fmt:public-private-css', context=bug)1547 >>> print test_tales('context/fmt:global-css', context=bug)
1548 public1548 public
15491549
1550If the private attribute is True, the class is 'private'.1550If the private attribute is True, the class is 'private'.
@@ -1554,8 +1554,8 @@
1554 >>> bug.setPrivate(True, owner)1554 >>> bug.setPrivate(True, owner)
1555 True1555 True
15561556
1557 >>> print test_tales('context/fmt:public-private-css', context=bug)1557 >>> print test_tales('context/fmt:global-css', context=bug)
1558 private1558 global-notification-visible private
15591559
1560 >>> login(ANONYMOUS)1560 >>> login(ANONYMOUS)
15611561
15621562
=== modified file 'lib/lp/app/javascript/banners/banner.js'
--- lib/lp/app/javascript/banners/banner.js 2012-05-15 17:17:34 +0000
+++ lib/lp/app/javascript/banners/banner.js 2012-05-22 22:30:27 +0000
@@ -23,7 +23,6 @@
23 slide_out: 0.2,23 slide_out: 0.2,
24 } 24 }
25 }25 }
26
27 return anim_times;26 return anim_times;
28 },27 },
2928
@@ -98,7 +97,6 @@
98 var banner_data = {97 var banner_data = {
99 badge: this.get('banner_icon'),98 badge: this.get('banner_icon'),
100 text: this.get('notification_text'),99 text: this.get('notification_text'),
101 extra_classes: this.get('css_class'),
102 banner_id: this.get('banner_id')100 banner_id: this.get('banner_id')
103 };101 };
104 return Y.lp.mustache.to_html(102 return Y.lp.mustache.to_html(
@@ -117,7 +115,7 @@
117 this._updateBanner(existing_banner);115 this._updateBanner(existing_banner);
118 } else {116 } else {
119 var banner_html = this._createBanner(); 117 var banner_html = this._createBanner();
120 this.get('target').append(banner_html);118 this.get('contentBox').append(banner_html);
121 }119 }
122 },120 },
123121
@@ -135,17 +133,11 @@
135 banner_id: { value: "base-banner" },133 banner_id: { value: "base-banner" },
136 notification_text: { value: "" },134 notification_text: { value: "" },
137 banner_icon: { value: "<span></span>" },135 banner_icon: { value: "<span></span>" },
138 target: {
139 valueFn: function() {
140 return Y.one('#maincontent');
141 }
142 },
143 banner_template: {136 banner_template: {
144 valueFn: function() {137 valueFn: function() {
145 return [138 return [
146 '<div id="{{ banner_id }}"', 139 '<div id="{{ banner_id }}"',
147 'class="global-notification transparent hidden',140 'class="global-notification transparent hidden">',
148 ' {{ extra_classes }}">',
149 '{{{ badge }}}',141 '{{{ badge }}}',
150 '<span class="banner-text">{{ text }}</span>',142 '<span class="banner-text">{{ text }}</span>',
151 "</div>"].join('')143 "</div>"].join('')
@@ -153,7 +145,6 @@
153 },145 },
154 skip_animation: { value: false },146 skip_animation: { value: false },
155 visible: { value: false },147 visible: { value: false },
156 css_class: { value: '' }
157 }148 }
158});149});
159150
160151
=== modified file 'lib/lp/app/javascript/banners/beta-notification.js'
--- lib/lp/app/javascript/banners/beta-notification.js 2012-05-14 22:07:29 +0000
+++ lib/lp/app/javascript/banners/beta-notification.js 2012-05-22 22:30:27 +0000
@@ -19,11 +19,11 @@
1919
20 renderUI: function () {20 renderUI: function () {
21 baseBanner.prototype.renderUI.apply(this, arguments);21 baseBanner.prototype.renderUI.apply(this, arguments);
22 var banner_node = Y.one('.beta-banner');22 var beta_node = Y.one('.global-notification');
23 var close_box = Y.Node.create(23 var close_box = Y.Node.create(
24 '<a href="#" class="global-notification-close">Hide' +24 '<a href="#" class="global-notification-close">Hide' +
25 '<span class="notification-close sprite" /></a>');25 '<span class="notification-close sprite" /></a>');
26 banner_node.appendChild(close_box);26 beta_node.appendChild(close_box);
27 },27 },
2828
29 bindUI: function () {29 bindUI: function () {
@@ -45,8 +45,7 @@
45 valueFn: function() {45 valueFn: function() {
46 return [46 return [
47 '<div id="{{ banner_id }}"', 47 '<div id="{{ banner_id }}"',
48 'class="global-notification beta-banner ',48 'class="global-notification transparent hidden">',
49 'transparent hidden">',
50 '{{{ badge }}}',49 '{{{ badge }}}',
51 '<span class="banner-text">',50 '<span class="banner-text">',
52 '{{ text }}{{{ features }}}',51 '{{ text }}{{{ features }}}',
5352
=== modified file 'lib/lp/app/javascript/banners/privacy.js'
--- lib/lp/app/javascript/banners/privacy.js 2012-05-15 17:17:34 +0000
+++ lib/lp/app/javascript/banners/privacy.js 2012-05-22 22:30:27 +0000
@@ -3,15 +3,7 @@
3var ns = Y.namespace('lp.app.banner.privacy');3var ns = Y.namespace('lp.app.banner.privacy');
4var baseBanner = Y.lp.app.banner.Banner;4var baseBanner = Y.lp.app.banner.Banner;
55
6ns.PrivacyBanner = Y.Base.create('privacyBanner', baseBanner, [], {6ns.PrivacyBanner = Y.Base.create('privacyBanner', baseBanner, [], {}, {
7
8 renderUI: function () {
9 var body = Y.one('body');
10 body.addClass('feature-flag-bugs-private-notification-enabled');
11 baseBanner.prototype.renderUI.apply(this, arguments);
12 }
13
14}, {
15 ATTRS: {7 ATTRS: {
16 banner_id: { value: "privacy-banner" },8 banner_id: { value: "privacy-banner" },
17 notification_text: {9 notification_text: {
@@ -20,7 +12,6 @@
20 banner_icon: {12 banner_icon: {
21 value: '<span class="sprite notification-private"></span>'13 value: '<span class="sprite notification-private"></span>'
22 },14 },
23 css_class: { value: 'privacy-banner' }
24 } 15 }
25});16});
2617
2718
=== modified file 'lib/lp/app/javascript/banners/tests/test_banner.js'
--- lib/lp/app/javascript/banners/tests/test_banner.js 2012-05-14 19:01:38 +0000
+++ lib/lp/app/javascript/banners/tests/test_banner.js 2012-05-22 22:30:27 +0000
@@ -17,8 +17,7 @@
17 },17 },
1818
19 tearDown: function () {19 tearDown: function () {
20 var main = Y.one('#maincontent');20 Y.one('body').get('children').remove(true);
21 main.remove(true);
22 },21 },
2322
24 test_library_exists: function () {23 test_library_exists: function () {
2524
=== modified file 'lib/lp/app/javascript/banners/tests/test_beta_notification.js'
--- lib/lp/app/javascript/banners/tests/test_beta_notification.js 2012-05-15 15:16:39 +0000
+++ lib/lp/app/javascript/banners/tests/test_beta_notification.js 2012-05-22 22:30:27 +0000
@@ -24,8 +24,7 @@
24 },24 },
2525
26 tearDown: function () {26 tearDown: function () {
27 var main = Y.one('#maincontent');27 Y.one('body').get('children').remove(true);
28 main.remove(true);
29 },28 },
3029
31 test_library_exists: function () {30 test_library_exists: function () {
@@ -103,7 +102,7 @@
103 url: 'http://example.org'102 url: 'http://example.org'
104 }};103 }};
105 Y.lp.app.banner.beta.show_beta_if_needed(); 104 Y.lp.app.banner.beta.show_beta_if_needed();
106 Y.Assert.isNull(Y.one('.beta-banner'));105 Y.Assert.isNull(Y.one('.global-notification'));
107 },106 },
108107
109 test_hide_beta_banner: function() {108 test_hide_beta_banner: function() {
@@ -118,7 +117,7 @@
118 betabanner.render();117 betabanner.render();
119 betabanner.show();118 betabanner.show();
120 var body = Y.one('body');119 var body = Y.one('body');
121 var banner = Y.one('.beta-banner');120 var banner = Y.one('.global-notification');
122 Y.Assert.isFalse(banner.hasClass('hidden'));121 Y.Assert.isFalse(banner.hasClass('hidden'));
123122
124 // Even with animation times set to 0, this test needs a slight123 // Even with animation times set to 0, this test needs a slight
125124
=== modified file 'lib/lp/app/javascript/banners/tests/test_privacy.js'
--- lib/lp/app/javascript/banners/tests/test_privacy.js 2012-05-15 14:52:16 +0000
+++ lib/lp/app/javascript/banners/tests/test_privacy.js 2012-05-22 22:30:27 +0000
@@ -19,8 +19,7 @@
19 },19 },
2020
21 tearDown: function () {21 tearDown: function () {
22 var main = Y.one('#maincontent');22 Y.one('body').get('children').remove(true);
23 main.remove(true);
24 },23 },
2524
26 test_library_exists: function () {25 test_library_exists: function () {
@@ -37,15 +36,6 @@
37 '<span class="sprite notification-private"></span>',36 '<span class="sprite notification-private"></span>',
38 banner.get('banner_icon'));37 banner.get('banner_icon'));
39 },38 },
40
41 test_render: function () {
42 var banner = new Y.lp.app.banner.privacy.PrivacyBanner();
43 banner.render();
44
45 var body = Y.one('body');
46 var exp_class = 'feature-flag-bugs-private-notification-enabled';
47 Y.Assert.isTrue(body.hasClass(exp_class));
48 }
49 }));39 }));
5040
51}, '0.1', {'requires': ['test', 'console', 'lp.app.banner.privacy']});41}, '0.1', {'requires': ['test', 'console', 'lp.app.banner.privacy']});
5242
=== added file 'lib/lp/app/templates/banner-macros.pt'
--- lib/lp/app/templates/banner-macros.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/templates/banner-macros.pt 2012-05-22 22:30:27 +0000
@@ -0,0 +1,43 @@
1<macros
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 i18n:domain="launchpad"
7 tal:omit-tag=""
8>
9
10<metal:privacy define-macro="privacy-banner">
11 <tal:show-banner condition="view/private">
12 <div class="yui3-privacybanner-content">
13 <div id="privacy-banner" class="global-notification">
14 <span class="sprite notification-private"></span>
15 <span class="banner-text">The information on this page is private.</span>
16 </div>
17 </div>
18 </tal:show-banner>
19</metal:privacy>
20
21<metal:beta define-macro="beta-banner">
22<tal:show-banner condition="view/beta_features">
23<div class="yui3-betabanner-content">
24 <div id="beta-banner" class="global-notification">
25 <span class="beta-warning">BETA!</span>
26 <span class="banner-text">
27 Some parts of this page are in beta:&nbsp;
28 <span class="beta-feature">
29 <tal:features
30 repeat="feature view/beta_features">
31 <tal:feature replace="feature/title" />
32 <tal:link condition="feature/url">
33 (<a tal:attributes="href feature/url" class="info-link">read more</a>)
34 </tal:link>
35 </tal:features>
36 <span>
37 </span>
38 </div>
39</div>
40</tal:show-banner>
41</metal:beta>
42
43</macros>
044
=== modified file 'lib/lp/app/templates/base-layout.pt'
--- lib/lp/app/templates/base-layout.pt 2012-04-12 19:42:35 +0000
+++ lib/lp/app/templates/base-layout.pt 2012-05-22 22:30:27 +0000
@@ -62,7 +62,7 @@
62 itemtype="http://schema.org/WebPage"62 itemtype="http://schema.org/WebPage"
63 tal:attributes="class string:tab-${view/menu:selectedfacetname}63 tal:attributes="class string:tab-${view/menu:selectedfacetname}
64 ${view/macro:pagetype}64 ${view/macro:pagetype}
65 ${view/fmt:public-private-css}65 ${view/fmt:global-css}
66 yui3-skin-sam">66 yui3-skin-sam">
67 <script type="text/javascript"67 <script type="text/javascript"
68 tal:condition="python: is_lpnet">68 tal:condition="python: is_lpnet">
@@ -97,6 +97,10 @@
97 <div class="yui-t4"97 <div class="yui-t4"
98 tal:omit-tag="not: view/macro:pagehas/portlets">98 tal:omit-tag="not: view/macro:pagehas/portlets">
99 <div id="maincontent" class="yui-main">99 <div id="maincontent" class="yui-main">
100 <metal:privacy-banner
101 use-macro="view/@@+banner-macros/beta-banner"/>
102 <metal:privacy-banner
103 use-macro="view/@@+banner-macros/privacy-banner"/>
100 <div class="yui-b"104 <div class="yui-b"
101 tal:attributes="105 tal:attributes="
102 lang view/lang|default_language|default;106 lang view/lang|default_language|default;
103107
=== modified file 'lib/lp/services/webapp/publisher.py'
--- lib/lp/services/webapp/publisher.py 2012-04-26 16:21:46 +0000
+++ lib/lp/services/webapp/publisher.py 2012-05-22 22:30:27 +0000
@@ -65,6 +65,7 @@
65from zope.traversing.browser.interfaces import IAbsoluteURL65from zope.traversing.browser.interfaces import IAbsoluteURL
6666
67from lp.app.errors import NotFoundError67from lp.app.errors import NotFoundError
68from lp.app.interfaces.launchpad import IPrivacy
68from lp.app.versioninfo import revno69from lp.app.versioninfo import revno
69from lp.layers import (70from lp.layers import (
70 LaunchpadLayer,71 LaunchpadLayer,
@@ -286,9 +287,19 @@
286 many templates not set via zcml, or you want to do287 many templates not set via zcml, or you want to do
287 rendering from Python.288 rendering from Python.
288 - publishTraverse() <-- override this to support traversing-through.289 - publishTraverse() <-- override this to support traversing-through.
290 - private <-- used to indicate if the view contains private data.
291 override this if the view has special privacy needs
292 (i.e. context doesn't properly indicate privacy).
289 """293 """
290294
291 private = False295 @property
296 def private(self):
297 """A view is private if its context is."""
298 privacy = IPrivacy(self.context, None)
299 if privacy is not None:
300 return privacy.private
301 else:
302 return False
292303
293 def __init__(self, context, request):304 def __init__(self, context, request):
294 self.context = context305 self.context = context
@@ -299,6 +310,16 @@
299 # IJSONRequestCache adapter.310 # IJSONRequestCache adapter.
300 if isinstance(request, FakeRequest):311 if isinstance(request, FakeRequest):
301 return312 return
313 # Several view objects may be created for one page request:
314 # One view for the main context and template, and other views
315 # for macros included in the main template.
316 cache = self._get_json_cache()
317 if cache is None:
318 return
319 related_features = cache.setdefault('related_features', {})
320 related_features.update(self.related_feature_info)
321
322 def _get_json_cache(self):
302 # Some tests create views without providing any request323 # Some tests create views without providing any request
303 # object at all; other tests run without the component324 # object at all; other tests run without the component
304 # infrastructure.325 # infrastructure.
@@ -306,12 +327,16 @@
306 cache = IJSONRequestCache(self.request).objects327 cache = IJSONRequestCache(self.request).objects
307 except TypeError, error:328 except TypeError, error:
308 if error.args[0] == 'Could not adapt':329 if error.args[0] == 'Could not adapt':
309 return330 cache = None
310 # Several view objects may be created for one page request:331 return cache
311 # One view for the main context and template, and other views332
312 # for macros included in the main template.333 @property
313 related_features = cache.setdefault('related_features', {})334 def beta_features(self):
314 related_features.update(self.related_feature_info)335 cache = self._get_json_cache()
336 if cache is None:
337 return []
338 related_features = cache.setdefault('related_features', {}).values()
339 return [f for f in related_features if f['is_beta']]
315340
316 def initialize(self):341 def initialize(self):
317 """Override this in subclasses.342 """Override this in subclasses.
318343
=== modified file 'lib/lp/services/webapp/tests/test_publisher.py'
--- lib/lp/services/webapp/tests/test_publisher.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_publisher.py 2012-05-22 22:30:27 +0000
@@ -13,7 +13,9 @@
13from lazr.restful.interfaces import IJSONRequestCache13from lazr.restful.interfaces import IJSONRequestCache
14import simplejson14import simplejson
15from zope.component import getUtility15from zope.component import getUtility
16from zope.interface import implements
1617
18from lp.app.interfaces.launchpad import IPrivacy
17from lp.services.features.flags import flag_info19from lp.services.features.flags import flag_info
18from lp.services.features.testing import FeatureFixture20from lp.services.features.testing import FeatureFixture
19from lp.services.webapp import publisher21from lp.services.webapp import publisher
@@ -295,6 +297,53 @@
295 # Or when no request at all is passed.297 # Or when no request at all is passed.
296 LaunchpadView(object(), None)298 LaunchpadView(object(), None)
297299
300 def test_view_privacy(self):
301 # View privacy is based on the context.
302 class PrivateObject(object):
303 implements(IPrivacy)
304
305 def __init__(self, private):
306 self.private = private
307
308 view = LaunchpadView(PrivateObject(True), FakeRequest())
309 self.assertTrue(view.private)
310
311 view = LaunchpadView(PrivateObject(False), FakeRequest())
312 self.assertFalse(view.private)
313
314 def test_view_beta_features_simple(self):
315 class TestView(LaunchpadView):
316 related_features = ['test_feature']
317
318 self.useFixture(FeatureFixture(
319 {}, self.makeFeatureFlagDictionaries(u'', u'on'),
320 override_scope_lookup=lambda scope_name: True))
321 request = LaunchpadTestRequest()
322 view = TestView(object(), request)
323 expected_beta_features = [{
324 'url': 'http://wiki.lp.dev/LEP/sample', 'is_beta': True,
325 'value': u'on', 'title': 'title'}]
326 self.assertEqual(expected_beta_features, view.beta_features)
327
328 def test_view_beta_features_mixed(self):
329 # With multiple related features, only those in a beta condition are
330 # reported as beta features.
331 class TestView(LaunchpadView):
332 related_features = ['test_feature', 'test_feature2']
333
334 # Select one flag on 'default', one flag not on 'default. 'default'
335 # setting determines whether flags correspond to 'beta' features.
336 raw_flag_dicts = self.makeFeatureFlagDictionaries(u'', u'on')
337 flag_dicts = [raw_flag_dicts[1], raw_flag_dicts[2]]
338
339 self.useFixture(FeatureFixture(
340 {}, flag_dicts, override_scope_lookup=lambda scope_name: True))
341 request = LaunchpadTestRequest()
342 view = TestView(object(), request)
343 expected_beta_features = [{
344 'url': 'http://wiki.lp.dev/LEP/sample', 'is_beta': True,
345 'value': u'on', 'title': 'title'}]
346 self.assertEqual(expected_beta_features, view.beta_features)
298347
299def test_suite():348def test_suite():
300 suite = TestSuite()349 suite = TestSuite()