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
1=== modified file 'lib/canonical/launchpad/icing/css/base.css'
2--- lib/canonical/launchpad/icing/css/base.css 2012-03-10 15:33:33 +0000
3+++ lib/canonical/launchpad/icing/css/base.css 2012-05-22 22:30:27 +0000
4@@ -6,10 +6,6 @@
5 }
6 body.private {
7 /* It must be obvious to the user that the context is private */
8- background: url("/@@/private-y-bg") top left repeat-y;
9- }
10-/* Override for when the feature flag is active */
11-body.feature-flag-bugs-private-notification-enabled.private {
12 background-image: none;
13 }
14 body.private .private-disallow {
15
16=== modified file 'lib/canonical/launchpad/icing/css/components/beta_banner.css'
17--- lib/canonical/launchpad/icing/css/components/beta_banner.css 2012-03-10 15:43:21 +0000
18+++ lib/canonical/launchpad/icing/css/components/beta_banner.css 2012-05-22 22:30:27 +0000
19@@ -1,7 +1,7 @@
20 /* ===========
21 Beta banner
22 */
23-.beta-banner {
24+.yui3-betabanner-content .global-notification {
25 position: fixed;
26 z-index: 9;
27 top: 0;
28@@ -17,6 +17,7 @@
29 text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.5);
30 font-size: 14px;
31 line-height: 21px;
32+ text-align: left;
33 }
34 .beta-banner .info-link {
35 color: #4884ef;
36@@ -40,8 +41,8 @@
37 .beta-feature {
38 font-weight: bold;
39 }
40-.beta-banner .global-notification-close,
41-.beta-banner .global-notification-close:active,
42-.beta-banner .global-notification-close:visited {
43+.yui3-betabanner-content .global-notification-close,
44+.yui3-betabanner-content .global-notification-close:active,
45+.yui3-betabanner-content .global-notification-close:visited {
46 color: #fff;
47 }
48
49=== modified file 'lib/canonical/launchpad/icing/css/components/global_notification.css'
50--- lib/canonical/launchpad/icing/css/components/global_notification.css 2012-05-15 17:17:34 +0000
51+++ lib/canonical/launchpad/icing/css/components/global_notification.css 2012-05-22 22:30:27 +0000
52@@ -21,8 +21,6 @@
53 font-weight: bold;
54 box-shadow: 0 0 5px #333;
55 }
56-
57-
58 .global-notification .sprite.notification-private {
59 float: left;
60 display: inline-block;
61@@ -58,10 +56,11 @@
62 Privacy
63 */
64
65-.privacy-banner {
66+.yui3-privacybanner-content .global-notification{
67 /* Define colour for browsers that don't support transparency */
68 background-color: #8d1f1f;
69 /* Set transparent background for browsers that support it */
70 background-color: rgba(125,0,0,0.9);
71 color: #fff;
72+ text-align: left;
73 }
74
75=== modified file 'lib/canonical/launchpad/icing/css/components/sidebar_components.css'
76--- lib/canonical/launchpad/icing/css/components/sidebar_components.css 2012-03-10 15:33:33 +0000
77+++ lib/canonical/launchpad/icing/css/components/sidebar_components.css 2012-05-22 22:30:27 +0000
78@@ -24,16 +24,6 @@
79 .side ul {
80 background: #fbfbfb;
81 }
82-#privacy.private {
83- background: url(/@@/private-bg) top left repeat-x; /* 8px high */
84- padding-top: 12px; /* = 8px + the usual 4px top padding */
85- }
86-/* Override for when the feature flag is active */
87-.feature-flag-bugs-private-notification-enabled #privacy.private {
88- background-image: none;
89- background-color: #FBFBFB;
90- padding-top: 0.5em;
91- }
92 .downloads li {
93 margin: 0;
94 padding: 2px 0 0;
95
96=== modified file 'lib/lp/app/browser/configure.zcml'
97--- lib/lp/app/browser/configure.zcml 2012-04-19 20:37:47 +0000
98+++ lib/lp/app/browser/configure.zcml 2012-05-22 22:30:27 +0000
99@@ -33,6 +33,13 @@
100 />
101 <browser:page
102 for="*"
103+ name="+banner-macros"
104+ template="../templates/banner-macros.pt"
105+ permission="zope.Public"
106+ class="lp.app.browser.launchpad.Macro"
107+ />
108+ <browser:page
109+ for="*"
110 name="launchpad_form"
111 layer="lp.layers.LaunchpadLayer"
112 permission="zope.Public"
113
114=== modified file 'lib/lp/app/browser/tales.py'
115--- lib/lp/app/browser/tales.py 2012-04-20 07:01:18 +0000
116+++ lib/lp/app/browser/tales.py 2012-05-22 22:30:27 +0000
117@@ -559,7 +559,7 @@
118 # Names which are allowed but can't be traversed further.
119 final_traversable_names = {
120 'pagetitle': 'pagetitle',
121- 'public-private-css': 'public_private_css',
122+ 'global-css': 'global_css',
123 }
124
125 def __init__(self, context):
126@@ -658,22 +658,20 @@
127 "No link implementation for %r, IPathAdapter implementation "
128 "for %r." % (self, self._context))
129
130- def public_private_css(self):
131- """Return the CSS class that represents the object's privacy."""
132- # If a view is marked as private, the context doesn't matter. It will
133- # always be displayed as private.
134+ def global_css(self):
135+ css_classes = set([])
136 view = self._context
137- private_view = getattr(view, 'private', False)
138- if private_view:
139- return 'private'
140-
141- # If the view is not marked as private, privacy is determined by the
142- # view's context.
143- privacy = IPrivacy(getattr(view, 'context', None), None)
144- if privacy is not None and privacy.private:
145- return 'private'
146+ private = getattr(view, 'private', False)
147+ if private:
148+ css_classes.add('private')
149+ css_classes.add('global-notification-visible')
150 else:
151- return 'public'
152+ css_classes.add('public')
153+ beta = getattr(view, 'beta_features', [])
154+ if beta != []:
155+ css_classes.add('global-notification-visible')
156+ return ' '.join(list(css_classes))
157+
158
159 def _getSaneBreadcrumbDetail(self, breadcrumb):
160 text = breadcrumb.detail
161
162=== modified file 'lib/lp/app/browser/tests/base-layout.txt'
163--- lib/lp/app/browser/tests/base-layout.txt 2012-03-14 04:41:36 +0000
164+++ lib/lp/app/browser/tests/base-layout.txt 2012-05-22 22:30:27 +0000
165@@ -157,9 +157,13 @@
166 Public and private presentation
167 -------------------------------
168
169-The base-layout master templates uses the fmt:public-private-css formatter to
170-add the 'public' or 'private' CSS class to the body tag. When the context is
171-private, the 'private' class is added to the body's class attribute.
172+The base-layout master templates uses the fmt:global-css formatter to
173+add the 'public' or 'private' CSS class to the body tag as well as the
174+'global-notification-visible' tag, if needed.
175+
176+When the context is private, the 'private' class is added to the body's class
177+attribute. If the context is private or there are beta features,
178+'global-notification-visible' is added to the body's class attribute.
179
180 >>> from lp.registry.interfaces.person import PersonVisibility
181
182@@ -170,7 +174,7 @@
183 >>> view = MainOnlyView(team, request)
184 >>> body = find_tag_by_id(view.render(), 'document')
185 >>> print body['class']
186- tab-overview main_only private yui3-skin-sam
187+ tab-overview main_only global-notification-visible private yui3-skin-sam
188
189 When the context is public, the 'public' class is in the class attribute.
190
191
192=== modified file 'lib/lp/app/browser/tests/test_formatters.py'
193--- lib/lp/app/browser/tests/test_formatters.py 2012-04-12 19:42:55 +0000
194+++ lib/lp/app/browser/tests/test_formatters.py 2012-05-22 22:30:27 +0000
195@@ -82,15 +82,17 @@
196 expected_title = u'%s...\u201d : Bugs : Fnord' % detail[0:64]
197 self.assertEqual(expected_title, formatter.pagetitle())
198
199- def test_public_private_css(self):
200+ def test_global_css(self):
201 person = self.factory.makePerson()
202 view = create_view(person, name="+index")
203 formatter = ObjectFormatterAPI(view)
204- self.assertEqual('public', formatter.public_private_css())
205+ self.assertEqual('public', formatter.global_css())
206
207 view = create_view(person, name="+archivesubscriptions")
208 formatter = ObjectFormatterAPI(view)
209- self.assertEqual('private', formatter.public_private_css())
210+ self.assertEqual(
211+ 'global-notification-visible private',
212+ formatter.global_css())
213
214 class TestPillarFormatterAPI(TestCaseWithFactory):
215
216
217=== modified file 'lib/lp/app/doc/tales.txt'
218--- lib/lp/app/doc/tales.txt 2012-04-10 14:01:17 +0000
219+++ lib/lp/app/doc/tales.txt 2012-05-22 22:30:27 +0000
220@@ -1533,7 +1533,7 @@
221 not need to implement IPrivacy.
222
223 >>> thing = object()
224- >>> print test_tales('context/fmt:public-private-css', context=thing)
225+ >>> print test_tales('context/fmt:global-css', context=thing)
226 public
227
228 The CSS class honors the state of the object's privacy if the object
229@@ -1544,7 +1544,7 @@
230 >>> print bug.private
231 False
232
233- >>> print test_tales('context/fmt:public-private-css', context=bug)
234+ >>> print test_tales('context/fmt:global-css', context=bug)
235 public
236
237 If the private attribute is True, the class is 'private'.
238@@ -1554,8 +1554,8 @@
239 >>> bug.setPrivate(True, owner)
240 True
241
242- >>> print test_tales('context/fmt:public-private-css', context=bug)
243- private
244+ >>> print test_tales('context/fmt:global-css', context=bug)
245+ global-notification-visible private
246
247 >>> login(ANONYMOUS)
248
249
250=== modified file 'lib/lp/app/javascript/banners/banner.js'
251--- lib/lp/app/javascript/banners/banner.js 2012-05-15 17:17:34 +0000
252+++ lib/lp/app/javascript/banners/banner.js 2012-05-22 22:30:27 +0000
253@@ -23,7 +23,6 @@
254 slide_out: 0.2,
255 }
256 }
257-
258 return anim_times;
259 },
260
261@@ -98,7 +97,6 @@
262 var banner_data = {
263 badge: this.get('banner_icon'),
264 text: this.get('notification_text'),
265- extra_classes: this.get('css_class'),
266 banner_id: this.get('banner_id')
267 };
268 return Y.lp.mustache.to_html(
269@@ -117,7 +115,7 @@
270 this._updateBanner(existing_banner);
271 } else {
272 var banner_html = this._createBanner();
273- this.get('target').append(banner_html);
274+ this.get('contentBox').append(banner_html);
275 }
276 },
277
278@@ -135,17 +133,11 @@
279 banner_id: { value: "base-banner" },
280 notification_text: { value: "" },
281 banner_icon: { value: "<span></span>" },
282- target: {
283- valueFn: function() {
284- return Y.one('#maincontent');
285- }
286- },
287 banner_template: {
288 valueFn: function() {
289 return [
290 '<div id="{{ banner_id }}"',
291- 'class="global-notification transparent hidden',
292- ' {{ extra_classes }}">',
293+ 'class="global-notification transparent hidden">',
294 '{{{ badge }}}',
295 '<span class="banner-text">{{ text }}</span>',
296 "</div>"].join('')
297@@ -153,7 +145,6 @@
298 },
299 skip_animation: { value: false },
300 visible: { value: false },
301- css_class: { value: '' }
302 }
303 });
304
305
306=== modified file 'lib/lp/app/javascript/banners/beta-notification.js'
307--- lib/lp/app/javascript/banners/beta-notification.js 2012-05-14 22:07:29 +0000
308+++ lib/lp/app/javascript/banners/beta-notification.js 2012-05-22 22:30:27 +0000
309@@ -19,11 +19,11 @@
310
311 renderUI: function () {
312 baseBanner.prototype.renderUI.apply(this, arguments);
313- var banner_node = Y.one('.beta-banner');
314+ var beta_node = Y.one('.global-notification');
315 var close_box = Y.Node.create(
316 '<a href="#" class="global-notification-close">Hide' +
317 '<span class="notification-close sprite" /></a>');
318- banner_node.appendChild(close_box);
319+ beta_node.appendChild(close_box);
320 },
321
322 bindUI: function () {
323@@ -45,8 +45,7 @@
324 valueFn: function() {
325 return [
326 '<div id="{{ banner_id }}"',
327- 'class="global-notification beta-banner ',
328- 'transparent hidden">',
329+ 'class="global-notification transparent hidden">',
330 '{{{ badge }}}',
331 '<span class="banner-text">',
332 '{{ text }}{{{ features }}}',
333
334=== modified file 'lib/lp/app/javascript/banners/privacy.js'
335--- lib/lp/app/javascript/banners/privacy.js 2012-05-15 17:17:34 +0000
336+++ lib/lp/app/javascript/banners/privacy.js 2012-05-22 22:30:27 +0000
337@@ -3,15 +3,7 @@
338 var ns = Y.namespace('lp.app.banner.privacy');
339 var baseBanner = Y.lp.app.banner.Banner;
340
341-ns.PrivacyBanner = Y.Base.create('privacyBanner', baseBanner, [], {
342-
343- renderUI: function () {
344- var body = Y.one('body');
345- body.addClass('feature-flag-bugs-private-notification-enabled');
346- baseBanner.prototype.renderUI.apply(this, arguments);
347- }
348-
349-}, {
350+ns.PrivacyBanner = Y.Base.create('privacyBanner', baseBanner, [], {}, {
351 ATTRS: {
352 banner_id: { value: "privacy-banner" },
353 notification_text: {
354@@ -20,7 +12,6 @@
355 banner_icon: {
356 value: '<span class="sprite notification-private"></span>'
357 },
358- css_class: { value: 'privacy-banner' }
359 }
360 });
361
362
363=== modified file 'lib/lp/app/javascript/banners/tests/test_banner.js'
364--- lib/lp/app/javascript/banners/tests/test_banner.js 2012-05-14 19:01:38 +0000
365+++ lib/lp/app/javascript/banners/tests/test_banner.js 2012-05-22 22:30:27 +0000
366@@ -17,8 +17,7 @@
367 },
368
369 tearDown: function () {
370- var main = Y.one('#maincontent');
371- main.remove(true);
372+ Y.one('body').get('children').remove(true);
373 },
374
375 test_library_exists: function () {
376
377=== modified file 'lib/lp/app/javascript/banners/tests/test_beta_notification.js'
378--- lib/lp/app/javascript/banners/tests/test_beta_notification.js 2012-05-15 15:16:39 +0000
379+++ lib/lp/app/javascript/banners/tests/test_beta_notification.js 2012-05-22 22:30:27 +0000
380@@ -24,8 +24,7 @@
381 },
382
383 tearDown: function () {
384- var main = Y.one('#maincontent');
385- main.remove(true);
386+ Y.one('body').get('children').remove(true);
387 },
388
389 test_library_exists: function () {
390@@ -103,7 +102,7 @@
391 url: 'http://example.org'
392 }};
393 Y.lp.app.banner.beta.show_beta_if_needed();
394- Y.Assert.isNull(Y.one('.beta-banner'));
395+ Y.Assert.isNull(Y.one('.global-notification'));
396 },
397
398 test_hide_beta_banner: function() {
399@@ -118,7 +117,7 @@
400 betabanner.render();
401 betabanner.show();
402 var body = Y.one('body');
403- var banner = Y.one('.beta-banner');
404+ var banner = Y.one('.global-notification');
405 Y.Assert.isFalse(banner.hasClass('hidden'));
406
407 // Even with animation times set to 0, this test needs a slight
408
409=== modified file 'lib/lp/app/javascript/banners/tests/test_privacy.js'
410--- lib/lp/app/javascript/banners/tests/test_privacy.js 2012-05-15 14:52:16 +0000
411+++ lib/lp/app/javascript/banners/tests/test_privacy.js 2012-05-22 22:30:27 +0000
412@@ -19,8 +19,7 @@
413 },
414
415 tearDown: function () {
416- var main = Y.one('#maincontent');
417- main.remove(true);
418+ Y.one('body').get('children').remove(true);
419 },
420
421 test_library_exists: function () {
422@@ -37,15 +36,6 @@
423 '<span class="sprite notification-private"></span>',
424 banner.get('banner_icon'));
425 },
426-
427- test_render: function () {
428- var banner = new Y.lp.app.banner.privacy.PrivacyBanner();
429- banner.render();
430-
431- var body = Y.one('body');
432- var exp_class = 'feature-flag-bugs-private-notification-enabled';
433- Y.Assert.isTrue(body.hasClass(exp_class));
434- }
435 }));
436
437 }, '0.1', {'requires': ['test', 'console', 'lp.app.banner.privacy']});
438
439=== added file 'lib/lp/app/templates/banner-macros.pt'
440--- lib/lp/app/templates/banner-macros.pt 1970-01-01 00:00:00 +0000
441+++ lib/lp/app/templates/banner-macros.pt 2012-05-22 22:30:27 +0000
442@@ -0,0 +1,43 @@
443+<macros
444+ xmlns="http://www.w3.org/1999/xhtml"
445+ xmlns:tal="http://xml.zope.org/namespaces/tal"
446+ xmlns:metal="http://xml.zope.org/namespaces/metal"
447+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
448+ i18n:domain="launchpad"
449+ tal:omit-tag=""
450+>
451+
452+<metal:privacy define-macro="privacy-banner">
453+ <tal:show-banner condition="view/private">
454+ <div class="yui3-privacybanner-content">
455+ <div id="privacy-banner" class="global-notification">
456+ <span class="sprite notification-private"></span>
457+ <span class="banner-text">The information on this page is private.</span>
458+ </div>
459+ </div>
460+ </tal:show-banner>
461+</metal:privacy>
462+
463+<metal:beta define-macro="beta-banner">
464+<tal:show-banner condition="view/beta_features">
465+<div class="yui3-betabanner-content">
466+ <div id="beta-banner" class="global-notification">
467+ <span class="beta-warning">BETA!</span>
468+ <span class="banner-text">
469+ Some parts of this page are in beta:&nbsp;
470+ <span class="beta-feature">
471+ <tal:features
472+ repeat="feature view/beta_features">
473+ <tal:feature replace="feature/title" />
474+ <tal:link condition="feature/url">
475+ (<a tal:attributes="href feature/url" class="info-link">read more</a>)
476+ </tal:link>
477+ </tal:features>
478+ <span>
479+ </span>
480+ </div>
481+</div>
482+</tal:show-banner>
483+</metal:beta>
484+
485+</macros>
486
487=== modified file 'lib/lp/app/templates/base-layout.pt'
488--- lib/lp/app/templates/base-layout.pt 2012-04-12 19:42:35 +0000
489+++ lib/lp/app/templates/base-layout.pt 2012-05-22 22:30:27 +0000
490@@ -62,7 +62,7 @@
491 itemtype="http://schema.org/WebPage"
492 tal:attributes="class string:tab-${view/menu:selectedfacetname}
493 ${view/macro:pagetype}
494- ${view/fmt:public-private-css}
495+ ${view/fmt:global-css}
496 yui3-skin-sam">
497 <script type="text/javascript"
498 tal:condition="python: is_lpnet">
499@@ -97,6 +97,10 @@
500 <div class="yui-t4"
501 tal:omit-tag="not: view/macro:pagehas/portlets">
502 <div id="maincontent" class="yui-main">
503+ <metal:privacy-banner
504+ use-macro="view/@@+banner-macros/beta-banner"/>
505+ <metal:privacy-banner
506+ use-macro="view/@@+banner-macros/privacy-banner"/>
507 <div class="yui-b"
508 tal:attributes="
509 lang view/lang|default_language|default;
510
511=== modified file 'lib/lp/services/webapp/publisher.py'
512--- lib/lp/services/webapp/publisher.py 2012-04-26 16:21:46 +0000
513+++ lib/lp/services/webapp/publisher.py 2012-05-22 22:30:27 +0000
514@@ -65,6 +65,7 @@
515 from zope.traversing.browser.interfaces import IAbsoluteURL
516
517 from lp.app.errors import NotFoundError
518+from lp.app.interfaces.launchpad import IPrivacy
519 from lp.app.versioninfo import revno
520 from lp.layers import (
521 LaunchpadLayer,
522@@ -286,9 +287,19 @@
523 many templates not set via zcml, or you want to do
524 rendering from Python.
525 - publishTraverse() <-- override this to support traversing-through.
526+ - private <-- used to indicate if the view contains private data.
527+ override this if the view has special privacy needs
528+ (i.e. context doesn't properly indicate privacy).
529 """
530
531- private = False
532+ @property
533+ def private(self):
534+ """A view is private if its context is."""
535+ privacy = IPrivacy(self.context, None)
536+ if privacy is not None:
537+ return privacy.private
538+ else:
539+ return False
540
541 def __init__(self, context, request):
542 self.context = context
543@@ -299,6 +310,16 @@
544 # IJSONRequestCache adapter.
545 if isinstance(request, FakeRequest):
546 return
547+ # Several view objects may be created for one page request:
548+ # One view for the main context and template, and other views
549+ # for macros included in the main template.
550+ cache = self._get_json_cache()
551+ if cache is None:
552+ return
553+ related_features = cache.setdefault('related_features', {})
554+ related_features.update(self.related_feature_info)
555+
556+ def _get_json_cache(self):
557 # Some tests create views without providing any request
558 # object at all; other tests run without the component
559 # infrastructure.
560@@ -306,12 +327,16 @@
561 cache = IJSONRequestCache(self.request).objects
562 except TypeError, error:
563 if error.args[0] == 'Could not adapt':
564- return
565- # Several view objects may be created for one page request:
566- # One view for the main context and template, and other views
567- # for macros included in the main template.
568- related_features = cache.setdefault('related_features', {})
569- related_features.update(self.related_feature_info)
570+ cache = None
571+ return cache
572+
573+ @property
574+ def beta_features(self):
575+ cache = self._get_json_cache()
576+ if cache is None:
577+ return []
578+ related_features = cache.setdefault('related_features', {}).values()
579+ return [f for f in related_features if f['is_beta']]
580
581 def initialize(self):
582 """Override this in subclasses.
583
584=== modified file 'lib/lp/services/webapp/tests/test_publisher.py'
585--- lib/lp/services/webapp/tests/test_publisher.py 2012-01-01 02:58:52 +0000
586+++ lib/lp/services/webapp/tests/test_publisher.py 2012-05-22 22:30:27 +0000
587@@ -13,7 +13,9 @@
588 from lazr.restful.interfaces import IJSONRequestCache
589 import simplejson
590 from zope.component import getUtility
591+from zope.interface import implements
592
593+from lp.app.interfaces.launchpad import IPrivacy
594 from lp.services.features.flags import flag_info
595 from lp.services.features.testing import FeatureFixture
596 from lp.services.webapp import publisher
597@@ -295,6 +297,53 @@
598 # Or when no request at all is passed.
599 LaunchpadView(object(), None)
600
601+ def test_view_privacy(self):
602+ # View privacy is based on the context.
603+ class PrivateObject(object):
604+ implements(IPrivacy)
605+
606+ def __init__(self, private):
607+ self.private = private
608+
609+ view = LaunchpadView(PrivateObject(True), FakeRequest())
610+ self.assertTrue(view.private)
611+
612+ view = LaunchpadView(PrivateObject(False), FakeRequest())
613+ self.assertFalse(view.private)
614+
615+ def test_view_beta_features_simple(self):
616+ class TestView(LaunchpadView):
617+ related_features = ['test_feature']
618+
619+ self.useFixture(FeatureFixture(
620+ {}, self.makeFeatureFlagDictionaries(u'', u'on'),
621+ override_scope_lookup=lambda scope_name: True))
622+ request = LaunchpadTestRequest()
623+ view = TestView(object(), request)
624+ expected_beta_features = [{
625+ 'url': 'http://wiki.lp.dev/LEP/sample', 'is_beta': True,
626+ 'value': u'on', 'title': 'title'}]
627+ self.assertEqual(expected_beta_features, view.beta_features)
628+
629+ def test_view_beta_features_mixed(self):
630+ # With multiple related features, only those in a beta condition are
631+ # reported as beta features.
632+ class TestView(LaunchpadView):
633+ related_features = ['test_feature', 'test_feature2']
634+
635+ # Select one flag on 'default', one flag not on 'default. 'default'
636+ # setting determines whether flags correspond to 'beta' features.
637+ raw_flag_dicts = self.makeFeatureFlagDictionaries(u'', u'on')
638+ flag_dicts = [raw_flag_dicts[1], raw_flag_dicts[2]]
639+
640+ self.useFixture(FeatureFixture(
641+ {}, flag_dicts, override_scope_lookup=lambda scope_name: True))
642+ request = LaunchpadTestRequest()
643+ view = TestView(object(), request)
644+ expected_beta_features = [{
645+ 'url': 'http://wiki.lp.dev/LEP/sample', 'is_beta': True,
646+ 'value': u'on', 'title': 'title'}]
647+ self.assertEqual(expected_beta_features, view.beta_features)
648
649 def test_suite():
650 suite = TestSuite()