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

Proposed by Richard Harding on 2012-10-04
Status: Merged
Approved by: Richard Harding on 2012-10-05
Approved revision: no longer in the source branch.
Merged at revision: 16120
Proposed branch: lp:~rharding/launchpad/edit_product_info_type
Merge into: lp:launchpad
Diff against target: 474 lines (+231/-106)
5 files modified
lib/lp/registry/browser/configure.zcml (+1/-1)
lib/lp/registry/browser/product.py (+30/-10)
lib/lp/registry/javascript/product_views.js (+127/-72)
lib/lp/registry/javascript/tests/test_product_views.js (+61/-22)
lib/lp/registry/templates/project-edit.pt (+12/-1)
To merge this branch: bzr merge lp:~rharding/launchpad/edit_product_info_type
Reviewer Review Type Date Requested Status
Abel Deuring (community) code 2012-10-04 Approve on 2012-10-05
Review via email: mp+128081@code.launchpad.net

Commit Message

Add the information type choice widget to project edit if the feature flag is enabled.

Description of the Change

= Summary =

There will be mistakes and users need to be able to change the information
type on their project using the ProjectEditView. This branch adds the
information type field and adds the JS to setup the choice widget and dealing
with the license setting in the same way it does for new project registration.

== Implementation Notes ==

In order to get the JS onto the page we needed to make sure we use
project-edit.pt.

The field is added in the same location as when registering a project. It adds
the choice widget.

There's a drive by moving the @property next_url up the class to be with the
other @property definitions.

The field is only available if the feature flag is set so we adjust the field
using setUpFields and make sure to omit the information_type if the feature
flag isn't set.

Large chunks of the JS are moved out into shared functions so that each View
class can share the code that watches the information type for changes and
respond with showing/hiding the license fields and such.

We add a new JS view EditProduct that is figured for the edit page. This only
sets up events if the information type field is set and is safe if the feature
flag is not set. It'll just be inert JS.

_information_type_change is changed to a public method since the new shared
bind_information_type method calls into it directly.

The tests are setup to share setUp, tearDown, and the set of asserts for the
license changes. The product edit view doesn't have bug_supervisor or driver
fields to update.

== Q/A ==

Register a new project with a private information type. Then, once it's
created, "Change details" and select a Public information type.

== Known Issues ==

If the project starts out as public you cannot currently change it to a private type as you don't have a commercial subscription. A follow up branch will add that into place.

== Tests ==

test_product_views.html

To post a comment you must log in.
Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/configure.zcml'
2--- lib/lp/registry/browser/configure.zcml 2012-08-15 16:05:54 +0000
3+++ lib/lp/registry/browser/configure.zcml 2012-10-05 14:06:23 +0000
4@@ -1496,7 +1496,7 @@
5 for="lp.registry.interfaces.product.IProduct"
6 class="lp.registry.browser.product.ProductEditView"
7 permission="launchpad.Edit"
8- template="../../app/templates/generic-edit.pt"/>
9+ template="../templates/project-edit.pt"/>
10 <browser:page
11 for="lp.registry.interfaces.product.IProduct"
12 permission="launchpad.Edit"
13
14=== modified file 'lib/lp/registry/browser/product.py'
15--- lib/lp/registry/browser/product.py 2012-10-05 13:29:39 +0000
16+++ lib/lp/registry/browser/product.py 2012-10-05 14:06:23 +0000
17@@ -1412,6 +1412,7 @@
18 "description",
19 "project",
20 "homepageurl",
21+ "information_type",
22 "sourceforgeproject",
23 "freshmeatproject",
24 "wikiurl",
25@@ -1424,12 +1425,41 @@
26 ]
27 custom_widget('licenses', LicenseWidget)
28 custom_widget('license_info', GhostWidget)
29+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
30+
31+ @property
32+ def next_url(self):
33+ """See `LaunchpadFormView`."""
34+ if self.context.active:
35+ return canonical_url(self.context)
36+ else:
37+ return canonical_url(getUtility(IProductSet))
38+
39+ cancel_url = next_url
40
41 @property
42 def page_title(self):
43 """The HTML page title."""
44 return "Change %s's details" % self.context.title
45
46+ def initialize(self):
47+ # The JSON cache must be populated before the super call, since
48+ # the form is rendered during LaunchpadFormView's initialize()
49+ # when an action is invoked.
50+ cache = IJSONRequestCache(self.request)
51+ json_dump_information_types(cache,
52+ PUBLIC_PROPRIETARY_INFORMATION_TYPES)
53+ super(ProductEditView, self).initialize()
54+
55+ def setUpFields(self):
56+ """See `LaunchpadFormView`."""
57+ super(ProductEditView, self).setUpFields()
58+
59+ private_projects_flag = 'disclosure.private_projects.enabled'
60+ private_projects = bool(getFeatureFlag(private_projects_flag))
61+ if not private_projects:
62+ self.form_fields = self.form_fields.omit('information_type')
63+
64 def showOptionalMarker(self, field_name):
65 """See `LaunchpadFormView`."""
66 # This has the effect of suppressing the ": (Optional)" stuff for the
67@@ -1444,16 +1474,6 @@
68 def change_action(self, action, data):
69 self.updateContextFromData(data)
70
71- @property
72- def next_url(self):
73- """See `LaunchpadFormView`."""
74- if self.context.active:
75- return canonical_url(self.context)
76- else:
77- return canonical_url(getUtility(IProductSet))
78-
79- cancel_url = next_url
80-
81
82 class ProductValidationMixin:
83
84
85=== modified file 'lib/lp/registry/javascript/product_views.js'
86--- lib/lp/registry/javascript/product_views.js 2012-10-04 13:14:50 +0000
87+++ lib/lp/registry/javascript/product_views.js 2012-10-05 14:06:23 +0000
88@@ -12,6 +12,109 @@
89 var COMMERCIAL_LICENSE = "OTHER_PROPRIETARY";
90
91 /**
92+ * Handle binding up events for our information type choice widget.
93+ *
94+ * @function bind_information_type
95+ * @param {View} view
96+ * @param {String} value
97+ */
98+ var bind_information_type = function (view) {
99+ var widget = Y.lp.app.choice.addPopupChoiceForRadioButtons(
100+ 'information_type',
101+ info_type.cache_to_choicesource(
102+ LP.cache.information_type_data));
103+
104+ // When the information type changes, check if we should show/hide
105+ // UI components.
106+ widget.on('save', function (ev) {
107+ view.information_type_change(ev.target.get('value'));
108+ });
109+
110+ // If we don't have a value to start with, always default to PUBLIC.
111+ if (widget.get('value')) {
112+ view.information_type_change(widget.get('value'));
113+ } else {
114+ view.information_type_change('PUBLIC');
115+ }
116+
117+ // Hold onto the reference to the information type widget so we
118+ // can test with it.
119+ view._information_type_widget = widget;
120+ };
121+
122+ /**
123+ * Update the visiblity and settings of the license field based on the
124+ * value of the information type.
125+ *
126+ * @function toggle_license_field
127+ * @param {String} value of the information type
128+ *
129+ */
130+ var toggle_license_field = function (value) {
131+ // The license code is nested in another table.
132+ var license = Y.one('input[name="field.licenses"]');
133+ var license_cont = license.ancestor('td').ancestor('td');
134+
135+ if (info_type.get_cache_data_from_key(value, 'value',
136+ 'is_private')) {
137+ // Hide the license field and set it to commercial.
138+ license_cont.hide();
139+
140+ // We have to set both checked and the value because value is
141+ // a DOM object property, but the UI is updated based on the
142+ // 'checked' attribute.
143+ var commercial = 'input[value="' + COMMERCIAL_LICENSE + '"]';
144+ Y.one(commercial).set('checked', 'checked');
145+ license.set('value', COMMERCIAL_LICENSE);
146+ Y.one('textarea[name="field.license_info"]').set(
147+ 'value', 'Launchpad 30-day trial commercial license');
148+ } else {
149+ // Show the license field.
150+ license_cont.show();
151+ }
152+ };
153+
154+
155+ /**
156+ * View handler for the Project Edit view.
157+ *
158+ * @class registry.views.EditProduct
159+ * @extends Y.View
160+ *
161+ */
162+ ns.EditProduct = Y.Base.create('edit-project-view', Y.View, [], {
163+ /**
164+ * Update the UI when the information type changes.
165+ *
166+ * @method _information_type_change
167+ * @param {String} information_type value
168+ */
169+ information_type_change: function (value) {
170+ toggle_license_field(value);
171+ },
172+
173+ /**
174+ * Render out the View into the active page.
175+ *
176+ * @method render
177+ */
178+ render: function (information_type_value) {
179+ // Only bind the information type if it's available due to the
180+ // feature flag.
181+ if (Y.one('input[name="field.information_type"]')) {
182+ // get the original value, if there is one set.
183+ bind_information_type(this);
184+ }
185+ }
186+ }, {
187+ ATTRS: {
188+ container: {
189+ value: '.yui-main'
190+ }
191+ }
192+ });
193+
194+ /**
195 * Handle setting up the JS for the new project View
196 *
197 * @class registry.views.NewProduct
198@@ -29,33 +132,6 @@
199 valid_char: new RegExp('^[a-z0-9][-.+a-z0-9]*$', 'i'),
200
201 /**
202- * Process binding the UI for the information type choice widget.
203- *
204- * @method _bind_information_type
205- * @private
206- */
207- _bind_information_type: function () {
208- var that = this;
209- var widget = Y.lp.app.choice.addPopupChoiceForRadioButtons(
210- 'information_type',
211- info_type.cache_to_choicesource(
212- LP.cache.information_type_data));
213-
214- // When the information type changes, check if we should show/hide
215- // UI components.
216- widget.on('save', function (ev) {
217- that._information_type_change(ev.target.get('value'));
218- });
219-
220- // And on first render, the information type is PUBLIC.
221- that._information_type_change('PUBLIC');
222-
223- // Hold onto the reference to the information type widget so we
224- // can test with it.
225- this._information_type_widget = widget;
226- },
227-
228- /**
229 * Bind the url/name field interaction
230 *
231 * @method _bind_name_field
232@@ -190,49 +266,6 @@
233 },
234
235 /**
236- * Update the UI when the information type changes.
237- *
238- * @method _information_type_change
239- * @param {String} information_type value
240- *
241- */
242- _information_type_change: function (value) {
243- var driver_cont = Y.one(
244- 'input[name="field.driver"]').ancestor('td');
245- var bug_super_cont = Y.one(
246- 'input[name="field.bug_supervisor"]').ancestor('td');
247-
248- // The license code is nested in another table.
249- var license = Y.one('input[name="field.licenses"]');
250- var license_cont = license.ancestor('td').ancestor('td');
251-
252- if (info_type.get_cache_data_from_key(value, 'value',
253- 'is_private')) {
254- // Hide the driver and bug supervisor fields.
255- driver_cont.show();
256- bug_super_cont.show();
257-
258- // Hide the license field and set it to commercial.
259- license_cont.hide();
260-
261- // We have to set both checked and the value because value is
262- // a DOM object property, but the UI is updated based on the
263- // 'checked' attribute.
264- var commercial = 'input[value="' + COMMERCIAL_LICENSE + '"]';
265- Y.one(commercial).set('checked', 'checked');
266- license.set('value', COMMERCIAL_LICENSE);
267- Y.one('textarea[name="field.license_info"]').set(
268- 'value', 'Launchpad 30-day trial commercial license');
269- } else {
270- driver_cont.hide();
271- bug_super_cont.hide();
272-
273- // Show the license field.
274- license_cont.show();
275- }
276- },
277-
278- /**
279 * Handle the reveals when there are search results.
280 *
281 * @method _show_separator
282@@ -290,7 +323,7 @@
283 */
284 bindUI: function () {
285 if (Y.one('input[name="field.information_type"]')) {
286- this._bind_information_type();
287+ bind_information_type(this);
288 }
289
290 if(Y.one('input[id="field.name"]')) {
291@@ -305,6 +338,28 @@
292 },
293
294 /**
295+ * Update the UI when the information type changes.
296+ *
297+ * @method _information_type_change
298+ * @param {String} information_type value
299+ */
300+ information_type_change: function (value) {
301+ toggle_license_field(value);
302+ var driver_cont = Y.one('input[name="field.driver"]').ancestor('td');
303+ var bug_super_cont = Y.one('input[name="field.bug_supervisor"]').ancestor('td');
304+
305+ if (info_type.get_cache_data_from_key(value, 'value',
306+ 'is_private')) {
307+ // Hide the driver and bug supervisor fields.
308+ driver_cont.show();
309+ bug_super_cont.show();
310+ } else {
311+ driver_cont.hide();
312+ bug_super_cont.hide();
313+ }
314+ },
315+
316+ /**
317 * Standard YUI init.
318 *
319 * @method initialize
320@@ -428,6 +483,6 @@
321 });
322
323 }, '0.1', {
324- 'requires': ['base', 'node', 'lp.ui.effects', 'lp.app.choice',
325+ 'requires': ['base', 'node', 'view', 'lp.ui.effects', 'lp.app.choice',
326 'lp.ui.choiceedit', 'lp.app.information_type']
327 });
328
329=== modified file 'lib/lp/registry/javascript/tests/test_product_views.js'
330--- lib/lp/registry/javascript/tests/test_product_views.js 2012-10-04 13:14:50 +0000
331+++ lib/lp/registry/javascript/tests/test_product_views.js 2012-10-05 14:06:23 +0000
332@@ -6,9 +6,8 @@
333 var ns = Y.namespace('registry.views');
334 tests.suite = new Y.Test.Suite('registry.product-views Tests');
335
336- tests.suite.add(new Y.Test.Case({
337- name: 'registry.product-views.new_tests',
338-
339+ // Share methods used in various test suites for the Views.
340+ var shared = {
341 setUp: function () {
342 window.LP = {
343 cache: {
344@@ -38,7 +37,6 @@
345 var tpl = Y.one('#tpl_information_type');
346 var html = Y.lp.mustache.to_html(tpl.getContent());
347 Y.one('#testdom').setContent(html);
348-
349 },
350
351 tearDown: function () {
352@@ -46,6 +44,31 @@
353 LP.cache = {};
354 },
355
356+ assert_license_updates: function () {
357+ var licenses = Y.one('input[name="field.licenses"]');
358+ var licenses_cont = licenses.ancestor('td').ancestor('td');
359+ Y.Assert.areEqual('none',
360+ licenses_cont.getComputedStyle('display'),
361+ 'License is hidden when EMBARGOED is selected.');
362+
363+ var new_license = Y.one('input[name="field.licenses"]');
364+ Y.Assert.areEqual('OTHER_PROPRIETARY', new_license.get('value'),
365+ 'License is updated to a commercial selection');
366+
367+ // license_info must also be filled in to ensure we don't
368+ // get form validation errors.
369+ var license_info = Y.one('textarea[name="field.license_info"]');
370+ Y.Assert.areEqual(
371+ 'Launchpad 30-day trial commercial license',
372+ license_info.get('value'));
373+ }
374+ };
375+
376+ tests.suite.add(new Y.Test.Case({
377+ name: 'registry.product-views.new_tests',
378+ setUp: shared.setUp,
379+ tearDown: shared.tearDown,
380+
381 test_library_exists: function () {
382 Y.Assert.isObject(ns.NewProduct,
383 "Could not locate the registry.views.NewProduct module");
384@@ -110,13 +133,7 @@
385 // Force the value to change to a private value and make sure the
386 // UI is updated.
387 widget._saveData('EMBARGOED');
388-
389- var licenses = Y.one('input[name="field.licenses"]');
390- var licenses_cont = licenses.ancestor('td').ancestor('td');
391- Y.Assert.areEqual('none',
392- licenses_cont.getComputedStyle('display'),
393- 'License is hidden when EMBARGOED is selected.');
394-
395+ shared.assert_license_updates();
396
397 var bug_super = Y.one('input[name="field.bug_supervisor"]');
398 var bug_super_cont = bug_super.ancestor('td');
399@@ -131,17 +148,39 @@
400 'none',
401 driver_cont.getComputedStyle('display'),
402 'Driver is shown when EMBARGOED is selected.');
403-
404- var new_license = Y.one('input[name="field.licenses"]');
405- Y.Assert.areEqual('OTHER_PROPRIETARY', new_license.get('value'),
406- 'License is updated to a commercial selection');
407-
408- // license_info must also be filled in to ensure we don't
409- // get form validation errors.
410- var license_info = Y.one('textarea[name="field.license_info"]');
411- Y.Assert.areEqual(
412- 'Launchpad 30-day trial commercial license',
413- license_info.get('value'));
414+ }
415+ }));
416+
417+
418+ tests.suite.add(new Y.Test.Case({
419+ name: 'registry.product-views.edit_tests',
420+ setUp: shared.setUp,
421+ tearDown: shared.tearDown,
422+
423+ test_library_exists: function () {
424+ Y.Assert.isObject(ns.EditProduct,
425+ "Could not locate the registry.views.EditProduct module");
426+ },
427+
428+ test_information_type_widget: function () {
429+ // Render will give us a pretty JS choice widget.
430+ var view = new ns.EditProduct();
431+ view.render();
432+ Y.Assert.isNotNull(Y.one('#testdom .yui3-ichoicesource'));
433+ },
434+
435+ test_information_type_choose_non_public: function () {
436+ // Selecting an information type not-public hides the license,
437+ // sets it to commercial
438+ var view = new ns.EditProduct();
439+ view.render();
440+
441+ var widget = view._information_type_widget;
442+
443+ // Force the value to change to a private value and make sure the
444+ // UI is updated.
445+ widget._saveData('EMBARGOED');
446+ shared.assert_license_updates();
447 }
448 }));
449
450
451=== modified file 'lib/lp/registry/templates/project-edit.pt'
452--- lib/lp/registry/templates/project-edit.pt 2009-08-05 19:26:37 +0000
453+++ lib/lp/registry/templates/project-edit.pt 2012-10-05 14:06:23 +0000
454@@ -6,9 +6,20 @@
455 metal:use-macro="view/macro:page/main_only"
456 i18n:domain="launchpad"
457 >
458+ <head>
459+ <tal:head-epilogue metal:fill-slot="head_epilogue">
460+ <script type="text/javascript">
461+ LPJS.use('registry.product-views', function(Y) {
462+ Y.on('domready', function() {
463+ var view = new Y.registry.views.EditProduct();
464+ view.render();
465+ });
466+ });
467+ </script>
468+ </tal:head-epilogue>
469+ </head>
470 <body>
471 <div metal:fill-slot="main">
472-
473 <div class="top-portlet"
474 metal:use-macro="context/@@launchpad_form/form">
475