Merge lp:~rvb/maas/ui-power-parameters into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 630
Proposed branch: lp:~rvb/maas/ui-power-parameters
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~rvb/maas/api-power-parameters
Diff against target: 423 lines (+196/-38)
10 files modified
src/maasserver/config_forms.py (+3/-0)
src/maasserver/context_processors.py (+2/-2)
src/maasserver/static/js/node_add.js (+26/-3)
src/maasserver/static/js/tests/test_node_add.html (+34/-15)
src/maasserver/static/js/tests/test_node_add.js (+64/-0)
src/maasserver/templates/maasserver/js-conf.html (+2/-0)
src/maasserver/templates/maasserver/node_edit.html (+0/-2)
src/maasserver/templates/maasserver/snippets.html (+14/-6)
src/maasserver/tests/test_config_forms.py (+5/-0)
src/maasserver/tests/test_views.py (+46/-10)
To merge this branch: bzr merge lp:~rvb/maas/ui-power-parameters
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+109621@code.launchpad.net

Commit message

Add power_type and power_parameters to the add node UI.

Description of the change

This branch add the fields power_type and power_parameters to the "add node" UI (if the user is an admin).

= Notes =

- I've changed DictCharField to return the empty string instead of '<fieldset></fieldset>' when the number of sub-fields is zero. I've done that because, for a reason I cannot fathom, http://paste.ubuntu.com/1035380/ does not validate. I suppose <fieldset> must contain a tag or something like that. Anyway, returning the empty string in that case is better anyway.

- I've added power_parameters.js and enums.js in js-conf.html (i.e. globally).

- no need for the "architecture" snippet (in snippet.html) to be a snippet on its own, I've included it in the general snippet "add-node".

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (3.1 KiB)

[1]

+ setupPowerParameterField: function() {
+ if (Y.Lang.isValue(Y.one('#id_power_type'))) {
...
+ widget.bindTo(Y.one('#id_power_type'), 'change');

Saving the result of this lookup will be more efficient and be clearer
to read:

    setupPowerParameterField: function() {
        var power_type_node = Y.one('#id_power_type');
        if (Y.Lang.isValue(power_type_node)) {
            ...
            widget.bindTo(power_type_node, 'change');

[2]

+ setUpPowerTypeEnum: function() {
+ // Override module.POWER_TYPE_ENUM for the sake of testing.
+ this.POWER_TYPE_ENUM = module.POWER_TYPE_ENUM;
+ module.POWER_TYPE_ENUM = ENUM;
+ },

Not for this branch, but it would be cool to have a this.patch()
method, like in the Python test suite, that arranges a clean-up hook.

[3]

+ <input type="text" maxlength="30" name="hostname" id="id_hostname" />
...
+ this.add_node_template = Y.one('#add-node').getContent();
+ this.add_node_admin_snippet = Y.one('#add-node-admin').getContent();

Why do some IDs have hyphens and some underscores? Is this Django
versus YUI?

[4]

+ self.assertEqual(
+ 1, len(snippets),
+ "The snippet '%s' does not exist." % template_selector)

There could be two.

[5]

+ def _assertTemplateExistsAndContains(self, content, template_selector,
+ contains_selector, reverse=False):
...
+ if reverse:
+ self.assertEqual(
+ 0, len(selects),
+ "The element '%s' does exist." % contains_selector)
+ else:
+ self.assertEqual(
+ 1, len(selects),
+ "The element '%s' does not exist." % contains_selector,)
+
+ def assertTemplateExistsAndContains(self, content, template_selector,
...
+ self._assertTemplateExistsAndContains(
+ content, template_selector, contains_selector)
+
+ def assertTemplateExistsAndDoesNotContain(self, content, template_selector,
...
+ self._assertTemplateExistsAndContains(
+ content, template_selector, contains_selector, reverse=True)

The above is a bit of an anti-pattern. The `reverse` condition should
ideally be moved elsewhere, like up into the calling functions:

    def findTemplate(self, content, selector):
        snippets = fromstring(content).cssselect(template_selector)
        self.assertEqual(
            1, len(snippets),
            "The snippet '%s' does not exist." % selector) # XXX: See [4]
        return fromstring(snippets[0].text)

    def assertTemplateExistsAndContains(self, content, template_selector,
        ...
        template = self.findTemplate(content, template_selector)
        self.assertNotEqual(
            0, len(template.cssselect(contains_selector)),
            "The element '%s' does not exist." % contains_selector)

    def assertTemplateExistsAndDoesNotContain(self, content, template_selector,
        ...
        template = self.findTemplate(content, template_selector)
        self.assertEqual(
            0, len(template.cssselect(contains_selector)),
            "The element '%s' should not exist." % contains_se...

Read more...

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

Thanks for the review Gavin!

> [1]
> [...]
> Saving the result of this lookup will be more efficient and be clearer
> to read:
> [...]

Done.

> [2]
> Not for this branch, but it would be cool to have a this.patch()
> method, like in the Python test suite, that arranges a clean-up hook.
> [...]

Indeed. We clearly lack our python test arsenal in Javascript…

> [3]
>
> + <input type="text" maxlength="30" name="hostname" id="id_hostname" />
> ...
> + this.add_node_template = Y.one('#add-node').getContent();
> + this.add_node_admin_snippet = Y.one('#add-node-admin').getContent();
>
> Why do some IDs have hyphens and some underscores? Is this Django
> versus YUI?

That's right, it's just a convention I use to remind me of what comes from Django and what does not.

> [4]
>
> + self.assertEqual(
> + 1, len(snippets),
> + "The snippet '%s' does not exist." % template_selector)
>
> There could be two.

Right, I've changed the error message.

> [5]
> [...]
> The above is a bit of an anti-pattern. The `reverse` condition should
> ideally be moved elsewhere, like up into the calling functions:
> [...]

Good point, I confess I was a little bit lazy on this one… fixed!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/config_forms.py'
2--- src/maasserver/config_forms.py 2012-06-07 15:57:27 +0000
3+++ src/maasserver/config_forms.py 2012-06-11 13:08:19 +0000
4@@ -279,6 +279,9 @@
5 widgets = self.widgets
6 if not isinstance(value, list):
7 value = self.decompress(value)
8+ if len(widgets) == 0:
9+ return mark_safe(self.format_output(''))
10+
11 output = ['<fieldset>']
12 final_attrs = self.build_attrs(attrs)
13 id_ = final_attrs.get('id', None)
14
15=== modified file 'src/maasserver/context_processors.py'
16--- src/maasserver/context_processors.py 2012-06-06 16:01:55 +0000
17+++ src/maasserver/context_processors.py 2012-06-11 13:08:19 +0000
18@@ -16,7 +16,7 @@
19
20 from django.conf import settings
21 from maasserver.components import get_persistent_errors
22-from maasserver.forms import NodeForm
23+from maasserver.forms import get_node_edit_form
24 from maasserver.models import Config
25 from maasserver.power_parameters import POWER_TYPE_PARAMETERS
26 from provisioningserver.enum import POWER_TYPE
27@@ -34,7 +34,7 @@
28 def global_options(context):
29 return {
30 'persistent_errors': get_persistent_errors(),
31- 'node_form': NodeForm(),
32+ 'node_form': get_node_edit_form(context.user)(),
33 'POWER_TYPE_PARAMETERS_FIELDS':
34 [(power_type, field.widget.render('power_parameters', []))
35 for power_type, field in POWER_TYPE_PARAMETERS.items()
36
37=== modified file 'src/maasserver/static/js/node_add.js'
38--- src/maasserver/static/js/node_add.js 2012-06-11 07:34:55 +0000
39+++ src/maasserver/static/js/node_add.js 2012-06-11 13:08:19 +0000
40@@ -25,6 +25,10 @@
41
42 AddNodeWidget.NAME = 'node-add-widget';
43
44+module.POWER_TYPE_ENUM = Y.maas.enums.POWER_TYPE;
45+
46+module.POWER_PARAM_TEMPLATE_PREFIX = '#power-param-form-';
47+
48 AddNodeWidget.ATTRS = {
49
50 /**
51@@ -141,7 +145,6 @@
52 .append(global_error)
53 .append(operation)
54 .append(Y.Node.create(this.add_node))
55- .append(Y.Node.create(this.add_architecture))
56 .append(Y.Node.create(this.add_macaddress))
57 .append(macaddress_add_link)
58 .append(buttons);
59@@ -207,16 +210,36 @@
60 renderUI: function() {
61 // Load form snippets.
62 this.add_macaddress = Y.one('#add-macaddress').getContent();
63- this.add_architecture = Y.one('#add-architecture').getContent();
64 this.add_node = Y.one('#add-node').getContent();
65 // Create panel's content.
66 var heading = Y.Node.create('<h2 />')
67 .set('text', "Add node");
68 this.get('srcNode').append(heading).append(this.createForm());
69+ this.setupPowerParameterField();
70 this.initializeNodes();
71 },
72
73 /**
74+ * If the 'power_type' field is present, setup the linked
75+ * 'power_parameter' field.
76+ *
77+ * @method setupPowerParameterField
78+ */
79+ setupPowerParameterField: function() {
80+ if (Y.Lang.isValue(Y.one('#id_power_type'))) {
81+ // If the 'power_type' field is present, setup the linked
82+ // field 'power_parameters'.
83+ var widget = new Y.maas.power_parameters.LinkedContentWidget({
84+ srcNode: '.power_parameters',
85+ driverEnum: module.POWER_TYPE_ENUM,
86+ templatePrefix: module.POWER_PARAM_TEMPLATE_PREFIX
87+ });
88+ widget.bindTo(Y.one('#id_power_type'), 'change');
89+ widget.render();
90+ }
91+ },
92+
93+ /**
94 * Show the widget
95 *
96 * @method showWidget
97@@ -379,5 +402,5 @@
98 };
99
100 }, '0.1', {'requires': ['io', 'node', 'widget', 'event', 'event-custom',
101- 'maas.morph']}
102+ 'maas.morph', 'maas.enums', 'maas.power_parameters']}
103 );
104
105=== modified file 'src/maasserver/static/js/tests/test_node_add.html'
106--- src/maasserver/static/js/tests/test_node_add.html 2012-03-29 04:21:34 +0000
107+++ src/maasserver/static/js/tests/test_node_add.html 2012-06-11 13:08:19 +0000
108@@ -9,6 +9,8 @@
109 <script type="text/javascript" src="../testing/testrunner.js"></script>
110 <script type="text/javascript" src="../testing/testing.js"></script>
111 <script type="text/javascript" src="../morph.js"></script>
112+ <script type="text/javascript" src="../power_parameters.js"></script>
113+ <script type="text/javascript" src="../enums.js"></script>
114 <!-- The module under test -->
115 <script type="text/javascript" src="../node_add.js"></script>
116 <!-- The test suite -->
117@@ -22,30 +24,47 @@
118 nodes_handler: '/api/nodes/'
119 }
120 };
121+ var ENUM = {
122+ "DEFAULT": "",
123+ "value1": "value1"
124+ };
125 // -->
126 </script>
127 </head>
128 <body>
129+ <script type="text/x-template" id="power-param-form-">
130+ </script>
131+ <script type="text/x-template" id="power-param-form-value1">
132+ </script>
133 <script type="text/x-template" id="add-macaddress">
134 <p></p>
135 </script>
136- <script type="text/x-template" id="add-architecture">
137- <p>
138- <label for="id_architecture">Architecture
139- </label>
140- <select id="id_architecture" name="architecture">
141- <option value="amd64">amd64</option>
142- <option value="i386">i386</option>
143- </select>
144- <input type="text" maxlength="30" name="hostname" id="id_hostname" />
145- </p>
146- </script>
147 <script type="text/x-template" id="add-node">
148 <p>
149- <label for="id_hostname">Hostname
150- <span class="form-optional">(optional)</span>
151- </label>
152- <input type="text" maxlength="30" name="hostname" id="id_hostname" />
153+ <label for="id_hostname">Hostname
154+ <span class="form-optional">(optional)</span>
155+ </label>
156+ <input type="text" maxlength="30" name="hostname" id="id_hostname" />
157+ </p>
158+ <p>
159+ <label for="id_architecture">Architecture</label>
160+ <select id="id_architecture" name="architecture">
161+ <option value="amd64">amd64</option>
162+ <option value="i386">i386</option>
163+ </select>
164+ <input type="text" maxlength="30" name="hostname" id="id_hostname" />
165+ </p>
166+ </script>
167+ <script type="text/x-template" id="add-node-admin">
168+ <p>
169+ <label for="id_power_type">Power type</label>
170+ <select name="power_type" id="id_power_type">
171+ <option value="" selected="selected">---------</option>
172+ <option value="value1">value1</option>
173+ </select>
174+ </p>
175+ <p class="power_parameters">
176+ <label for="id_power_parameters">Power parameters</label>
177 </p>
178 </script>
179 <div id="target_node"></div>
180
181=== modified file 'src/maasserver/static/js/tests/test_node_add.js'
182--- src/maasserver/static/js/tests/test_node_add.js 2012-04-02 05:21:40 +0000
183+++ src/maasserver/static/js/tests/test_node_add.js 2012-06-11 13:08:19 +0000
184@@ -211,6 +211,70 @@
185
186 }));
187
188+suite.add(new Y.maas.testing.TestCase({
189+ name: 'test-add-node-widget-add-node-admin',
190+
191+ setUp: function() {
192+ this.setUpAdminTemplate();
193+ this.setUpPowerTypeEnum();
194+ },
195+
196+ setUpPowerTypeEnum: function() {
197+ // Override module.POWER_TYPE_ENUM for the sake of testing.
198+ this.POWER_TYPE_ENUM = module.POWER_TYPE_ENUM;
199+ module.POWER_TYPE_ENUM = ENUM;
200+ },
201+
202+ setUpAdminTemplate: function() {
203+ // Append the snippet that will be seen by an admin user to the
204+ // general template.
205+ this.add_node_template = Y.one('#add-node').getContent();
206+ this.add_node_admin_snippet = Y.one('#add-node-admin').getContent();
207+ Y.one('#add-node').set(
208+ 'innerHTML',
209+ this.add_node_template + this.add_node_admin_snippet);
210+ },
211+
212+ tearDown: function() {
213+ this.tearDownAdminTemplate();
214+ this.tearDownPowerTypeEnum();
215+ },
216+
217+ tearDownAdminTemplate: function() {
218+ Y.one('#add-node').set('innerHTML', this.add_node_template);
219+ },
220+
221+ tearDownPowerTypeEnum: function() {
222+ module.POWER_TYPE_ENUM = this.POWER_TYPE_ENUM;
223+ },
224+
225+ testFormContainsPowerType: function() {
226+ module.showAddNodeWidget({targetNode: '#target_node', animate: false});
227+ Y.Assert.isNotNull(find_widget().one('#id_power_type'));
228+ },
229+
230+ testPowerParameterHidden: function() {
231+ // The power_parameter field starts hidden.
232+ module.showAddNodeWidget({targetNode: '#target_node', animate: false});
233+ Y.Assert.isTrue(
234+ find_widget().one('.power_parameters').hasClass('hidden'));
235+ },
236+
237+ testPowerParameterDynamicallyLinked: function() {
238+ // When the option of the 'select' tag changes to a non empty
239+ // string, the 'power_parameters' field is shown.
240+ module.showAddNodeWidget({targetNode: '#target_node', animate: false});
241+ var select = Y.one('#id_power_type');
242+ select.set('value', 'value1');
243+ select.simulate('change');
244+ Y.Assert.isFalse(
245+ find_widget().one('.power_parameters').hasClass('hidden'));
246+ }
247+
248+
249+}));
250+
251+
252 namespace.suite = suite;
253
254 }, '0.1', {'requires': [
255
256=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
257--- src/maasserver/templates/maasserver/js-conf.html 2012-03-16 09:02:13 +0000
258+++ src/maasserver/templates/maasserver/js-conf.html 2012-06-11 13:08:19 +0000
259@@ -30,3 +30,5 @@
260 <script type="text/javascript" src="{{ STATIC_URL }}js/utils.js"></script>
261 <script type="text/javascript" src="{{ STATIC_URL }}js/node_views.js"></script>
262 <script type="text/javascript" src="{{ STATIC_URL }}js/longpoll.js"></script>
263+<script type="text/javascript" src="{{ STATIC_URL }}js/enums.js"></script>
264+<script type="text/javascript" src="{{ STATIC_URL }}js/power_parameters.js"></script>
265
266=== modified file 'src/maasserver/templates/maasserver/node_edit.html'
267--- src/maasserver/templates/maasserver/node_edit.html 2012-06-08 10:05:57 +0000
268+++ src/maasserver/templates/maasserver/node_edit.html 2012-06-11 13:08:19 +0000
269@@ -8,8 +8,6 @@
270 {% endblock %}
271
272 {% block head %}
273- <script type="text/javascript" src="{{ STATIC_URL }}js/power_parameters.js"></script>
274- <script type="text/javascript" src="{{ STATIC_URL }}js/enums.js"></script>
275 <script type="text/javascript">
276 <!--
277 YUI().use(
278
279=== modified file 'src/maasserver/templates/maasserver/snippets.html'
280--- src/maasserver/templates/maasserver/snippets.html 2012-06-06 16:01:55 +0000
281+++ src/maasserver/templates/maasserver/snippets.html 2012-06-11 13:08:19 +0000
282@@ -6,12 +6,6 @@
283 <div class="field-help">e.g AA:BB:CC:DD:EE:FF</div>
284 </p>
285 </script>
286-<script type="text/x-template" id="add-architecture">
287- <p>
288- <label for="id_architecture">Architecture</label>
289- {{ node_form.architecture }}
290- </p>
291-</script>
292 <script type="text/x-template" id="add-node">
293 <p>
294 <label for="id_hostname">Hostname
295@@ -25,6 +19,20 @@
296 <label for="id_after_commissioning_action">After commissioning</label>
297 {{ node_form.after_commissioning_action }}
298 </p>
299+ <p>
300+ <label for="id_architecture">Architecture</label>
301+ {{ node_form.architecture }}
302+ </p>
303+ {% if node_form.power_type %}
304+ <p>
305+ <label for="id_power_type">Power type</label>
306+ {{ node_form.power_type }}
307+ </p>
308+ <p class="power_parameters">
309+ <label for="id_power_parameters">Power parameters</label>
310+ {{ node_form.power_parameters }}
311+ </p>
312+ {% endif %}
313 </script>
314 {% for power_type, fieldset in POWER_TYPE_PARAMETERS_FIELDS %}
315 <script type="text/x-template" id="power-param-form-{{ power_type}}">
316
317=== modified file 'src/maasserver/tests/test_config_forms.py'
318--- src/maasserver/tests/test_config_forms.py 2012-06-07 14:34:27 +0000
319+++ src/maasserver/tests/test_config_forms.py 2012-06-11 13:08:19 +0000
320@@ -216,6 +216,11 @@
321 ),
322 widget.render(name, values))
323
324+ def test_empty_DictCharWidget_renders_as_empty_string(self):
325+ widget = DictCharWidget(
326+ [widgets.CheckboxInput], [], [], skip_check=True)
327+ self.assertEqual('', widget.render(factory.getRandomString(), ''))
328+
329 def test_DictCharWidget_value_from_datadict_values_from_data(self):
330 # 'value_from_datadict' extracts the values corresponding to the
331 # field as a dictionary.
332
333=== modified file 'src/maasserver/tests/test_views.py'
334--- src/maasserver/tests/test_views.py 2012-04-30 16:26:38 +0000
335+++ src/maasserver/tests/test_views.py 2012-06-11 13:08:19 +0000
336@@ -31,9 +31,9 @@
337 LoggedInTestCase,
338 TestCase,
339 )
340+from maasserver.utils import map_enum
341 from maasserver.views import HelpfulDeleteView
342 from maasserver.views.nodes import NodeEdit
343-from maasserver.utils import map_enum
344 from maastesting.matchers import ContainsAll
345 from provisioningserver.enum import PSERV_FAULT
346
347@@ -70,24 +70,47 @@
348
349 class TestSnippets(LoggedInTestCase):
350
351- def assertTemplateExistsAndContains(self, content, template_selector,
352- contains_selector):
353- """Assert that the provided html 'content' contains a snippet as
354- selected by 'template_selector' which in turn contains an element
355- selected by 'contains_selector'.
356- """
357+ def _assertTemplateExistsAndContains(self, content, template_selector,
358+ contains_selector, reverse=False):
359 doc = fromstring(content)
360 snippets = doc.cssselect(template_selector)
361 # The snippet exists.
362- self.assertEqual(1, len(snippets))
363+ self.assertEqual(
364+ 1, len(snippets),
365+ "The snippet '%s' does not exist." % template_selector)
366 # It contains the required element.
367 selects = fromstring(snippets[0].text).cssselect(contains_selector)
368- self.assertEqual(1, len(selects))
369+ if reverse:
370+ self.assertEqual(
371+ 0, len(selects),
372+ "The element '%s' does exist." % contains_selector)
373+ else:
374+ self.assertEqual(
375+ 1, len(selects),
376+ "The element '%s' does not exist." % contains_selector,)
377+
378+ def assertTemplateExistsAndContains(self, content, template_selector,
379+ contains_selector, reverse=False):
380+ """Assert that the provided html 'content' contains a snippet as
381+ selected by 'template_selector' which in turn contains an element
382+ selected by 'contains_selector'.
383+ """
384+ self._assertTemplateExistsAndContains(
385+ content, template_selector, contains_selector)
386+
387+ def assertTemplateExistsAndDoesNotContain(self, content, template_selector,
388+ contains_selector):
389+ """Assert that the provided html 'content' contains a snippet as
390+ selected by 'template_selector' which does not contains an element
391+ selected by 'contains_selector'.
392+ """
393+ self._assertTemplateExistsAndContains(
394+ content, template_selector, contains_selector, reverse=True)
395
396 def test_architecture_snippet(self):
397 response = self.client.get('/')
398 self.assertTemplateExistsAndContains(
399- response.content, '#add-architecture', 'select#id_architecture')
400+ response.content, '#add-node', 'select#id_architecture')
401
402 def test_hostname(self):
403 response = self.client.get('/')
404@@ -100,6 +123,19 @@
405 response.content, '#add-node',
406 'select#id_after_commissioning_action')
407
408+ def test_power_type_does_not_exist_if_not_admin(self):
409+ response = self.client.get('/')
410+ self.assertTemplateExistsAndDoesNotContain(
411+ response.content, '#add-node',
412+ 'select#id_power_type')
413+
414+ def test_power_type_exists_if_admin(self):
415+ self.become_admin()
416+ response = self.client.get('/')
417+ self.assertTemplateExistsAndContains(
418+ response.content, '#add-node',
419+ 'select#id_power_type')
420+
421
422 class FakeDeletableModel:
423 """A fake model class, with a delete method."""