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:
parent
aab619550f
commit
37d4e841d7
@ -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)
|
||||
|
@ -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"""
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user