Merge lp:~rvb/launchpad/bug-869221-ui 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-ui
Merge into: lp:launchpad
Prerequisite: lp:~rvb/launchpad/bug-869221-expose-nominatedarchindep
Diff against target: 433 lines (+293/-11)
5 files modified
lib/lp/app/javascript/formwidgets/formwidgets.js (+69/-3)
lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js (+80/-6)
lib/lp/registry/javascript/distroseries/initseries.js (+18/-2)
lib/lp/registry/javascript/distroseries/tests/test_widgets.js (+67/-0)
lib/lp/registry/javascript/distroseries/widgets.js (+59/-0)
To merge this branch: bzr merge lp:~rvb/launchpad/bug-869221-ui
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+78802@code.launchpad.net

Commit message

[r=benji][bug=869221] Force the user to select at least one architecture that can be used to build architecture independent packages.

Description of the change

This branch is another branch towards a proper fix for #869221. It changes the ui (+initseries) so that the user will get an error if no architecture suitable to build architecture independent packages is selected.

To achieve that it:
- changes the way FormRowWidget.showError displays error to mimic what non js LaunchpadFormView does;
- add a default validate method to FormRowWidget;
- modify FormActionsWidget to be able to register widgets and call validate on each of these 'encapsulated' widgets;
- add the required methods in ArchitecturesChoiceListWidget to fetch the nominatedarchindep from each parent and use them to validate the architecture list;
- Change lib/lp/registry/javascript/distroseries/initseries.js to register all the widgets and call the validation method before submitting the form.

= Tests =

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

= QA =

Create a new series and go to ../myseries/+initseries. Inherit from two series and make sure that if none of the arch-indep builders architecture are selected, the initialization is not possible.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good.

I can't claim it's better but I thought you might like to see another
way to use call chaining in showError:

    this.fieldNode
        .append(Y.Node.create('<div />')
            .addClass('message')
            .set("text", error))
        .ancestor('tr')
            .addClass('error');

To my eyes the above makes the structure of what's happening a little
clearer; not that it was particularly obscure to start with. The
structural subscription JS uses this pattern to good effect.

Line 233 of the diff is indented by one space too many. Oh, and you
don't really need the "check" variable.

    check_and_submit: function() {
        if (this.validate()) {
            this.submit();
        }
    }

The join in testValidateFails surprised me. Is there a reason string
concatenation won't work there?

    Assert.areEqual(
        "The distroseries has no architectures selected to "
        + "build architecture independent binaries.",
        this.widget.fieldNode.one('.message').get('text'));

review: Approve (code)
Revision history for this message
Raphaël Badin (rvb) wrote :

> I can't claim it's better but I thought you might like to see another
> way to use call chaining in showError:
>
> this.fieldNode
> .append(Y.Node.create('<div />')
> .addClass('message')
> .set("text", error))
> .ancestor('tr')
> .addClass('error');
>
> To my eyes the above makes the structure of what's happening a little
> clearer; not that it was particularly obscure to start with. The
> structural subscription JS uses this pattern to good effect.

I agree, it's more compact this way.

> Line 233 of the diff is indented by one space too many. Oh, and you
> don't really need the "check" variable.
>
> check_and_submit: function() {
> if (this.validate()) {
> this.submit();
> }
> }

Fixed.

> The join in testValidateFails surprised me. Is there a reason string
> concatenation won't work there?
>
> Assert.areEqual(
> "The distroseries has no architectures selected to "
> + "build architecture independent binaries.",
> this.widget.fieldNode.one('.message').get('text'));

Force of habit I guess ;) I've changed that.

Thanks for the review.

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-08-10 10:23:57 +0000
3+++ lib/lp/app/javascript/formwidgets/formwidgets.js 2011-10-18 07:37:29 +0000
4@@ -158,9 +158,35 @@
5 * @method showError
6 */
7 showError: function(error) {
8- var message = Y.Node.create('<p />').set("text", error);
9- this.fieldNode.empty().append(message);
10- Y.lp.anim.red_flash({node: message}).run();
11+ this.fieldNode
12+ .append(Y.Node.create('<div />')
13+ .addClass('message')
14+ .set("text", error))
15+ .ancestor('tr')
16+ .addClass('error');
17+ Y.lp.anim.red_flash({node: this.fieldNode.one('.message')}).run();
18+ },
19+
20+ /**
21+ * Hide any previously displayed error.
22+ *
23+ * @method hideError
24+ */
25+ hideError: function(error) {
26+ this.fieldNode.all('.message').remove();
27+ this.fieldNode.ancestor('tr').removeClass('error');
28+ },
29+
30+ /**
31+ * Validate this widget. Subclasses can override this method.
32+ * It should return true if the widget validates and false otherwise.
33+ * It is also responsible for setting any error on the widget using
34+ * this.showError.
35+ *
36+ * @method hideError
37+ */
38+ validate: function() {
39+ return true;
40 }
41
42 });
43@@ -458,11 +484,50 @@
44 this.error_handler.clearProgressUI = Y.bind(this.hideSpinner, this);
45 this.error_handler.showError = Y.bind(this.showError, this);
46 this.submitButtonNode = config.submitButtonNode;
47+ this._widgets = [];
48 this.spinnerNode = Y.Node.create(
49 '<img src="/@@/spinner" alt="Loading..." />');
50 },
51
52 /**
53+ * Register a widget as part of this encapsulating widget.
54+ *
55+ * @method registerWidget
56+ */
57+ registerWidget: function(widget) {
58+ this._widgets.push(widget);
59+ },
60+
61+ /**
62+ * Validate all the registered widgets. Display a global error
63+ * displaying the number of errors if any of the encapsulated widgets
64+ * fails to validate.
65+ *
66+ * @method validate
67+ */
68+ validate: function() {
69+ this.hideErrors();
70+ var i = 0;
71+ var error_number = 0;
72+ for (i; i<this._widgets.length; i++) {
73+ if (this._widgets[i].validate() === false) {
74+ error_number = error_number + 1;
75+ }
76+ }
77+ if (error_number !== 0) {
78+ if (error_number === 1) {
79+ this.showError('There is 1 error.');
80+ }
81+ else {
82+ this.showError('There are ' + error_number + ' errors.');
83+ }
84+ return false;
85+ }
86+ return true;
87+ },
88+
89+
90+ /**
91 * Show the spinner, and hide the submit button.
92 *
93 * @method showSpinner
94@@ -486,6 +551,7 @@
95 * @method showError
96 */
97 showError: function(error) {
98+ this.hideErrors();
99 Y.Node.create('<p class="error message" />')
100 .appendTo(this.get("contentBox"))
101 .set("text", error);
102
103=== modified file 'lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js'
104--- lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js 2011-08-09 16:31:48 +0000
105+++ lib/lp/app/javascript/formwidgets/tests/test_formwidgets.js 2011-10-18 07:37:29 +0000
106@@ -159,10 +159,20 @@
107 },
108
109 testShowError: function() {
110+ this.widget.render(this.container);
111 this.widget.showError("Unrealistic expectations.");
112 Assert.areSame(
113 "Unrealistic expectations.",
114- this.widget.fieldNode.one("p").get("text"));
115+ this.widget.fieldNode.one("div.message").get("text"));
116+ Assert.isTrue(this.container.one('tr').hasClass('error'));
117+ },
118+
119+ testHideError: function() {
120+ this.widget.render(this.container);
121+ this.widget.showError("Unrealistic expectations.");
122+ this.widget.hideError();
123+ Assert.isNull(this.widget.fieldNode.one("div.message"));
124+ Assert.isFalse(this.container.one('tr').hasClass('error'));
125 }
126
127 };
128@@ -553,11 +563,75 @@
129 this.actions.one("p.error.message").get("text"));
130 },
131
132- testHideErrors: function() {
133- this.widget.showError("The Man From U.N.C.L.E.");
134- this.widget.showError("The Woman From A.U.N.T.I.E.");
135- this.widget.hideErrors();
136- Assert.isNull(this.actions.one("p.error.message"));
137+ testRegisterWidget: function() {
138+ var fake_widget = 'Fake Widget';
139+ Assert.areEqual(0, this.widget._widgets.length);
140+ this.widget.registerWidget(fake_widget);
141+ Assert.areEqual(1, this.widget._widgets.length);
142+ Assert.areSame(
143+ "Fake Widget",
144+ this.widget._widgets[0]);
145+ },
146+
147+ createMockWidget: function(return_value) {
148+ var mockWidget = Y.Mock();
149+ Y.Mock.expect(mockWidget, {
150+ method: "validate",
151+ returns: return_value
152+ });
153+ return mockWidget;
154+ },
155+
156+ testValidateCallsValidateOnWidget: function() {
157+ var widget = this.createMockWidget(true);
158+ this.widget.registerWidget(widget);
159+ var result = this.widget.validate();
160+ Y.Mock.verify(widget);
161+ Assert.isTrue(result);
162+ },
163+
164+ testValidateCallsValidateOnWidgets: function() {
165+ var widget1 = this.createMockWidget(true);
166+ var widget2 = this.createMockWidget(true);
167+ this.widget.registerWidget(widget1);
168+ this.widget.registerWidget(widget2);
169+ var result = this.widget.validate();
170+ Y.Mock.verify(widget1);
171+ Y.Mock.verify(widget2);
172+ Assert.isTrue(result);
173+ Assert.isNull(this.widget.get("contentBox").one('.message'));
174+ },
175+
176+ testValidateOneWidgetFails: function() {
177+ this.widget.render(this.container);
178+ var widget1 = this.createMockWidget(true);
179+ var widget2 = this.createMockWidget(false);
180+ this.widget.registerWidget(widget1);
181+ this.widget.registerWidget(widget2);
182+ var result = this.widget.validate();
183+ Y.Mock.verify(widget1);
184+ Y.Mock.verify(widget2);
185+ Assert.isFalse(result);
186+ Y.log(this.widget.fieldNode);
187+ Assert.areEqual(
188+ 'There is 1 error.',
189+ this.widget.get("contentBox").one('.message').get('text'));
190+ },
191+
192+ testValidateManyWidgetsFail: function() {
193+ this.widget.render(this.container);
194+ var widget1 = this.createMockWidget(false);
195+ var widget2 = this.createMockWidget(false);
196+ this.widget.registerWidget(widget1);
197+ this.widget.registerWidget(widget2);
198+ var result = this.widget.validate();
199+ Y.Mock.verify(widget1);
200+ Y.Mock.verify(widget2);
201+ Assert.isFalse(result);
202+ Y.log(this.widget.fieldNode);
203+ Assert.areEqual(
204+ 'There are 2 errors.',
205+ this.widget.get("contentBox").one('.message').get('text'));
206 }
207
208 };
209
210=== modified file 'lib/lp/registry/javascript/distroseries/initseries.js'
211--- lib/lp/registry/javascript/distroseries/initseries.js 2011-08-22 14:03:02 +0000
212+++ lib/lp/registry/javascript/distroseries/initseries.js 2011-10-18 07:37:29 +0000
213@@ -42,9 +42,13 @@
214 initializer: function(config) {
215 this.context = config.context;
216 this.deriveFromChoices = config.deriveFromChoices;
217+ this.registerWidget(this.deriveFromChoices);
218 this.architectureChoice = config.architectureChoice;
219+ this.registerWidget(this.architectureChoice);
220 this.packagesetChoice = config.packagesetChoice;
221+ this.registerWidget(this.packagesetChoice);
222 this.packageCopyOptions = config.packageCopyOptions;
223+ this.registerWidget(this.packageCopyOptions);
224 this.form_container = config.form_container;
225 },
226
227@@ -70,6 +74,18 @@
228 },
229
230 /**
231+ * Validate all the widgets and call submit as appropriate.
232+ *
233+ * @method submit
234+ */
235+ check_and_submit: function() {
236+ var check = this.validate();
237+ if (this.validate()) {
238+ this.submit();
239+ }
240+ },
241+
242+ /**
243 * Call deriveDistroSeries via the API.
244 *
245 * @method submit
246@@ -299,10 +315,10 @@
247 form_actions.deriveFromChoices);
248 }
249
250- // Wire up the form submission.
251+ // Wire up the form check and submission.
252 form_actions.form_container.one("form").on(
253 "submit", function(event) {
254- event.halt(); form_actions.submit(); });
255+ event.halt(); form_actions.check_and_submit(); });
256
257 // Show the form.
258 form_actions.form_container.removeClass("unseen");
259
260=== modified file 'lib/lp/registry/javascript/distroseries/tests/test_widgets.js'
261--- lib/lp/registry/javascript/distroseries/tests/test_widgets.js 2011-08-09 14:32:42 +0000
262+++ lib/lp/registry/javascript/distroseries/tests/test_widgets.js 2011-10-18 07:37:29 +0000
263@@ -190,6 +190,72 @@
264 attrselect("value")(this.widget.get("choices")));
265 },
266
267+ test_populate_archindep_tags_InitiatesIO: function() {
268+ var distroseries = {api_uri: "ubuntu/hoary", value: 3};
269+ var io = false;
270+ this.widget.client = {
271+ get: function(path, config) {
272+ io = true;
273+ Assert.areEqual(
274+ "/api/devel/ubuntu/hoary/nominatedarchindep",
275+ path);
276+ Assert.isObject(config.on);
277+ Assert.isFunction(config.on.success);
278+ Assert.isFunction(config.on.failure);
279+ }
280+ };
281+ this.widget._populate_archindep_tags(distroseries);
282+ Assert.isTrue(io, "No IO initiated.");
283+ },
284+
285+ test_populate_archindep_tags: function() {
286+ var distroseries = {api_uri: "ubuntu/hoary", value: "3"};
287+ this.widget.client = {
288+ get: function(path, config) {
289+ var nominatedarchindep = {
290+ get: function(key) {
291+ Assert.areEqual("architecture_tag", key);
292+ return 'i386';
293+ }
294+ };
295+ config.on.success(nominatedarchindep);
296+ }
297+ };
298+ this.widget._populate_archindep_tags(distroseries);
299+ Assert.areEqual("i386", this.widget._archindep_tags["3"]);
300+ },
301+
302+ testValidateOk: function() {
303+ this.widget.render(this.container);
304+ this.widget.add_choices(['i386', 'hppa']);
305+ this.widget.set('choice', ['i386']);
306+ this.widget._archindep_tags['3'] = 'i386';
307+ var res = this.widget.validate();
308+ Assert.isTrue(res);
309+ },
310+
311+ testValidateOkEmpty: function() {
312+ this.widget.render(this.container);
313+ this.widget.add_choices(['i386', 'hppa']);
314+ this.widget.set('choice', []);
315+ this.widget._archindep_tags['3'] = 'i386';
316+ var res = this.widget.validate();
317+ Assert.isTrue(res);
318+ },
319+
320+ testValidateFails: function() {
321+ this.widget.render(this.container);
322+ this.widget.add_choices(['i386', 'hppa']);
323+ this.widget.set('choice', ['i386']);
324+ this.widget._archindep_tags['3'] = 'hppa';
325+ var res = this.widget.validate();
326+ Assert.isFalse(res);
327+ Assert.areEqual(
328+ "The distroseries has no architectures selected to "
329+ + "build architecture independent binaries.",
330+ this.widget.fieldNode.one('.message').get('text'));
331+ },
332+
333 testAddDistroSeriesInitiatesIO: function() {
334 var io = false;
335 this.widget.client = {
336@@ -240,6 +306,7 @@
337 ArrayAssert.itemsAreEqual(
338 ["amd64", "i386"],
339 values(this.widget.get("choices")));
340+ Assert.isUndefined(this.widget._archindep_tags["4"]);
341 },
342
343 testSetDistroSeriesError: function() {
344
345=== modified file 'lib/lp/registry/javascript/distroseries/widgets.js'
346--- lib/lp/registry/javascript/distroseries/widgets.js 2011-08-09 16:19:19 +0000
347+++ lib/lp/registry/javascript/distroseries/widgets.js 2011-10-18 07:37:29 +0000
348@@ -347,6 +347,7 @@
349 this.error_handler.clearProgressUI = Y.bind(this.hideSpinner, this);
350 this.error_handler.showError = Y.bind(this.showError, this);
351 this._distroseries = {};
352+ this._archindep_tags = {};
353 this.clean_display();
354 },
355
356@@ -377,6 +378,7 @@
357 var on = {
358 success: function (results) {
359 self.add_distroarchseries(distroseries_id, results);
360+ self._populate_archindep_tags(distroseries);
361 },
362 failure: this.error_handler.getFailureHandler()
363 };
364@@ -384,12 +386,69 @@
365 },
366
367 /**
368+ * Given a newly added distroseries object, populate the
369+ * _archindep_tags field with it's architecture independent arch_tag.
370+ *
371+ * @method _populate_archindep_tags
372+ * @param {Object} distroseries The distroseries object
373+ * ({value:distroseries_id, api_uri:distroseries_uri}).
374+ */
375+ _populate_archindep_tags: function(distroseries) {
376+ var path = Y.lp.client.get_field_uri(
377+ distroseries.api_uri, 'nominatedarchindep');
378+ var self = this;
379+ var on = {
380+ success: function (nominatedarchindep) {
381+ var arch_tag = nominatedarchindep.get('architecture_tag');
382+ self._archindep_tags[distroseries.value] = arch_tag;
383+ },
384+ failure: this.error_handler.getFailureHandler()
385+ };
386+ this.client.get(path, {on: on});
387+ },
388+
389+ /**
390+ * Validate the architectures choice: make sure that at least one
391+ * architecture is suitable to build architecture independent packages.
392+ *
393+ * @method validate
394+ */
395+ validate: function() {
396+ this.hideError();
397+ var choice_objects = this.get('choice');
398+ var choices = choice_objects.map(
399+ function(choice_object) { return choice_object.value; });
400+ if (choice_objects.length === 0) {
401+ // None architectures selected implies that all the architectures
402+ // are copied over so we're sure that the arch-indep arch from one
403+ // of the parents is among them.
404+ return true;
405+ }
406+ var ds, choice;
407+ for (ds in this._archindep_tags) {
408+ var arch_tag = this._archindep_tags[ds];
409+ var i = 0;
410+ for (i; i<choices.length; i++) {
411+ if (Y.Lang.isValue(choices[i]) && choices[i] === arch_tag) {
412+ return true;
413+ }
414+ }
415+ }
416+ this.showError(
417+ ["The distroseries has no architectures selected to ",
418+ "build architecture independent binaries."].join(''));
419+ return false;
420+ },
421+
422+ /**
423 * Remove a parent distroseries, remove the architectures only
424 * present in this parent series from the possible choices.
425 *
426 * @method remove_distroseries
427 */
428 remove_distroseries: function(distroseries_id) {
429+ // Cleanup the corresponding _archindep_tags' entry.
430+ delete this._archindep_tags[distroseries_id];
431 // Compute which das is only in the distroseries to be removed.
432 var arch_to_remove = [];
433 var das = this._distroseries[distroseries_id];