Implement value_omitted_from_data on Block

This allows Django >=1.10.2's ModelForm logic to determine whether or not the field has been omitted
from the form submission (and should thus revert to the initial or default value), since the default
rule of "look for an item in the postdata matching the field's name" doesn't work for Block-based
fields such as StreamField.

Fixes #2994 for Django 1.10.2 and above (assuming https://github.com/django/django/pull/7217 gets merged
without major changes...)
This commit is contained in:
Matt Westcott 2016-09-21 15:03:35 +01:00
parent bb1ae7551f
commit facf19df95
6 changed files with 72 additions and 0 deletions

View file

@ -170,6 +170,14 @@ class Block(six.with_metaclass(BaseBlock, object)):
def value_from_datadict(self, data, files, prefix):
raise NotImplementedError('%s.value_from_datadict' % self.__class__)
def value_omitted_from_data(self, data, files, name):
"""
Used only for top-level blocks wrapped by BlockWidget (i.e.: typically only StreamBlock)
to inform ModelForm logic on Django >=1.10.2 whether the field is absent from the form
submission (and should therefore revert to the field default).
"""
return name not in data
def bind(self, value, prefix=None, errors=None):
"""
Return a BoundBlock which represents the association of this block definition with a value
@ -595,6 +603,9 @@ class BlockWidget(forms.Widget):
def value_from_datadict(self, data, files, name):
return self.block_def.value_from_datadict(data, files, name)
def value_omitted_from_data(self, data, files, name):
return self.block_def.value_omitted_from_data(data, files, name)
class BlockField(forms.Field):
"""Wraps a block object as a form field so that it can be incorporated into a Django form"""

View file

@ -68,6 +68,9 @@ class FieldBlock(Block):
def value_from_datadict(self, data, files, prefix):
return self.value_from_form(self.field.widget.value_from_datadict(data, files, prefix))
def value_omitted_from_data(self, data, files, prefix):
return self.field.widget.value_omitted_from_data(data, files, prefix)
def clean(self, value):
# We need an annoying value_for_form -> value_from_form round trip here to account for
# the possibility that the form field is set up to validate a different value type to

View file

@ -108,6 +108,9 @@ class ListBlock(Block):
values_with_indexes.sort()
return [v for (i, v) in values_with_indexes]
def value_omitted_from_data(self, data, files, prefix):
return ('%s-count' % prefix) not in data
def clean(self, value):
result = []
errors = []

View file

@ -171,6 +171,9 @@ class BaseStreamBlock(Block):
for (index, child_block_type_name, value) in values_with_indexes
])
def value_omitted_from_data(self, data, files, prefix):
return ('%s-count' % prefix) not in data
def clean(self, value):
cleaned_data = []
errors = {}

View file

@ -97,6 +97,12 @@ class BaseStructBlock(Block):
for name, block in self.child_blocks.items()
])
def value_omitted_from_data(self, data, files, prefix):
return all(
block.value_omitted_from_data(data, files, '%s-%s' % (prefix, name))
for name, block in self.child_blocks.items()
)
def clean(self, value):
result = [] # build up a list of (name, value) tuples to be passed to the StructValue constructor
errors = {}

View file

@ -9,6 +9,7 @@ import warnings
from decimal import Decimal
# non-standard import name for ugettext_lazy, to prevent strings from being picked up for translation
import django
from django import forms
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
@ -692,6 +693,13 @@ class TestRawHTMLBlock(unittest.TestCase):
self.assertEqual(result, '<blink>BÖÖM</blink>')
self.assertIsInstance(result, SafeData)
@unittest.skipIf(django.VERSION < (1, 10, 2), "value_omitted_from_data is not available")
def test_value_omitted_from_data(self):
block = blocks.RawHTMLBlock()
self.assertFalse(block.value_omitted_from_data({'rawhtml': 'ohai'}, {}, 'rawhtml'))
self.assertFalse(block.value_omitted_from_data({'rawhtml': ''}, {}, 'rawhtml'))
self.assertTrue(block.value_omitted_from_data({'nothing-here': 'nope'}, {}, 'rawhtml'))
def test_clean_required_field(self):
block = blocks.RawHTMLBlock()
result = block.clean(mark_safe('<blink>BÖÖM</blink>'))
@ -1096,6 +1104,17 @@ class TestStructBlock(SimpleTestCase):
self.assertTrue(isinstance(struct_val, blocks.StructValue))
self.assertTrue(isinstance(struct_val.bound_blocks['link'].block, blocks.URLBlock))
@unittest.skipIf(django.VERSION < (1, 10, 2), "value_omitted_from_data is not available")
def test_value_omitted_from_data(self):
block = blocks.StructBlock([
('title', blocks.CharBlock()),
('link', blocks.URLBlock()),
])
# overall value is considered present in the form if any sub-field is present
self.assertFalse(block.value_omitted_from_data({'mylink-title': 'Torchbox'}, {}, 'mylink'))
self.assertTrue(block.value_omitted_from_data({'nothing-here': 'nope'}, {}, 'mylink'))
def test_default_is_returned_as_structvalue(self):
"""When returning the default value of a StructBlock (e.g. because it's
a child of another StructBlock, and the outer value is missing that key)
@ -1408,6 +1427,18 @@ class TestListBlock(unittest.TestCase):
self.assertEqual(content, ["Wagtail", "Django"])
@unittest.skipIf(django.VERSION < (1, 10, 2), "value_omitted_from_data is not available")
def test_value_omitted_from_data(self):
block = blocks.ListBlock(blocks.CharBlock())
# overall value is considered present in the form if the 'count' field is present
self.assertFalse(block.value_omitted_from_data({'mylist-count': '0'}, {}, 'mylist'))
self.assertFalse(block.value_omitted_from_data({
'mylist-count': '1',
'mylist-0-value': 'hello', 'mylist-0-deleted': '', 'mylist-0-order': '0'
}, {}, 'mylist'))
self.assertTrue(block.value_omitted_from_data({'nothing-here': 'nope'}, {}, 'mylist'))
def test_ordering_in_form_submission_uses_order_field(self):
block = blocks.ListBlock(blocks.CharBlock())
@ -1784,6 +1815,21 @@ class TestStreamBlock(SimpleTestCase):
html
)
@unittest.skipIf(django.VERSION < (1, 10, 2), "value_omitted_from_data is not available")
def test_value_omitted_from_data(self):
block = blocks.StreamBlock([
('heading', blocks.CharBlock()),
])
# overall value is considered present in the form if the 'count' field is present
self.assertFalse(block.value_omitted_from_data({'mystream-count': '0'}, {}, 'mystream'))
self.assertFalse(block.value_omitted_from_data({
'mystream-count': '1',
'mystream-0-type': 'heading', 'mystream-0-value': 'hello',
'mystream-0-deleted': '', 'mystream-0-order': '0'
}, {}, 'mystream'))
self.assertTrue(block.value_omitted_from_data({'nothing-here': 'nope'}, {}, 'mystream'))
def test_validation_errors(self):
class ValidatedBlock(blocks.StreamBlock):
char = blocks.CharBlock()