Merge lp:~blake-rouse/maas/add-system-to-node-html-js into lp:maas

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: 2301
Merged at revision: 2384
Proposed branch: lp:~blake-rouse/maas/add-system-to-node-html-js
Merge into: lp:maas
Prerequisite: lp:~blake-rouse/maas/add-osystem-to-node-form-api
Diff against target: 546 lines (+456/-2)
8 files modified
src/maasserver/context_processors.py (+1/-0)
src/maasserver/static/js/node_add.js (+19/-1)
src/maasserver/static/js/os_distro_select.js (+155/-0)
src/maasserver/static/js/tests/test_os_distro_select.html (+38/-0)
src/maasserver/static/js/tests/test_os_distro_select.js (+198/-0)
src/maasserver/templates/maasserver/node_edit.html (+9/-1)
src/maasserver/templates/maasserver/settings.html (+32/-0)
src/maasserver/templates/maasserver/snippets.html (+4/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/add-system-to-node-html-js
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+219526@code.launchpad.net

Commit message

Add html and js to show operating system in WebUI. Release field is not dependent on the selected operating system field.

Description of the change

This replaces the large merge of add-osystem-to-node, and separates it into 3 different mergers. This is the third one.

In the WebUI you can select an operating system and release version from the dropdowns. The release versions change based on the selected operating system, so it looks correct, and removes the possibility to select an operating system with an incorrect release.

Note: osystem was used throughout the code instead of os, as the python os module would conflict throughout the code base.

To post a comment you must log in.
2299. By Blake Rouse

Merge add-osystem-to-node-form-api.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Good stuff. Well-documented, well-factored, and the description gives a nice perspective. The smaller branches are appreciated. Most of my review notes are about comments, so no biggies.

This is my first time trying Launchpad's inline comments... no idea how they will turn out. I'm trying not to nitpick on impulse. That's an easy trap to fall into when you can just double-click some code and start typing. :-)

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Ah. I need to tick the “Publish inline comments” box.

2300. By Blake Rouse

Fix test for os_distro_select.

2301. By Blake Rouse

Merge node-form-api + trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/context_processors.py'
2--- src/maasserver/context_processors.py 2014-04-01 06:54:45 +0000
3+++ src/maasserver/context_processors.py 2014-05-29 17:10:27 +0000
4@@ -63,6 +63,7 @@
5 'js/node_views.js',
6 'js/longpoll.js',
7 'js/enums.js',
8+ 'js/os_distro_select.js',
9 'js/power_parameters.js',
10 'js/nodes_chart.js',
11 'js/reveal.js',
12
13=== modified file 'src/maasserver/static/js/node_add.js'
14--- src/maasserver/static/js/node_add.js 2014-03-03 06:33:34 +0000
15+++ src/maasserver/static/js/node_add.js 2014-05-29 17:10:27 +0000
16@@ -228,11 +228,28 @@
17 var heading = Y.Node.create('<h2 />')
18 .set('text', "Add node");
19 this.get('srcNode').append(heading).append(this.createForm());
20+ this.setUpDistroSeriesField();
21 this.setUpPowerParameterField();
22 this.initializeNodes();
23 },
24
25 /**
26+ * If the 'distro_series' field is present, link it to operating
27+ * system field.
28+ *
29+ * @method setUpDistroSeriesField
30+ */
31+ setUpDistroSeriesField: function() {
32+ if (Y.Lang.isValue(Y.one('#id_distro_series'))) {
33+ var widget = new Y.maas.os_distro_select.OSReleaseWidget({
34+ srcNode: '#id_distro_series'
35+ });
36+ widget.bindTo(Y.one('#id_osystem'), 'change');
37+ widget.render();
38+ }
39+ },
40+
41+ /**
42 * If the 'power_type' field is present, setup the linked
43 * 'power_parameter' field.
44 *
45@@ -415,5 +432,6 @@
46 };
47
48 }, '0.1', {'requires': ['io', 'node', 'widget', 'event', 'event-custom',
49- 'maas.morph', 'maas.enums', 'maas.power_parameters']}
50+ 'maas.morph', 'maas.enums', 'maas.power_parameters',
51+ 'maas.os_distro_select']}
52 );
53
54=== added file 'src/maasserver/static/js/os_distro_select.js'
55--- src/maasserver/static/js/os_distro_select.js 1970-01-01 00:00:00 +0000
56+++ src/maasserver/static/js/os_distro_select.js 2014-05-29 17:10:27 +0000
57@@ -0,0 +1,155 @@
58+/* Copyright 2014 Canonical Ltd. This software is licensed under the
59+ * GNU Affero General Public License version 3 (see the file LICENSE).
60+ *
61+ * OS/Release seletion utilities.
62+ *
63+ * @module Y.maas.power_parameter
64+ */
65+
66+YUI.add('maas.os_distro_select', function(Y) {
67+
68+Y.log('loading maas.os_distro_select');
69+var module = Y.namespace('maas.os_distro_select');
70+
71+// Only used to mockup io in tests.
72+module._io = new Y.IO();
73+
74+var OSReleaseWidget;
75+
76+/**
77+ * A widget class that modifies the viewable options of the release
78+ * field, based on the selected operating system field.
79+ *
80+ */
81+OSReleaseWidget = function() {
82+ OSReleaseWidget.superclass.constructor.apply(this, arguments);
83+};
84+
85+OSReleaseWidget.NAME = 'os-release-widget';
86+
87+Y.extend(OSReleaseWidget, Y.Widget, {
88+
89+ /**
90+ * Initialize the widget.
91+ * - cfg.srcNode is the node which will be updated when the selected
92+ * value of the 'os node' will change.
93+ * - cfg.osNode is the node containing a 'select' element. When
94+ * the selected element will change, the srcNode HTML will be
95+ * updated.
96+ *
97+ * @method initializer
98+ */
99+ initializer: function(cfg) {
100+ this.initialSkip = true;
101+ },
102+
103+ /**
104+ * Bind the widget to events (to name 'event_name') generated by the given
105+ * 'osNode'.
106+ *
107+ * @method bindTo
108+ */
109+ bindTo: function(osNode, event_name) {
110+ var self = this;
111+ Y.one(osNode).on(event_name, function(e) {
112+ var osValue = e.currentTarget.get('value');
113+ self.switchTo(osValue);
114+ });
115+ var osValue = Y.one(osNode).get('value');
116+ self.switchTo(osValue);
117+ },
118+
119+ /**
120+ * React to a new value of the os node: update the HTML of
121+ * 'srcNode'.
122+ *
123+ * @method switchTo
124+ */
125+ switchTo: function(newOSValue) {
126+ var srcNode = this.get('srcNode');
127+ var options = srcNode.all('option');
128+ var selected = false;
129+ Y.Array.each(options, function(option) {
130+ var sel = this.modifyOption(option, newOSValue);
131+ if(selected == false) {
132+ selected = sel;
133+ }
134+ }, this);
135+
136+ // We skip selection on first load, as Django will already
137+ // provide the users, current selection. Without this the
138+ // current selection will be clobered.
139+ if(this.initialSkip == true) {
140+ this.initialSkip = false;
141+ return;
142+ }
143+
144+ // See if a selection was made, if not then we need
145+ // to select the first visible as a default is not
146+ // present.
147+ if(!selected) {
148+ this.selectVisableOption(options);
149+ }
150+ },
151+
152+ /**
153+ * Modify an option to make it visible or hidden. Returns true
154+ * if the method also make the selection active.
155+ *
156+ * @method modifyOption
157+ */
158+ modifyOption: function(option, newOSValue) {
159+ var selected = false;
160+ var value = option.get('value');
161+ var split_value = value.split("/");
162+
163+ // If "Default OS" is selected, then
164+ // only show "Default OS Release".
165+ if(newOSValue == '') {
166+ if(value == '') {
167+ option.removeClass('hidden');
168+ option.set('selected', 'selected');
169+ }
170+ else {
171+ option.addClass('hidden');
172+ }
173+ }
174+ else {
175+ if(split_value[0] == newOSValue) {
176+ option.removeClass('hidden');
177+ if(split_value[1] == '' && !this.initialSkip) {
178+ selected = true;
179+ option.set('selected', 'selected');
180+ }
181+ }
182+ else {
183+ option.addClass('hidden');
184+ }
185+ }
186+ return selected;
187+ },
188+
189+ /**
190+ * Selected the first option that is not hidden.
191+ *
192+ * @method selectVisableOption
193+ */
194+ selectVisableOption: function(options) {
195+ var first_option = null;
196+ Y.Array.each(options, function(option) {
197+ if(!option.hasClass('hidden')) {
198+ if(first_option == null) {
199+ first_option = option;
200+ }
201+ }
202+ });
203+ if(first_option != null) {
204+ first_option.set('selected', 'selected');
205+ }
206+ }
207+});
208+
209+module.OSReleaseWidget = OSReleaseWidget;
210+
211+}, '0.1', {'requires': ['widget', 'io']}
212+);
213
214=== added file 'src/maasserver/static/js/tests/test_os_distro_select.html'
215--- src/maasserver/static/js/tests/test_os_distro_select.html 1970-01-01 00:00:00 +0000
216+++ src/maasserver/static/js/tests/test_os_distro_select.html 2014-05-29 17:10:27 +0000
217@@ -0,0 +1,38 @@
218+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
219+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
220+ <head>
221+ <title>Test maas.os_distro_select</title>
222+
223+ <!-- YUI and test setup -->
224+ <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
225+ <script type="text/javascript" src="/usr/share/javascript/yui3/yui/yui.js"></script>
226+ <script type="text/javascript" src="../testing/testrunner.js"></script>
227+ <script type="text/javascript" src="../testing/testing.js"></script>
228+ <!-- The module under test -->
229+ <script type="text/javascript" src="../os_distro_select.js"></script>
230+ <!-- The test suite -->
231+ <script type="text/javascript" src="test_os_distro_select.js"></script>
232+ </head>
233+ <body>
234+ <span id="suite">maas.os_distro_select.tests</span>
235+ <script type="text/x-template" id="select_node">
236+ <select id="id_osystem">
237+ <option value="" selected="selected">Default OS</option>
238+ <option value="value1">Value1</option>
239+ <option value="value2">Value2</option>
240+ </select>
241+ </script>
242+ <script type="text/x-template" id="target_node">
243+ <select id="id_distro_series">
244+ <option value="" selected="selected">Default Release</option>
245+ <option value="value1/series1">Value1Series1</option>
246+ <option value="value1/series2">Value1Series2</option>
247+ <option value="value1/series3">Value1Series3</option>
248+ <option value="value2/series1">Value2Series1</option>
249+ <option value="value2/series2">Value2Series2</option>
250+ <option value="value2/series3">Value2Series3</option>
251+ </select>
252+ </script>
253+ <div id="placeholder"></div>
254+ </body>
255+</html>
256
257=== added file 'src/maasserver/static/js/tests/test_os_distro_select.js'
258--- src/maasserver/static/js/tests/test_os_distro_select.js 1970-01-01 00:00:00 +0000
259+++ src/maasserver/static/js/tests/test_os_distro_select.js 2014-05-29 17:10:27 +0000
260@@ -0,0 +1,198 @@
261+/* Copyright 2014 Canonical Ltd. This software is licensed under the
262+ * GNU Affero General Public License version 3 (see the file LICENSE).
263+ */
264+
265+YUI({ useBrowserConsole: true }).add(
266+ 'maas.os_distro_select.tests', function(Y) {
267+
268+Y.log('loading maas.os_distro_select.tests');
269+var namespace = Y.namespace('maas.os_distro_select.tests');
270+
271+var module = Y.maas.os_distro_select;
272+var suite = new Y.Test.Suite("maas.os_distro_select Tests");
273+
274+var select_node_template = Y.one('#select_node').getContent();
275+var target_node_template = Y.one('#target_node').getContent();
276+
277+suite.add(new Y.maas.testing.TestCase({
278+ name: 'test-os_distro_select',
279+
280+ setUp: function () {
281+ Y.one('#placeholder').empty().append(
282+ Y.Node.create(select_node_template).append(
283+ Y.Node.create(target_node_template)));
284+ this.widget = new Y.maas.os_distro_select.OSReleaseWidget({
285+ srcNode: '#id_distro_series'
286+ });
287+ },
288+
289+ testBindCallsSwitchTo: function() {
290+ var called = false;
291+ this.widget.switchTo = function() {
292+ called = true;
293+ }
294+ this.widget.bindTo(Y.one('#id_osystem'), 'change');
295+ Y.Assert.isTrue(called);
296+ },
297+
298+ testSwitchToCalledModifyOptionOnAll: function() {
299+ var options = [];
300+ this.widget.modifyOption = function(option, value) {
301+ options.push(option);
302+ }
303+ this.widget.bindTo(Y.one('#id_osystem'), 'change');
304+ var expected = Y.one('#id_distro_series').all('option');
305+ Y.ArrayAssert.containsItems(expected, options);
306+ },
307+
308+ testSwitchToTogglesInitialSkip: function() {
309+ this.widget.bindTo(Y.one('#id_osystem'), 'change');
310+ Y.Assert.isFalse(this.widget.initialSkip);
311+ },
312+
313+ testSwitchToCallsSelectVisableOption: function() {
314+ var called = false;
315+ this.widget.selectVisableOption = function() {
316+ called = true;
317+ }
318+ this.widget.initialSkip = false;
319+ this.widget.bindTo(Y.one('#id_osystem'), 'change');
320+ Y.Assert.isTrue(called);
321+ },
322+
323+ testModifyOptionSelectsDefault: function() {
324+ var option = Y.Mock();
325+ Y.Mock.expect(option, {
326+ method: "get",
327+ args: ["value"],
328+ returns: ""
329+ });
330+ Y.Mock.expect(option, {
331+ method: "removeClass",
332+ args: ["hidden"]
333+ });
334+ Y.Mock.expect(option, {
335+ method: "set",
336+ args: ["selected", "selected"]
337+ });
338+ var selected = this.widget.modifyOption(option, '');
339+ Y.Mock.verify(option);
340+ Y.Assert.isFalse(selected);
341+ },
342+
343+ testModifyOptionHidesNonDefault: function() {
344+ var option = Y.Mock();
345+ Y.Mock.expect(option, {
346+ method: "get",
347+ args: ["value"],
348+ returns: "value1"
349+ });
350+ Y.Mock.expect(option, {
351+ method: "addClass",
352+ args: ["hidden"]
353+ });
354+ var selected = this.widget.modifyOption(option, '');
355+ Y.Mock.verify(option);
356+ Y.Assert.isFalse(selected);
357+ },
358+
359+ testModifyOptionShowsOSMatch: function() {
360+ var option = Y.Mock();
361+ Y.Mock.expect(option, {
362+ method: "get",
363+ args: ["value"],
364+ returns: "os/release"
365+ });
366+ Y.Mock.expect(option, {
367+ method: "removeClass",
368+ args: ["hidden"]
369+ });
370+ var selected = this.widget.modifyOption(option, 'os');
371+ Y.Mock.verify(option);
372+ Y.Assert.isFalse(selected);
373+ },
374+
375+ testModifyOptionSelectsOSDefault: function() {
376+ var option = Y.Mock();
377+ Y.Mock.expect(option, {
378+ method: "get",
379+ args: ["value"],
380+ returns: "os/"
381+ });
382+ Y.Mock.expect(option, {
383+ method: "removeClass",
384+ args: ["hidden"]
385+ });
386+ Y.Mock.expect(option, {
387+ method: "set",
388+ args: ["selected", "selected"]
389+ });
390+ this.widget.initialSkip = false;
391+ var selected = this.widget.modifyOption(option, 'os');
392+ Y.Mock.verify(option);
393+ Y.Assert.isTrue(selected);
394+ },
395+
396+ testModifyOptionSelectsOSDefaultSkippedOnInitial: function() {
397+ var option = Y.Mock();
398+ Y.Mock.expect(option, {
399+ method: "get",
400+ args: ["value"],
401+ returns: "os/"
402+ });
403+ Y.Mock.expect(option, {
404+ method: "removeClass",
405+ args: ["hidden"]
406+ });
407+ var selected = this.widget.modifyOption(option, 'os');
408+ Y.Mock.verify(option);
409+ Y.Assert.isFalse(selected);
410+ },
411+
412+ testModifyOptionHidesOSMismatch: function() {
413+ var option = Y.Mock();
414+ Y.Mock.expect(option, {
415+ method: "get",
416+ args: ["value"],
417+ returns: "os/release"
418+ });
419+ Y.Mock.expect(option, {
420+ method: "addClass",
421+ args: ["hidden"]
422+ });
423+ var selected = this.widget.modifyOption(option, 'other');
424+ Y.Mock.verify(option);
425+ Y.Assert.isFalse(selected);
426+ },
427+
428+ testSelectVisableOptionShowsFirstVisable: function() {
429+ var option = Y.Mock();
430+ var option2 = Y.Mock();
431+ Y.Mock.expect(option, {
432+ method: "hasClass",
433+ args: ["hidden"],
434+ returns: true
435+ });
436+ Y.Mock.expect(option2, {
437+ method: "hasClass",
438+ args: ["hidden"],
439+ returns: false
440+ });
441+ Y.Mock.expect(option2, {
442+ method: "set",
443+ args: ["selected", "selected"]
444+ });
445+
446+ var options = Y.Array([option, option2])
447+ this.widget.selectVisableOption(options);
448+ Y.Mock.verify(option);
449+ Y.Mock.verify(option2);
450+ }
451+
452+}));
453+
454+namespace.suite = suite;
455+
456+}, '0.1', {'requires': [
457+ 'node-event-simulate', 'test', 'maas.testing', 'maas.os_distro_select']}
458+);
459
460=== modified file 'src/maasserver/templates/maasserver/node_edit.html'
461--- src/maasserver/templates/maasserver/node_edit.html 2014-03-03 09:44:25 +0000
462+++ src/maasserver/templates/maasserver/node_edit.html 2014-05-29 17:10:27 +0000
463@@ -11,9 +11,17 @@
464 <script type="text/javascript">
465 <!--
466 YUI().use(
467- 'maas.enums', 'maas.power_parameters',
468+ 'maas.enums', 'maas.power_parameters', 'maas.os_distro_select',
469 function (Y) {
470 Y.on('load', function() {
471+ // Create OSDistroWidget so that the release field will be
472+ // updated each time the value of the os field changes.
473+ var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({
474+ srcNode: '#id_distro_series'
475+ });
476+ releaseWidget.bindTo(Y.one('.osystem').one('select'), 'change');
477+ releaseWidget.render();
478+
479 // Create LinkedContentWidget widget so that the power_parameter field
480 // will be updated each time the value of the power_type field changes.
481 var power_types = {{ power_types }};
482
483=== modified file 'src/maasserver/templates/maasserver/settings.html'
484--- src/maasserver/templates/maasserver/settings.html 2014-04-10 04:29:20 +0000
485+++ src/maasserver/templates/maasserver/settings.html 2014-05-29 17:10:27 +0000
486@@ -6,6 +6,23 @@
487 {% block page-title %}Settings{% endblock %}
488
489 {% block head %}
490+ <script type="text/javascript">
491+ <!--
492+ YUI().use(
493+ 'maas.os_distro_select',
494+ function (Y) {
495+ Y.on('load', function() {
496+ // Create OSDistroWidget so that the release field will be
497+ // updated each time the value of the os field changes.
498+ var releaseWidget = new Y.maas.os_distro_select.OSReleaseWidget({
499+ srcNode: '#id_deploy-default_distro_series'
500+ });
501+ releaseWidget.bindTo(Y.one('#id_deploy-default_osystem'), 'change');
502+ releaseWidget.render();
503+ });
504+ });
505+ // -->
506+ </script>
507 {% endblock %}
508
509 {% block content %}
510@@ -90,6 +107,21 @@
511 <div class="clear"></div>
512 </div>
513 <div class="divider"></div>
514+ <div id="deploy" class="block size7 first">
515+ <h2>Deploy</h2>
516+ <form action="{% url "settings" %}" method="post">
517+ {% csrf_token %}
518+ <ul>
519+ {% for field in deploy_form %}
520+ {% include "maasserver/form_field.html" %}
521+ {% endfor %}
522+ </ul>
523+ <input type="hidden" name="deploy_submit" value="1" />
524+ <input type="submit" class="button right" value="Save" />
525+ </form>
526+ <div class="clear"></div>
527+ </div>
528+ <div class="divider"></div>
529 <div id="ubuntu" class="block size7 first">
530 <h2>Ubuntu</h2>
531 <form action="{% url "settings" %}" method="post">
532
533=== modified file 'src/maasserver/templates/maasserver/snippets.html'
534--- src/maasserver/templates/maasserver/snippets.html 2014-03-25 13:45:52 +0000
535+++ src/maasserver/templates/maasserver/snippets.html 2014-05-29 17:10:27 +0000
536@@ -16,6 +16,10 @@
537 </div>
538 </p>
539 <p>
540+ <label for="id_osystem">OS</label>
541+ {{ node_form.osystem }}
542+ </p>
543+ <p>
544 <label for="id_distro_series">Release</label>
545 {{ node_form.distro_series }}
546 </p>