Merge ~newell-jensen/maas:fix-1686678 into maas:master

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: 194c62cf7ff3fbce84540d9fafa6ea04a2d75fcc
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~newell-jensen/maas:fix-1686678
Merge into: maas:master
Diff against target: 408 lines (+130/-19)
8 files modified
src/maasserver/forms/__init__.py (+6/-1)
src/maasserver/forms/tests/test_controller.py (+13/-0)
src/maasserver/static/js/angular/controllers/node_details.js (+25/-2)
src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+64/-4)
src/maasserver/static/partials/node-details.html (+6/-4)
src/maasserver/websockets/handlers/controller.py (+10/-2)
src/maasserver/websockets/handlers/tests/test_controller.py (+4/-4)
src/maasserver/websockets/protocol.py (+2/-2)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+326634@code.launchpad.net

Commit message

Allows the domain of controllers to be changed in the UI.

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

Looks good.

You have a few minor alignment problems with the HTML that affect read ability, but otherwise it looks good.

review: Approve
~newell-jensen/maas:fix-1686678 updated
194c62c... by Newell Jensen

Fix html alignment issues.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
2index 2dd682c..5e752c1 100644
3--- a/src/maasserver/forms/__init__.py
4+++ b/src/maasserver/forms/__init__.py
5@@ -875,19 +875,24 @@ class ControllerForm(MAASModelForm, WithPowerTypeMixin):
6 class Meta:
7 model = Controller
8
9- fields = ['zone']
10+ fields = ['zone', 'domain']
11
12 zone = forms.ModelChoiceField(
13 label="Physical zone", required=False,
14 initial=Zone.objects.get_default_zone,
15 queryset=Zone.objects.all(), to_field_name='name')
16
17+ domain = forms.ModelChoiceField(
18+ required=False, initial=Domain.objects.get_default_domain,
19+ queryset=Domain.objects.all(), to_field_name='name')
20+
21 def __init__(self, data=None, instance=None, request=None, **kwargs):
22 super(ControllerForm, self).__init__(
23 data=data, instance=instance, **kwargs)
24 WithPowerTypeMixin.set_up_power_fields(self, data, instance)
25 if instance is not None:
26 self.initial['zone'] = instance.zone.name
27+ self.initial['domain'] = instance.domain.name
28
29 def clean(self):
30 cleaned_data = super(ControllerForm, self).clean()
31diff --git a/src/maasserver/forms/tests/test_controller.py b/src/maasserver/forms/tests/test_controller.py
32index b7ca4b0..6481447 100644
33--- a/src/maasserver/forms/tests/test_controller.py
34+++ b/src/maasserver/forms/tests/test_controller.py
35@@ -18,6 +18,7 @@ class TestControllerForm(MAASServerTestCase):
36 self.assertItemsEqual(
37 [
38 'zone',
39+ 'domain',
40 'power_type',
41 'power_parameters',
42 ],
43@@ -70,3 +71,15 @@ class TestControllerForm(MAASServerTestCase):
44 instance=rack)
45 rack = form.save()
46 self.assertEqual(zone.name, rack.zone.name)
47+
48+ def test__sets_domain(self):
49+ rack = factory.make_RackController()
50+ domain = factory.make_Domain()
51+ form = ControllerForm(
52+ data={
53+ 'domain': domain.name,
54+ 'power_parameters_skip_check': 'true',
55+ },
56+ instance=rack)
57+ rack = form.save()
58+ self.assertEqual(domain.name, rack.domain.name)
59diff --git a/src/maasserver/static/js/angular/controllers/node_details.js b/src/maasserver/static/js/angular/controllers/node_details.js
60index e712a14..85869f4 100644
61--- a/src/maasserver/static/js/angular/controllers/node_details.js
62+++ b/src/maasserver/static/js/angular/controllers/node_details.js
63@@ -60,6 +60,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
64 // Node header section.
65 $scope.header = {
66 editing: false,
67+ editing_domain: false,
68 hostname: {
69 value: ""
70 },
71@@ -127,7 +128,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
72 function updateHeader() {
73 // Don't update the value if in editing mode. As this would
74 // overwrite the users changes.
75- if($scope.header.editing) {
76+ if($scope.header.editing || $scope.header.editing_domain) {
77 return;
78 }
79 $scope.header.hostname.value = $scope.node.fqdn;
80@@ -757,6 +758,25 @@ angular.module('MAAS').controller('NodeDetailsController', [
81 !$scope.isController);
82 };
83
84+ // Called to edit the domain name.
85+ $scope.editHeaderDomain = function() {
86+ if($scope.canEdit()) {
87+ return;
88+ }
89+
90+ // Do nothing if already editing because we don't want to reset
91+ // the current value.
92+ if($scope.header.editing_domain) {
93+ return;
94+ }
95+ $scope.header.editing = false;
96+ $scope.header.editing_domain = true;
97+
98+ // Set the value to the hostname, as hostname and domain are edited
99+ // using different fields.
100+ $scope.header.hostname.value = $scope.node.hostname;
101+ };
102+
103 // Called to edit the node name.
104 $scope.editHeader = function() {
105 if(!$scope.canEdit()) {
106@@ -769,6 +789,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
107 return;
108 }
109 $scope.header.editing = true;
110+ $scope.header.editing_domain = false;
111
112 // Set the value to the hostname, as hostname and domain are edited
113 // using different fields.
114@@ -778,7 +799,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
115 // Return true when the hostname or domain in the header is invalid.
116 $scope.editHeaderInvalid = function() {
117 // Not invalid unless editing.
118- if(!$scope.header.editing) {
119+ if(!$scope.header.editing && !$scope.header.editing_domain) {
120 return false;
121 }
122
123@@ -793,6 +814,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
124 // Called to cancel editing of the node hostname and domain.
125 $scope.cancelEditHeader = function() {
126 $scope.header.editing = false;
127+ $scope.header.editing_domain = false;
128 updateHeader();
129 };
130
131@@ -803,6 +825,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
132 return;
133 }
134 $scope.header.editing = false;
135+ $scope.header.editing_domain = false;
136
137 // Copy the node and make the changes.
138 var node = angular.copy($scope.node);
139diff --git a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
140index 96207dd..d782d63 100644
141--- a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
142+++ b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
143@@ -1486,23 +1486,74 @@ describe("NodeDetailsController", function() {
144 });
145 });
146
147+ describe("editHeaderDomain", function() {
148+
149+ it("doesnt set editing false and editing_domain true if cannot edit",
150+ function() {
151+ var controller = makeController();
152+ spyOn($scope, "canEdit").and.returnValue(true);
153+ $scope.header.editing = true;
154+ $scope.header.editing_domain = false;
155+ $scope.editHeaderDomain();
156+ expect($scope.header.editing).toBe(true);
157+ expect($scope.header.editing_domain).toBe(false);
158+ });
159+
160+ it("sets editing to false and editing_domain to true if able",
161+ function() {
162+ var controller = makeController();
163+ $scope.node = node;
164+ spyOn($scope, "canEdit").and.returnValue(false);
165+ $scope.header.editing = true;
166+ $scope.header.editing_domain = false;
167+ $scope.editHeaderDomain();
168+ expect($scope.header.editing).toBe(false);
169+ expect($scope.header.editing_domain).toBe(true);
170+ });
171+
172+ it("sets header.hostname.value to node hostname", function() {
173+ var controller = makeController();
174+ $scope.node = node;
175+ spyOn($scope, "canEdit").and.returnValue(false);
176+ $scope.editHeaderDomain();
177+ expect($scope.header.hostname.value).toBe(node.hostname);
178+ });
179+
180+ it("doesnt reset header.hostname.value on multiple calls", function() {
181+ var controller = makeController();
182+ $scope.node = node;
183+ spyOn($scope, "canEdit").and.returnValue(false);
184+ $scope.editHeaderDomain();
185+ var updatedName = makeName("name");
186+ $scope.header.hostname.value = updatedName;
187+ $scope.editHeaderDomain();
188+ expect($scope.header.hostname.value).toBe(updatedName);
189+ });
190+ });
191+
192 describe("editHeader", function() {
193
194- it("doesnt set editing to true if cannot edit", function() {
195+ it("doesnt set editing true and editing_domain false if cannot edit",
196+ function() {
197 var controller = makeController();
198 spyOn($scope, "canEdit").and.returnValue(false);
199 $scope.header.editing = false;
200+ $scope.header.editing_domain = true;
201 $scope.editHeader();
202 expect($scope.header.editing).toBe(false);
203+ expect($scope.header.editing_domain).toBe(true);
204 });
205
206- it("sets editing to true if able", function() {
207+ it("sets editing to true and editing_domain to false if able",
208+ function() {
209 var controller = makeController();
210 $scope.node = node;
211 spyOn($scope, "canEdit").and.returnValue(true);
212 $scope.header.editing = false;
213+ $scope.header.editing_domain = true;
214 $scope.editHeader();
215 expect($scope.header.editing).toBe(true);
216+ expect($scope.header.editing_domain).toBe(false);
217 });
218
219 it("sets header.hostname.value to node hostname", function() {
220@@ -1527,9 +1578,10 @@ describe("NodeDetailsController", function() {
221
222 describe("editHeaderInvalid", function() {
223
224- it("returns false if not editing", function() {
225+ it("returns false if not editing and not editing_domain", function() {
226 var controller = makeController();
227 $scope.header.editing = false;
228+ $scope.header.editing_domain = false;
229 $scope.header.hostname.value = "abc_invalid.local";
230 expect($scope.editHeaderInvalid()).toBe(false);
231 });
232@@ -1537,6 +1589,7 @@ describe("NodeDetailsController", function() {
233 it("returns true for bad values", function() {
234 var controller = makeController();
235 $scope.header.editing = true;
236+ $scope.header.editing_domain = false;
237 var values = [
238 {
239 input: "aB0-z",
240@@ -1564,12 +1617,15 @@ describe("NodeDetailsController", function() {
241
242 describe("cancelEditHeader", function() {
243
244- it("sets editing to false for nameHeader section", function() {
245+ it("sets editing and editing_domain to false for nameHeader section",
246+ function() {
247 var controller = makeController();
248 $scope.node = node;
249 $scope.header.editing = true;
250+ $scope.header.editing_domain = true;
251 $scope.cancelEditHeader();
252 expect($scope.header.editing).toBe(false);
253+ expect($scope.header.editing_domain).toBe(false);
254 });
255
256 it("sets header.hostname.value back to fqdn", function() {
257@@ -1590,8 +1646,10 @@ describe("NodeDetailsController", function() {
258 spyOn($scope, "editHeaderInvalid").and.returnValue(true);
259 var sentinel = {};
260 $scope.header.editing = sentinel;
261+ $scope.header.editing_domain = sentinel;
262 $scope.saveEditHeader();
263 expect($scope.header.editing).toBe(sentinel);
264+ expect($scope.header.editing_domain).toBe(sentinel);
265 });
266
267 it("sets editing to false", function() {
268@@ -1602,10 +1660,12 @@ describe("NodeDetailsController", function() {
269
270 $scope.node = node;
271 $scope.header.editing = true;
272+ $scope.header.editing_domain = true;
273 $scope.header.hostname.value = makeName("name");
274 $scope.saveEditHeader();
275
276 expect($scope.header.editing).toBe(false);
277+ expect($scope.header.editing_domain).toBe(false);
278 });
279
280 it("calls updateItem with copy of node", function() {
281diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html
282index 00d0b11..bff8c1a 100755
283--- a/src/maasserver/static/partials/node-details.html
284+++ b/src/maasserver/static/partials/node-details.html
285@@ -11,21 +11,23 @@
286 <h1 class="page-header__title">
287 <span contenteditable="true"
288 data-ng-class="{ editable: canEdit(), 'has-error': editHeaderInvalid(), editmode: header.editing }"
289+ data-ng-show="canEdit()"
290 data-ng-model="header.hostname.value"
291 data-ng-disabled="!canEdit()"
292 data-maas-editing="editHeader()"></span>
293- <span class="page-header__title-dot ng-hide" data-ng-show="header.editing">.</span>
294+ <span ng-click="editHeaderDomain()" data-ng-show="!canEdit()">{$ header.hostname.value $}</span>
295+ <span class="page-header__title-dot ng-hide" data-ng-show="header.editing || header.editing_domain">.</span>
296 <select class="page-header__title-domain ng-hide"
297- data-ng-show="header.editing"
298+ data-ng-show="header.editing || header.editing_domain"
299 data-ng-model="header.domain.selected"
300 data-ng-options="domain as domain.name for domain in header.domain.options">
301 </select>
302 </h1>
303 <button class="button--base button--inline u-margin--bottom-small ng-hide"
304- data-ng-show="header.editing"
305+ data-ng-show="header.editing || header.editing_domain"
306 data-ng-click="cancelEditHeader()">Cancel</button>
307 <button class="button--positive button--inline u-margin--bottom-small ng-hide"
308- data-ng-show="header.editing"
309+ data-ng-show="header.editing || header.editing_domain"
310 data-ng-disabled="editHeaderInvalid()"
311 data-ng-click="saveEditHeader()">Save</button>
312
313diff --git a/src/maasserver/websockets/handlers/controller.py b/src/maasserver/websockets/handlers/controller.py
314index 0aa8ff3..ca85333 100644
315--- a/src/maasserver/websockets/handlers/controller.py
316+++ b/src/maasserver/websockets/handlers/controller.py
317@@ -8,11 +8,12 @@ __all__ = [
318 ]
319
320 from maasserver.enum import NODE_PERMISSION
321-from maasserver.forms import AdminMachineWithMACAddressesForm
322+from maasserver.forms import ControllerForm
323 from maasserver.models.node import (
324 Controller,
325 RackController,
326 )
327+from maasserver.websockets.base import HandlerError
328 from maasserver.websockets.handlers.machine import MachineHandler
329 from maasserver.websockets.handlers.node import node_prefetch
330
331@@ -40,7 +41,7 @@ class ControllerHandler(MachineHandler):
332 'link_subnet',
333 'unlink_subnet',
334 ]
335- form = AdminMachineWithMACAddressesForm
336+ form = ControllerForm
337 exclude = [
338 "status_expires",
339 "parent",
340@@ -74,6 +75,13 @@ class ControllerHandler(MachineHandler):
341 "controller",
342 ]
343
344+ def get_form_class(self, action):
345+ """Return the form class used for `action`."""
346+ if action in ("create", "update"):
347+ return ControllerForm
348+ else:
349+ raise HandlerError("Unknown action: %s" % action)
350+
351 def get_queryset(self):
352 """Return `QuerySet` for controllers only viewable by `user`."""
353 return Controller.controllers.get_nodes(
354diff --git a/src/maasserver/websockets/handlers/tests/test_controller.py b/src/maasserver/websockets/handlers/tests/test_controller.py
355index 1a23df1..111ed57 100644
356--- a/src/maasserver/websockets/handlers/tests/test_controller.py
357+++ b/src/maasserver/websockets/handlers/tests/test_controller.py
358@@ -1,4 +1,4 @@
359-# Copyright 2016 Canonical Ltd. This software is licensed under the
360+# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
361 # GNU Affero General Public License version 3 (see the file LICENSE).
362
363 """Tests for `maasserver.websockets.handlers.controller`"""
364@@ -6,7 +6,7 @@
365 __all__ = []
366
367 from maasserver.enum import NODE_TYPE
368-from maasserver.forms import AdminMachineWithMACAddressesForm
369+from maasserver.forms import ControllerForm
370 from maasserver.testing.factory import factory
371 from maasserver.testing.testcase import MAASServerTestCase
372 from maasserver.websockets.base import dehydrate_datetime
373@@ -68,14 +68,14 @@ class TestControllerHandler(MAASServerTestCase):
374 user = factory.make_admin()
375 handler = ControllerHandler(user, {})
376 self.assertEqual(
377- AdminMachineWithMACAddressesForm,
378+ ControllerForm,
379 handler.get_form_class("create"))
380
381 def test_get_form_class_for_update(self):
382 user = factory.make_admin()
383 handler = ControllerHandler(user, {})
384 self.assertEqual(
385- AdminMachineWithMACAddressesForm,
386+ ControllerForm,
387 handler.get_form_class("update"))
388
389 def test_check_images(self):
390diff --git a/src/maasserver/websockets/protocol.py b/src/maasserver/websockets/protocol.py
391index af5c195..f143fd7 100644
392--- a/src/maasserver/websockets/protocol.py
393+++ b/src/maasserver/websockets/protocol.py
394@@ -1,4 +1,4 @@
395-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
396+# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
397 # GNU Affero General Public License version 3 (see the file LICENSE).
398
399 """The MAAS WebSockets protocol."""
400@@ -218,7 +218,7 @@ class WebSocketProtocol(Protocol):
401 def dataReceived(self, data):
402 """Received message from client and queue up the message."""
403 try:
404- message = json.loads(data.decode("ascii"))
405+ message = json.loads(data.decode('utf-8'))
406 except ValueError:
407 # Only accept JSON data over the protocol. Close the connect
408 # with invalid data.

Subscribers

People subscribed via source and target branches