Merge lp:~jtv/maas/bug-1374388 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 3117
Proposed branch: lp:~jtv/maas/bug-1374388
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 82 lines (+39/-0)
3 files modified
src/maasserver/forms.py (+19/-0)
src/maasserver/templates/maasserver/node_edit.html (+1/-0)
src/maasserver/tests/test_forms_node.py (+19/-0)
To merge this branch: bzr merge lp:~jtv/maas/bug-1374388
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Raphaël Badin (community) Approve
Review via email: mp+236291@code.launchpad.net

Commit message

Work around an “impedance mismatch” between Django's BooleanField in forms, and our use of forms for both UI and API.

The Node.disable_ipv4 checkbox could be enabled in the UI, but never disabled. In the API it could go both ways. The reason is that Django never submits a False value for an unchecked checkbox; it just doesn't include the value in the form submission. Our forms are rigged to interpret missing fields as ones that should stay unchanged. As a result, once disable_ipv4 was set to True, the UI simply ignored attempts to set it back to False.

The workaround adds an invisible input field to tell a UI-originated form submission from an API-originated one. A UI form shows all fields, so a missing boolean field means “set to False.” An API form must deal with callers omitting fields that were left out in order to keep them unchanged, so a missing boolean field does what it does now. This is particularly important with fields that are added in later versions.

Description of the change

The solution is one we can re-use in NodeGroup.default_disable_ipv4 as well. I had to store the un-processed version of the form's submitted data, because we previously just discarded it.

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Nice! Although I understand the immediate goal is this fix the bug linked to this branch, I'm thinking about how we could generalize this so that similar problems can be fixed with minimal changes to the code (and ideally no change to the templates).

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

Thanks for the suggestion. I'll see if I can do that in a follow-up branch. I'll start by renaming our ModelForm so that the difference from Django's ModelForm becomes more visible. That was one of those things that hid the problem from me today.

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

Did I just vote Approve instead of marking the branch as Approved? Death march symptoms. :(

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/forms.py'
2--- src/maasserver/forms.py 2014-09-25 21:24:03 +0000
3+++ src/maasserver/forms.py 2014-09-29 10:01:07 +0000
4@@ -188,10 +188,13 @@
5 First it ensures that missing fields are not errors, then it removes
6 None values from cleaned data. This means that missing fields result
7 in no change instead of an error.
8+
9+ :ivar submitted_data: The `data` as originally submitted.
10 """
11
12 def full_clean(self):
13 """For missing fields, default to the model's existing value."""
14+ self.submitted_data = self.data
15 self.data = get_overridden_query_dict(
16 self.initial, self.data, self.fields)
17 super(APIEditMixin, self).full_clean()
18@@ -385,6 +388,22 @@
19 def clean_distro_series(self):
20 return clean_distro_series_field(self, 'distro_series', 'osystem')
21
22+ def clean_disable_ipv4(self):
23+ # Boolean fields only show up in UI form submissions as "true" (if the
24+ # box was checked) or not at all (if the box was not checked). This
25+ # is different from API submissions which can submit "false" values.
26+ # Our forms are rigged to interpret missing fields as unchanged, but
27+ # that doesn't # work for the UI. A form in the UI always submits all
28+ # its fields, so in that case, no value means False.
29+ #
30+ # To kludge around this, the UI form submits a hidden input field named
31+ # "ui-submission" that doesn't exist in the API. If this field is
32+ # present, go with the UI-style behaviour.
33+ form_data = self.submitted_data
34+ if 'ui-submission' in form_data and 'disable_ipv4' not in form_data:
35+ self.cleaned_data['disable_ipv4'] = False
36+ return self.cleaned_data['disable_ipv4']
37+
38 def clean(self):
39 cleaned_data = super(NodeForm, self).clean()
40 if self.new_node and self.data.get('disable_ipv4') is None:
41
42=== modified file 'src/maasserver/templates/maasserver/node_edit.html'
43--- src/maasserver/templates/maasserver/node_edit.html 2014-06-09 19:58:06 +0000
44+++ src/maasserver/templates/maasserver/node_edit.html 2014-09-29 10:01:07 +0000
45@@ -77,6 +77,7 @@
46 </a>
47 </li>
48 </ul>
49+ <input name="ui-submission" type="hidden" value="Submitted from the UI" />
50 <input type="submit" value="Save node" class="right" />
51 <a class="link-button" href="{% url 'node-view' node.system_id %}">Cancel</a>
52 </form>
53
54=== modified file 'src/maasserver/tests/test_forms_node.py'
55--- src/maasserver/tests/test_forms_node.py 2014-09-23 14:16:33 +0000
56+++ src/maasserver/tests/test_forms_node.py 2014-09-29 10:01:07 +0000
57@@ -369,6 +369,25 @@
58 node = form.save()
59 self.assertEqual(setting, node.disable_ipv4)
60
61+ def test_takes_missing_disable_ipv4_as_False_in_UI(self):
62+ form = NodeForm(
63+ instance=factory.make_Node(disable_ipv4=True),
64+ data={
65+ 'architecture': make_usable_architecture(self),
66+ 'ui-submission': True,
67+ })
68+ node = form.save()
69+ self.assertFalse(node.disable_ipv4)
70+
71+ def test_takes_missing_disable_ipv4_as_Unchanged_in_API(self):
72+ form = NodeForm(
73+ instance=factory.make_Node(disable_ipv4=True),
74+ data={
75+ 'architecture': make_usable_architecture(self),
76+ })
77+ node = form.save()
78+ self.assertTrue(node.disable_ipv4)
79+
80 def test_takes_True_disable_ipv4_from_cluster_by_default(self):
81 setting = True
82 cluster = factory.make_NodeGroup(default_disable_ipv4=setting)