Merge lp:~newell-jensen/maas/maas-maasserver-ui-curtin-install-log into lp:~maas-committers/maas/trunk
- maas-maasserver-ui-curtin-install-log
- Merge into trunk
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 |
Related bugs: |
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.
Description of the change
Newell Jensen (newell-jensen) wrote : | # |
Ready for another review.
Blake Rouse (blake-rouse) wrote : | # |
I think its close. Couple of questions.
Newell Jensen (newell-jensen) wrote : | # |
> I think its close. Couple of questions.
See inline for responses.
Newell Jensen (newell-jensen) wrote : | # |
See inline for responses.
Newell Jensen (newell-jensen) wrote : | # |
Have another look BRAH...okay that was last one.
Newell Jensen (newell-jensen) wrote : | # |
Added edit permissions changes and tests. Have another look.
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.
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.
Blake Rouse (blake-rouse) wrote : | # |
Looking good now!
Preview Diff
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 |
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.