Merge lp:~abentley/launchpad/blueprint-info-type-ui into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 15911
Proposed branch: lp:~abentley/launchpad/blueprint-info-type-ui
Merge into: lp:launchpad
Diff against target: 976 lines (+358/-240)
16 files modified
lib/lp/app/javascript/information_type.js (+191/-1)
lib/lp/app/javascript/tests/test_information_type.html (+4/-6)
lib/lp/app/javascript/tests/test_information_type.js (+21/-17)
lib/lp/blueprints/browser/configure.zcml (+3/-0)
lib/lp/blueprints/browser/specification.py (+22/-3)
lib/lp/blueprints/browser/tests/test_specification.py (+44/-2)
lib/lp/blueprints/interfaces/specification.py (+6/-0)
lib/lp/blueprints/model/specification.py (+8/-2)
lib/lp/blueprints/templates/blueprint-portlet-privacy.pt (+16/-0)
lib/lp/blueprints/templates/specification-index.pt (+12/-3)
lib/lp/blueprints/tests/test_specification.py (+9/-7)
lib/lp/bugs/javascript/bugtask_index.js (+6/-3)
lib/lp/bugs/javascript/information_type_choice.js (+0/-190)
lib/lp/services/features/flags.py (+6/-0)
lib/lp/testing/__init__.py (+6/-4)
lib/lp/testing/factory.py (+4/-2)
To merge this branch: bzr merge lp:~abentley/launchpad/blueprint-info-type-ui
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+122140@code.launchpad.net

Commit message

Initial UI for Specification.information_type.

Description of the change

= Summary =
Partial implementation of Specification.information_type UI

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
Generalize the implementation of the Bugs information_type UI.

== Tests ==
xvfb-run bin/test --layer=YUITestLayer

== Demo and Q/A ==
Change the the type of a bug to private. It should change, and you should be subscribed to the bug. Reload to ensure that the change stuck.

Go to a blueprint. You should see no information_type UI.

Set the 'blueprints.information_type.enabled' feature flag to "on".

Reload the blueprint. You should see the information_type chooser.

Attempt to change the information type. It should inform you you do not have the correct permission.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/templates/specification-index.pt
  lib/lp/app/javascript/information_type.js
  lib/lp/blueprints/templates/blueprint-portlet-privacy.pt
  lib/lp/testing/factory.py
  lib/lp/app/javascript/tests/test_information_type.js
  lib/lp/blueprints/model/specification.py
  lib/lp/bugs/javascript/bugtask_index.js
  lib/lp/services/features/flags.py
  lib/lp/blueprints/browser/configure.zcml
  lib/lp/blueprints/browser/specification.py
  lib/lp/blueprints/browser/tests/test_specification.py
  lib/lp/app/javascript/tests/test_information_type.html
  lib/lp/blueprints/interfaces/specification.py

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

This looks good to me. I would add two points to your QA:

1) You'll want to test transition back from private to non-private as well
2) You'll want to check the behavior on +filebug when you declare the bug private/security.

review: Approve
Revision history for this message
j.c.sackett (jcsackett) wrote :

Actually, I had a concern that this wasn't getting all the call sites for the bugs.information_type module, but I don't see it on the +filebug related pages anymore in devel; perhaps that area is safe from changes, or was updated already?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/information_type.js'
2--- lib/lp/app/javascript/information_type.js 2012-08-28 23:53:32 +0000
3+++ lib/lp/app/javascript/information_type.js 2012-09-04 14:00:36 +0000
4@@ -8,6 +8,87 @@
5
6 var namespace = Y.namespace('lp.app.information_type');
7
8+// For testing.
9+var skip_animation = false;
10+
11+/**
12+ * Save the new information type. If validate_change is true, then a check
13+ * will be done to ensure the bug will not become invisible. If the bug will
14+ * become invisible, a confirmation popup is used to confirm the user's
15+ * intention. Then this method is called again with validate_change set to
16+ * false to allow the change to proceed.
17+ *
18+ * @param widget
19+ * @param initial_value
20+ * @param value
21+ * @param lp_client
22+ * @param validate_change
23+ */
24+namespace.save_information_type = function(widget, initial_value, value,
25+ lp_client, context,
26+ subscribers_list, validate_change) {
27+ var error_handler = new Y.lp.client.FormErrorHandler();
28+ error_handler.showError = function(error_msg) {
29+ Y.lp.app.errors.display_error(
30+ Y.one('#information-type'), error_msg);
31+ };
32+ error_handler.handleError = function(ioId, response) {
33+ if( response.status === 400
34+ && response.statusText === 'Bug Visibility') {
35+ namespace._confirm_information_type_change(
36+ widget, initial_value, lp_client, context,
37+ subscribers_list);
38+ return true;
39+ }
40+ var orig_value = namespace.information_type_value_from_key(
41+ context.information_type, 'name', 'value');
42+ widget.set('value', orig_value);
43+ widget._showFailed();
44+ namespace.update_privacy_portlet(orig_value);
45+ return false;
46+ };
47+ var submit_url = document.URL + "/+secrecy";
48+ var qs = Y.lp.client.append_qs('', 'field.actions.change', 'Change');
49+ qs = Y.lp.client.append_qs(qs, 'field.information_type', value);
50+ qs = Y.lp.client.append_qs(
51+ qs, 'field.validate_change', validate_change?'on':'off');
52+ var config = {
53+ method: "POST",
54+ headers: {'Accept': 'application/xhtml;application/json'},
55+ data: qs,
56+ on: {
57+ start: function () {
58+ widget._uiSetWaiting();
59+ if (Y.Lang.isValue(subscribers_list)){
60+ subscribers_list.subscribers_list.startActivity(
61+ 'Updating subscribers...');
62+ }
63+ },
64+ end: function () {
65+ widget._uiClearWaiting();
66+ if (Y.Lang.isValue(subscribers_list)){
67+ subscribers_list.subscribers_list.stopActivity();
68+ }
69+ },
70+ success: function (id, response) {
71+ var result_data = null;
72+ if (response.responseText !== '' &&
73+ response.getResponseHeader('Content-Type') ===
74+ 'application/json')
75+ {
76+ result_data = Y.JSON.parse(response.responseText);
77+ }
78+ namespace.information_type_save_success(
79+ widget, context, value, subscribers_list, result_data);
80+ Y.lp.client.display_notifications(
81+ response.getResponseHeader('X-Lazr-Notifications'));
82+ },
83+ failure: error_handler.getFailureHandler()
84+ }
85+ };
86+ lp_client.io_provider.io(submit_url, config);
87+};
88+
89 var get_information_type_banner_text = function(value) {
90 var text_template = "This page contains {info_type} information.";
91 var info_type = namespace.information_type_value_from_key(
92@@ -15,6 +96,113 @@
93 return Y.Lang.sub(text_template, {'info_type': info_type});
94 };
95
96+namespace.information_type_save_success = function(widget, context, value,
97+ subscribers_list,
98+ subscribers_data) {
99+ context.information_type =
100+ namespace.information_type_value_from_key(
101+ value, 'value', 'name');
102+ namespace.update_privacy_banner(value);
103+ widget._showSucceeded();
104+ if (Y.Lang.isObject(subscribers_data)) {
105+ var subscribers = subscribers_data.subscription_data;
106+ subscribers_list._loadSubscribersFromList(subscribers);
107+ var cache_data = subscribers_data.cache_data;
108+ var item;
109+ for (item in cache_data) {
110+ if (cache_data.hasOwnProperty(item)) {
111+ LP.cache[item] = cache_data[item];
112+ }
113+ }
114+ }
115+ if (Y.Lang.isValue(subscribers_list)){
116+ var ns = Y.lp.bugs.bugtask_index.portlets.subscription;
117+ ns.update_subscription_status(skip_animation);
118+ }
119+};
120+
121+/**
122+ * Possibly prompt the user to confirm the change of information type.
123+ * If the old value is public, and the new value is private, we want to
124+ * confirm that the user really wants to make the change.
125+ *
126+ * @param widget
127+ * @param initial_value
128+ * @param lp_client
129+ * @private
130+ */
131+namespace._confirm_information_type_change = function(widget, initial_value,
132+ lp_client, context,
133+ subscribers_list) {
134+ var value = widget.get('value');
135+ var do_save = function() {
136+ namespace.update_privacy_portlet(value);
137+ namespace.save_information_type(
138+ widget, initial_value, value, lp_client, context, subscribers_list,
139+ false);
140+ };
141+ // Reset the widget back to it's original value so the user doesn't see it
142+ // change while the confirmation dialog is showing.
143+ var new_value = widget.get('value');
144+ widget.set('value', initial_value);
145+ namespace.update_privacy_portlet(initial_value);
146+ var confirm_text_template = [
147+ '<p class="block-sprite large-warning">',
148+ ' You are about to mark this bug as ',
149+ ' <strong>{information_type}</strong>.<br>',
150+ ' The bug will become invisible because there is no-one with',
151+ ' permissions to see {information_type} bugs.',
152+ '</p><p>',
153+ ' <strong>Please confirm you really want to do this.</strong>',
154+ '</p>'
155+ ].join('');
156+ var title = namespace.information_type_value_from_key(
157+ value, 'value', 'name');
158+ var confirm_text = Y.Lang.sub(confirm_text_template,
159+ {information_type: title});
160+ var co = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
161+ submit_fn: function() {
162+ widget.set('value', new_value);
163+ namespace.update_privacy_portlet(new_value);
164+ do_save();
165+ },
166+ form_content: confirm_text,
167+ headerContent: '<h2>Confirm information type change</h2>',
168+ submit_text: 'Confirm'
169+ });
170+ co.show();
171+};
172+
173+namespace.setup_information_type_choice = function(privacy_link, lp_client,
174+ context, subscribers_list,
175+ skip_anim) {
176+ skip_animation = skip_anim;
177+ var initial_value = namespace.information_type_value_from_key(
178+ context.information_type, 'name', 'value');
179+ var information_type_value = Y.one('#information-type');
180+ var information_type_edit = new Y.ChoiceSource({
181+ editicon: privacy_link,
182+ contentBox: Y.one('#privacy'),
183+ value_location: information_type_value,
184+ value: initial_value,
185+ title: "Change information type",
186+ items: LP.cache.information_type_data,
187+ backgroundColor: '#FFFF99',
188+ flashEnabled: false
189+ });
190+ Y.lp.app.choice.hook_up_choicesource_spinner(information_type_edit);
191+ information_type_edit.render();
192+ information_type_edit.on("save", function(e) {
193+ var value = information_type_edit.get('value');
194+ namespace.update_privacy_portlet(value);
195+ namespace.save_information_type(
196+ information_type_edit, initial_value, value, lp_client, context,
197+ subscribers_list, true);
198+ });
199+ privacy_link.addClass('js-action');
200+ return information_type_edit;
201+};
202+
203 /**
204 * Lookup the information_type property, keyed on the named value.
205 *
206@@ -80,4 +268,6 @@
207 }
208 };
209
210-}, "0.1", {"requires": ["base", "oop", "node", "lp.app.banner.privacy"]});
211+}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
212+ "lazr.choiceedit", "lp.bugs.bugtask_index",
213+ "lp.app.banner.privacy", "lp.app.choice"]});
214
215=== renamed file 'lib/lp/bugs/javascript/tests/test_information_type_choice.html' => 'lib/lp/app/javascript/tests/test_information_type.html'
216--- lib/lp/bugs/javascript/tests/test_information_type_choice.html 2012-08-29 14:02:57 +0000
217+++ lib/lp/app/javascript/tests/test_information_type.html 2012-09-04 14:00:36 +0000
218@@ -6,7 +6,7 @@
219
220 <html>
221 <head>
222- <title>lp.bugs.information_type_choice Tests</title>
223+ <title>lp.app.information_type Tests</title>
224
225 <!-- YUI and test setup -->
226 <script type="text/javascript"
227@@ -30,8 +30,6 @@
228 <script type="text/javascript"
229 src="../../../../../build/js/lp/app/choice.js"></script>
230 <script type="text/javascript"
231- src="../../../../../build/js/lp/app/information_type.js"></script>
232- <script type="text/javascript"
233 src="../../../../../build/js/lp/app/testing/mockio.js"></script>
234 <script type="text/javascript"
235 src="../../../../../build/js/lp/app/client.js"></script>
236@@ -79,18 +77,18 @@
237 src="../../../../../build/js/lp/bugs/bugtask_index.js"></script>
238
239 <!-- The module under test. -->
240- <script type="text/javascript" src="../information_type_choice.js"></script>
241+ <script type="text/javascript" src="../information_type.js"></script>
242
243 <!-- Placeholder for any css asset for this module. -->
244 <!-- <link rel="stylesheet" href="../assets/bugs.information_type_choice-core.css" /> -->
245
246 <!-- The test suite -->
247- <script type="text/javascript" src="test_information_type_choice.js"></script>
248+ <script type="text/javascript" src="test_information_type.js"></script>
249
250 </head>
251 <body class="yui3-skin-sam">
252 <ul id="suites">
253- <li>lp.bugs.information_type_choice.test</li>
254+ <li>lp.app.information_type.test</li>
255 </ul>
256 <div id="fixture"></div>
257 <script type="text/x-template" id="portlet-template">
258
259=== renamed file 'lib/lp/bugs/javascript/tests/test_information_type_choice.js' => 'lib/lp/app/javascript/tests/test_information_type.js'
260--- lib/lp/bugs/javascript/tests/test_information_type_choice.js 2012-08-28 01:52:34 +0000
261+++ lib/lp/app/javascript/tests/test_information_type.js 2012-09-04 14:00:36 +0000
262@@ -1,13 +1,13 @@
263 /* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
264
265-YUI.add('lp.bugs.information_type_choice.test', function (Y) {
266+YUI.add('lp.app.information_type.test', function (Y) {
267
268- var tests = Y.namespace('lp.bugs.information_type_choice.test');
269- var ns = Y.lp.bugs.information_type_choice;
270- tests.suite = new Y.Test.Suite('lp.bugs.information_type_choice Tests');
271+ var tests = Y.namespace('lp.app.information_type.test');
272+ var ns = Y.lp.app.information_type;
273+ tests.suite = new Y.Test.Suite('lp.app.information_type Tests');
274
275 tests.suite.add(new Y.Test.Case({
276- name: 'lp.bugs.information_type_choice_tests',
277+ name: 'lp.app.information_type_tests',
278
279 setUp: function() {
280 window.LP = {
281@@ -64,7 +64,7 @@
282 makeWidget: function() {
283 var privacy_link = Y.one('#privacy-link');
284 this.widget = ns.setup_information_type_choice(
285- privacy_link, this.lp_client, true);
286+ privacy_link, this.lp_client, LP.cache.bug, null, true);
287 },
288
289 _shim_privacy_banner: function () {
290@@ -84,8 +84,8 @@
291 },
292
293 test_library_exists: function () {
294- Y.Assert.isObject(Y.lp.bugs.information_type_choice,
295- "Cannot locate the lp.bugs.information_type_choice module");
296+ Y.Assert.isObject(Y.lp.app.information_type,
297+ "Cannot locate the lp.app.information_type module");
298 },
299
300 // The save XHR call works as expected.
301@@ -93,7 +93,7 @@
302 this.makeWidget();
303 var orig_save_success = ns.information_type_save_success;
304 var save_success_called = false;
305- ns.information_type_save_success = function(widget, value,
306+ ns.information_type_save_success = function(widget, context, value,
307 subscribers_list,
308 subscribers_data) {
309 Y.Assert.areEqual('USERDATA', value);
310@@ -104,7 +104,8 @@
311 save_success_called = true;
312 };
313 ns.save_information_type(
314- this.widget, 'PUBLIC', 'USERDATA', this.lp_client, true);
315+ this.widget, 'PUBLIC', 'USERDATA', this.lp_client,
316+ LP.cache.bug, null, true);
317 this.mockio.success({
318 responseText: '{"subscription_data": "subscribers",' +
319 '"cache_data": {"item": "value"}}',
320@@ -133,7 +134,8 @@
321 update_flag = true;
322 });
323
324- ns.information_type_save_success(this.widget, 'PROPRIETARY');
325+ ns.information_type_save_success(this.widget, LP.cache.bug,
326+ 'PROPRIETARY');
327 var body = Y.one('body');
328 Y.Assert.isTrue(body.hasClass('private'));
329 Y.Assert.isTrue(hide_flag);
330@@ -188,7 +190,8 @@
331 }
332 };
333 ns.information_type_save_success(
334- this.widget, 'PUBLIC', subscribers_list, subscribers_data);
335+ this.widget, LP.cache.bug, 'PUBLIC', subscribers_list,
336+ subscribers_data);
337 Y.Assert.isTrue(load_subscribers_called);
338 Y.Assert.areEqual('value1', window.LP.cache.item1);
339 Y.Assert.areEqual('value2', window.LP.cache.item2);
340@@ -227,7 +230,7 @@
341 var function_called = false;
342 ns.save_information_type =
343 function(widget, initial_value, value, lp_client,
344- validate_change) {
345+ context, subscribers_list, validate_change) {
346 // We only care if the function is called with
347 // validate_change = false
348 Y.Assert.areEqual('PUBLIC', initial_value);
349@@ -259,7 +262,7 @@
350 var function_called = false;
351 ns.save_information_type =
352 function(widget, initial_value, value, lp_client,
353- validate_change) {
354+ context, subscribers_list, validate_change) {
355 // We only care if the function is called with
356 // validate_change = false
357 function_called = !validate_change;
358@@ -285,7 +288,8 @@
359 this.makeWidget();
360 this.widget.set('value', 'USERDATA');
361 ns.save_information_type(
362- this.widget, 'PUBLIC', 'USERDATA', this.lp_client);
363+ this.widget, 'PUBLIC', 'USERDATA', this.lp_client,
364+ LP.cache.bug);
365 this.mockio.last_request.respond({
366 status: 500,
367 statusText: 'An error occurred'
368@@ -304,5 +308,5 @@
369 }));
370
371 }, '0.1', {'requires': ['test', 'console', 'event', 'node-event-simulate',
372- 'lp.testing.mockio', 'lp.client', 'lp.app.informatin_type',
373- 'lp.bugs.information_type_choice', 'lp.bugs.subscribers']});
374+ 'lp.testing.mockio', 'lp.client', 'lp.app.information_type',
375+ 'lp.bugs.subscribers']});
376
377=== modified file 'lib/lp/blueprints/browser/configure.zcml'
378--- lib/lp/blueprints/browser/configure.zcml 2012-08-01 19:02:49 +0000
379+++ lib/lp/blueprints/browser/configure.zcml 2012-09-04 14:00:36 +0000
380@@ -311,6 +311,9 @@
381 <browser:page
382 name="+listing-detailed"
383 template="../templates/specification-listing-detailed.pt"/>
384+ <browser:page
385+ name="+portlet-privacy"
386+ template="../templates/blueprint-portlet-privacy.pt"/>
387 </browser:pages>
388 <browser:page
389 for="lp.blueprints.interfaces.specification.ISpecification"
390
391=== modified file 'lib/lp/blueprints/browser/specification.py'
392--- lib/lp/blueprints/browser/specification.py 2012-08-20 19:14:22 +0000
393+++ lib/lp/blueprints/browser/specification.py 2012-09-04 14:00:36 +0000
394@@ -78,6 +78,7 @@
395 )
396
397 from lp import _
398+from lp.app.browser.informationtype import InformationTypePortletMixin
399 from lp.app.browser.launchpad import AppFrontPageSearchView
400 from lp.app.browser.launchpadform import (
401 action,
402@@ -111,6 +112,7 @@
403 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
404 from lp.blueprints.interfaces.sprintspecification import ISprintSpecification
405 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
406+from lp.registry.enums import PRIVATE_INFORMATION_TYPES
407 from lp.registry.interfaces.distribution import IDistribution
408 from lp.registry.interfaces.product import IProduct
409 from lp.services.config import config
410@@ -423,7 +425,7 @@
411 'linkbug', 'unlinkbug', 'linkbranch',
412 'adddependency', 'removedependency',
413 'dependencytree', 'linksprint', 'supersede',
414- 'retarget']
415+ 'retarget', 'information_type']
416
417 @enabled_with_permission('launchpad.Edit')
418 def milestone(self):
419@@ -528,8 +530,13 @@
420 text = 'Link a related branch'
421 return Link('+linkbranch', text, icon='add')
422
423-
424-class SpecificationSimpleView(LaunchpadView):
425+ def information_type(self):
426+ """Return the 'Set privacy/security' Link."""
427+ text = 'Change privacy/security'
428+ return Link('#', text)
429+
430+
431+class SpecificationSimpleView(InformationTypePortletMixin, LaunchpadView):
432 """Used to render portlets and listing items that need browser code."""
433
434 @cachedproperty
435@@ -545,6 +552,17 @@
436 def bug_links(self):
437 return self.context.getLinkedBugTasks(self.user)
438
439+ @cachedproperty
440+ def privacy_portlet_css(self):
441+ if self.private:
442+ return 'portlet private'
443+ else:
444+ return 'portlet public'
445+
446+ @cachedproperty
447+ def private(self):
448+ return self.context.information_type in PRIVATE_INFORMATION_TYPES
449+
450
451 class SpecificationView(SpecificationSimpleView):
452 """Used to render the main view of a specification."""
453@@ -562,6 +580,7 @@
454 return self.context.summary
455
456 def initialize(self):
457+ super(SpecificationView, self).initialize()
458 # The review that the user requested on this spec, if any.
459 self.notices = []
460
461
462=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
463--- lib/lp/blueprints/browser/tests/test_specification.py 2012-01-01 02:58:52 +0000
464+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-09-04 14:00:36 +0000
465@@ -1,4 +1,4 @@
466-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
467+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
468 # GNU Affero General Public License version 3 (see the file LICENSE).
469
470 __metaclass__ = type
471@@ -8,10 +8,14 @@
472
473 from BeautifulSoup import BeautifulSoup
474 import pytz
475-from testtools.matchers import Equals
476+from testtools.matchers import (
477+ Equals,
478+ Not,
479+ )
480 from zope.component import getUtility
481 from zope.publisher.interfaces import NotFound
482 from zope.security.proxy import removeSecurityProxy
483+import soupmatchers
484
485 from lp.app.browser.tales import format_link
486 from lp.blueprints.browser import specification
487@@ -20,7 +24,9 @@
488 ISpecification,
489 ISpecificationSet,
490 )
491+from lp.registry.enums import InformationType
492 from lp.registry.interfaces.person import PersonVisibility
493+from lp.services.features.testing import FeatureFixture
494 from lp.services.webapp.interfaces import BrowserNotificationLevel
495 from lp.services.webapp.publisher import canonical_url
496 from lp.testing import (
497@@ -165,6 +171,42 @@
498 "... Registered by Some Person ... ago ..."))
499
500
501+class TestSpecificationInformationType(BrowserTestCase):
502+
503+ layer = DatabaseFunctionalLayer
504+
505+ portlet_tag = soupmatchers.Tag('info-type-portlet', True,
506+ attrs=dict(id='information-type-summary'))
507+
508+ def setUp(self):
509+ super(TestSpecificationInformationType, self).setUp()
510+ self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
511+ 'true'}))
512+
513+ def assertBrowserMatches(self, matcher):
514+ browser = self.getViewBrowser(self.factory.makeSpecification())
515+ self.assertThat(browser.contents, matcher)
516+
517+ def test_has_privacy_portlet(self):
518+ self.assertBrowserMatches(soupmatchers.HTMLContains(self.portlet_tag))
519+
520+ def test_privacy_portlet_requires_flag(self):
521+ self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
522+ ''}))
523+ self.assertBrowserMatches(
524+ Not(soupmatchers.HTMLContains(self.portlet_tag)))
525+
526+ def test_has_privacy_banner(self):
527+ owner = self.factory.makePerson()
528+ spec = self.factory.makeSpecification(
529+ information_type=InformationType.PROPRIETARY, owner=owner)
530+ browser = self.getViewBrowser(spec, user=owner)
531+ privacy_banner = soupmatchers.Tag('privacy-banner', True,
532+ attrs={'class': 'banner-text'})
533+ self.assertThat(browser.contents,
534+ soupmatchers.HTMLContains(privacy_banner))
535+
536+
537 class TestSpecificationViewPrivateArtifacts(BrowserTestCase):
538 """ Tests that specifications with private team artifacts can be viewed.
539
540
541=== modified file 'lib/lp/blueprints/interfaces/specification.py'
542--- lib/lp/blueprints/interfaces/specification.py 2012-08-22 15:41:05 +0000
543+++ lib/lp/blueprints/interfaces/specification.py 2012-09-04 14:00:36 +0000
544@@ -565,6 +565,12 @@
545 :param user: The user doing the search.
546 """
547
548+ def getAllowedInformationTypes(who):
549+ """Get a list of acceptable `InformationType`s for this spec.
550+
551+ The intersection of the affected pillars' allowed types is permitted.
552+ """
553+
554
555 class ISpecificationEditRestricted(Interface):
556 """Specification's attributes and methods protected with launchpad.Edit.
557
558=== modified file 'lib/lp/blueprints/model/specification.py'
559--- lib/lp/blueprints/model/specification.py 2012-08-22 15:42:58 +0000
560+++ lib/lp/blueprints/model/specification.py 2012-09-04 14:00:36 +0000
561@@ -815,6 +815,9 @@
562 return '<Specification %s %r for %r>' % (
563 self.id, self.name, self.target.name)
564
565+ def getAllowedInformationTypes(self, who):
566+ return set(InformationType.items)
567+
568 @property
569 def private(self):
570 return self.information_type in PRIVATE_INFORMATION_TYPES
571@@ -1080,10 +1083,12 @@
572 def new(self, name, title, specurl, summary, definition_status,
573 owner, approver=None, product=None, distribution=None, assignee=None,
574 drafter=None, whiteboard=None, workitems_text=None,
575- priority=SpecificationPriority.UNDEFINED):
576+ priority=SpecificationPriority.UNDEFINED, information_type=None):
577 """See ISpecificationSet."""
578 # Adapt the NewSpecificationDefinitionStatus item to a
579 # SpecificationDefinitionStatus item.
580+ if information_type is None:
581+ information_type = InformationType.PUBLIC
582 status_name = definition_status.name
583 status_names = NewSpecificationDefinitionStatus.items.mapping.keys()
584 if status_name not in status_names:
585@@ -1095,7 +1100,8 @@
586 summary=summary, priority=priority,
587 definition_status=definition_status, owner=owner,
588 approver=approver, product=product, distribution=distribution,
589- assignee=assignee, drafter=drafter, whiteboard=whiteboard)
590+ assignee=assignee, drafter=drafter, whiteboard=whiteboard,
591+ information_type=information_type)
592
593 def getDependencyDict(self, specifications):
594 """See `ISpecificationSet`."""
595
596=== added file 'lib/lp/blueprints/templates/blueprint-portlet-privacy.pt'
597--- lib/lp/blueprints/templates/blueprint-portlet-privacy.pt 1970-01-01 00:00:00 +0000
598+++ lib/lp/blueprints/templates/blueprint-portlet-privacy.pt 2012-09-04 14:00:36 +0000
599@@ -0,0 +1,16 @@
600+<div
601+ xmlns:tal="http://xml.zope.org/namespaces/tal"
602+ xmlns:metal="http://xml.zope.org/namespaces/metal"
603+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
604+ id="privacy"
605+ tal:attributes="class view/privacy_portlet_css"
606+ tal:define="link context/menu:context/information_type"
607+>
608+ <span id="information-type-summary"
609+ tal:attributes="class view/information_type_css;">This blueprint
610+ contains <strong id="information-type" tal:content="view/information_type"></strong> information</span>&nbsp;<a class="sprite edit action-icon"
611+ id="privacy-link" tal:attributes="href link/path"
612+ tal:condition="link/enabled" style="display: none" >Edit</a>
613+
614+ <div id="information-type-description" style="padding-top: 5px" tal:content="view/information_type_description"></div>
615+</div>
616
617=== modified file 'lib/lp/blueprints/templates/specification-index.pt'
618--- lib/lp/blueprints/templates/specification-index.pt 2012-08-23 13:55:00 +0000
619+++ lib/lp/blueprints/templates/specification-index.pt 2012-09-04 14:00:36 +0000
620@@ -310,7 +310,15 @@
621 </div>
622
623 <script type="text/javascript">
624- LPJS.use('lp.anim', 'lp.deprecated.ui', 'node', 'widget', function(Y) {
625+ LPJS.use('lp.anim', 'lp.client', 'lp.deprecated.ui',
626+ 'lp.app.information_type', 'node', 'widget', function(Y) {
627+ Y.on('domready', function(){
628+ var privacy_link = Y.one('#privacy-link');
629+ Y.lp.app.information_type.setup_information_type_choice(
630+ privacy_link, new Y.lp.client.Launchpad(), LP.cache.context,
631+ null);
632+ privacy_link.setStyle('display', 'inline');
633+ });
634
635 Y.on('lp:context:implementation_status:changed', function(e) {
636 var icon = Y.one('#informational-icon');
637@@ -345,7 +353,8 @@
638 });
639 Y.on('lp:context:title:changed', function(e) {
640 // change the window title and breadcrumb.
641- Y.lp.deprecated.ui.update_field('ol.breadcrumbs li:last-child', e.new_value);
642+ Y.lp.deprecated.ui.update_field('ol.breadcrumbs li:last-child',
643+ e.new_value);
644 var title = window.document.title;
645 title = e.new_value + title.substring(e.old_value.length);
646 window.document.title = title;
647@@ -377,7 +386,7 @@
648
649 <tal:side metal:fill-slot="side">
650 <tal:menu replace="structure context/@@+global-actions" />
651-
652+ <tal:privacy replace="structure context/@@+portlet-privacy" condition="features/blueprints.information_type.enabled" />
653 <div tal:replace="structure context/@@+portlet-subscribers" />
654 </tal:side>
655 </body>
656
657=== modified file 'lib/lp/blueprints/tests/test_specification.py'
658--- lib/lp/blueprints/tests/test_specification.py 2012-08-20 16:38:10 +0000
659+++ lib/lp/blueprints/tests/test_specification.py 2012-09-04 14:00:36 +0000
660@@ -159,12 +159,13 @@
661 'date_started', 'datecreated', 'declineBy',
662 'definition_status', 'dependencies', 'direction_approved',
663 'distribution', 'distroseries', 'drafter', 'drafterID',
664- 'getBranchLink', 'getDelta', 'getLinkedBugTasks',
665- 'getSprintSpecification', 'getSubscriptionByName', 'goal',
666- 'goal_decider', 'goal_proposer', 'goalstatus',
667- 'has_accepted_goal', 'implementation_status', 'informational',
668- 'isSubscribed', 'is_blocked', 'is_complete', 'is_incomplete',
669- 'is_started', 'lifecycle_status', 'linkBranch', 'linkSprint',
670+ 'getBranchLink', 'getDelta', 'getAllowedInformationTypes',
671+ 'getLinkedBugTasks', 'getSprintSpecification',
672+ 'getSubscriptionByName', 'goal', 'goal_decider',
673+ 'goal_proposer', 'goalstatus', 'has_accepted_goal',
674+ 'implementation_status', 'informational', 'isSubscribed',
675+ 'is_blocked', 'is_complete', 'is_incomplete', 'is_started',
676+ 'lifecycle_status', 'linkBranch', 'linkSprint',
677 'linked_branches', 'man_days', 'milestone', 'name',
678 'notificationRecipientAddresses', 'owner', 'priority',
679 'product', 'productseries', 'proposeGoal', 'removeDependency',
680@@ -172,7 +173,8 @@
681 'subscribers', 'subscription', 'subscriptions', 'summary',
682 'superseded_by', 'target', 'title', 'unlinkBranch',
683 'unlinkSprint', 'unsubscribe', 'updateLifecycleStatus',
684- 'validateMove', 'whiteboard', 'work_items', 'workitems_text')),
685+ 'validateMove', 'whiteboard', 'work_items',
686+ 'workitems_text')),
687 'launchpad.Edit': set((
688 'newWorkItem', 'retarget', 'setDefinitionStatus',
689 'setImplementationStatus', 'setTarget', 'updateWorkItems')),
690
691=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
692--- lib/lp/bugs/javascript/bugtask_index.js 2012-08-30 02:52:33 +0000
693+++ lib/lp/bugs/javascript/bugtask_index.js 2012-09-04 14:00:36 +0000
694@@ -49,8 +49,11 @@
695
696 privacy_link = Y.one('#privacy-link');
697 if (privacy_link) {
698- Y.lp.bugs.information_type_choice.setup_information_type_choice(
699- privacy_link, lp_client);
700+ var sub_list_node = Y.one('#other-bug-subscribers');
701+ var subscribers_list = sub_list_node.getData(
702+ 'subscribers_loader');
703+ Y.lp.app.information_type.setup_information_type_choice(
704+ privacy_link, lp_client, LP.cache.bug, subscribers_list);
705 }
706 setup_add_attachment();
707 setup_link_branch_picker();
708@@ -1126,7 +1129,7 @@
709 "lazr.formoverlay", "lp.anim", "lazr.overlay",
710 "lazr.choiceedit", "lp.app.picker",
711 "lp.bugs.bugtask_index.portlets.subscription",
712- "lp.bugs.information_type_choice",
713+ "lp.app.information_type",
714 "lp.app.widgets.expander", "lp.client", "escape",
715 "lp.client.plugins", "lp.app.errors",
716 "lp.app.banner.privacy",
717
718=== removed file 'lib/lp/bugs/javascript/information_type_choice.js'
719--- lib/lp/bugs/javascript/information_type_choice.js 2012-08-30 02:40:04 +0000
720+++ lib/lp/bugs/javascript/information_type_choice.js 1970-01-01 00:00:00 +0000
721@@ -1,190 +0,0 @@
722-/* Copyright 2012 Canonical Ltd. This software is licensed under the
723- * GNU Affero General Public License version 3 (see the file LICENSE).
724- *
725- * Information Type choice widget for bug pages.
726- */
727-
728-YUI.add('lp.bugs.information_type_choice', function(Y) {
729-
730-var namespace = Y.namespace('lp.bugs.information_type_choice');
731-var information_type = Y.namespace('lp.app.information_type');
732-
733-// For testing.
734-var skip_animation = false;
735-
736-/**
737- * Save the new information type. If validate_change is true, then a check
738- * will be done to ensure the bug will not become invisible. If the bug will
739- * become invisible, a confirmation popup is used to confirm the user's
740- * intention. Then this method is called again with validate_change set to
741- * false to allow the change to proceed.
742- *
743- * @param widget
744- * @param initial_value
745- * @param value
746- * @param lp_client
747- * @param validate_change
748- */
749-namespace.save_information_type = function(widget, initial_value, value,
750- lp_client, validate_change) {
751- var error_handler = new Y.lp.client.FormErrorHandler();
752- error_handler.showError = function(error_msg) {
753- Y.lp.app.errors.display_error(
754- Y.one('#information-type'), error_msg);
755- };
756- error_handler.handleError = function(ioId, response) {
757- if( response.status === 400
758- && response.statusText === 'Bug Visibility') {
759- namespace._confirm_information_type_change(
760- widget, initial_value, lp_client);
761- return true;
762- }
763- var orig_value = information_type.information_type_value_from_key(
764- LP.cache.bug.information_type, 'name', 'value');
765- widget.set('value', orig_value);
766- widget._showFailed();
767- information_type.update_privacy_portlet(orig_value);
768- return false;
769- };
770- var submit_url = document.URL + "/+secrecy";
771- var qs = Y.lp.client.append_qs('', 'field.actions.change', 'Change');
772- qs = Y.lp.client.append_qs(qs, 'field.information_type', value);
773- qs = Y.lp.client.append_qs(
774- qs, 'field.validate_change', validate_change?'on':'off');
775- var sub_list_node = Y.one('#other-bug-subscribers');
776- var subscribers_list = sub_list_node.getData('subscribers_loader');
777- var config = {
778- method: "POST",
779- headers: {'Accept': 'application/xhtml;application/json'},
780- data: qs,
781- on: {
782- start: function () {
783- widget._uiSetWaiting();
784- subscribers_list.subscribers_list.startActivity(
785- 'Updating subscribers...');
786- },
787- end: function () {
788- widget._uiClearWaiting();
789- subscribers_list.subscribers_list.stopActivity();
790- },
791- success: function (id, response) {
792- var result_data = null;
793- if (response.responseText !== '') {
794- result_data = Y.JSON.parse(response.responseText);
795- }
796- namespace.information_type_save_success(
797- widget, value, subscribers_list, result_data);
798- Y.lp.client.display_notifications(
799- response.getResponseHeader('X-Lazr-Notifications'));
800- },
801- failure: error_handler.getFailureHandler()
802- }
803- };
804- lp_client.io_provider.io(submit_url, config);
805-};
806-
807-namespace.information_type_save_success = function(widget, value,
808- subscribers_list,
809- subscribers_data) {
810- LP.cache.bug.information_type =
811- information_type.information_type_value_from_key(
812- value, 'value', 'name');
813- information_type.update_privacy_banner(value);
814- widget._showSucceeded();
815- if (Y.Lang.isObject(subscribers_data)) {
816- var subscribers = subscribers_data.subscription_data;
817- subscribers_list._loadSubscribersFromList(subscribers);
818- var cache_data = subscribers_data.cache_data;
819- var item;
820- for (item in cache_data) {
821- if (cache_data.hasOwnProperty(item)) {
822- LP.cache[item] = cache_data[item];
823- }
824- }
825- }
826- var ns = Y.lp.bugs.bugtask_index.portlets.subscription;
827- ns.update_subscription_status(skip_animation);
828-};
829-
830-/**
831- * Possibly prompt the user to confirm the change of information type.
832- * If the old value is public, and the new value is private, we want to
833- * confirm that the user really wants to make the change.
834- *
835- * @param widget
836- * @param initial_value
837- * @param lp_client
838- * @private
839- */
840-namespace._confirm_information_type_change = function(widget, initial_value,
841- lp_client) {
842- var value = widget.get('value');
843- var do_save = function() {
844- information_type.update_privacy_portlet(value);
845- namespace.save_information_type(
846- widget, initial_value, value, lp_client, false);
847- };
848- // Reset the widget back to it's original value so the user doesn't see it
849- // change while the confirmation dialog is showing.
850- var new_value = widget.get('value');
851- widget.set('value', initial_value);
852- information_type.update_privacy_portlet(initial_value);
853- var confirm_text_template = [
854- '<p class="block-sprite large-warning">',
855- ' You are about to mark this bug as ',
856- ' <strong>{information_type}</strong>.<br>',
857- ' The bug will become invisible because there is no-one with',
858- ' permissions to see {information_type} bugs.',
859- '</p><p>',
860- ' <strong>Please confirm you really want to do this.</strong>',
861- '</p>'
862- ].join('');
863- var title = information_type.information_type_value_from_key(
864- value, 'value', 'name');
865- var confirm_text = Y.Lang.sub(confirm_text_template,
866- {information_type: title});
867- var co = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
868- submit_fn: function() {
869- widget.set('value', new_value);
870- information_type.update_privacy_portlet(new_value);
871- do_save();
872- },
873- form_content: confirm_text,
874- headerContent: '<h2>Confirm information type change</h2>',
875- submit_text: 'Confirm'
876- });
877- co.show();
878-};
879-
880-namespace.setup_information_type_choice = function(privacy_link, lp_client,
881- skip_anim) {
882- skip_animation = skip_anim;
883- var initial_value = information_type.information_type_value_from_key(
884- LP.cache.bug.information_type, 'name', 'value');
885- var information_type_value = Y.one('#information-type');
886- var information_type_edit = new Y.ChoiceSource({
887- editicon: privacy_link,
888- contentBox: Y.one('#privacy'),
889- value_location: information_type_value,
890- value: initial_value,
891- title: "Change information type",
892- items: LP.cache.information_type_data,
893- backgroundColor: '#FFFF99',
894- flashEnabled: false
895- });
896- Y.lp.app.choice.hook_up_choicesource_spinner(information_type_edit);
897- information_type_edit.render();
898- information_type_edit.on("save", function(e) {
899- var value = information_type_edit.get('value');
900- information_type.update_privacy_portlet(value);
901- namespace.save_information_type(
902- information_type_edit, initial_value, value, lp_client, true);
903-
904- });
905- privacy_link.addClass('js-action');
906- return information_type_edit;
907-};
908-}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
909- "lazr.choiceedit", "lp.bugs.bugtask_index",
910- "lp.app.banner.privacy", "lp.app.choice",
911- "lp.app.information_type"]});
912
913=== modified file 'lib/lp/services/features/flags.py'
914--- lib/lp/services/features/flags.py 2012-08-30 11:52:28 +0000
915+++ lib/lp/services/features/flags.py 2012-09-04 14:00:36 +0000
916@@ -57,6 +57,12 @@
917 '',
918 '',
919 ''),
920+ ('blueprints.information_type.enabled',
921+ 'boolean',
922+ 'Enable UI for information_type on Blueprints.',
923+ 'Disable UI',
924+ 'Blueprint information_type UI',
925+ 'https://dev.launchpad.net/LEP/PrivateProjects'),
926 ('bugs.affected_count_includes_dupes.disabled',
927 'boolean',
928 ("Disable adding up affected users across all duplicate bugs."),
929
930=== modified file 'lib/lp/testing/__init__.py'
931--- lib/lp/testing/__init__.py 2012-08-17 09:31:57 +0000
932+++ lib/lp/testing/__init__.py 2012-09-04 14:00:36 +0000
933@@ -850,13 +850,15 @@
934
935 def getViewBrowser(self, context, view_name=None, no_login=False,
936 rootsite=None, user=None):
937- if user is None:
938+ if no_login:
939+ user = ANONYMOUS
940+ elif user is None:
941 user = self.user
942 # Make sure that there is a user interaction in order to generate the
943 # canonical url for the context object.
944- login(ANONYMOUS)
945- url = canonical_url(context, view_name=view_name, rootsite=rootsite)
946- logout()
947+ with person_logged_in(user):
948+ url = canonical_url(context, view_name=view_name,
949+ rootsite=rootsite)
950 if no_login:
951 from lp.testing.pages import setupBrowser
952 browser = setupBrowser()
953
954=== modified file 'lib/lp/testing/factory.py'
955--- lib/lp/testing/factory.py 2012-08-30 23:50:35 +0000
956+++ lib/lp/testing/factory.py 2012-09-04 14:00:36 +0000
957@@ -2069,7 +2069,8 @@
958 status=NewSpecificationDefinitionStatus.NEW,
959 implementation_status=None, goal=None, specurl=None,
960 assignee=None, drafter=None, approver=None,
961- priority=None, whiteboard=None, milestone=None):
962+ priority=None, whiteboard=None, milestone=None,
963+ information_type=None):
964 """Create and return a new, arbitrary Blueprint.
965
966 :param product: The product to make the blueprint on. If one is
967@@ -2106,7 +2107,8 @@
968 approver=approver,
969 product=product,
970 distribution=distribution,
971- priority=priority)
972+ priority=priority,
973+ information_type=information_type)
974 naked_spec = removeSecurityProxy(spec)
975 if status.name not in status_names:
976 # Set the closed status after the status has a sane initial state.