diff --git a/wagtail/wagtailcore/blocks/__init__.py b/wagtail/wagtailcore/blocks/__init__.py
index 4b4cb288d..6df3b79fc 100644
--- a/wagtail/wagtailcore/blocks/__init__.py
+++ b/wagtail/wagtailcore/blocks/__init__.py
@@ -20,7 +20,7 @@ import six
from wagtail.wagtailcore.utils import escape_script
-from .utils import indent, js_dict
+from .utils import js_dict
# =========================================
@@ -648,257 +648,6 @@ class ListBlock(Block):
return content
-# ===========
-# StreamBlock
-# ===========
-
-class BaseStreamBlock(Block):
- # TODO: decide what it means to pass a 'default' arg to StreamBlock's constructor. Logically we want it to be
- # of type StreamValue, but we can't construct one of those because it needs a reference back to the StreamBlock
- # that we haven't constructed yet...
- class Meta:
- @property
- def default(self):
- return StreamValue(self, [])
-
- def __init__(self, local_blocks=None, **kwargs):
- self._constructor_kwargs = kwargs
-
- super(BaseStreamBlock, self).__init__(**kwargs)
-
- self.child_blocks = self.base_blocks.copy() # create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
- if local_blocks:
- for name, block in local_blocks:
- block.set_name(name)
- self.child_blocks[name] = block
-
- self.dependencies = self.child_blocks.values()
-
- def render_list_member(self, block_type_name, value, prefix, index, errors=None):
- """
- Render the HTML for a single list item. This consists of an
wrapper, hidden fields
- to manage ID/deleted state/type, delete/reorder buttons, and the child block's own HTML.
- """
- child_block = self.child_blocks[block_type_name]
- child = child_block.bind(value, prefix="%s-value" % prefix, errors=errors)
- return render_to_string('wagtailadmin/block_forms/stream_member.html', {
- 'child_blocks': self.child_blocks.values(),
- 'block_type_name': block_type_name,
- 'prefix': prefix,
- 'child': child,
- 'index': index,
- })
-
- def html_declarations(self):
- return format_html_join(
- '\n', '',
- [
- (
- self.definition_prefix,
- name,
- mark_safe(escape_script(self.render_list_member(name, child_block.meta.default, '__PREFIX__', '')))
- )
- for name, child_block in self.child_blocks.items()
- ]
- )
-
- @property
- def media(self):
- return forms.Media(js=['wagtailadmin/js/blocks/sequence.js', 'wagtailadmin/js/blocks/stream.js'])
-
- def js_initializer(self):
- # compile a list of info dictionaries, one for each available block type
- child_blocks = []
- for name, child_block in self.child_blocks.items():
- # each info dictionary specifies at least a block name
- child_block_info = {'name': "'%s'" % name}
-
- # if the child defines a JS initializer function, include that in the info dict
- # along with the param that needs to be passed to it for initializing an empty/default block
- # of that type
- child_js_initializer = child_block.js_initializer()
- if child_js_initializer:
- child_block_info['initializer'] = child_js_initializer
-
- child_blocks.append(indent(js_dict(child_block_info)))
-
- opts = {
- 'definitionPrefix': "'%s'" % self.definition_prefix,
- 'childBlocks': '[\n%s\n]' % ',\n'.join(child_blocks),
- }
-
- return "StreamBlock(%s)" % js_dict(opts)
-
- def render_form(self, value, prefix='', errors=None):
- if errors:
- if len(errors) > 1:
- # We rely on ListBlock.clean throwing a single ValidationError with a specially crafted
- # 'params' attribute that we can pull apart and distribute to the child blocks
- raise TypeError('ListBlock.render_form unexpectedly received multiple errors')
- error_list = errors.as_data()[0].params
- else:
- error_list = None
-
- # drop any child values that are an unrecognised block type
- valid_children = [child for child in value if child.block_type in self.child_blocks]
-
- list_members_html = [
- self.render_list_member(child.block_type, child.value, "%s-%d" % (prefix, i), i,
- errors=error_list[i] if error_list else None)
- for (i, child) in enumerate(valid_children)
- ]
-
- return render_to_string('wagtailadmin/block_forms/stream.html', {
- 'label': self.label,
- 'prefix': prefix,
- 'list_members_html': list_members_html,
- 'child_blocks': self.child_blocks.values(),
- 'header_menu_prefix': '%s-before' % prefix,
- })
-
- def value_from_datadict(self, data, files, prefix):
- count = int(data['%s-count' % prefix])
- values_with_indexes = []
- for i in range(0, count):
- if data['%s-%d-deleted' % (prefix, i)]:
- continue
- block_type_name = data['%s-%d-type' % (prefix, i)]
- try:
- child_block = self.child_blocks[block_type_name]
- except KeyError:
- continue
-
- values_with_indexes.append(
- (
- int(data['%s-%d-order' % (prefix, i)]),
- block_type_name,
- child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
- )
- )
-
- values_with_indexes.sort()
- return StreamValue(self, [
- (block_type_name, value)
- for (index, block_type_name, value) in values_with_indexes
- ])
-
- def clean(self, value):
- cleaned_data = []
- errors = []
- for child in value: # child is a BoundBlock instance
- try:
- cleaned_data.append(
- (child.block.name, child.block.clean(child.value))
- )
- except ValidationError as e:
- errors.append(ErrorList([e]))
- else:
- errors.append(None)
-
- if any(errors):
- # The message here is arbitrary - outputting error messages is delegated to the child blocks,
- # which only involves the 'params' list
- raise ValidationError('Validation error in StreamBlock', params=errors)
-
- return StreamValue(self, cleaned_data)
-
- 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 (type, value) tuples
- return StreamValue(self, [
- (child_data['type'], self.child_blocks[child_data['type']].to_python(child_data['value']))
- for child_data in value
- if child_data['type'] in self.child_blocks
- ])
-
- def get_prep_value(self, value):
- if value is None:
- return None
-
- return [
- {'type': child.block.name, 'value': child.block.get_prep_value(child.value)}
- for child in value # child is a BoundBlock instance
- ]
-
- def render_basic(self, value):
- return format_html_join('\n', '{0}
',
- [(child, child.block_type) for child in value]
- )
-
- def get_searchable_content(self, value):
- content = []
-
- for child in value:
- content.extend(child.block.get_searchable_content(child.value))
-
- return content
-
- def deconstruct(self):
- """
- Always deconstruct StreamBlock instances as if they were plain StreamBlocks with all of the
- field definitions passed to the constructor - even if in reality this is a subclass of StreamBlock
- with the fields defined declaratively, or some combination of the two.
-
- This ensures that the field definitions get frozen into migrations, rather than leaving a reference
- to a custom subclass in the user's models.py that may or may not stick around.
- """
- path = 'wagtail.wagtailcore.blocks.StreamBlock'
- args = [self.child_blocks.items()]
- kwargs = self._constructor_kwargs
- return (path, args, kwargs)
-
-
-class StreamBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStreamBlock)):
- pass
-
-
-@python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Py2
-class StreamValue(collections.Sequence):
- """
- Custom type used to represent the value of a StreamBlock; behaves as a sequence of BoundBlocks
- (which keep track of block types in a way that the values alone wouldn't).
- """
-
- @python_2_unicode_compatible
- class StreamChild(BoundBlock):
- """Provides some extensions to BoundBlock to make it more natural to work with on front-end templates"""
- def __str__(self):
- """Render the value according to the block's native rendering"""
- return self.block.render(self.value)
-
- @property
- def block_type(self):
- """
- Syntactic sugar so that we can say child.block_type instead of child.block.name.
- (This doesn't belong on BoundBlock itself because the idea of block.name denoting
- the child's "type" ('heading', 'paragraph' etc) is unique to StreamBlock, and in the
- wider context people are liable to confuse it with the block class (CharBlock etc).
- """
- return self.block.name
-
- def __init__(self, stream_block, stream_data):
- 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__
-
- def __getitem__(self, i):
- if i not in self._bound_blocks:
- type_name, value = self.stream_data[i]
- child_block = self.stream_block.child_blocks[type_name]
- self._bound_blocks[i] = StreamValue.StreamChild(child_block, value)
-
- return self._bound_blocks[i]
-
- def __len__(self):
- return len(self.stream_data)
-
- def __repr__(self):
- return repr(list(self))
-
- def __str__(self):
- return self.stream_block.render(self)
-
-
# ========================
# django.forms integration
# ========================
@@ -952,5 +701,6 @@ class BlockField(forms.Field):
return self.block.clean(value)
-# Import block types defined in wagtail.wagtailcore.blocks.field into the wagtail.wagtailcore.blocks namespace
+# Import block types defined in submodules into the wagtail.wagtailcore.blocks namespace
from .field import * # NOQA
+from .stream import * # NOQA
diff --git a/wagtail/wagtailcore/blocks/stream.py b/wagtail/wagtailcore/blocks/stream.py
new file mode 100644
index 000000000..873b1f74d
--- /dev/null
+++ b/wagtail/wagtailcore/blocks/stream.py
@@ -0,0 +1,266 @@
+from __future__ import absolute_import, unicode_literals
+
+import collections
+
+from django import forms
+from django.core.exceptions import ValidationError
+from django.forms.utils import ErrorList
+from django.template.loader import render_to_string
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.html import format_html_join
+from django.utils.safestring import mark_safe
+
+import six
+
+from wagtail.wagtailcore.blocks import Block, DeclarativeSubBlocksMetaclass, BoundBlock
+from wagtail.wagtailcore.utils import escape_script
+
+from .utils import indent, js_dict
+
+__all__ = ['BaseStreamBlock', 'StreamBlock', 'StreamValue']
+
+class BaseStreamBlock(Block):
+ # TODO: decide what it means to pass a 'default' arg to StreamBlock's constructor. Logically we want it to be
+ # of type StreamValue, but we can't construct one of those because it needs a reference back to the StreamBlock
+ # that we haven't constructed yet...
+ class Meta:
+ @property
+ def default(self):
+ return StreamValue(self, [])
+
+ def __init__(self, local_blocks=None, **kwargs):
+ self._constructor_kwargs = kwargs
+
+ super(BaseStreamBlock, self).__init__(**kwargs)
+
+ self.child_blocks = self.base_blocks.copy() # create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
+ if local_blocks:
+ for name, block in local_blocks:
+ block.set_name(name)
+ self.child_blocks[name] = block
+
+ self.dependencies = self.child_blocks.values()
+
+ def render_list_member(self, block_type_name, value, prefix, index, errors=None):
+ """
+ Render the HTML for a single list item. This consists of an wrapper, hidden fields
+ to manage ID/deleted state/type, delete/reorder buttons, and the child block's own HTML.
+ """
+ child_block = self.child_blocks[block_type_name]
+ child = child_block.bind(value, prefix="%s-value" % prefix, errors=errors)
+ return render_to_string('wagtailadmin/block_forms/stream_member.html', {
+ 'child_blocks': self.child_blocks.values(),
+ 'block_type_name': block_type_name,
+ 'prefix': prefix,
+ 'child': child,
+ 'index': index,
+ })
+
+ def html_declarations(self):
+ return format_html_join(
+ '\n', '',
+ [
+ (
+ self.definition_prefix,
+ name,
+ mark_safe(escape_script(self.render_list_member(name, child_block.meta.default, '__PREFIX__', '')))
+ )
+ for name, child_block in self.child_blocks.items()
+ ]
+ )
+
+ @property
+ def media(self):
+ return forms.Media(js=['wagtailadmin/js/blocks/sequence.js', 'wagtailadmin/js/blocks/stream.js'])
+
+ def js_initializer(self):
+ # compile a list of info dictionaries, one for each available block type
+ child_blocks = []
+ for name, child_block in self.child_blocks.items():
+ # each info dictionary specifies at least a block name
+ child_block_info = {'name': "'%s'" % name}
+
+ # if the child defines a JS initializer function, include that in the info dict
+ # along with the param that needs to be passed to it for initializing an empty/default block
+ # of that type
+ child_js_initializer = child_block.js_initializer()
+ if child_js_initializer:
+ child_block_info['initializer'] = child_js_initializer
+
+ child_blocks.append(indent(js_dict(child_block_info)))
+
+ opts = {
+ 'definitionPrefix': "'%s'" % self.definition_prefix,
+ 'childBlocks': '[\n%s\n]' % ',\n'.join(child_blocks),
+ }
+
+ return "StreamBlock(%s)" % js_dict(opts)
+
+ def render_form(self, value, prefix='', errors=None):
+ if errors:
+ if len(errors) > 1:
+ # We rely on ListBlock.clean throwing a single ValidationError with a specially crafted
+ # 'params' attribute that we can pull apart and distribute to the child blocks
+ raise TypeError('ListBlock.render_form unexpectedly received multiple errors')
+ error_list = errors.as_data()[0].params
+ else:
+ error_list = None
+
+ # drop any child values that are an unrecognised block type
+ valid_children = [child for child in value if child.block_type in self.child_blocks]
+
+ list_members_html = [
+ self.render_list_member(child.block_type, child.value, "%s-%d" % (prefix, i), i,
+ errors=error_list[i] if error_list else None)
+ for (i, child) in enumerate(valid_children)
+ ]
+
+ return render_to_string('wagtailadmin/block_forms/stream.html', {
+ 'label': self.label,
+ 'prefix': prefix,
+ 'list_members_html': list_members_html,
+ 'child_blocks': self.child_blocks.values(),
+ 'header_menu_prefix': '%s-before' % prefix,
+ })
+
+ def value_from_datadict(self, data, files, prefix):
+ count = int(data['%s-count' % prefix])
+ values_with_indexes = []
+ for i in range(0, count):
+ if data['%s-%d-deleted' % (prefix, i)]:
+ continue
+ block_type_name = data['%s-%d-type' % (prefix, i)]
+ try:
+ child_block = self.child_blocks[block_type_name]
+ except KeyError:
+ continue
+
+ values_with_indexes.append(
+ (
+ int(data['%s-%d-order' % (prefix, i)]),
+ block_type_name,
+ child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i)),
+ )
+ )
+
+ values_with_indexes.sort()
+ return StreamValue(self, [
+ (block_type_name, value)
+ for (index, block_type_name, value) in values_with_indexes
+ ])
+
+ def clean(self, value):
+ cleaned_data = []
+ errors = []
+ for child in value: # child is a BoundBlock instance
+ try:
+ cleaned_data.append(
+ (child.block.name, child.block.clean(child.value))
+ )
+ except ValidationError as e:
+ errors.append(ErrorList([e]))
+ else:
+ errors.append(None)
+
+ if any(errors):
+ # The message here is arbitrary - outputting error messages is delegated to the child blocks,
+ # which only involves the 'params' list
+ raise ValidationError('Validation error in StreamBlock', params=errors)
+
+ return StreamValue(self, cleaned_data)
+
+ 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 (type, value) tuples
+ return StreamValue(self, [
+ (child_data['type'], self.child_blocks[child_data['type']].to_python(child_data['value']))
+ for child_data in value
+ if child_data['type'] in self.child_blocks
+ ])
+
+ def get_prep_value(self, value):
+ if value is None:
+ return None
+
+ return [
+ {'type': child.block.name, 'value': child.block.get_prep_value(child.value)}
+ for child in value # child is a BoundBlock instance
+ ]
+
+ def render_basic(self, value):
+ return format_html_join('\n', '{0}
',
+ [(child, child.block_type) for child in value]
+ )
+
+ def get_searchable_content(self, value):
+ content = []
+
+ for child in value:
+ content.extend(child.block.get_searchable_content(child.value))
+
+ return content
+
+ def deconstruct(self):
+ """
+ Always deconstruct StreamBlock instances as if they were plain StreamBlocks with all of the
+ field definitions passed to the constructor - even if in reality this is a subclass of StreamBlock
+ with the fields defined declaratively, or some combination of the two.
+
+ This ensures that the field definitions get frozen into migrations, rather than leaving a reference
+ to a custom subclass in the user's models.py that may or may not stick around.
+ """
+ path = 'wagtail.wagtailcore.blocks.StreamBlock'
+ args = [self.child_blocks.items()]
+ kwargs = self._constructor_kwargs
+ return (path, args, kwargs)
+
+
+class StreamBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStreamBlock)):
+ pass
+
+
+@python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Py2
+class StreamValue(collections.Sequence):
+ """
+ Custom type used to represent the value of a StreamBlock; behaves as a sequence of BoundBlocks
+ (which keep track of block types in a way that the values alone wouldn't).
+ """
+
+ @python_2_unicode_compatible
+ class StreamChild(BoundBlock):
+ """Provides some extensions to BoundBlock to make it more natural to work with on front-end templates"""
+ def __str__(self):
+ """Render the value according to the block's native rendering"""
+ return self.block.render(self.value)
+
+ @property
+ def block_type(self):
+ """
+ Syntactic sugar so that we can say child.block_type instead of child.block.name.
+ (This doesn't belong on BoundBlock itself because the idea of block.name denoting
+ the child's "type" ('heading', 'paragraph' etc) is unique to StreamBlock, and in the
+ wider context people are liable to confuse it with the block class (CharBlock etc).
+ """
+ return self.block.name
+
+ def __init__(self, stream_block, stream_data):
+ 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__
+
+ def __getitem__(self, i):
+ if i not in self._bound_blocks:
+ type_name, value = self.stream_data[i]
+ child_block = self.stream_block.child_blocks[type_name]
+ self._bound_blocks[i] = StreamValue.StreamChild(child_block, value)
+
+ return self._bound_blocks[i]
+
+ def __len__(self):
+ return len(self.stream_data)
+
+ def __repr__(self):
+ return repr(list(self))
+
+ def __str__(self):
+ return self.stream_block.render(self)