0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-30 01:46:24 +01:00

Provide a raw_data accessor on StreamValue to allow accessing and modifying 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).
This commit is contained in:
Matt Westcott 2020-10-21 20:24:45 +01:00
parent aab619550f
commit 37d4e841d7
2 changed files with 100 additions and 5 deletions

View File

@ -7,6 +7,7 @@ from django import forms
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 _
@ -413,6 +414,49 @@ class StreamValue(MutableSequence):
"""
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,
@ -485,6 +529,10 @@ class StreamValue(MutableSequence):
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
@ -519,11 +567,7 @@ class StreamValue(MutableSequence):
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)

View File

@ -3436,6 +3436,57 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
{'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"""