mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-17 03:31:11 +00:00
Refactor StreamField and the rest of the blocks API to explicitly distinguish between the renderable values that we work with, and their json-serialisable form
This commit is contained in:
parent
3694756e4e
commit
440e9a850b
2 changed files with 94 additions and 33 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import re
|
||||
from collections import OrderedDict
|
||||
import collections
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.html import format_html, format_html_join
|
||||
|
|
@ -159,13 +159,20 @@ class Block(object):
|
|||
"""
|
||||
return value
|
||||
|
||||
def renderable(self, value):
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Return 'value' in the most convenient version for use in templates. In simple cases this might
|
||||
be the value itself; alternatively, it might be a 'smart' version of the value which behaves mostly
|
||||
Convert 'value' from a simple (JSON-serialisable) value to a (possibly complex) Python value to be
|
||||
used in the rest of the block API and within front-end templates . In simple cases this might be
|
||||
the value itself; alternatively, it might be a 'smart' version of the value which behaves mostly
|
||||
like the original value but provides a native HTML rendering when inserted into a template; or it
|
||||
might be something totally different (e.g. an image chooser will use the image ID as the clean value,
|
||||
and turn this back into an actual image object here).
|
||||
might be something totally different (e.g. an image chooser will use the image ID as the clean
|
||||
value, and turn this back into an actual image object here).
|
||||
"""
|
||||
return value
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"""
|
||||
The reverse of to_python; convert the python value into JSON-serialisable form.
|
||||
"""
|
||||
return value
|
||||
|
||||
|
|
@ -356,9 +363,17 @@ class BaseStructBlock(Block):
|
|||
|
||||
return result
|
||||
|
||||
def renderable(self, value):
|
||||
def to_python(self, value):
|
||||
# recursively call to_python on children and return as a RenderableStructBlock
|
||||
return RenderableStructBlock(self, [
|
||||
(name, self.child_blocks[name].renderable(val))
|
||||
(name, self.child_blocks[name].to_python(val))
|
||||
for name, val in value.items()
|
||||
])
|
||||
|
||||
def get_prep_value(self, value):
|
||||
# recursively call get_prep_value on children and return as a plain dict
|
||||
return dict([
|
||||
(name, self.child_blocks[name].get_prep_value(val))
|
||||
for name, val in value.items()
|
||||
])
|
||||
|
||||
|
|
@ -386,13 +401,13 @@ class DeclarativeSubBlocksMetaclass(type):
|
|||
value.set_name(key)
|
||||
attrs.pop(key)
|
||||
current_blocks.sort(key=lambda x: x[1].creation_counter)
|
||||
attrs['declared_blocks'] = OrderedDict(current_blocks)
|
||||
attrs['declared_blocks'] = collections.OrderedDict(current_blocks)
|
||||
|
||||
new_class = (super(DeclarativeSubBlocksMetaclass, mcs)
|
||||
.__new__(mcs, name, bases, attrs))
|
||||
|
||||
# Walk through the MRO.
|
||||
declared_blocks = OrderedDict()
|
||||
declared_blocks = collections.OrderedDict()
|
||||
for base in reversed(new_class.__mro__):
|
||||
# Collect sub-blocks from base class.
|
||||
if hasattr(base, 'declared_blocks'):
|
||||
|
|
@ -514,9 +529,17 @@ class ListBlock(Block):
|
|||
|
||||
return result
|
||||
|
||||
def renderable(self, value):
|
||||
def to_python(self, value):
|
||||
# recursively call to_python on children and return as a list
|
||||
return [
|
||||
self.child_block.renderable(item)
|
||||
self.child_block.to_python(item)
|
||||
for item in value
|
||||
]
|
||||
|
||||
def get_prep_value(self, value):
|
||||
# recursively call get_prep_value on children and return as a list
|
||||
return [
|
||||
self.child_block.get_prep_value(item)
|
||||
for item in value
|
||||
]
|
||||
|
||||
|
|
@ -596,9 +619,9 @@ class BaseStreamBlock(Block):
|
|||
|
||||
def render_form(self, value, prefix='', error=None):
|
||||
list_members_html = [
|
||||
self.render_list_member(member['type'], member['value'], "%s-%d" % (prefix, i), i,
|
||||
self.render_list_member(block_type, child_value, "%s-%d" % (prefix, i), i,
|
||||
error=error.params[i] if error else None)
|
||||
for (i, member) in enumerate(value)
|
||||
for (i, (child_value, block_type)) in enumerate(value.values_with_types)
|
||||
]
|
||||
|
||||
return render_to_string('wagtailadmin/block_forms/stream.html', {
|
||||
|
|
@ -621,24 +644,23 @@ class BaseStreamBlock(Block):
|
|||
values_with_indexes.append(
|
||||
(
|
||||
data['%s-%d-order' % (prefix, i)],
|
||||
child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
|
||||
block_type_name,
|
||||
child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i))
|
||||
)
|
||||
)
|
||||
|
||||
values_with_indexes.sort()
|
||||
return [{'type': t, 'value': v} for (i, t, v) in values_with_indexes]
|
||||
return StreamValue([(val, typ) for (index, val, typ) in values_with_indexes])
|
||||
|
||||
def clean(self, value):
|
||||
result = []
|
||||
errors = []
|
||||
for child_val in value:
|
||||
child_block = self.child_blocks[child_val['type']]
|
||||
for child_type, child_val in value.values_with_types:
|
||||
child_block = self.child_blocks[child_type]
|
||||
try:
|
||||
result.append({
|
||||
'type': child_val['type'],
|
||||
'value': child_block.clean(child_val['value']),
|
||||
})
|
||||
result.append(
|
||||
(child_block.clean(child_val), child_type)
|
||||
)
|
||||
except ValidationError as e:
|
||||
errors.append(e)
|
||||
else:
|
||||
|
|
@ -649,13 +671,40 @@ class BaseStreamBlock(Block):
|
|||
# which only involves the 'params' list
|
||||
raise ValidationError('Validation error in StreamBlock', params=errors)
|
||||
|
||||
return result
|
||||
return StreamValue(result)
|
||||
|
||||
def renderable(self, value):
|
||||
return [
|
||||
self.child_blocks[item['type']].renderable(item['value'])
|
||||
def to_python(self, value):
|
||||
# the incoming JSONish representation is a list of dicts, each with a 'type' and 'value' field.
|
||||
# Convert this to a StreamValue backed by a list of (value, type) tuples
|
||||
return StreamValue([
|
||||
(self.child_blocks[item['type']].to_python(item['value']), item['type'])
|
||||
for item in value
|
||||
])
|
||||
|
||||
def get_prep_value(self, value):
|
||||
return [
|
||||
{'type': block_type, 'value': self.child_blocks[block_type].get_prep_value(child_value)}
|
||||
for child_value, block_type in value.values_with_types
|
||||
]
|
||||
|
||||
class StreamBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStreamBlock)):
|
||||
pass
|
||||
|
||||
|
||||
class StreamValue(collections.Sequence):
|
||||
"""
|
||||
Custom type used to represent the value of a StreamBlock; behaves as a sequence of block values
|
||||
so that we can naturally iterate over it in template code, but also allows retrieval of
|
||||
(value, type) tuples as required for the form (and other complex rendering logic) to work.
|
||||
"""
|
||||
def __init__(self, values_with_types):
|
||||
self.values_with_types = values_with_types
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.values_with_types[i][0]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.values_with_types)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(list(self))
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ from __future__ import absolute_import, unicode_literals
|
|||
import json
|
||||
|
||||
from django.db import models
|
||||
from django.forms import Textarea
|
||||
from django import forms
|
||||
from django.utils.six import with_metaclass
|
||||
|
||||
from wagtail.wagtailcore.rich_text import DbWhitelister, expand_db_html
|
||||
from wagtail.utils.widgets import WidgetWithScript
|
||||
from wagtail.wagtailadmin.blocks import StreamBlock # FIXME: wagtailcore shouldn't be depending on wagtailadmin
|
||||
from wagtail.wagtailadmin.blocks import StreamBlock, StreamValue # FIXME: wagtailcore shouldn't be depending on wagtailadmin
|
||||
|
||||
|
||||
class RichTextArea(WidgetWithScript, Textarea):
|
||||
class RichTextArea(WidgetWithScript, forms.Textarea):
|
||||
def get_panel(self):
|
||||
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel
|
||||
return RichTextFieldPanel
|
||||
|
|
@ -40,7 +40,7 @@ class RichTextField(models.TextField):
|
|||
return super(RichTextField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class StreamField(with_metaclass(models.SubfieldBase, models.TextField)):
|
||||
class StreamField(with_metaclass(models.SubfieldBase, models.Field)):
|
||||
def __init__(self, block_types, **kwargs):
|
||||
self.block_types = block_types
|
||||
self.stream_block = StreamBlock(block_types)
|
||||
|
|
@ -54,11 +54,23 @@ class StreamField(with_metaclass(models.SubfieldBase, models.TextField)):
|
|||
def to_python(self, value):
|
||||
if value is None:
|
||||
return []
|
||||
elif isinstance(value, list):
|
||||
elif isinstance(value, StreamValue):
|
||||
return value
|
||||
else: # assume string
|
||||
return self.stream_block.renderable(json.loads(value))
|
||||
return self.stream_block.to_python(json.loads(value))
|
||||
|
||||
def get_prep_value(self, value):
|
||||
return json.dumps(value)
|
||||
return json.dumps(self.stream_block.get_prep_value(value))
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
"""
|
||||
Override formfield to use a plain forms.Field so that we do no transformation on the value
|
||||
(as distinct from the usual fallback of forms.CharField, which transforms it into a string).
|
||||
"""
|
||||
defaults = {'form_class': forms.Field}
|
||||
defaults.update(kwargs)
|
||||
return super(StreamField, self).formfield(**defaults)
|
||||
|
||||
def value_to_string(self, obj):
|
||||
value = self._get_val_from_obj(obj)
|
||||
return self.get_prep_value(value)
|
||||
|
|
|
|||
Loading…
Reference in a new issue