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
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2014-09-25 21:24:03 +0000
+++ src/maasserver/forms.py 2014-09-29 10:01:07 +0000
@@ -188,10 +188,13 @@
188 First it ensures that missing fields are not errors, then it removes188 First it ensures that missing fields are not errors, then it removes
189 None values from cleaned data. This means that missing fields result189 None values from cleaned data. This means that missing fields result
190 in no change instead of an error.190 in no change instead of an error.
191
192 :ivar submitted_data: The `data` as originally submitted.
191 """193 """
192194
193 def full_clean(self):195 def full_clean(self):
194 """For missing fields, default to the model's existing value."""196 """For missing fields, default to the model's existing value."""
197 self.submitted_data = self.data
195 self.data = get_overridden_query_dict(198 self.data = get_overridden_query_dict(
196 self.initial, self.data, self.fields)199 self.initial, self.data, self.fields)
197 super(APIEditMixin, self).full_clean()200 super(APIEditMixin, self).full_clean()
@@ -385,6 +388,22 @@
385 def clean_distro_series(self):388 def clean_distro_series(self):
386 return clean_distro_series_field(self, 'distro_series', 'osystem')389 return clean_distro_series_field(self, 'distro_series', 'osystem')
387390
391 def clean_disable_ipv4(self):
392 # Boolean fields only show up in UI form submissions as "true" (if the
393 # box was checked) or not at all (if the box was not checked). This
394 # is different from API submissions which can submit "false" values.
395 # Our forms are rigged to interpret missing fields as unchanged, but
396 # that doesn't # work for the UI. A form in the UI always submits all
397 # its fields, so in that case, no value means False.
398 #
399 # To kludge around this, the UI form submits a hidden input field named
400 # "ui-submission" that doesn't exist in the API. If this field is
401 # present, go with the UI-style behaviour.
402 form_data = self.submitted_data
403 if 'ui-submission' in form_data and 'disable_ipv4' not in form_data:
404 self.cleaned_data['disable_ipv4'] = False
405 return self.cleaned_data['disable_ipv4']
406
388 def clean(self):407 def clean(self):
389 cleaned_data = super(NodeForm, self).clean()408 cleaned_data = super(NodeForm, self).clean()
390 if self.new_node and self.data.get('disable_ipv4') is None:409 if self.new_node and self.data.get('disable_ipv4') is None:
391410
=== modified file 'src/maasserver/templates/maasserver/node_edit.html'
--- src/maasserver/templates/maasserver/node_edit.html 2014-06-09 19:58:06 +0000
+++ src/maasserver/templates/maasserver/node_edit.html 2014-09-29 10:01:07 +0000
@@ -77,6 +77,7 @@
77 </a>77 </a>
78 </li>78 </li>
79 </ul>79 </ul>
80 <input name="ui-submission" type="hidden" value="Submitted from the UI" />
80 <input type="submit" value="Save node" class="right" />81 <input type="submit" value="Save node" class="right" />
81 <a class="link-button" href="{% url 'node-view' node.system_id %}">Cancel</a>82 <a class="link-button" href="{% url 'node-view' node.system_id %}">Cancel</a>
82 </form>83 </form>
8384
=== modified file 'src/maasserver/tests/test_forms_node.py'
--- src/maasserver/tests/test_forms_node.py 2014-09-23 14:16:33 +0000
+++ src/maasserver/tests/test_forms_node.py 2014-09-29 10:01:07 +0000
@@ -369,6 +369,25 @@
369 node = form.save()369 node = form.save()
370 self.assertEqual(setting, node.disable_ipv4)370 self.assertEqual(setting, node.disable_ipv4)
371371
372 def test_takes_missing_disable_ipv4_as_False_in_UI(self):
373 form = NodeForm(
374 instance=factory.make_Node(disable_ipv4=True),
375 data={
376 'architecture': make_usable_architecture(self),
377 'ui-submission': True,
378 })
379 node = form.save()
380 self.assertFalse(node.disable_ipv4)
381
382 def test_takes_missing_disable_ipv4_as_Unchanged_in_API(self):
383 form = NodeForm(
384 instance=factory.make_Node(disable_ipv4=True),
385 data={
386 'architecture': make_usable_architecture(self),
387 })
388 node = form.save()
389 self.assertTrue(node.disable_ipv4)
390
372 def test_takes_True_disable_ipv4_from_cluster_by_default(self):391 def test_takes_True_disable_ipv4_from_cluster_by_default(self):
373 setting = True392 setting = True
374 cluster = factory.make_NodeGroup(default_disable_ipv4=setting)393 cluster = factory.make_NodeGroup(default_disable_ipv4=setting)