Merge lp:~abentley/launchpad/view-flags into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 14333
Proposed branch: lp:~abentley/launchpad/view-flags
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/next-prev
Diff against target: 495 lines (+159/-117)
7 files modified
lib/canonical/launchpad/webapp/publisher.py (+25/-25)
lib/canonical/launchpad/webapp/tests/test_publisher.py (+87/-63)
lib/canonical/launchpad/webapp/tests/test_view_model.py (+3/-3)
lib/lp/app/javascript/beta-notification.js (+11/-16)
lib/lp/app/javascript/tests/test_beta_notification.html (+2/-0)
lib/lp/app/javascript/tests/test_beta_notification.js (+30/-9)
lib/lp/bugs/browser/bugtask.py (+1/-1)
To merge this branch: bzr merge lp:~abentley/launchpad/view-flags
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+82570@code.launchpad.net

Commit message

Provide access to feature flags in JavaScript.

Description of the change

= Summary =
Make feature flags available to JavaScript

== Proposed fix ==
Tweak the existing beta_features mechanism so that it reports about "related_features", rather than beta features. The beta status is reflected by a boolean, and the current value is reported.

== Pre-implementation notes ==
Discussed with abel

== Implementation details ==
Changed the JSON output to a dict so that it is more legible, and easier to add value and beta to. Consolidated all functionality into a single for-loop to simplify the code and avoid re-retrieving data.

Tweaked beta banner generation so that the banner is shown if any notifications were generated, rather than if there are any entries (because now there can be non-beta entries.)

== Tests ==
bin/test -t test_publisher -t test_view_name

== Demo and Q/A ==
Ensure dynamic bug listings are enabled for you. Go to a +bugs page. You should see the beta banner. View source. You should see that the value of 'bugs.dynamic_bug_listings.enabled' is 'on'.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/beta-notification.js
  lib/lp/app/javascript/tests/test_beta_notification.js
  lib/canonical/launchpad/webapp/publisher.py
  lib/lp/bugs/javascript/buglisting.js
  lib/canonical/launchpad/webapp/tests/test_publisher.py
  lib/canonical/launchpad/webapp/tests/test_view_model.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/javascript/tests/test_buglisting.js

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

[1]

+ var chunks = ['<span class="beta-feature"> ', info.title];
+ if (info.url.length > 0) {
+ chunks.push(' (<a href="', info.url, '" class="info-link">',
+ 'read more</a>)');
+ }
+ chunks.push('</span>');
+ notifications.push(Y.Node.create(chunks.join('')));

This is XSS territory. This must be done something like:

  var node = Y.Node.create(
      '<span class="beta-feature"><a class="info-link"></a></span>');
  node.set("text", info.title);
  node.one("a").set("href", info.url);

Obviously that's not the whole story, but string concatenation is
going to get you in a lot of trouble.

... Review in progress ...

Revision history for this message
Gavin Panella (allenap) wrote :

[2]

+ LP.cache.related_features = {
+ related_features: {
+ is_beta: false,
+ title: 'Non-beta feature',
+ url: 'http://example.org'
+ }};

Having a feature called related_features is a bit confusing.

Nice change, r=me.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Actually, on second thoughts, Needs Fixing for [1].

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

+ var notifications = Mustache.to_html([

Hehe, you rule :)

Also, I later realised that you had just moved the code, not created
it, so sorry if I was telling you things you already know.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/webapp/publisher.py'
2--- lib/canonical/launchpad/webapp/publisher.py 2011-11-08 20:32:38 +0000
3+++ lib/canonical/launchpad/webapp/publisher.py 2011-11-18 18:04:13 +0000
4@@ -80,7 +80,6 @@
5 from lp.app.errors import NotFoundError
6 from lp.services.encoding import is_ascii_only
7 from lp.services.features import (
8- currentScope,
9 defaultFlagValue,
10 getFeatureFlag,
11 )
12@@ -278,13 +277,8 @@
13 # Several view objects may be created for one page request:
14 # One view for the main context and template, and other views
15 # for macros included in the main template.
16- if 'beta_features' not in cache:
17- cache['beta_features'] = self.active_beta_features
18- else:
19- beta_info = cache['beta_features']
20- for feature in self.active_beta_features:
21- if feature not in beta_info:
22- beta_info.append(feature)
23+ related_features = cache.setdefault('related_features', {})
24+ related_features.update(self.related_feature_info)
25
26 def initialize(self):
27 """Override this in subclasses.
28@@ -390,29 +384,35 @@
29 # Those that can be must override this method.
30 raise NotFound(self, name, request=request)
31
32- # Flags for new features in beta status which affect a view.
33- beta_features = ()
34+ # Names of feature flags which affect a view.
35+ related_features = ()
36
37 @property
38- def active_beta_features(self):
39- """Beta feature flags that are active for this context and scope.
40-
41- This property consists of all feature flags from beta_features
42- whose current value is not the default value.
43+ def related_feature_info(self):
44+ """Related feature flags that are active for this context and scope.
45+
46+ This property describes all features marked as related_features in the
47+ view. is_beta means that the value is not the default value.
48+
49+ Return a dict of flags keyed by flag_name, with title and url as given
50+ by the flag's description. Value is the value in the current scope,
51+ and is_beta is true if this is not the default value.
52 """
53 # Avoid circular imports.
54 from lp.services.features.flags import flag_info
55
56- def flag_in_beta_status(flag):
57- return (
58- currentScope(flag) not in ('default', None) and
59- defaultFlagValue(flag) != getFeatureFlag(flag))
60-
61- active_beta_flags = set(
62- flag for flag in self.beta_features if flag_in_beta_status(flag))
63- beta_info = [
64- info for info in flag_info if info[0] in active_beta_flags
65- ]
66+ beta_info = {}
67+ for (flag_name, value_domain, documentation, default_behavior, title,
68+ url) in flag_info:
69+ if flag_name not in self.related_features:
70+ continue
71+ value = getFeatureFlag(flag_name)
72+ beta_info[flag_name] = {
73+ 'is_beta': (defaultFlagValue(flag_name) != value),
74+ 'title': title,
75+ 'url': url,
76+ 'value': value,
77+ }
78 return beta_info
79
80
81
82=== modified file 'lib/canonical/launchpad/webapp/tests/test_publisher.py'
83--- lib/canonical/launchpad/webapp/tests/test_publisher.py 2011-11-08 20:32:38 +0000
84+++ lib/canonical/launchpad/webapp/tests/test_publisher.py 2011-11-18 18:04:13 +0000
85@@ -49,7 +49,7 @@
86
87 def test_getCacheJSON_non_resource_context(self):
88 view = LaunchpadView(object(), LaunchpadTestRequest())
89- self.assertEqual('{"beta_features": []}', view.getCacheJSON())
90+ self.assertEqual('{"related_features": {}}', view.getCacheJSON())
91
92 @staticmethod
93 def getCanada():
94@@ -77,7 +77,8 @@
95 IJSONRequestCache(request).objects['my_bool'] = True
96 with person_logged_in(self.factory.makePerson()):
97 self.assertEqual(
98- '{"beta_features": [], "my_bool": true}', view.getCacheJSON())
99+ '{"related_features": {}, "my_bool": true}',
100+ view.getCacheJSON())
101
102 def test_getCacheJSON_resource_object(self):
103 request = LaunchpadTestRequest()
104@@ -112,25 +113,30 @@
105 self.assertIs(None, view.user)
106 self.assertNotIn('user@domain', view.getCacheJSON())
107
108- def test_active_beta_features__default(self):
109- # By default, LaunchpadView.active_beta_features is empty.
110- request = LaunchpadTestRequest()
111- view = LaunchpadView(object(), request)
112- self.assertEqual(0, len(view.active_beta_features))
113-
114- def test_active_beta_features__with_beta_feature_nothing_enabled(self):
115- # If a view has a non-empty sequence of beta feature flags but if
116- # no matching feature rules are defined, its property
117- # active_beta_features is empty.
118- request = LaunchpadTestRequest()
119- view = LaunchpadView(object(), request)
120- view.beta_features = ['test_feature']
121- self.assertEqual(0, len(view.active_beta_features))
122-
123- def test_active_beta_features__default_scope_only(self):
124- # If a view has a non-empty sequence of beta feature flags but if
125- # only a default scope is defined, its property
126- # active_beta_features is empty.
127+ def test_related_feature_info__default(self):
128+ # By default, LaunchpadView.related_feature_info is empty.
129+ request = LaunchpadTestRequest()
130+ view = LaunchpadView(object(), request)
131+ self.assertEqual(0, len(view.related_feature_info))
132+
133+ def test_related_feature_info__with_related_feature_nothing_enabled(self):
134+ # If a view has a non-empty sequence of related feature flags but if
135+ # no matching feature rules are defined, is_beta is False.
136+ request = LaunchpadTestRequest()
137+ view = LaunchpadView(object(), request)
138+ view.related_features = ['test_feature']
139+ self.assertEqual({
140+ 'test_feature': {
141+ 'is_beta': False,
142+ 'title': 'title',
143+ 'url': 'http://wiki.lp.dev/LEP/sample',
144+ 'value': None,
145+ }
146+ }, view.related_feature_info)
147+
148+ def test_related_feature_info__default_scope_only(self):
149+ # If a view has a non-empty sequence of related feature flags but if
150+ # only a default scope is defined, it is not considered beta.
151 self.useFixture(FeatureFixture(
152 {},
153 (
154@@ -143,13 +149,18 @@
155 )))
156 request = LaunchpadTestRequest()
157 view = LaunchpadView(object(), request)
158- view.beta_features = ['test_feature']
159- self.assertEqual([], view.active_beta_features)
160+ view.related_features = ['test_feature']
161+ self.assertEqual({'test_feature': {
162+ 'is_beta': False,
163+ 'title': 'title',
164+ 'url': 'http://wiki.lp.dev/LEP/sample',
165+ 'value': 'on',
166+ }}, view.related_feature_info)
167
168- def test_active_beta_features__enabled_feature(self):
169- # If a view has a non-empty sequence of beta feature flags and if
170+ def test_active_related_features__enabled_feature(self):
171+ # If a view has a non-empty sequence of related feature flags and if
172 # only a non-default scope is defined and active, the property
173- # active_beta_features contains this feature flag.
174+ # active_related_features contains this feature flag.
175 self.useFixture(FeatureFixture(
176 {},
177 (
178@@ -162,11 +173,15 @@
179 )))
180 request = LaunchpadTestRequest()
181 view = LaunchpadView(object(), request)
182- view.beta_features = ['test_feature']
183- self.assertEqual(
184- [('test_feature', 'boolean', 'documentation', 'default_value_1',
185- 'title', 'http://wiki.lp.dev/LEP/sample')],
186- view.active_beta_features)
187+ view.related_features = ['test_feature']
188+ self.assertEqual({
189+ 'test_feature': {
190+ 'is_beta': True,
191+ 'title': 'title',
192+ 'url': 'http://wiki.lp.dev/LEP/sample',
193+ 'value': 'on'}
194+ },
195+ view.related_feature_info)
196
197 def makeFeatureFlagDictionaries(self, default_value, scope_value):
198 # Return two dictionaries describing a feature for each test feature.
199@@ -185,42 +200,49 @@
200 makeFeatureDict('test_feature_2', default_value, u'default', 0),
201 makeFeatureDict('test_feature_2', scope_value, u'pageid:bar', 10))
202
203- def test_active_beta_features__enabled_feature_with_default(self):
204+ def test_related_features__enabled_feature_with_default(self):
205 # If a view
206- # * has a non-empty sequence of beta feature flags,
207+ # * has a non-empty sequence of related feature flags,
208 # * the default scope and a non-default scope are defined
209 # but have different values,
210- # then the property active_beta_features contains this feature flag.
211+ # then the property related_feature_info contains this feature flag.
212 self.useFixture(FeatureFixture(
213 {}, self.makeFeatureFlagDictionaries(u'', u'on')))
214 request = LaunchpadTestRequest()
215 view = LaunchpadView(object(), request)
216- view.beta_features = ['test_feature']
217- self.assertEqual(
218-
219- [('test_feature', 'boolean', 'documentation', 'default_value_1',
220- 'title', 'http://wiki.lp.dev/LEP/sample')],
221- view.active_beta_features)
222-
223- def test_active_beta_features__enabled_feature_with_default_same_value(
224+ view.related_features = ['test_feature']
225+ self.assertEqual({
226+ 'test_feature': {
227+ 'is_beta': True,
228+ 'title': 'title',
229+ 'url': 'http://wiki.lp.dev/LEP/sample',
230+ 'value': 'on',
231+ }},
232+ view.related_feature_info)
233+
234+ def test_related_feature_info__enabled_feature_with_default_same_value(
235 self):
236 # If a view
237- # * has a non-empty sequence of beta feature flags,
238+ # * has a non-empty sequence of related feature flags,
239 # * the default scope and a non-default scope are defined
240 # and have the same values,
241- # then the property active_beta_features does not contain this
242- # feature flag.
243+ # then is_beta is false.
244 self.useFixture(FeatureFixture(
245 {}, self.makeFeatureFlagDictionaries(u'on', u'on')))
246 request = LaunchpadTestRequest()
247 view = LaunchpadView(object(), request)
248- view.beta_features = ['test_feature']
249- self.assertEqual([], view.active_beta_features)
250+ view.related_features = ['test_feature']
251+ self.assertEqual({'test_feature': {
252+ 'is_beta': False,
253+ 'title': 'title',
254+ 'url': 'http://wiki.lp.dev/LEP/sample',
255+ 'value': 'on',
256+ }}, view.related_feature_info)
257
258- def test_json_cache_has_beta_features(self):
259- # The property beta_features is copied into the JSON cache.
260+ def test_json_cache_has_related_features(self):
261+ # The property related_features is copied into the JSON cache.
262 class TestView(LaunchpadView):
263- beta_features = ['test_feature']
264+ related_features = ['test_feature']
265
266 self.useFixture(FeatureFixture(
267 {}, self.makeFeatureFlagDictionaries(u'', u'on')))
268@@ -228,20 +250,23 @@
269 view = TestView(object(), request)
270 with person_logged_in(self.factory.makePerson()):
271 self.assertEqual(
272- '{"beta_features": [["test_feature", "boolean", '
273- '"documentation", "default_value_1", "title", '
274- '"http://wiki.lp.dev/LEP/sample"]]}',
275+ '{"related_features": {"test_feature": {'
276+ '"url": "http://wiki.lp.dev/LEP/sample", '
277+ '"is_beta": true, '
278+ '"value": "on", '
279+ '"title": "title"'
280+ '}}}',
281 view.getCacheJSON())
282
283- def test_json_cache_collects_beta_features_from_all_views(self):
284+ def test_json_cache_collects_related_features_from_all_views(self):
285 # A typical page includes data from more than one view,
286- # for example, from macros. Beta features from these sub-views
287+ # for example, from macros. Related features from these sub-views
288 # are included in the JSON cache.
289 class TestView(LaunchpadView):
290- beta_features = ['test_feature']
291+ related_features = ['test_feature']
292
293 class TestView2(LaunchpadView):
294- beta_features = ['test_feature_2']
295+ related_features = ['test_feature_2']
296
297 self.useFixture(FeatureFixture(
298 {}, self.makeFeatureFlagDictionaries(u'', u'on')))
299@@ -250,12 +275,11 @@
300 TestView2(object(), request)
301 with person_logged_in(self.factory.makePerson()):
302 self.assertEqual(
303- '{"beta_features": [["test_feature", "boolean", '
304- '"documentation", "default_value_1", "title", '
305- '"http://wiki.lp.dev/LEP/sample"], '
306- '["test_feature_2", "boolean", "documentation", '
307- '"default_value_2", "title", '
308- '"http://wiki.lp.dev/LEP/sample2"]]}',
309+ '{"related_features": '
310+ '{"test_feature_2": {"url": "http://wiki.lp.dev/LEP/sample2",'
311+ ' "is_beta": true, "value": "on", "title": "title"}, '
312+ '"test_feature": {"url": "http://wiki.lp.dev/LEP/sample", '
313+ '"is_beta": true, "value": "on", "title": "title"}}}',
314 view.getCacheJSON())
315
316 def test_view_creation_with_fake_or_none_request(self):
317
318=== modified file 'lib/canonical/launchpad/webapp/tests/test_view_model.py'
319--- lib/canonical/launchpad/webapp/tests/test_view_model.py 2011-11-04 11:10:23 +0000
320+++ lib/canonical/launchpad/webapp/tests/test_view_model.py 2011-11-18 18:04:13 +0000
321@@ -123,7 +123,7 @@
322 self.configZCML()
323 browser = self.getUserBrowser(self.url)
324 cache = loads(browser.contents)
325- self.assertEqual(['beta_features', 'context'], cache.keys())
326+ self.assertEqual(['related_features', 'context'], cache.keys())
327
328 def test_JsonModel_custom_cache(self):
329 # Adding an item to the cache in the initialize method results in it
330@@ -141,7 +141,7 @@
331 browser = self.getUserBrowser(self.url)
332 cache = loads(browser.contents)
333 self.assertThat(
334- cache, KeysEqual('beta_features', 'context', 'target_info'))
335+ cache, KeysEqual('related_features', 'context', 'target_info'))
336
337 def test_JsonModel_custom_cache_wrong_method(self):
338 # Adding an item to the cache in some other method is not recognized,
339@@ -166,4 +166,4 @@
340 browser = self.getUserBrowser(self.url)
341 cache = loads(browser.contents)
342 self.assertThat(
343- cache, KeysEqual('beta_features', 'context', 'target_info'))
344+ cache, KeysEqual('related_features', 'context', 'target_info'))
345
346=== modified file 'lib/lp/app/javascript/beta-notification.js'
347--- lib/lp/app/javascript/beta-notification.js 2011-11-10 15:16:02 +0000
348+++ lib/lp/app/javascript/beta-notification.js 2011-11-18 18:04:13 +0000
349@@ -18,11 +18,18 @@
350 * This should be called after the page has loaded e.g. on 'domready'.
351 */
352 function display_beta_notification() {
353- if (LP.cache.beta_features.length === 0) {
354+ var notifications = Mustache.to_html([
355+ '{{#features}}{{#is_beta}}',
356+ '<span class="beta-feature"> {{title}}',
357+ '{{#url}}',
358+ ' (<a href="{{url}}" class="info-link">read more</a>)',
359+ '{{/url}}',
360+ '</span>',
361+ '{{/is_beta}}{{/features}}'].join(''),
362+ {features: Y.Object.values(LP.cache.related_features)});
363+ if (notifications.length === 0) {
364 return;
365 }
366-
367- var beta_features = LP.cache.beta_features;
368 var body = Y.one('body');
369 body.addClass('global-notification-visible');
370 var main = Y.one('#maincontent');
371@@ -37,19 +44,7 @@
372 '<span class="notification-close sprite" /></a>');
373 beta_notification_node.appendChild(close_box);
374 beta_notification_node.append('Some parts of this page are in beta: ');
375- var index;
376- for (index = 0; index < beta_features.length; index++) {
377- var feature_name = beta_features[index][4];
378- var info_link = beta_features[index][5];
379- if (info_link.length > 0) {
380- info_link =
381- ' (<a href="' + info_link + '" class="info-link">' +
382- 'read more</a>)';
383- }
384- beta_notification_node.appendChild(Y.Node.create(
385- '<span class="beta-feature"> ' + feature_name + info_link +
386- '</span>'));
387- }
388+ beta_notification_node.append(notifications);
389 close_box.on('click', function(e) {
390 e.halt();
391 var fade_out = new Y.Anim({
392
393=== modified file 'lib/lp/app/javascript/tests/test_beta_notification.html'
394--- lib/lp/app/javascript/tests/test_beta_notification.html 2011-11-10 10:32:25 +0000
395+++ lib/lp/app/javascript/tests/test_beta_notification.html 2011-11-18 18:04:13 +0000
396@@ -9,6 +9,8 @@
397 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
398 <script type="text/javascript"
399 src="../../../app/javascript/testing/testrunner.js"></script>
400+ <script type="text/javascript"
401+ src="../../../contrib/javascript/mustache.js"></script>
402
403 <!-- The module under test. -->
404 <script type="text/javascript" src="../beta-notification.js"></script>
405
406=== modified file 'lib/lp/app/javascript/tests/test_beta_notification.js'
407--- lib/lp/app/javascript/tests/test_beta_notification.js 2011-11-10 10:32:25 +0000
408+++ lib/lp/app/javascript/tests/test_beta_notification.js 2011-11-18 18:04:13 +0000
409@@ -40,8 +40,12 @@
410 },
411
412 test_beta_banner_one_beta_feature: function() {
413- LP.cache.beta_features = [
414- ['', '', '', '', 'A beta feature', 'http://lp.dev/LEP/one']];
415+ LP.cache.related_features = {
416+ '': {
417+ is_beta: true,
418+ title: 'A beta feature',
419+ url: 'http://lp.dev/LEP/one'
420+ }};
421 Y.lp.app.beta_features.display_beta_notification();
422
423 var body = Y.one('body');
424@@ -63,9 +67,17 @@
425 },
426
427 test_beta_banner_two_beta_features: function() {
428- LP.cache.beta_features = [
429- ['', '', '', '', 'Beta feature 1', 'http://lp.dev/LEP/one'],
430- ['', '', '', '', 'Beta feature 2', '']];
431+ LP.cache.related_features = {
432+ '1': {
433+ is_beta: true,
434+ title: 'Beta feature 1',
435+ url: 'http://lp.dev/LEP/one'
436+ },
437+ '2': {
438+ is_beta: true,
439+ title: 'Beta feature 2',
440+ url: ''
441+ }};
442 Y.lp.app.beta_features.display_beta_notification();
443
444 var body = Y.one('body');
445@@ -81,7 +93,7 @@
446 info_link = feature_info.get('children').item(0);
447 Y.Assert.areEqual('http://lp.dev/LEP/one', info_link.get('href'));
448
449- // If an entry in LP.cache.beta_features does not provide a
450+ // If an entry in LP.cache.related_features does not provide a
451 // "read more" link, the corrsponding node is not added.
452 feature_info = sub_nodes.filter('.beta-feature').item(1);
453 Y.Assert.areEqual(
454@@ -90,7 +102,12 @@
455 },
456
457 test_beta_banner_no_beta_features_defined: function() {
458- LP.cache.beta_features = [];
459+ LP.cache.related_features = {
460+ foo_feature: {
461+ is_beta: false,
462+ title: 'Non-beta feature',
463+ url: 'http://example.org'
464+ }};
465 Y.lp.app.beta_features.display_beta_notification();
466
467 var body = Y.one('body');
468@@ -100,8 +117,12 @@
469 },
470
471 test_hide_beta_banner: function() {
472- LP.cache.beta_features = [
473- ['', '', '', '', 'A beta feature', 'http://lp.dev/LEP/one']];
474+ LP.cache.related_features = {
475+ '': {
476+ is_beta: true,
477+ title: 'A beta feature',
478+ url: 'http://lp.dev/LEP/one'
479+ }};
480 Y.lp.app.beta_features.display_beta_notification();
481 var body = Y.one('body');
482 var banner = Y.one('.beta-banner');
483
484=== modified file 'lib/lp/bugs/browser/bugtask.py'
485--- lib/lp/bugs/browser/bugtask.py 2011-11-18 04:43:38 +0000
486+++ lib/lp/bugs/browser/bugtask.py 2011-11-18 18:04:13 +0000
487@@ -2455,7 +2455,7 @@
488
489 implements(IBugTaskSearchListingMenu)
490
491- beta_features = ['bugs.dynamic_bug_listings.enabled']
492+ related_features = ['bugs.dynamic_bug_listings.enabled']
493
494 # Only include <link> tags for bug feeds when using this view.
495 feed_types = (