From 31f45a8a631ec0fb274677366584afd8c9cf33cf Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 8 Jun 2015 19:09:23 +0100 Subject: [PATCH] Add a mechanism for preserving the raw text value of StreamFields when they fail to parse as JSON --- wagtail/wagtailcore/blocks/stream_block.py | 9 +++++- wagtail/wagtailcore/fields.py | 33 ++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/wagtail/wagtailcore/blocks/stream_block.py b/wagtail/wagtailcore/blocks/stream_block.py index 9b41501095..db973c46d7 100644 --- a/wagtail/wagtailcore/blocks/stream_block.py +++ b/wagtail/wagtailcore/blocks/stream_block.py @@ -260,7 +260,7 @@ class StreamValue(collections.Sequence): """ return self.block.name - def __init__(self, stream_block, stream_data, is_lazy=False): + def __init__(self, stream_block, stream_data, is_lazy=False, raw_text=None): """ Construct a StreamValue linked to the given StreamBlock, with child values given in stream_data. @@ -273,11 +273,18 @@ class StreamValue(collections.Sequence): Passing is_lazy=False means that stream_data consists of immediately usable native values. In this mode, stream_data is a list of (type_name, value) tuples. + + raw_text exists solely as a way of representing StreamField content that is + not valid JSON; this may legitimately occur if an existing text field is + migrated to a StreamField. In this situation we return a blank StreamValue + with the raw text accessible under the `raw_text` attribute, so that migration + code can be rewritten to convert it as desired. """ self.is_lazy = is_lazy self.stream_block = stream_block # the StreamBlock object that handles this value self.stream_data = stream_data # a list of (type_name, value) tuples self._bound_blocks = {} # populated lazily from stream_data as we access items through __getitem__ + self.raw_text = raw_text def __getitem__(self, i): if i not in self._bound_blocks: diff --git a/wagtail/wagtailcore/fields.py b/wagtail/wagtailcore/fields.py index 73fa59912c..3b9e3fc097 100644 --- a/wagtail/wagtailcore/fields.py +++ b/wagtail/wagtailcore/fields.py @@ -5,7 +5,7 @@ import json from django.db import models from django import forms from django.core.serializers.json import DjangoJSONEncoder -from django.utils.six import with_metaclass +from django.utils.six import with_metaclass, string_types from wagtail.wagtailcore.rich_text import DbWhitelister, expand_db_html from wagtail.utils.widgets import WidgetWithScript @@ -69,17 +69,16 @@ class StreamField(with_metaclass(models.SubfieldBase, models.Field)): return StreamValue(self.stream_block, []) elif isinstance(value, StreamValue): return value - else: # assume string + elif isinstance(value, string_types): try: unpacked_value = json.loads(value) except ValueError: # value is not valid JSON; most likely, this field was previously a # rich text field before being migrated to StreamField, and the data - # was left intact in the migration. Return an empty stream instead. - - # TODO: keep this raw text data around as a property of the StreamValue - # so that it can be retrieved in data migrations - return StreamValue(self.stream_block, []) + # was left intact in the migration. Return an empty stream instead + # (but keep the raw text available as an attribute, so that it can be + # used to migrate that data to StreamField) + return StreamValue(self.stream_block, [], raw_text=value) if unpacked_value is None: # we get here if value is the literal string 'null'. This should probably @@ -88,9 +87,27 @@ class StreamField(with_metaclass(models.SubfieldBase, models.Field)): return StreamValue(self.stream_block, []) return self.stream_block.to_python(unpacked_value) + else: + # See if it looks like the standard non-smart representation of a + # StreamField value: a list of (block_name, value) tuples + try: + [None for (x, y) in value] + except (TypeError, ValueError): + # Give up trying to make sense of the value + raise TypeError("Cannot handle %r (type %r) as a value of StreamField" % (value, type(value))) + + # Test succeeded, so return as a StreamValue-ified version of that value + return StreamValue(self.stream_block, value) def get_prep_value(self, value): - return json.dumps(self.stream_block.get_prep_value(value), cls=DjangoJSONEncoder) + if isinstance(value, StreamValue) and not(value) and value.raw_text is not None: + # An empty StreamValue with a nonempty raw_text attribute should have that + # raw_text attribute written back to the db. (This is probably only useful + # for reverse migrations that convert StreamField data back into plain text + # fields.) + return value.raw_text + else: + return json.dumps(self.stream_block.get_prep_value(value), cls=DjangoJSONEncoder) def formfield(self, **kwargs): """