Skip to content

Commit

Permalink
Provide a raw_data accessor on StreamValue to allow accessing and mod…
Browse files Browse the repository at this point in the history
…ifying the raw JSON representation

This matches the old behaviour of stream_data for lazy StreamValues. Given that a lazy StreamValue is what you get as standard when retrieving a model instance from the database (or from a PageRevision), any existing user code that casually tinkered with stream_data unaware of the lazy versus non-lazy gotcha is almost certainly expecting it to behave this way - therefore in 99% of cases those users should be able to replace `stream_data` with `raw_data` and have their code work as before, with the added bonus that it won't fall over on page previews (which get their data from a form submission in Python format rather than JSON, and are thus NOT lazy).
  • Loading branch information
gasman committed Dec 8, 2020
1 parent aab6195 commit 37d4e84
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 5 deletions.
54 changes: 49 additions & 5 deletions wagtail/core/blocks/stream_block.py
Expand Up @@ -7,6 +7,7 @@
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.utils.functional import cached_property
from django.utils.html import format_html_join
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -413,6 +414,49 @@ def block_type(self):
"""
return self.block.name

def get_prep_value(self):
return {
'type': self.block_type,
'value': self.block.get_prep_value(self.value),
'id': self.id,
}

class RawDataView(MutableSequence):
"""
Internal helper class to present the stream data in raw JSONish format. For backwards
compatibility with old code that manipulated StreamValue.stream_data, this is considered
mutable to some extent, with the proviso that once the BoundBlock representation has been
accessed, any changes to fields within raw data will not propagate back to the BoundBlock
and will not be saved back when calling get_prep_value.
"""
def __init__(self, stream_value):
self.stream_value = stream_value

def __getitem__(self, i):
item = self.stream_value._raw_data[i]
if item is None:
# reconstruct raw data from the bound block
item = self.stream_value._bound_blocks[i].get_prep_value()
self.stream_value._raw_data[i] = item

return item

def __len__(self):
return len(self.stream_value._raw_data)

def __setitem__(self, i, item):
self.stream_value._raw_data[i] = item
# clear the cached bound_block for this item
self.stream_value._bound_blocks[i] = None

def __delitem__(self, i):
# same as deletion on the stream itself - delete both the raw and bound_block data
del self.stream_value[i]

def insert(self, i, item):
self.stream_value._raw_data.insert(i, item)
self.stream_value._bound_blocks.insert(i, None)

def __init__(self, stream_block, stream_data, is_lazy=False, raw_text=None):
"""
Construct a StreamValue linked to the given StreamBlock,
Expand Down Expand Up @@ -485,6 +529,10 @@ def insert(self, i, item):
self._bound_blocks.insert(i, self._construct_stream_child(item))
self._raw_data.insert(i, None)

@cached_property
def raw_data(self):
return StreamValue.RawDataView(self)

def _prefetch_blocks(self, type_name):
"""
Populate _bound_blocks with all items in this stream of type `type_name` that exist in
Expand Down Expand Up @@ -519,11 +567,7 @@ def get_prep_value(self):
if not item.id:
item.id = str(uuid.uuid4())

prep_value.append({
'type': item.block_type,
'value': item.block.get_prep_value(item.value),
'id': item.id,
})
prep_value.append(item.get_prep_value())
else:
# item has not been converted to a BoundBlock, so its _raw_data entry is
# still usable (but ensure it has an ID before returning it)
Expand Down
51 changes: 51 additions & 0 deletions wagtail/core/tests/test_blocks.py
Expand Up @@ -3436,6 +3436,57 @@ class ArticleBlock(blocks.StreamBlock):
{'type': 'paragraph', 'value': 'of warcraft', 'id': '0003'},
])

def test_streamvalue_raw_data(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()

block = ArticleBlock()
stream = block.to_python([
{'type': 'heading', 'value': 'hello', 'id': '0001'},
{'type': 'paragraph', 'value': 'world', 'id': '0002'},
])

self.assertEqual(stream.raw_data[0], {'type': 'heading', 'value': 'hello', 'id': '0001'})
stream.raw_data[0]['value'] = 'bonjour'
self.assertEqual(stream.raw_data[0], {'type': 'heading', 'value': 'bonjour', 'id': '0001'})

# changes to raw_data will be written back via get_prep_value...
raw_data = block.get_prep_value(stream)
self.assertEqual(raw_data, [
{'type': 'heading', 'value': 'bonjour', 'id': '0001'},
{'type': 'paragraph', 'value': 'world', 'id': '0002'},
])

# ...but once the bound-block representation has been accessed, that takes precedence
self.assertEqual(stream[0].value, 'bonjour')
stream.raw_data[0]['value'] = 'guten tag'
self.assertEqual(stream.raw_data[0]['value'], 'guten tag')
self.assertEqual(stream[0].value, 'bonjour')
raw_data = block.get_prep_value(stream)
self.assertEqual(raw_data, [
{'type': 'heading', 'value': 'bonjour', 'id': '0001'},
{'type': 'paragraph', 'value': 'world', 'id': '0002'},
])

# Replacing a raw_data entry outright will propagate to the bound block, though
stream.raw_data[0] = {'type': 'heading', 'value': 'konnichiwa', 'id': '0003'}
raw_data = block.get_prep_value(stream)
self.assertEqual(raw_data, [
{'type': 'heading', 'value': 'konnichiwa', 'id': '0003'},
{'type': 'paragraph', 'value': 'world', 'id': '0002'},
])
self.assertEqual(stream[0].value, 'konnichiwa')

# deletions / insertions on raw_data will also propagate to the bound block representation
del stream.raw_data[1]
stream.raw_data.insert(0, {'type': 'paragraph', 'value': 'hello kitty says', 'id': '0004'})
raw_data = block.get_prep_value(stream)
self.assertEqual(raw_data, [
{'type': 'paragraph', 'value': 'hello kitty says', 'id': '0004'},
{'type': 'heading', 'value': 'konnichiwa', 'id': '0003'},
])

def test_render_with_classname_via_kwarg(self):
"""form_classname from kwargs to be used as an additional class when rendering stream block"""

Expand Down

0 comments on commit 37d4e84

Please sign in to comment.