Merge lp:~newell-jensen/maas/maas-maasserver-ui-curtin-install-log into lp:~maas-committers/maas/trunk

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: no longer in the source branch.
Merged at revision: 2776
Proposed branch: lp:~newell-jensen/maas/maas-maasserver-ui-curtin-install-log
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 676 lines (+304/-75)
11 files modified
src/maasserver/api/results.py (+3/-3)
src/maasserver/api/tests/test_commissioning.py (+4/-4)
src/maasserver/templates/maasserver/node_view.html (+28/-10)
src/maasserver/templates/metadataserver/nodeinstallresult.html (+35/-0)
src/maasserver/testing/factory.py (+16/-0)
src/maasserver/urls.py (+10/-5)
src/maasserver/urls_api.py (+7/-4)
src/maasserver/views/noderesult.py (+25/-7)
src/maasserver/views/nodes.py (+11/-4)
src/maasserver/views/tests/test_noderesult.py (+92/-25)
src/maasserver/views/tests/test_nodes.py (+73/-13)
To merge this branch: bzr merge lp:~newell-jensen/maas/maas-maasserver-ui-curtin-install-log
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+231271@code.launchpad.net

Commit message

Adding UI changes to handle NodeResult result_type results.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I think the big issue is that you do not display multiple installation files on the view. I know that chances are you will only have one result per installation, but since the model supports it I would allow it. There could be a chance that an installation could store multiple results, maybe d-i or a custom image.

Other than that most looks good, just a few other comments.

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) wrote :

Ready for another review.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

I think its close. Couple of questions.

review: Needs Information
Revision history for this message
Newell Jensen (newell-jensen) wrote :

> I think its close. Couple of questions.

See inline for responses.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

See inline for responses.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

Have another look BRAH...okay that was last one.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

Added edit permissions changes and tests. Have another look.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

You need to fix the views for both commissioning and installing. They currently only allow an admin to view those pages, but your showing the links to those pages if its the owner of the node as well.

So you need to make sure that owners of the node can view those pages.

Also you should move the installation results above the commissioning results. I think it is more important.

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) wrote :

> You need to fix the views for both commissioning and installing. They
> currently only allow an admin to view those pages, but your showing the links
> to those pages if its the owner of the node as well.
>
> So you need to make sure that owners of the node can view those pages.
>
> Also you should move the installation results above the commissioning results.
> I think it is more important.

Added permissions for node owners and admins.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looking good now!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== renamed file 'src/maasserver/api/commissioning_results.py' => 'src/maasserver/api/results.py'
2--- src/maasserver/api/commissioning_results.py 2014-08-17 02:19:33 +0000
3+++ src/maasserver/api/results.py 2014-08-20 20:35:23 +0000
4@@ -13,7 +13,7 @@
5
6 __metaclass__ = type
7 __all__ = [
8- 'CommissioningResultsHandler',
9+ 'NodeResultsHandler',
10 ]
11
12 from maasserver.api.support import (
13@@ -29,7 +29,7 @@
14 from metadataserver.models import NodeResult
15
16
17-class CommissioningResultsHandler(OperationsHandler):
18+class NodeResultsHandler(OperationsHandler):
19 """Read the collection of NodeResult in the MAAS."""
20 api_doc_section_name = "Commissioning results"
21 create = read = update = delete = None
22@@ -69,4 +69,4 @@
23
24 @classmethod
25 def resource_uri(cls, result=None):
26- return ('commissioning_results_handler', [])
27+ return ('node_results_handler', [])
28
29=== modified file 'src/maasserver/api/tests/test_commissioning.py'
30--- src/maasserver/api/tests/test_commissioning.py 2014-08-16 05:43:33 +0000
31+++ src/maasserver/api/tests/test_commissioning.py 2014-08-20 20:35:23 +0000
32@@ -255,7 +255,7 @@
33 commissioning_results = [
34 factory.make_node_commission_result()
35 for counter in range(3)]
36- url = reverse('commissioning_results_handler')
37+ url = reverse('node_results_handler')
38 response = self.client.get(url, {'op': 'list'})
39 self.assertEqual(httplib.OK, response.status_code, response.content)
40 parsed_results = json.loads(response.content)
41@@ -284,7 +284,7 @@
42 commissioning_results = [
43 factory.make_node_commission_result()
44 for counter in range(3)]
45- url = reverse('commissioning_results_handler')
46+ url = reverse('node_results_handler')
47 response = self.client.get(
48 url,
49 {
50@@ -306,7 +306,7 @@
51 commissioning_results = [
52 factory.make_node_commission_result()
53 for counter in range(3)]
54- url = reverse('commissioning_results_handler')
55+ url = reverse('node_results_handler')
56 response = self.client.get(
57 url,
58 {
59@@ -323,7 +323,7 @@
60 def test_list_displays_only_visible_nodes(self):
61 node = factory.make_node(owner=factory.make_user())
62 factory.make_node_commission_result(node)
63- url = reverse('commissioning_results_handler')
64+ url = reverse('node_results_handler')
65 response = self.client.get(url, {'op': 'list'})
66 self.assertEqual(httplib.OK, response.status_code, response.content)
67 parsed_results = json.loads(response.content)
68
69=== modified file 'src/maasserver/templates/maasserver/node_view.html'
70--- src/maasserver/templates/maasserver/node_view.html 2014-08-13 21:49:35 +0000
71+++ src/maasserver/templates/maasserver/node_view.html 2014-08-20 20:35:23 +0000
72@@ -211,16 +211,34 @@
73 <span>{{ node.routers|join:", " }}</span>
74 </li>
75 {% endif %}
76- {% if user.is_superuser and nodecommissionresults > 0 %}
77- <li class="block first size5 separate" id="nodecommissionresults">
78- <h4>Commissioning output</h4>
79- <span>
80- <a href="{% url 'nodecommissionresult-list' %}?node={{node.system_id}}">
81- {{ nodecommissionresults }}
82- output file{{ nodecommissionresults|pluralize }}
83- </a>
84- </span>
85- </li>
86+ {% if can_edit %}
87+ {% if nodeinstallresults != None %}
88+ <li class="block first size5 separate" id="nodeinstallresults">
89+ <h4>Installation output</h4>
90+ <ul>
91+ {% for install_result in nodeinstallresults %}
92+ <li>
93+ <span>
94+ <a href="{% url 'nodeinstallresult-view' install_result.id %}">
95+ {{ install_result.name }}
96+ </a>
97+ </span>
98+ </li>
99+ {% endfor %}
100+ </ul>
101+ </li>
102+ {% endif %}
103+ {% if nodecommissionresults > 0 %}
104+ <li class="block first size5 separate" id="nodecommissionresults">
105+ <h4>Commissioning output</h4>
106+ <span>
107+ <a href="{% url 'nodecommissionresult-list' %}?node={{node.system_id}}">
108+ {{ nodecommissionresults }}
109+ output file{{ nodecommissionresults|pluralize }}
110+ </a>
111+ </span>
112+ </li>
113+ {% endif %}
114 {% endif %}
115 <li class="block first separate">
116 <h2>Latest node events</h2>
117
118=== renamed file 'src/maasserver/templates/metadataserver/noderesult.html' => 'src/maasserver/templates/metadataserver/nodecommissionresult.html'
119=== renamed file 'src/maasserver/templates/metadataserver/noderesult_list.html' => 'src/maasserver/templates/metadataserver/nodecommissionresult_list.html'
120=== added file 'src/maasserver/templates/metadataserver/nodeinstallresult.html'
121--- src/maasserver/templates/metadataserver/nodeinstallresult.html 1970-01-01 00:00:00 +0000
122+++ src/maasserver/templates/metadataserver/nodeinstallresult.html 2014-08-20 20:35:23 +0000
123@@ -0,0 +1,35 @@
124+{% extends "maasserver/base.html" %}
125+
126+{% block nav-active-settings %}active{% endblock %}
127+{% block title %}Installing result for node {{ object.node.hostname }}, script {{ object.name }}{% endblock %}
128+{% block page-title %}Installing result for node: {{ object.node.hostname }}{% endblock %}
129+
130+{% block content %}
131+ <ul class="data-list">
132+ <li class="block size7 first" id="name">
133+ <h4>Output file</h4>
134+ <span>{{ object.name }}</span>
135+ </li>
136+ <li class="block size8" id="node">
137+ <h4>Node</h4>
138+ <span>
139+ <a href="{% url 'node-view' object.node.system_id %}">
140+ {{ object.node.hostname }}
141+ </a>
142+ </span>
143+ </li>
144+ <li class="block size7 first" id="time">
145+ <h4>Result registered at</h4>
146+ <span>{{ object.created }}</span>
147+ </li>
148+ <li class="block first separate" id="output">
149+ {% if object.data %}
150+ <h2>Output</h2>
151+<pre>
152+{{ object.get_data_as_html }}
153+</pre>
154+ {% endif %}
155+ </li>
156+ </ul>
157+
158+{% endblock %}
159
160=== modified file 'src/maasserver/testing/factory.py'
161--- src/maasserver/testing/factory.py 2014-08-19 05:21:31 +0000
162+++ src/maasserver/testing/factory.py 2014-08-20 20:35:23 +0000
163@@ -445,6 +445,22 @@
164 ncr.save()
165 return ncr
166
167+ def make_node_install_result(self, node=None, name=None,
168+ script_result=None, data=None):
169+ if node is None:
170+ node = self.make_node()
171+ if name is None:
172+ name = "ncrname-" + self.make_string(92)
173+ if data is None:
174+ data = b"ncrdata-" + self.make_bytes()
175+ if script_result is None:
176+ script_result = random.randint(0, 10)
177+ ncr = NodeResult(
178+ node=node, name=name, script_result=script_result,
179+ result_type=RESULT_TYPE.INSTALLING, data=Bin(data))
180+ ncr.save()
181+ return ncr
182+
183 def make_MAC(self):
184 """Generate a random MAC address, in the form of a MAC object."""
185 return MAC(self.getRandomMACAddress())
186
187=== modified file 'src/maasserver/urls.py'
188--- src/maasserver/urls.py 2014-08-15 01:38:30 +0000
189+++ src/maasserver/urls.py 2014-08-20 20:35:23 +0000
190@@ -48,9 +48,10 @@
191 NetworkListView,
192 NetworkView,
193 )
194-from maasserver.views.nodecommissionresult import (
195+from maasserver.views.noderesult import (
196 NodeCommissionResultListView,
197 NodeCommissionResultView,
198+ NodeInstallResultView,
199 )
200 from maasserver.views.nodes import (
201 enlist_preseed_view,
202@@ -137,6 +138,14 @@
203 url(
204 r'^account/prefs/sslkey/delete/(?P<keyid>\d*)/$',
205 SSLKeyDeleteView.as_view(), name='prefs-delete-sslkey'),
206+ url(
207+ r'^commissioning-results/(?P<id>[0-9]+)/$',
208+ NodeCommissionResultView.as_view(),
209+ name='nodecommissionresult-view'),
210+ url(
211+ r'^installation-results/(?P<id>[0-9]+)/$',
212+ NodeInstallResultView.as_view(),
213+ name='nodeinstallresult-view'),
214 )
215 # Logout view.
216 urlpatterns += patterns(
217@@ -248,10 +257,6 @@
218 r'^commissioning-results/$',
219 NodeCommissionResultListView.as_view(),
220 name='nodecommissionresult-list'),
221- adminurl(
222- r'^commissioning-results/(?P<id>[0-9]+)/$',
223- NodeCommissionResultView.as_view(),
224- name='nodecommissionresult-view'),
225 )
226
227 # Tag views.
228
229=== modified file 'src/maasserver/urls_api.py'
230--- src/maasserver/urls_api.py 2014-08-18 13:44:26 +0000
231+++ src/maasserver/urls_api.py 2014-08-20 20:35:23 +0000
232@@ -32,7 +32,6 @@
233 BootSourceHandler,
234 BootSourcesHandler,
235 )
236-from maasserver.api.commissioning_results import CommissioningResultsHandler
237 from maasserver.api.commissioning_scripts import (
238 CommissioningScriptHandler,
239 CommissioningScriptsHandler,
240@@ -72,6 +71,7 @@
241 NodesHandler,
242 )
243 from maasserver.api.pxeconfig import pxeconfig
244+from maasserver.api.results import NodeResultsHandler
245 from maasserver.api.ssh_keys import (
246 SSHKeyHandler,
247 SSHKeysHandler,
248@@ -126,8 +126,8 @@
249 tag_handler = RestrictedResource(TagHandler, authentication=api_auth)
250 tags_handler = RestrictedResource(TagsHandler, authentication=api_auth)
251 version_handler = RestrictedResource(VersionHandler)
252-commissioning_results_handler = RestrictedResource(
253- CommissioningResultsHandler, authentication=api_auth)
254+node_results_handler = RestrictedResource(
255+ NodeResultsHandler, authentication=api_auth)
256 sshkey_handler = RestrictedResource(SSHKeyHandler, authentication=api_auth)
257 sshkeys_handler = RestrictedResource(SSHKeysHandler, authentication=api_auth)
258 sslkey_handler = RestrictedResource(SSLKeyHandler, authentication=api_auth)
259@@ -216,7 +216,10 @@
260 url(r'^tags/$', tags_handler, name='tags_handler'),
261 url(
262 r'^commissioning-results/$',
263- commissioning_results_handler, name='commissioning_results_handler'),
264+ node_results_handler, name='node_results_handler'),
265+ url(
266+ r'^installation-results/$',
267+ node_results_handler, name='node_results_handler'),
268 url(r'^users/$', users_handler, name='users_handler'),
269 url(r'^users/(?P<username>[^/]+)/$', user_handler, name='user_handler'),
270 url(r'^zones/(?P<name>[^/]+)/$', zone_handler, name='zone_handler'),
271
272=== renamed file 'src/maasserver/views/nodecommissionresult.py' => 'src/maasserver/views/noderesult.py'
273--- src/maasserver/views/nodecommissionresult.py 2014-08-13 21:49:35 +0000
274+++ src/maasserver/views/noderesult.py 2014-08-20 20:35:23 +0000
275@@ -1,7 +1,7 @@
276 # Copyright 2014 Canonical Ltd. This software is licensed under the
277 # GNU Affero General Public License version 3 (see the file LICENSE).
278
279-"""Views for node commissioning results."""
280+"""Views for node commissioning/installing results."""
281
282 from __future__ import (
283 absolute_import,
284@@ -16,6 +16,7 @@
285 'NodeCommissionResultListView',
286 ]
287
288+from django.core.exceptions import PermissionDenied
289 from django.shortcuts import get_object_or_404
290 from django.views.generic import DetailView
291 from maasserver.models import Node
292@@ -25,7 +26,7 @@
293
294 class NodeCommissionResultListView(PaginatedListView):
295
296- template_name = 'maasserver/nodecommissionresult-list.html'
297+ template_name = 'metadataserver/nodecommissionresult_list.html'
298 context_object_name = 'results_list'
299
300 def get_filter_system_ids(self):
301@@ -52,8 +53,25 @@
302
303 class NodeCommissionResultView(DetailView):
304
305- template_name = 'metadataserver/noderesult.html'
306-
307- def get_object(self):
308- result_id = self.kwargs.get('id')
309- return get_object_or_404(NodeResult, id=result_id)
310+ template_name = 'metadataserver/nodecommissionresult.html'
311+
312+ def get_object(self):
313+ result_id = self.kwargs.get('id')
314+ result = get_object_or_404(NodeResult, id=result_id)
315+ if not self.request.user.is_superuser and \
316+ self.request.user != result.node.owner:
317+ raise PermissionDenied
318+ return result
319+
320+
321+class NodeInstallResultView(DetailView):
322+
323+ template_name = 'metadataserver/nodeinstallresult.html'
324+
325+ def get_object(self):
326+ result_id = self.kwargs.get('id')
327+ result = get_object_or_404(NodeResult, id=result_id)
328+ if not self.request.user.is_superuser and \
329+ self.request.user != result.node.owner:
330+ raise PermissionDenied
331+ return result
332
333=== modified file 'src/maasserver/views/nodes.py'
334--- src/maasserver/views/nodes.py 2014-08-13 21:49:35 +0000
335+++ src/maasserver/views/nodes.py 2014-08-20 20:35:23 +0000
336@@ -91,6 +91,7 @@
337 HelpfulDeleteView,
338 PaginatedListView,
339 )
340+from metadataserver.enum import RESULT_TYPE
341 from metadataserver.models import NodeResult
342 from netaddr import (
343 EUI,
344@@ -554,13 +555,19 @@
345 # the call to get_single_probed_details() because here the
346 # details will be guaranteed well-formed.
347 if len(probed_details.xpath('/*/*')) == 0:
348- context["probed_details"] = None
349+ context['probed_details'] = None
350 else:
351- context["probed_details"] = etree.tostring(
352+ context['probed_details'] = etree.tostring(
353 probed_details, encoding=unicode, pretty_print=True)
354
355- results = NodeResult.objects.filter(node=node).count()
356- context['nodecommissionresults'] = results
357+ commissioning_results = NodeResult.objects.filter(
358+ node=node, result_type=RESULT_TYPE.COMMISSIONING).count()
359+ context['nodecommissionresults'] = commissioning_results
360+
361+ installation_results = NodeResult.objects.filter(
362+ node=node, result_type=RESULT_TYPE.INSTALLING)
363+ if len(installation_results) > 0:
364+ context['nodeinstallresults'] = installation_results
365
366 context['third_party_drivers_enabled'] = Config.objects.get_config(
367 'enable_third_party_drivers')
368
369=== renamed file 'src/maasserver/views/tests/test_nodecommissionresults.py' => 'src/maasserver/views/tests/test_noderesult.py'
370--- src/maasserver/views/tests/test_nodecommissionresults.py 2014-08-13 21:49:35 +0000
371+++ src/maasserver/views/tests/test_noderesult.py 2014-08-20 20:35:23 +0000
372@@ -25,7 +25,7 @@
373 from maasserver.testing.factory import factory
374 from maasserver.testing.testcase import MAASServerTestCase
375 from maasserver.utils.orm import get_one
376-from maasserver.views.nodecommissionresult import NodeCommissionResultListView
377+from maasserver.views.noderesult import NodeCommissionResultListView
378 from mock import Mock
379 from testtools.matchers import HasLength
380
381@@ -35,6 +35,73 @@
382 return ' '.join(text.split())
383
384
385+def extract_field(doc, field_name, containing_tag='span'):
386+ """Get the content text from one of the <li> fields on the page.
387+
388+ This works on the basis that each of the fields has an `id` attribute
389+ which is unique on the page, and contains exactly one tag of the type
390+ given as `containing_tag`, which holds the field value.
391+ """
392+ field = get_one(doc.cssselect('#' + field_name))
393+ value = get_one(field.cssselect(containing_tag))
394+ return value.text_content().strip()
395+
396+
397+class TestNodeInstallResultView(MAASServerTestCase):
398+
399+ def request_page(self, result):
400+ """Request and parse the page for the given `NodeResult`.
401+
402+ :return: The page's main content as an `lxml.html.HtmlElement`.
403+ """
404+ link = reverse('nodeinstallresult-view', args=[result.id])
405+ response = self.client.get(link)
406+ self.assertEqual(httplib.OK, response.status_code, response.content)
407+ doc = html.fromstring(response.content)
408+ return get_one(doc.cssselect('#content'))
409+
410+ def test_installing_forbidden_without_edit_perm(self):
411+ self.client_log_in(as_admin=False)
412+ result = factory.make_node_install_result()
413+ response = self.client.get(
414+ reverse('nodeinstallresult-view', args=[result.id]))
415+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
416+
417+ def test_installing_allowed_with_edit_perm(self):
418+ password = 'test'
419+ user = factory.make_user(password=password)
420+ node = factory.make_node(owner=user)
421+ self.client.login(username=user.username, password=password)
422+ self.logged_in_user = user
423+ result = factory.make_node_install_result(node=node)
424+ response = self.client.get(
425+ reverse('nodeinstallresult-view', args=[result.id]))
426+ self.assertEqual(httplib.OK, response.status_code)
427+
428+ def test_installing_escapes_html_in_output(self):
429+ self.client_log_in(as_admin=True)
430+ # It's actually surprisingly hard to test for this, because lxml
431+ # un-escapes on parsing, and is very tolerant of malformed input.
432+ # Parsing an un-escaped A<B>C, however, would produce text "AC"
433+ # (because the <B> looks like a tag).
434+ result = factory.make_node_install_result(data=b'A<B>C')
435+ doc = self.request_page(result)
436+ self.assertEqual('A<B>C', extract_field(doc, 'output', 'pre'))
437+
438+ def test_installing_escapes_binary_in_output(self):
439+ self.client_log_in(as_admin=True)
440+ result = factory.make_node_install_result(data=b'A\xffB')
441+ doc = self.request_page(result)
442+ self.assertEqual('A\ufffdB', extract_field(doc, 'output', 'pre'))
443+
444+ def test_installing_hides_output_if_empty(self):
445+ self.client_log_in(as_admin=True)
446+ result = factory.make_node_install_result(data=b'')
447+ doc = self.request_page(result)
448+ field = get_one(doc.cssselect('#output'))
449+ self.assertEqual('', field.text_content().strip())
450+
451+
452 class TestNodeCommissionResultView(MAASServerTestCase):
453
454 def request_page(self, result):
455@@ -48,40 +115,40 @@
456 doc = html.fromstring(response.content)
457 return get_one(doc.cssselect('#content'))
458
459- def extract_field(self, doc, field_name, containing_tag='span'):
460- """Get the content text from one of the <li> fields on the page.
461-
462- This works on the basis that each of the fields has an `id` attribute
463- which is unique on the page, and contains exactly one tag of the type
464- given as `containing_tag`, which holds the field value.
465- """
466- field = get_one(doc.cssselect('#' + field_name))
467- value = get_one(field.cssselect(containing_tag))
468- return value.text_content().strip()
469-
470- def test_requires_admin(self):
471+ def test_commissioning_forbidden_without_edit_perm(self):
472 self.client_log_in(as_admin=False)
473 result = factory.make_node_commission_result()
474 response = self.client.get(
475 reverse('nodecommissionresult-view', args=[result.id]))
476- self.assertEqual(reverse('login'), extract_redirect(response))
477-
478- def test_displays_result(self):
479+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
480+
481+ def test_commissioning_allowed_with_edit_perm(self):
482+ password = 'test'
483+ user = factory.make_user(password=password)
484+ node = factory.make_node(owner=user)
485+ self.client.login(username=user.username, password=password)
486+ self.logged_in_user = user
487+ result = factory.make_node_commission_result(node=node)
488+ response = self.client.get(
489+ reverse('nodecommissionresult-view', args=[result.id]))
490+ self.assertEqual(httplib.OK, response.status_code)
491+
492+ def test_commissioning_displays_result(self):
493 self.client_log_in(as_admin=True)
494 result = factory.make_node_commission_result(
495 data=factory.make_string().encode('ascii'))
496 doc = self.request_page(result)
497
498- self.assertEqual(result.name, self.extract_field(doc, 'name'))
499+ self.assertEqual(result.name, extract_field(doc, 'name'))
500 self.assertEqual(
501 result.node.hostname,
502- self.extract_field(doc, 'node'))
503+ extract_field(doc, 'node'))
504 self.assertEqual(
505 "%d" % result.script_result,
506- self.extract_field(doc, 'script-result'))
507- self.assertEqual(result.data, self.extract_field(doc, 'output', 'pre'))
508+ extract_field(doc, 'script-result'))
509+ self.assertEqual(result.data, extract_field(doc, 'output', 'pre'))
510
511- def test_escapes_html_in_output(self):
512+ def test_commissioning_escapes_html_in_output(self):
513 self.client_log_in(as_admin=True)
514 # It's actually surprisingly hard to test for this, because lxml
515 # un-escapes on parsing, and is very tolerant of malformed input.
516@@ -89,15 +156,15 @@
517 # (because the <B> looks like a tag).
518 result = factory.make_node_commission_result(data=b'A<B>C')
519 doc = self.request_page(result)
520- self.assertEqual('A<B>C', self.extract_field(doc, 'output', 'pre'))
521+ self.assertEqual('A<B>C', extract_field(doc, 'output', 'pre'))
522
523- def test_escapes_binary_in_output(self):
524+ def test_commissioning_escapes_binary_in_output(self):
525 self.client_log_in(as_admin=True)
526 result = factory.make_node_commission_result(data=b'A\xffB')
527 doc = self.request_page(result)
528- self.assertEqual('A\ufffdB', self.extract_field(doc, 'output', 'pre'))
529+ self.assertEqual('A\ufffdB', extract_field(doc, 'output', 'pre'))
530
531- def test_hides_output_if_empty(self):
532+ def test_commissioning_hides_output_if_empty(self):
533 self.client_log_in(as_admin=True)
534 result = factory.make_node_commission_result(data=b'')
535 doc = self.request_page(result)
536
537=== modified file 'src/maasserver/views/tests/test_nodes.py'
538--- src/maasserver/views/tests/test_nodes.py 2014-08-19 18:38:26 +0000
539+++ src/maasserver/views/tests/test_nodes.py 2014-08-20 20:35:23 +0000
540@@ -76,6 +76,7 @@
541 NodeView,
542 )
543 from maastesting.djangotestcase import count_queries
544+from metadataserver.enum import RESULT_TYPE
545 from metadataserver.models.commissioningscript import (
546 LIST_MODALIASES_OUTPUT_NAME,
547 LLDP_OUTPUT_NAME,
548@@ -1264,10 +1265,12 @@
549 nodes_views.construct_third_party_drivers_notice(True).strip())
550
551
552-class NodeCommissionResultsDisplayTest(MAASServerTestCase):
553- """Tests for the link to node commissioning results on the Node page."""
554+class NodeResultsDisplayTest(MAASServerTestCase):
555+ """Tests for the link to node commissioning/installing
556+ results on the Node page.
557+ """
558
559- def request_results_display(self, node):
560+ def request_results_display(self, node, result_type):
561 """Request the page for `node`, and extract the results display.
562
563 Fails if generating, loading or parsing the page failed; or if
564@@ -1281,7 +1284,10 @@
565 response = self.client.get(node_link)
566 self.assertEqual(httplib.OK, response.status_code, response.content)
567 doc = fromstring(response.content)
568- results_display = doc.cssselect('#nodecommissionresults')
569+ if result_type == RESULT_TYPE.COMMISSIONING:
570+ results_display = doc.cssselect('#nodecommissionresults')
571+ elif result_type == RESULT_TYPE.INSTALLING:
572+ results_display = doc.cssselect('#nodeinstallresults')
573 if len(results_display) == 0:
574 return None
575 elif len(results_display) == 1:
576@@ -1313,7 +1319,8 @@
577 def test_view_node_links_to_commissioning_results_if_appropriate(self):
578 self.client_log_in(as_admin=True)
579 result = factory.make_node_commission_result()
580- section = self.request_results_display(result.node)
581+ section = self.request_results_display(
582+ result.node, RESULT_TYPE.COMMISSIONING)
583 link = self.get_results_link(section)
584 results_list = reverse('nodecommissionresult-list')
585 self.assertEqual(
586@@ -1323,17 +1330,39 @@
587 def test_view_node_shows_commissioning_results_only_if_present(self):
588 self.client_log_in(as_admin=True)
589 node = factory.make_node()
590- self.assertIsNone(self.request_results_display(node))
591-
592- def test_view_node_shows_commissioning_results_only_to_superuser(self):
593- self.client_log_in(as_admin=False)
594- result = factory.make_node_commission_result()
595- self.assertIsNone(self.request_results_display(result.node))
596+ self.assertIsNone(
597+ self.request_results_display(node, RESULT_TYPE.COMMISSIONING))
598+
599+ def test_view_node_shows_commissioning_results_with_edit_perm(self):
600+ password = 'test'
601+ user = factory.make_user(password=password)
602+ node = factory.make_node(owner=user)
603+ self.client.login(username=user.username, password=password)
604+ self.logged_in_user = user
605+ result = factory.make_node_commission_result(node=node)
606+ section = self.request_results_display(
607+ result.node, RESULT_TYPE.COMMISSIONING)
608+ link = self.get_results_link(section)
609+ self.assertEqual(
610+ "1 output file",
611+ self.normalise_whitespace(link.text_content()))
612+
613+ def test_view_node_shows_commissioning_results_requires_edit_perm(self):
614+ password = 'test'
615+ user = factory.make_user(password=password)
616+ node = factory.make_node()
617+ self.client.login(username=user.username, password=password)
618+ self.logged_in_user = user
619+ result = factory.make_node_commission_result(node=node)
620+ self.assertIsNone(
621+ self.request_results_display(
622+ result.node, RESULT_TYPE.COMMISSIONING))
623
624 def test_view_node_shows_single_commissioning_result(self):
625 self.client_log_in(as_admin=True)
626 result = factory.make_node_commission_result()
627- section = self.request_results_display(result.node)
628+ section = self.request_results_display(
629+ result.node, RESULT_TYPE.COMMISSIONING)
630 link = self.get_results_link(section)
631 self.assertEqual(
632 "1 output file",
633@@ -1345,12 +1374,43 @@
634 num_results = randint(2, 5)
635 for _ in range(num_results):
636 factory.make_node_commission_result(node=node)
637- section = self.request_results_display(node)
638+ section = self.request_results_display(
639+ node, RESULT_TYPE.COMMISSIONING)
640 link = self.get_results_link(section)
641 self.assertEqual(
642 "%d output files" % num_results,
643 self.normalise_whitespace(link.text_content()))
644
645+ def test_view_node_shows_installing_results_only_if_present(self):
646+ self.client_log_in(as_admin=True)
647+ node = factory.make_node()
648+ self.assertIsNone(
649+ self.request_results_display(node, RESULT_TYPE.INSTALLING))
650+
651+ def test_view_node_shows_installing_results_with_edit_perm(self):
652+ password = 'test'
653+ user = factory.make_user(password=password)
654+ node = factory.make_node(owner=user)
655+ self.client.login(username=user.username, password=password)
656+ self.logged_in_user = user
657+ result = factory.make_node_install_result(node=node)
658+ section = self.request_results_display(
659+ result.node, RESULT_TYPE.INSTALLING)
660+ link = self.get_results_link(section)
661+ self.assertIsNotNone(
662+ self.normalise_whitespace(link.text_content()))
663+
664+ def test_view_node_shows_installing_results_requires_edit_perm(self):
665+ password = 'test'
666+ user = factory.make_user(password=password)
667+ node = factory.make_node()
668+ self.client.login(username=user.username, password=password)
669+ self.logged_in_user = user
670+ result = factory.make_node_install_result(node=node)
671+ self.assertIsNone(
672+ self.request_results_display(
673+ result.node, RESULT_TYPE.INSTALLING))
674+
675
676 class NodeListingSelectionJSControls(SeleniumTestCase):
677