Merge lp:~rvb/launchpad/bug-869221-archindepwidget into lp:launchpad

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 14166
Proposed branch: lp:~rvb/launchpad/bug-869221-archindepwidget
Merge into: lp:launchpad
Prerequisite: lp:~rvb/launchpad/bug-869221-forcederivedarchinep
Diff against target: 1055 lines (+539/-95)
7 files modified
lib/lp/app/javascript/formwidgets/formwidgets.js (+62/-15)
lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js (+56/-8)
lib/lp/registry/javascript/distroseries/initseries.js (+83/-11)
lib/lp/registry/javascript/distroseries/tests/test_initseries.html (+18/-0)
lib/lp/registry/javascript/distroseries/tests/test_initseries.js (+131/-32)
lib/lp/registry/javascript/distroseries/tests/test_widgets.js (+107/-22)
lib/lp/registry/javascript/distroseries/widgets.js (+82/-7)
To merge this branch: bzr merge lp:~rvb/launchpad/bug-869221-archindepwidget
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+79561@code.launchpad.net

Commit message

[r=benji][bug=869221] Add a widget on +initseries to let the user force the arch tag used as the architecture independent builder.

Description of the change

This branch is the last one to fix bug 869221. It adds a widget to the +initseries page to let the user force the arch tag for the architecture independent builder.

Here is how the new widget looks like before any parent is added:
http://people.canonical.com/~rvb/new_widget.png
and after parents are added:
http://people.canonical.com/~rvb/new_widget_populated.png

Two errors can happen:
- if the option selected on the new widget is left to the default (Auto select) and that none of the parents' arch indep arch tags are selected, then the backend won't be able to "auto select" a arch indep arch tag for the derived series so we display an error: http://people.canonical.com/~rvb/new_widget_no_arch_indep.png
- if the arch indep arch tag is not among the selected architectures this is obviously wrong: http://people.canonical.com/~rvb/new_widget_arch_indep_not_among.png

= Tests =

lib/lp/registry/javascript/distroseries/tests/test_initseries.html
lib/lp/app/javascript/formwidgets/tests/test_formwidgets.html
lib/lp/registry/javascript/distroseries/tests/test_initseries.html

= QA =

This branch (and all the others from which it depends) should let one initialize a series while specifying the arch indep builder arch tag.

To post a comment you must log in.
Benji York (benji) wrote :

This branch looks good. Here are a couple of small things I noticed:

It doesn't seem to me that hideError needs the "error" parameter.

The parenthesis around the second argument to .set on line 385 aren't
necessary. Not that object, I just thought you might like to know.

review: Approve (code)
Raphaël Badin (rvb) wrote :

> This branch looks good. Here are a couple of small things I noticed:
>
> It doesn't seem to me that hideError needs the "error" parameter.
>
> The parenthesis around the second argument to .set on line 385 aren't
> necessary. Not that object, I just thought you might like to know.

Thanks for the review Benji!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/formwidgets/formwidgets.js'
2--- lib/lp/app/javascript/formwidgets/formwidgets.js 2011-10-18 07:48:33 +0000
3+++ lib/lp/app/javascript/formwidgets/formwidgets.js 2011-10-18 07:48:34 +0000
4@@ -183,7 +183,7 @@
5 * It is also responsible for setting any error on the widget using
6 * this.showError.
7 *
8- * @method hideError
9+ * @method validate
10 */
11 validate: function() {
12 return true;
13@@ -382,6 +382,20 @@
14 },
15
16 /**
17+ * Utility method to validate that the list of selected choices is a
18+ * subset of the given list.
19+ *
20+ * @method validate_choice_among
21+ */
22+ validate_choice_among: function(superset) {
23+ var choices = this.get(
24+ "multiple") ? this.get('choice') : [this.get('choice')];
25+ return !Y.Array.some(choices, function(choice) {
26+ return (Y.Array.indexOf(superset, choice.value) === -1);
27+ });
28+ },
29+
30+ /**
31 * Remove a list of choices from the possible widget's choices.
32 *
33 * @method remove_choices
34@@ -395,12 +409,13 @@
35 }
36 );
37 // Filter out the choices mentioned.
38- choices = this.get("choices").filter(function(choice) {
39+ var newchoices = this.get("choices").filter(function(choice) {
40 return !owns(choicemap, choice.value);
41 });
42 // Set back again.
43- this.set("choices", choices);
44+ this.set("choices", newchoices);
45 // Tell everyone!
46+ this.fire("removed_choices", this, choices);
47 Y.lp.anim.green_flash({node: this.fieldNode}).run();
48 },
49
50@@ -418,7 +433,7 @@
51 }
52 );
53 // Allow existing choices to be redefined.
54- choices = this.get("choices").map(function(choice) {
55+ var newchoices = this.get("choices").map(function(choice) {
56 if (owns(choicemap, choice.value)) {
57 choice = choicemap[choice.value];
58 delete choicemap[choice.value];
59@@ -426,10 +441,11 @@
60 return choice;
61 });
62 // Add the new choices (i.e. what remains in choicemap).
63- choices = choices.concat(values(choicemap));
64+ newchoices = newchoices.concat(values(choicemap));
65 // Set back again.
66- this.set("choices", choices);
67+ this.set("choices", newchoices);
68 // Tell everyone!
69+ this.fire("added_choices", this, choices);
70 Y.lp.anim.green_flash({node: this.fieldNode}).run();
71 }
72
73@@ -499,6 +515,18 @@
74 },
75
76 /**
77+ * Validate the coherence of the contained widgets.
78+ * Subclasses can override this method. It is meant to be used when
79+ * there is validation involving more than one widget that needs to
80+ * happen.
81+ *
82+ * @method validate_all
83+ */
84+ validate_all: function() {
85+ return true;
86+ },
87+
88+ /**
89 * Validate all the registered widgets. Display a global error
90 * displaying the number of errors if any of the encapsulated widgets
91 * fails to validate.
92@@ -506,27 +534,32 @@
93 * @method validate
94 */
95 validate: function() {
96- this.hideErrors();
97- var i = 0;
98+ var i;
99 var error_number = 0;
100- for (i; i<this._widgets.length; i++) {
101+ for (i = 0; i < this._widgets.length; i++) {
102 if (this._widgets[i].validate() === false) {
103 error_number = error_number + 1;
104 }
105 }
106 if (error_number !== 0) {
107+ this.showNErrors(error_number);
108+ return false;
109+ }
110+ return this.validate_all();
111+ },
112+
113+ showNErrors: function(error_number) {
114+ this.hideError();
115+ if (error_number !== 0) {
116 if (error_number === 1) {
117 this.showError('There is 1 error.');
118 }
119 else {
120 this.showError('There are ' + error_number + ' errors.');
121 }
122- return false;
123 }
124- return true;
125 },
126
127-
128 /**
129 * Show the spinner, and hide the submit button.
130 *
131@@ -551,19 +584,33 @@
132 * @method showError
133 */
134 showError: function(error) {
135- this.hideErrors();
136+ this.hideError();
137 Y.Node.create('<p class="error message" />')
138 .appendTo(this.get("contentBox"))
139 .set("text", error);
140 },
141
142 /**
143- * Remove all errors that have been previously displayed by showError.
144+ * Remove the error that has been previously displayed by showError.
145+ *
146+ * @method hideError
147+ */
148+ hideError: function() {
149+ this.get("contentBox").all("p.error.message").remove();
150+ },
151+
152+ /**
153+ * Remove all the errors (including these displayed on the contained
154+ * widgets).
155 *
156 * @method hideErrors
157 */
158 hideErrors: function(error) {
159- this.get("contentBox").all("p.error.message").remove();
160+ this.hideError();
161+ var i;
162+ for (i = 0; i < this._widgets.length; i++) {
163+ this._widgets[i].hideError();
164+ }
165 }
166
167 });
168
169=== modified file 'lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js'
170--- lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js 2011-10-18 07:48:33 +0000
171+++ lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js 2011-10-18 07:48:34 +0000
172@@ -277,6 +277,19 @@
173 this.container.all("li > input").getAttribute("type"));
174 },
175
176+ testAddChoicesFiresEvent: function() {
177+ var event_fired = false;
178+ var handleEvent = function(e, new_choices) {
179+ event_fired = true;
180+ ArrayAssert.itemsAreEqual(["a", "b"], new_choices);
181+ };
182+ this.widget.on(
183+ this.widget.name + ":added_choices",
184+ handleEvent, this.widget);
185+ this.widget.add_choices(["a", "b"]);
186+ Assert.isTrue(event_fired);
187+ },
188+
189 testRenderRemoveChoices: function() {
190 this.widget.set("choices", ["a", "b", "c", "d"]);
191 this.widget.render(this.container);
192@@ -289,6 +302,20 @@
193 this.container.all("li > label").get("text"));
194 },
195
196+ testRemoveChoicesFiresEvent: function() {
197+ this.widget.add_choices(["a", "b", "c"]);
198+ var event_fired = false;
199+ var handleEvent = function(e, removed_choices) {
200+ event_fired = true;
201+ ArrayAssert.itemsAreEqual(["a", "b"], removed_choices);
202+ };
203+ this.widget.on(
204+ this.widget.name + ":removed_choices",
205+ handleEvent, this.widget);
206+ this.widget.remove_choices(["a", "b"]);
207+ Assert.isTrue(event_fired);
208+ },
209+
210 testRenderChoicesChangeMultiple: function() {
211 this.widget.set("choices", ["a", "b"]);
212 this.widget.render(this.container);
213@@ -500,8 +527,20 @@
214 ArrayAssert.containsMatch(
215 function(event) { return event.attrName === "choices"; },
216 events);
217+ },
218+
219+ testValidateChoicesAmong: function() {
220+ this.widget.add_choices(
221+ [{value: 'c', text: 'c', data: 'c'},
222+ {value: 'b', text: 'b', data: 'b'},
223+ {value: 'a', text: 'a', data: 'a'}
224+ ]);
225+ this.widget.set('choice', ['a', 'b']);
226+ Assert.isTrue(
227+ this.widget.validate_choice_among(['a', 'b', 'd']));
228+ Assert.isFalse(
229+ this.widget.validate_choice_among(['a', 'e', 'd']));
230 }
231-
232 };
233
234 testChoiceListWidget = Y.merge(
235@@ -565,13 +604,22 @@
236
237 testRegisterWidget: function() {
238 var fake_widget = 'Fake Widget';
239- Assert.areEqual(0, this.widget._widgets.length);
240+ var length = this.widget._widgets.length;
241 this.widget.registerWidget(fake_widget);
242- Assert.areEqual(1, this.widget._widgets.length);
243+ Assert.areEqual(
244+ length + 1, this.widget._widgets.length);
245 Assert.areSame(
246 "Fake Widget",
247- this.widget._widgets[0]);
248- },
249+ this.widget._widgets[length]);
250+ }
251+
252+ };
253+
254+ suite.add(new Y.Test.Case(testFormActionsWidget));
255+
256+
257+ var testFormActionsWidgetValidation = {
258+ name: 'TestFormActionsWidgetValidation',
259
260 createMockWidget: function(return_value) {
261 var mockWidget = Y.Mock();
262@@ -612,7 +660,6 @@
263 Y.Mock.verify(widget1);
264 Y.Mock.verify(widget2);
265 Assert.isFalse(result);
266- Y.log(this.widget.fieldNode);
267 Assert.areEqual(
268 'There is 1 error.',
269 this.widget.get("contentBox").one('.message').get('text'));
270@@ -628,7 +675,6 @@
271 Y.Mock.verify(widget1);
272 Y.Mock.verify(widget2);
273 Assert.isFalse(result);
274- Y.log(this.widget.fieldNode);
275 Assert.areEqual(
276 'There are 2 errors.',
277 this.widget.get("contentBox").one('.message').get('text'));
278@@ -636,7 +682,9 @@
279
280 };
281
282- suite.add(new Y.Test.Case(testFormActionsWidget));
283+ testFormActionsWidgetValidation = Y.merge(
284+ testFormActionsWidgetValidation, testFormActionsWidget);
285+ suite.add(new Y.Test.Case(testFormActionsWidgetValidation));
286
287 // Exports.
288 namespace.testFormRowWidget = testFormRowWidget;
289
290=== modified file 'lib/lp/registry/javascript/distroseries/initseries.js'
291--- lib/lp/registry/javascript/distroseries/initseries.js 2011-10-18 07:48:33 +0000
292+++ lib/lp/registry/javascript/distroseries/initseries.js 2011-10-18 07:48:34 +0000
293@@ -45,6 +45,8 @@
294 this.registerWidget(this.deriveFromChoices);
295 this.architectureChoice = config.architectureChoice;
296 this.registerWidget(this.architectureChoice);
297+ this.architectureIndepChoice = config.architectureIndepChoice;
298+ this.registerWidget(this.architectureIndepChoice);
299 this.packagesetChoice = config.packagesetChoice;
300 this.registerWidget(this.packagesetChoice);
301 this.packageCopyOptions = config.packageCopyOptions;
302@@ -74,12 +76,54 @@
303 },
304
305 /**
306+ * Validate the coherence of the fields. Returns true if the form is
307+ * fit for submission. Returns false otherwise and displays appropriate
308+ * errors.
309+ *
310+ * @method validate
311+ */
312+ validate: function() {
313+ this.hideErrors();
314+ var arch_indep_choice = this.architectureIndepChoice.get(
315+ 'choice').value;
316+ if (arch_indep_choice === this.architectureIndepChoice.AUTOSELECT) {
317+ // If no arch indep arch tag has been explicitely selected
318+ // check that one from the parents' is present among the selected
319+ // architectures.
320+ if (!this.architectureChoice.validate_arch_indep()) {
321+ this.architectureChoice.show_error_arch_indep();
322+ this.architectureIndepChoice.showError(
323+ 'Alternatively, you can specify the architecture ' +
324+ 'independent builder.');
325+ return false;
326+ }
327+ }
328+ else {
329+ // Check that the arch indep arch tag is among the selected
330+ // architectures.
331+ var choices_objs = this.architectureChoice.get('choice');
332+ if (choices_objs.length !== 0) {
333+ var choices = Y.Array.map(
334+ choices_objs,
335+ function(choice_obj) { return choice_obj.value; });
336+ if (!this.architectureIndepChoice.validate_choice_among(
337+ choices)) {
338+ this.architectureIndepChoice.showError(
339+ 'The selected architecture independent builder is ' +
340+ 'not among the selected architectures.');
341+ return false;
342+ }
343+ }
344+ }
345+ return true;
346+ },
347+
348+ /**
349 * Validate all the widgets and call submit as appropriate.
350 *
351 * @method submit
352 */
353 check_and_submit: function() {
354- var check = this.validate();
355 if (this.validate()) {
356 this.submit();
357 }
358@@ -93,6 +137,8 @@
359 submit: function() {
360 var self = this;
361 var values = attrselect("value");
362+ var arch_indep = values(
363+ this.architectureIndepChoice.get("choice"))[0];
364 var config = {
365 on: {
366 start: function() {
367@@ -111,6 +157,9 @@
368 parents: this.deriveFromChoices.get("parents"),
369 architectures:
370 values(this.architectureChoice.get("choice")),
371+ archindep_archtag:
372+ arch_indep === this.architectureIndepChoice.AUTOSELECT ?
373+ null : arch_indep,
374 packagesets: this.packagesetChoice !== null ?
375 values(this.packagesetChoice.get("choice")) : [],
376 rebuild:
377@@ -197,18 +246,26 @@
378 new widgets.ParentSeriesListWidget()
379 .set("name", "field.parent")
380 .set("label", "Parent Series:")
381- .set("description", (
382- "Choose and configure the parent series."))
383+ .set("description",
384+ "Choose and configure the parent series.")
385 .render(form_table_body);
386 var architecture_choice =
387 new widgets.ArchitecturesChoiceListWidget()
388 .set("name", "field.architectures")
389 .set("label", "Architectures:")
390- .set("description", (
391+ .set("description",
392 "Choose the architectures you want to " +
393 "use from the parent series (or select none " +
394 "if you want to use all the available " +
395- "architectures)."))
396+ "architectures).")
397+ .render(form_table_body);
398+ var arch_indep_choice =
399+ new widgets.ArchIndepChoiceListWidget()
400+ .set("name", "field.archindep_archtag")
401+ .set("label", "Architecture independent builder:")
402+ .set("description",
403+ "Choose the architecture tag that should be " +
404+ "used to build architecture independent binaries.")
405 .render(form_table_body);
406 var packageset_choice = null;
407 if (cache.is_first_derivation) {
408@@ -220,11 +277,11 @@
409 text: 'Packagesets help'})
410 .set("multiple", true)
411 .set("label", "Package sets to copy from parent:")
412- .set("description", (
413+ .set("description",
414 "The package sets that will be imported " +
415 "into the derived distroseries (select none " +
416 "if you want to import all the available " +
417- "package sets)."))
418+ "package sets).")
419 .render(form_table_body);
420 }
421 var package_copy_options =
422@@ -246,6 +303,7 @@
423 srcNode: form_container.one("div.actions"),
424 deriveFromChoices: parents_selection,
425 architectureChoice: architecture_choice,
426+ architectureIndepChoice: arch_indep_choice,
427 packagesetChoice: packageset_choice,
428 packageCopyOptions: package_copy_options,
429 form_container: form_container
430@@ -280,6 +338,20 @@
431 }
432 });
433
434+ form_actions.architectureChoice.on(
435+ form_actions.architectureChoice.name + ":added_choices",
436+ function(e, arch_list) {
437+ this.add_choices(arch_list);
438+ },
439+ form_actions.architectureIndepChoice);
440+
441+ form_actions.architectureChoice.on(
442+ form_actions.architectureChoice.name + ":removed_choices",
443+ function(e, arch_list) {
444+ this.remove_choices(arch_list);
445+ },
446+ form_actions.architectureIndepChoice);
447+
448 if (cache.is_first_derivation) {
449 // Wire the architecture and packageset pickers to the parent picker.
450 Y.on('parent_added', function(parent) {
451@@ -297,15 +369,15 @@
452 form_actions.packageCopyOptions.fieldNode
453 .one('input[value="rebuild"]')
454 .set('disabled', 'disabled');
455- form_actions.packageCopyOptions.set("description", (
456+ form_actions.packageCopyOptions.set("description",
457 "Note that you cannot rebuild sources because the " +
458- "distribution already has an initialized series."));
459+ "distribution already has an initialized series.");
460 // The architectures are those from the previous_series.
461- form_actions.architectureChoice.set("description", (
462+ form_actions.architectureChoice.set("description",
463 "Choose the architectures you want to " +
464 "use from the previous series (or select none " +
465 "if you want to use all the available " +
466- "architectures)."));
467+ "architectures).");
468 form_actions.architectureChoice.add_distroseries(
469 cache.previous_series);
470 // Setup the pre-selected parents (parents from the previous series).
471
472=== modified file 'lib/lp/registry/javascript/distroseries/tests/test_initseries.html'
473--- lib/lp/registry/javascript/distroseries/tests/test_initseries.html 2011-08-09 15:51:53 +0000
474+++ lib/lp/registry/javascript/distroseries/tests/test_initseries.html 2011-10-18 07:48:34 +0000
475@@ -57,5 +57,23 @@
476 <ul id="suites">
477 <li>lp.registry.distroseries.initseries.test</li>
478 </ul>
479+
480+ <script type="text/x-template" id="init-form">
481+ <div style="display:none" class="unseen"
482+ id="initseries-form-container">
483+ <div>
484+ <form action="">
485+ <table class="form" id="launchpad-form-widgets">
486+ </table>
487+ <div class="actions" id="launchpad-form-actions">
488+ <input type="submit" id="field.actions.initialize"
489+ name="field.actions.initialize"
490+ value="Initialize Series" class="button" />
491+ </div>
492+ </form>
493+ </div>
494+ </div>
495+ </script>
496+
497 </body>
498 </html>
499
500=== modified file 'lib/lp/registry/javascript/distroseries/tests/test_initseries.js'
501--- lib/lp/registry/javascript/distroseries/tests/test_initseries.js 2011-08-22 14:03:02 +0000
502+++ lib/lp/registry/javascript/distroseries/tests/test_initseries.js 2011-10-18 07:48:34 +0000
503@@ -18,6 +18,7 @@
504
505 var suite = new Y.Test.Suite("distroseries.initseries Tests");
506 var initseries = Y.lp.registry.distroseries.initseries;
507+ var widgets = Y.lp.registry.distroseries.widgets;
508
509 var testDeriveDistroSeriesActionsWidget = {
510 name: 'TestDeriveDistroSeriesActionsWidget',
511@@ -61,6 +62,13 @@
512 ];
513 }
514 },
515+ architectureIndepChoice: {
516+ AUTOSELECT: '-',
517+ get: function(name) {
518+ Assert.areEqual("choice", name);
519+ return {value: "sparc", text: "sparc"};
520+ }
521+ },
522 packagesetChoice: {
523 get: function(name) {
524 Assert.areEqual("choice", name);
525@@ -132,6 +140,9 @@
526 ["i386", "sparc"],
527 config.parameters.architectures);
528 ArrayAssert.itemsAreEqual(
529+ "sparc",
530+ config.parameters.archindep_archtag);
531+ ArrayAssert.itemsAreEqual(
532 ["4", "5"],
533 config.parameters.packagesets);
534 Assert.isTrue(config.parameters.rebuild);
535@@ -151,26 +162,97 @@
536 testDeriveDistroSeriesActionsWidget);
537 suite.add(new Y.Test.Case(testDeriveDistroSeriesActionsWidget));
538
539+
540+ var init_form = Y.one('#init-form').getContent();
541+
542 var testDeriveDistroSeriesSetup = {
543 name: 'TestDeriveDistroSeriesSetup',
544
545 setUp: function() {
546- var node = Y.Node.create(
547- '<div style="display:none;"' +
548- ' class="unseen" id="initseries-form-container">' +
549- ' <div>' +
550- ' <form action="">' +
551- ' <table class="form" id="launchpad-form-widgets">' +
552- ' </table>' +
553- ' <div class="actions" id="launchpad-form-actions">' +
554- ' <input type="submit" id="field.actions.initialize"' +
555- ' name="field.actions.initialize" ' +
556- ' value="Initialize Series" class="button" />' +
557- ' </div>' +
558- ' </form>' +
559- ' </div>' +
560- '</div>');
561- Y.one('body').appendChild(node);
562+ var node = Y.Node.create(init_form);
563+ Y.one('body').appendChild(node);
564+ },
565+
566+ setUpArches: function(arch_choices, archindep_tags) {
567+ var cache = {is_first_derivation: true};
568+ this.form_actions = initseries.setupWidgets(cache);
569+ this.form_actions.architectureChoice.set(
570+ 'choices', arch_choices);
571+ this.form_actions.architectureChoice._archindep_tags =
572+ archindep_tags;
573+ this.form_actions.architectureIndepChoice.add_choices(
574+ arch_choices);
575+ },
576+
577+ assertShowsError: function(field, expected_error_msg) {
578+ var error_msg = field.get(
579+ 'contentBox').one('.message').get('text');
580+ Assert.areEqual(expected_error_msg, error_msg);
581+ },
582+
583+ testValidateNoArchIndepChoiceOk: function() {
584+ this.setUpArches(['i386', 'hppa'], {'3': 'i386'});
585+ this.form_actions.architectureChoice.set(
586+ 'choice', 'i386');
587+ this.form_actions.architectureIndepChoice.set(
588+ 'choice', 'i386');
589+ Assert.isTrue(this.form_actions.validate());
590+ },
591+
592+ testValidateNoArchIndepChoiceFailNotAmong: function() {
593+ // Validation fails if the selected arch indep architecture
594+ // is not among the selected architectures for the derived series.
595+ this.setUpArches(['i386', 'hppa'], {'3': 'i386'});
596+ this.form_actions.architectureChoice.set(
597+ 'choice', 'i386');
598+ this.form_actions.architectureIndepChoice.set(
599+ 'choice', 'hppa');
600+ Assert.isFalse(this.form_actions.validate());
601+ this.assertShowsError(
602+ this.form_actions.architectureIndepChoice,
603+ 'The selected architecture independent builder is not ' +
604+ 'among the selected architectures.');
605+ },
606+
607+ testValidateAutoArchIndepOk: function() {
608+ this.setUpArches(['i386', 'hppa'], {'3': 'i386'});
609+ this.form_actions.architectureChoice.set(
610+ 'choice', 'i386');
611+ this.form_actions.architectureIndepChoice.set(
612+ 'choice',
613+ this.form_actions.architectureIndepChoice.AUTOSELECT);
614+ Assert.isTrue(this.form_actions.validate());
615+ },
616+
617+ testValidateAutoArchIndepOkAll: function() {
618+ // If no architecture is selected, it means that all the
619+ // architectures from the parents will be copied over.
620+ this.setUpArches(['i386', 'hppa'], {'3': 'i386'});
621+ this.form_actions.architectureIndepChoice.set(
622+ 'choice',
623+ this.form_actions.architectureIndepChoice.AUTOSELECT);
624+ Assert.isTrue(this.form_actions.validate());
625+ },
626+
627+ testValidateAutoArchIndepChoiceFail: function() {
628+ // Validation fails if the arch indep architecture is not
629+ // specified and none of the parents' arch indep architectures
630+ // has been selected.
631+ this.setUpArches(['i386', 'hppa'], {'3': 'i386'});
632+ this.form_actions.architectureChoice.set(
633+ 'choice', 'hppa');
634+ this.form_actions.architectureIndepChoice.set(
635+ 'choice',
636+ this.form_actions.architectureIndepChoice.AUTOSELECT);
637+ Assert.isFalse(this.form_actions.validate());
638+ this.assertShowsError(
639+ this.form_actions.architectureChoice,
640+ 'The distroseries has no architectures selected to build ' +
641+ 'architecture independent binaries.');
642+ this.assertShowsError(
643+ this.form_actions.architectureIndepChoice,
644+ 'Alternatively, you can specify the architecture ' +
645+ 'independent builder.');
646 },
647
648 testIsFirstDerivation: function() {
649@@ -192,6 +274,23 @@
650 form_actions.packageCopyOptions.get('choice').value);
651 },
652
653+ getFakeClient: function(res_sequence, return_obj) {
654+ var count = 0;
655+ var client = {
656+ get: function(path, config) {
657+ Assert.areEqual(path, res_sequence[count][0]);
658+ var return_obj = res_sequence[count][1];
659+ count = count + 1;
660+ if (return_obj instanceof Array) {
661+ return_obj = new Y.lp.client.Collection(
662+ null, {entries: return_obj}, null);
663+ }
664+ config.on.success(return_obj);
665+ }
666+ };
667+ return client;
668+ },
669+
670 testIsNotFirstDerivation: function() {
671 var cache = {
672 is_first_derivation: false,
673@@ -209,21 +308,19 @@
674 };
675 var form_actions = initseries.setupWidgets(cache);
676 // Monkeypatch LP client.
677- var client = {
678- get: function(path, config) {
679- Assert.areEqual(
680- "/ubuntu/natty/architectures",
681- path);
682- // Return previous_series' architectures.
683- var arches = new Y.lp.client.Collection(
684- null,
685- {entries: [
686- {'architecture_tag': 'hppa'},
687- {'architecture_tag': 'i386'}]},
688- null);
689- config.on.success(arches);
690- }
691- };
692+ var mockNominatedArchIndep = Y.Mock();
693+ Y.Mock.expect(mockNominatedArchIndep, {
694+ method: "get",
695+ args: ["architecture_tag"],
696+ returns: "i386"
697+ });
698+ var client = this.getFakeClient(
699+ [["/ubuntu/natty/architectures",
700+ [{'architecture_tag': 'hppa'},
701+ {'architecture_tag': 'i386'}]],
702+ ["/api/devel/ubuntu/natty/nominatedarchindep",
703+ mockNominatedArchIndep]]
704+ );
705 form_actions.architectureChoice.client = client;
706 initseries.setupInteraction(form_actions, cache);
707
708@@ -236,8 +333,10 @@
709 // The architecture picker features the architectures
710 // from the previous series.
711 ArrayAssert.itemsAreEqual(
712- ['hppa', 'i386'], attrselect("value")(
713+ ['hppa', 'i386'],
714+ attrselect("value")(
715 form_actions.architectureChoice.get("choices")));
716+ Y.Mock.verify(mockNominatedArchIndep);
717 }
718 };
719
720
721=== modified file 'lib/lp/registry/javascript/distroseries/tests/test_widgets.js'
722--- lib/lp/registry/javascript/distroseries/tests/test_widgets.js 2011-10-18 07:48:33 +0000
723+++ lib/lp/registry/javascript/distroseries/tests/test_widgets.js 2011-10-18 07:48:34 +0000
724@@ -173,23 +173,42 @@
725 this.container.remove(true);
726 },
727
728+ setupDistroArchSerieses: function(arches) {
729+ var distro_arch_serieses = [];
730+ var i;
731+ for (i=0; i<arches.length; i++) {
732+ distro_arch_serieses.push({architecture_tag: arches[i]});
733+ }
734+ var res = new Y.lp.client.Collection(
735+ null, {entries: distro_arch_serieses}, null);
736+ return res;
737+ },
738+
739 testAddDistroArchSerieses: function() {
740- var distro_arch_serieses = [
741- {architecture_tag: "i386"},
742- {architecture_tag: "amd64"},
743- {architecture_tag: "i386"}
744- ];
745- var distro_arch_serieses_collection =
746- new Y.lp.client.Collection(
747- null, {entries: distro_arch_serieses}, null);
748- this.widget.add_distroarchseries(
749- 3,
750- distro_arch_serieses_collection);
751+ var collection = this.setupDistroArchSerieses(
752+ ["i386", "amd64", "i386"]);
753+ this.widget.add_distroarchseries(3, collection);
754 ArrayAssert.itemsAreEqual(
755 ["amd64", "i386"],
756 attrselect("value")(this.widget.get("choices")));
757 },
758
759+ testAddDistroArchSeriesesFiresEvent: function() {
760+ var collection = this.setupDistroArchSerieses(
761+ ["i386", "amd64", "i386"]);
762+ var event_fired = false;
763+ var handleEvent = function(e, arch_list) {
764+ event_fired = true;
765+ ArrayAssert.itemsAreEqual(
766+ ["i386", "amd64", "i386"], arch_list);
767+ };
768+ this.widget.on(
769+ this.widget.name + ":added_choices",
770+ handleEvent, this.widget);
771+ this.widget.add_distroarchseries(3, collection);
772+ Assert.isTrue(event_fired);
773+ },
774+
775 test_populate_archindep_tags_InitiatesIO: function() {
776 var distroseries = {api_uri: "ubuntu/hoary", value: 3};
777 var io = false;
778@@ -208,8 +227,7 @@
779 Assert.isTrue(io, "No IO initiated.");
780 },
781
782- test_populate_archindep_tags: function() {
783- var distroseries = {api_uri: "ubuntu/hoary", value: "3"};
784+ setup_widget_client: function() {
785 this.widget.client = {
786 get: function(path, config) {
787 var nominatedarchindep = {
788@@ -221,35 +239,42 @@
789 config.on.success(nominatedarchindep);
790 }
791 };
792+ },
793+
794+ test_populate_archindep_tags: function() {
795+ this.setup_widget_client();
796+ distroseries = {api_uri: "ubuntu/hoary", value: "3"};
797 this.widget._populate_archindep_tags(distroseries);
798 Assert.areEqual("i386", this.widget._archindep_tags["3"]);
799 },
800
801- testValidateOk: function() {
802- this.widget.render(this.container);
803+ test_validate_arch_indep_Ok: function() {
804 this.widget.add_choices(['i386', 'hppa']);
805 this.widget.set('choice', ['i386']);
806 this.widget._archindep_tags['3'] = 'i386';
807- var res = this.widget.validate();
808+ var res = this.widget.validate_arch_indep();
809 Assert.isTrue(res);
810 },
811
812- testValidateOkEmpty: function() {
813- this.widget.render(this.container);
814+ test_validate_arch_indep_Ok_empty: function() {
815 this.widget.add_choices(['i386', 'hppa']);
816 this.widget.set('choice', []);
817 this.widget._archindep_tags['3'] = 'i386';
818- var res = this.widget.validate();
819+ var res = this.widget.validate_arch_indep();
820 Assert.isTrue(res);
821 },
822
823- testValidateFails: function() {
824- this.widget.render(this.container);
825+ test_validate_arch_indep_Fails: function() {
826 this.widget.add_choices(['i386', 'hppa']);
827 this.widget.set('choice', ['i386']);
828 this.widget._archindep_tags['3'] = 'hppa';
829- var res = this.widget.validate();
830+ var res = this.widget.validate_arch_indep();
831 Assert.isFalse(res);
832+ },
833+
834+ test_show_error_arch_indep: function() {
835+ this.widget.render(this.container);
836+ this.widget.show_error_arch_indep();
837 Assert.areEqual(
838 "The distroseries has no architectures selected to "
839 + "build architecture independent binaries.",
840@@ -309,6 +334,25 @@
841 Assert.isUndefined(this.widget._archindep_tags["4"]);
842 },
843
844+ testRemoveDistroSeriesFiresEvent: function() {
845+ var collection = this.setupDistroArchSerieses(
846+ ["i386", "amd64", "i386"]);
847+ this.widget.add_distroarchseries(3, collection);
848+ var collection2 = this.setupDistroArchSerieses(
849+ ["i386", "amd64", "hppa"]);
850+ this.widget.add_distroarchseries(4, collection2);
851+ var event_fired = false;
852+ var handleEvent = function(e, arch_list) {
853+ event_fired = true;
854+ ArrayAssert.itemsAreEqual(["hppa"], arch_list);
855+ };
856+ this.widget.on(
857+ this.widget.name + ":removed_choices",
858+ handleEvent, this.widget);
859+ this.widget.remove_distroseries("4");
860+ Assert.isTrue(event_fired);
861+ },
862+
863 testSetDistroSeriesError: function() {
864 var widget = this.widget;
865 widget.client = {
866@@ -331,6 +375,47 @@
867 testArchitecturesChoiceListWidget);
868 suite.add(new Y.Test.Case(testArchitecturesChoiceListWidget));
869
870+
871+ var testArchIndepChoiceListWidget = {
872+ name: 'TestArchIndepChoiceListWidget',
873+
874+ setUp: function() {
875+ this.container = Y.Node.create("<div />");
876+ this.widget = new widgets.ArchIndepChoiceListWidget();
877+ },
878+
879+ tearDown: function() {
880+ this.container.remove(true);
881+ },
882+
883+ testInitialized: function() {
884+ Assert.isFalse(this.widget.get('multiple'));
885+ ArrayAssert.itemsAreEqual(
886+ ["Auto select"],
887+ attrselect("text")(this.widget.get("choices")));
888+ ArrayAssert.itemsAreEqual(
889+ [this.widget.AUTOSELECT],
890+ attrselect("value")(this.widget.get("choices")));
891+ Assert.areEqual(
892+ this.widget.AUTOSELECT,
893+ attrselect("value")(this.widget.get('choice')));
894+ },
895+
896+ test_default_select_called: function() {
897+ this.widget.add_choices(['4', '5']);
898+ this.widget.set('choice', '5');
899+ this.widget.remove_choices(['5']);
900+ Y.log(this.widget.get('choice'));
901+ Assert.areEqual(
902+ this.widget.AUTOSELECT,
903+ attrselect("value")(this.widget.get('choice')));
904+ }
905+
906+ };
907+
908+ suite.add(new Y.Test.Case(testArchIndepChoiceListWidget));
909+
910+
911 var testPackagesetPickerWidget = {
912 name: 'TestPackagesetPickerWidget',
913
914
915=== modified file 'lib/lp/registry/javascript/distroseries/widgets.js'
916--- lib/lp/registry/javascript/distroseries/widgets.js 2011-10-18 07:48:33 +0000
917+++ lib/lp/registry/javascript/distroseries/widgets.js 2011-10-18 07:48:34 +0000
918@@ -387,7 +387,7 @@
919
920 /**
921 * Given a newly added distroseries object, populate the
922- * _archindep_tags field with it's architecture independent arch_tag.
923+ * _archindep_tags field with its architecture independent arch_tag.
924 *
925 * @method _populate_archindep_tags
926 * @param {Object} distroseries The distroseries object
927@@ -400,21 +400,31 @@
928 var on = {
929 success: function (nominatedarchindep) {
930 var arch_tag = nominatedarchindep.get('architecture_tag');
931- self._archindep_tags[distroseries.value] = arch_tag;
932+ self._add_archindep_archtag_for_series(
933+ distroseries.value, arch_tag);
934 },
935 failure: this.error_handler.getFailureHandler()
936 };
937 this.client.get(path, {on: on});
938 },
939
940+ _add_archindep_archtag_for_series: function(distroseries_id, arch_tag) {
941+ this._archindep_tags[distroseries_id] = arch_tag;
942+ },
943+
944+ _remove_archindep_archtag_for_series: function(distroseries_id) {
945+ delete this._archindep_tags[distroseries_id];
946+ },
947+
948 /**
949 * Validate the architectures choice: make sure that at least one
950 * architecture is suitable to build architecture independent packages.
951+ * This will be called only if no architecture independent architecture
952+ * arch tag is selected on another widget.
953 *
954- * @method validate
955+ * @method validate_arch_indep
956 */
957- validate: function() {
958- this.hideError();
959+ validate_arch_indep: function() {
960 var choice_objects = this.get('choice');
961 var choices = choice_objects.map(
962 function(choice_object) { return choice_object.value; });
963@@ -434,10 +444,14 @@
964 }
965 }
966 }
967+ return false;
968+ },
969+
970+ show_error_arch_indep: function() {
971+ this.hideError();
972 this.showError(
973 ["The distroseries has no architectures selected to ",
974 "build architecture independent binaries."].join(''));
975- return false;
976 },
977
978 /**
979@@ -448,7 +462,7 @@
980 */
981 remove_distroseries: function(distroseries_id) {
982 // Cleanup the corresponding _archindep_tags' entry.
983- delete this._archindep_tags[distroseries_id];
984+ this._remove_archindep_archtag_for_series(distroseries_id);
985 // Compute which das is only in the distroseries to be removed.
986 var arch_to_remove = [];
987 var das = this._distroseries[distroseries_id];
988@@ -503,6 +517,67 @@
989
990
991 /**
992+ * A special form of ChoiceListWidget for choosing the architecture
993+ * independent architecture tag. It is initially only populated with
994+ * the 'Auto select' option which means that the architecture independent
995+ * tag will be selected among the parents'.
996+ *
997+ * @class ArchIndepChoiceListWidget
998+ */
999+var ArchIndepChoiceListWidget;
1000+
1001+ArchIndepChoiceListWidget = function() {
1002+ ArchIndepChoiceListWidget
1003+ .superclass.constructor.apply(this, arguments);
1004+};
1005+
1006+Y.mix(ArchIndepChoiceListWidget, {
1007+
1008+ NAME: 'archIndepChoiceListWidget',
1009+
1010+ ATTRS: {
1011+ }
1012+
1013+});
1014+
1015+Y.extend(ArchIndepChoiceListWidget, formwidgets.ChoiceListWidget, {
1016+
1017+ AUTOSELECT: '-',
1018+
1019+ initializer: function(config) {
1020+ this.client = new Y.lp.client.Launchpad();
1021+ this.error_handler = new Y.lp.client.ErrorHandler();
1022+ this.error_handler.clearProgressUI = Y.bind(this.hideSpinner, this);
1023+ this.error_handler.showError = Y.bind(this.showError, this);
1024+ this._distroseries = {};
1025+ this.set('multiple', false);
1026+ this.add_choices([{value:this.AUTOSELECT, text:'Auto select'}]);
1027+ this.default_select();
1028+
1029+ this.on(
1030+ this.name + ":removed_choices",
1031+ Y.rbind(this.default_select, this));
1032+ },
1033+
1034+
1035+ /**
1036+ * Make sure that one choice is selected. If none is selected, then
1037+ * select the default choice ('Auto select').
1038+ *
1039+ * @method default_select
1040+ */
1041+ default_select: function() {
1042+ if (!Y.Lang.isValue(this.get('choice'))) {
1043+ this.set('choice', this.AUTOSELECT);
1044+ }
1045+ }
1046+
1047+});
1048+
1049+namespace.ArchIndepChoiceListWidget = ArchIndepChoiceListWidget;
1050+
1051+
1052+/**
1053 * A special form of ChoiceListWidget for choosing packagesets.
1054 *
1055 * @class PackagesetPickerWidget