diff --git a/wagtail/wagtailcore/blocks/__init__.py b/wagtail/wagtailcore/blocks/__init__.py
index 8f95e70d9..62c930274 100644
--- a/wagtail/wagtailcore/blocks/__init__.py
+++ b/wagtail/wagtailcore/blocks/__init__.py
@@ -1,409 +1,7 @@
-from __future__ import absolute_import, unicode_literals
-# unicode_literals ensures that any render / __str__ methods returning HTML via calls to mark_safe / format_html
-# return a SafeText, not SafeBytes; necessary so that it doesn't get re-encoded when the template engine
-# calls force_text, which would cause it to lose its 'safe' flag
-
-import collections
-from importlib import import_module
-
-from django.core.exceptions import ImproperlyConfigured
-from django.utils.safestring import mark_safe
-from django.utils.text import capfirst
-from django.utils.encoding import force_text
-from django.template.loader import render_to_string
-from django import forms
-
-import six
-
-
-# =========================================
-# Top-level superclasses and helper objects
-# =========================================
-
-
-class BaseBlock(type):
- def __new__(mcs, name, bases, attrs):
- meta_class = attrs.pop('Meta', None)
-
- cls = super(BaseBlock, mcs).__new__(mcs, name, bases, attrs)
-
- base_meta_class = getattr(cls, '_meta_class', None)
- bases = tuple(cls for cls in [meta_class, base_meta_class] if cls) or ()
- cls._meta_class = type(str(name + 'Meta'), bases + (object, ), {})
-
- return cls
-
-
-class Block(six.with_metaclass(BaseBlock, object)):
- name = ''
- creation_counter = 0
-
- class Meta:
- label = None
- icon = "placeholder"
- classname = None
-
- """
- Setting a 'dependencies' list serves as a shortcut for the common case where a complex block type
- (such as struct, list or stream) relies on one or more inner block objects, and needs to ensure that
- the responses from the 'media' and 'html_declarations' include the relevant declarations for those inner
- blocks, as well as its own. Specifying these inner block objects in a 'dependencies' list means that
- the base 'media' and 'html_declarations' methods will return those declarations; the outer block type can
- then add its own declarations to the list by overriding those methods and using super().
- """
- dependencies = []
-
- def __new__(cls, *args, **kwargs):
- # adapted from django.utils.deconstruct.deconstructible; capture the arguments
- # so that we can return them in the 'deconstruct' method
- obj = super(Block, cls).__new__(cls, *args, **kwargs)
- obj._constructor_args = (args, kwargs)
- return obj
-
- def all_blocks(self):
- """
- Return a list consisting of self and all block objects that are direct or indirect dependencies
- of this block
- """
- result = [self]
- for dep in self.dependencies:
- result.extend(dep.all_blocks())
- return result
-
- def all_media(self):
- media = forms.Media()
- for block in self.all_blocks():
- media += block.media
- return media
-
- def all_html_declarations(self):
- declarations = filter(bool, [block.html_declarations() for block in self.all_blocks()])
- return mark_safe('\n'.join(declarations))
-
- def __init__(self, **kwargs):
- self.meta = self._meta_class()
-
- for attr, value in kwargs.items():
- setattr(self.meta, attr, value)
-
- # Increase the creation counter, and save our local copy.
- self.creation_counter = Block.creation_counter
- Block.creation_counter += 1
- self.definition_prefix = 'blockdef-%d' % self.creation_counter
-
- self.label = self.meta.label or ''
-
- def set_name(self, name):
- self.name = name
- if not self.meta.label:
- self.label = capfirst(force_text(name).replace('_', ' '))
-
- @property
- def media(self):
- return forms.Media()
-
- def html_declarations(self):
- """
- Return an HTML fragment to be rendered on the form page once per block definition -
- as opposed to once per occurrence of the block. For example, the block definition
- ListBlock(label="Shopping list", CharBlock(label="Product"))
- needs to output a block containing the HTML for
- a 'product' text input, to that these can be dynamically added to the list. This
- template block must only occur once in the page, even if there are multiple 'shopping list'
- blocks on the page.
-
- Any element IDs used in this HTML fragment must begin with definition_prefix.
- (More precisely, they must either be definition_prefix itself, or begin with definition_prefix
- followed by a '-' character)
- """
- return ''
-
- def js_initializer(self):
- """
- Returns a Javascript expression string, or None if this block does not require any
- Javascript behaviour. This expression evaluates to an initializer function, a function that
- takes the ID prefix and applies JS behaviour to the block instance with that value and prefix.
-
- The parent block of this block (or the top-level page code) must ensure that this
- expression is not evaluated more than once. (The resulting initializer function can and will be
- called as many times as there are instances of this block, though.)
- """
- return None
-
- def render_form(self, value, prefix='', errors=None):
- """
- Render the HTML for this block with 'value' as its content.
- """
- raise NotImplementedError('%s.render_form' % self.__class__)
-
- def value_from_datadict(self, data, files, prefix):
- raise NotImplementedError('%s.value_from_datadict' % self.__class__)
-
- def bind(self, value, prefix=None, errors=None):
- """
- Return a BoundBlock which represents the association of this block definition with a value
- and a prefix (and optionally, a ValidationError to be rendered).
- BoundBlock primarily exists as a convenience to allow rendering within templates:
- bound_block.render() rather than blockdef.render(value, prefix) which can't be called from
- within a template.
- """
- return BoundBlock(self, value, prefix=prefix, errors=errors)
-
- def prototype_block(self):
- """
- Return a BoundBlock that can be used as a basis for new empty block instances to be added on the fly
- (new list items, for example). This will have a prefix of '__PREFIX__' (to be dynamically replaced with
- a real prefix when it's inserted into the page) and a value equal to the block's default value.
- """
- return self.bind(self.meta.default, '__PREFIX__')
-
- def clean(self, value):
- """
- Validate value and return a cleaned version of it, or throw a ValidationError if validation fails.
- The thrown ValidationError instance will subsequently be passed to render() to display the
- error message; nested blocks therefore need to wrap child validations like this:
- https://docs.djangoproject.com/en/dev/ref/forms/validation/#raising-multiple-errors
-
- NB The ValidationError must have an error_list property (which can be achieved by passing a
- list or an individual error message to its constructor), NOT an error_dict -
- Django has problems nesting ValidationErrors with error_dicts.
- """
- return value
-
- def to_python(self, value):
- """
- 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).
- """
- return value
-
- def get_prep_value(self, value):
- """
- The reverse of to_python; convert the python value into JSON-serialisable form.
- """
- return value
-
- def render(self, value):
- """
- Return a text rendering of 'value', suitable for display on templates. By default, this will
- use a template if a 'template' property is specified on the block, and fall back on render_basic
- otherwise.
- """
- template = getattr(self.meta, 'template', None)
- if template:
- return render_to_string(template, {'self': value})
- else:
- return self.render_basic(value)
-
- def render_basic(self, value):
- """
- Return a text rendering of 'value', suitable for display on templates. render() will fall back on
- this if the block does not define a 'template' property.
- """
- return force_text(value)
-
- def get_searchable_content(self, value):
- """
- Returns a list of strings containing text content within this block to be used in a search engine.
- """
- return []
-
- def deconstruct(self):
- # adapted from django.utils.deconstruct.deconstructible
- module_name = self.__module__
- name = self.__class__.__name__
-
- # Make sure it's actually there and not an inner class
- module = import_module(module_name)
- if not hasattr(module, name):
- raise ValueError(
- "Could not find object %s in %s.\n"
- "Please note that you cannot serialize things like inner "
- "classes. Please move the object into the main module "
- "body to use migrations.\n"
- % (name, module_name))
-
- # if the module defines a DECONSTRUCT_ALIASES dictionary, see if the class has an entry in there;
- # if so, use that instead of the real path
- try:
- path = module.DECONSTRUCT_ALIASES[self.__class__]
- except (AttributeError, KeyError):
- path = '%s.%s' % (module_name, name)
-
- return (
- path,
- self._constructor_args[0],
- self._constructor_args[1],
- )
-
- def __eq__(self, other):
- """
- The deep_deconstruct method in django.db.migrations.autodetector.MigrationAutodetector does not
- recurse into arbitrary lists and dicts. As a result, when it is passed a field such as:
- StreamField([
- ('heading', CharBlock()),
- ])
- the CharBlock object will be left in its constructed form. This causes problems when
- MigrationAutodetector compares two separate instances of the StreamField from different project
- states: since the CharBlocks are different objects, it will report a change where there isn't one.
-
- To prevent this, we implement the equality operator on Block instances such that the two CharBlocks
- are reported as equal. Since block objects are intended to be immutable with the exception of
- set_name(), it is sufficient to compare the 'name' property and the constructor args/kwargs of the
- two block objects. The 'deconstruct' method provides a convenient way to access the latter.
- """
-
- if not isinstance(other, Block):
- # if the other object isn't a block at all, it clearly isn't equal.
- return False
-
- # Note that we do not require the two blocks to be of the exact same class. This is because
- # we may wish the following blocks to be considered equal:
- #
- # class FooBlock(StructBlock):
- # first_name = CharBlock()
- # surname = CharBlock()
- #
- # class BarBlock(StructBlock):
- # first_name = CharBlock()
- # surname = CharBlock()
- #
- # FooBlock() == BarBlock() == StructBlock([('first_name', CharBlock()), ('surname': CharBlock())])
- #
- # For this to work, StructBlock will need to ensure that 'deconstruct' returns the same signature
- # in all of these cases, including reporting StructBlock as the path:
- #
- # FooBlock().deconstruct() == (
- # 'wagtail.wagtailcore.blocks.StructBlock',
- # [('first_name', CharBlock()), ('surname': CharBlock())],
- # {}
- # )
- #
- # This has the bonus side effect that the StructBlock field definition gets frozen into
- # the migration, rather than leaving the migration vulnerable to future changes to FooBlock / BarBlock
- # in models.py.
-
- return (self.name == other.name) and (self.deconstruct() == other.deconstruct())
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- # Making block instances hashable in a way that's consistent with __eq__ is non-trivial, because
- # self.deconstruct() is liable to contain unhashable data (e.g. lists and dicts). So let's set
- # Block to be explicitly unhashable - Python 3 will do this automatically when defining __eq__,
- # but Python 2 won't, and we'd like the behaviour to be consistent on both.
- __hash__ = None
-
-
-class BoundBlock(object):
- def __init__(self, block, value, prefix=None, errors=None):
- self.block = block
- self.value = value
- self.prefix = prefix
- self.errors = errors
-
- def render_form(self):
- return self.block.render_form(self.value, self.prefix, errors=self.errors)
-
- def render(self):
- return self.block.render(self.value)
-
-
-class DeclarativeSubBlocksMetaclass(BaseBlock):
- """
- Metaclass that collects sub-blocks declared on the base classes.
- (cheerfully stolen from https://github.com/django/django/blob/master/django/forms/forms.py)
- """
- def __new__(mcs, name, bases, attrs):
- # Collect sub-blocks from current class.
- current_blocks = []
- for key, value in list(attrs.items()):
- if isinstance(value, Block):
- current_blocks.append((key, value))
- value.set_name(key)
- attrs.pop(key)
- current_blocks.sort(key=lambda x: x[1].creation_counter)
- attrs['declared_blocks'] = collections.OrderedDict(current_blocks)
-
- new_class = (super(DeclarativeSubBlocksMetaclass, mcs)
- .__new__(mcs, name, bases, attrs))
-
- # Walk through the MRO.
- declared_blocks = collections.OrderedDict()
- for base in reversed(new_class.__mro__):
- # Collect sub-blocks from base class.
- if hasattr(base, 'declared_blocks'):
- declared_blocks.update(base.declared_blocks)
-
- # Field shadowing.
- for attr, value in base.__dict__.items():
- if value is None and attr in declared_blocks:
- declared_blocks.pop(attr)
-
- new_class.base_blocks = declared_blocks
- new_class.declared_blocks = declared_blocks
-
- return new_class
-
-
-# ========================
-# django.forms integration
-# ========================
-
-class BlockWidget(forms.Widget):
- """Wraps a block object as a widget so that it can be incorporated into a Django form"""
- def __init__(self, block_def, attrs=None):
- super(BlockWidget, self).__init__(attrs=attrs)
- self.block_def = block_def
-
- def render_with_errors(self, name, value, attrs=None, errors=None):
- bound_block = self.block_def.bind(value, prefix=name, errors=errors)
- js_initializer = self.block_def.js_initializer()
- if js_initializer:
- js_snippet = """
-
- """ % (js_initializer, name)
- else:
- js_snippet = ''
- return mark_safe(bound_block.render_form() + js_snippet)
-
- def render(self, name, value, attrs=None):
- return self.render_with_errors(name, value, attrs=attrs, errors=None)
-
- @property
- def media(self):
- return self.block_def.all_media()
-
- def value_from_datadict(self, data, files, name):
- return self.block_def.value_from_datadict(data, files, name)
-
-
-class BlockField(forms.Field):
- """Wraps a block object as a form field so that it can be incorporated into a Django form"""
- def __init__(self, block=None, **kwargs):
- if block is None:
- raise ImproperlyConfigured("BlockField was not passed a 'block' object")
- self.block = block
-
- if 'widget' not in kwargs:
- kwargs['widget'] = BlockWidget(block)
-
- super(BlockField, self).__init__(**kwargs)
-
- def clean(self, value):
- return self.block.clean(value)
-
+from __future__ import absolute_import
# Import block types defined in submodules into the wagtail.wagtailcore.blocks namespace
+from .base import * # NOQA
from .field_block import * # NOQA
from .struct_block import * # NOQA
from .list_block import * # NOQA
diff --git a/wagtail/wagtailcore/blocks/base.py b/wagtail/wagtailcore/blocks/base.py
new file mode 100644
index 000000000..127aefcb2
--- /dev/null
+++ b/wagtail/wagtailcore/blocks/base.py
@@ -0,0 +1,411 @@
+from __future__ import absolute_import, unicode_literals
+# unicode_literals ensures that any render / __str__ methods returning HTML via calls to mark_safe / format_html
+# return a SafeText, not SafeBytes; necessary so that it doesn't get re-encoded when the template engine
+# calls force_text, which would cause it to lose its 'safe' flag
+
+import collections
+from importlib import import_module
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
+from django.utils.encoding import force_text
+from django.template.loader import render_to_string
+from django import forms
+
+import six
+
+
+__all__ = ['BaseBlock', 'Block', 'BoundBlock', 'DeclarativeSubBlocksMetaclass', 'BlockWidget', 'BlockField']
+
+
+# =========================================
+# Top-level superclasses and helper objects
+# =========================================
+
+
+class BaseBlock(type):
+ def __new__(mcs, name, bases, attrs):
+ meta_class = attrs.pop('Meta', None)
+
+ cls = super(BaseBlock, mcs).__new__(mcs, name, bases, attrs)
+
+ base_meta_class = getattr(cls, '_meta_class', None)
+ bases = tuple(cls for cls in [meta_class, base_meta_class] if cls) or ()
+ cls._meta_class = type(str(name + 'Meta'), bases + (object, ), {})
+
+ return cls
+
+
+class Block(six.with_metaclass(BaseBlock, object)):
+ name = ''
+ creation_counter = 0
+
+ class Meta:
+ label = None
+ icon = "placeholder"
+ classname = None
+
+ """
+ Setting a 'dependencies' list serves as a shortcut for the common case where a complex block type
+ (such as struct, list or stream) relies on one or more inner block objects, and needs to ensure that
+ the responses from the 'media' and 'html_declarations' include the relevant declarations for those inner
+ blocks, as well as its own. Specifying these inner block objects in a 'dependencies' list means that
+ the base 'media' and 'html_declarations' methods will return those declarations; the outer block type can
+ then add its own declarations to the list by overriding those methods and using super().
+ """
+ dependencies = []
+
+ def __new__(cls, *args, **kwargs):
+ # adapted from django.utils.deconstruct.deconstructible; capture the arguments
+ # so that we can return them in the 'deconstruct' method
+ obj = super(Block, cls).__new__(cls, *args, **kwargs)
+ obj._constructor_args = (args, kwargs)
+ return obj
+
+ def all_blocks(self):
+ """
+ Return a list consisting of self and all block objects that are direct or indirect dependencies
+ of this block
+ """
+ result = [self]
+ for dep in self.dependencies:
+ result.extend(dep.all_blocks())
+ return result
+
+ def all_media(self):
+ media = forms.Media()
+ for block in self.all_blocks():
+ media += block.media
+ return media
+
+ def all_html_declarations(self):
+ declarations = filter(bool, [block.html_declarations() for block in self.all_blocks()])
+ return mark_safe('\n'.join(declarations))
+
+ def __init__(self, **kwargs):
+ self.meta = self._meta_class()
+
+ for attr, value in kwargs.items():
+ setattr(self.meta, attr, value)
+
+ # Increase the creation counter, and save our local copy.
+ self.creation_counter = Block.creation_counter
+ Block.creation_counter += 1
+ self.definition_prefix = 'blockdef-%d' % self.creation_counter
+
+ self.label = self.meta.label or ''
+
+ def set_name(self, name):
+ self.name = name
+ if not self.meta.label:
+ self.label = capfirst(force_text(name).replace('_', ' '))
+
+ @property
+ def media(self):
+ return forms.Media()
+
+ def html_declarations(self):
+ """
+ Return an HTML fragment to be rendered on the form page once per block definition -
+ as opposed to once per occurrence of the block. For example, the block definition
+ ListBlock(label="Shopping list", CharBlock(label="Product"))
+ needs to output a block containing the HTML for
+ a 'product' text input, to that these can be dynamically added to the list. This
+ template block must only occur once in the page, even if there are multiple 'shopping list'
+ blocks on the page.
+
+ Any element IDs used in this HTML fragment must begin with definition_prefix.
+ (More precisely, they must either be definition_prefix itself, or begin with definition_prefix
+ followed by a '-' character)
+ """
+ return ''
+
+ def js_initializer(self):
+ """
+ Returns a Javascript expression string, or None if this block does not require any
+ Javascript behaviour. This expression evaluates to an initializer function, a function that
+ takes the ID prefix and applies JS behaviour to the block instance with that value and prefix.
+
+ The parent block of this block (or the top-level page code) must ensure that this
+ expression is not evaluated more than once. (The resulting initializer function can and will be
+ called as many times as there are instances of this block, though.)
+ """
+ return None
+
+ def render_form(self, value, prefix='', errors=None):
+ """
+ Render the HTML for this block with 'value' as its content.
+ """
+ raise NotImplementedError('%s.render_form' % self.__class__)
+
+ def value_from_datadict(self, data, files, prefix):
+ raise NotImplementedError('%s.value_from_datadict' % self.__class__)
+
+ def bind(self, value, prefix=None, errors=None):
+ """
+ Return a BoundBlock which represents the association of this block definition with a value
+ and a prefix (and optionally, a ValidationError to be rendered).
+ BoundBlock primarily exists as a convenience to allow rendering within templates:
+ bound_block.render() rather than blockdef.render(value, prefix) which can't be called from
+ within a template.
+ """
+ return BoundBlock(self, value, prefix=prefix, errors=errors)
+
+ def prototype_block(self):
+ """
+ Return a BoundBlock that can be used as a basis for new empty block instances to be added on the fly
+ (new list items, for example). This will have a prefix of '__PREFIX__' (to be dynamically replaced with
+ a real prefix when it's inserted into the page) and a value equal to the block's default value.
+ """
+ return self.bind(self.meta.default, '__PREFIX__')
+
+ def clean(self, value):
+ """
+ Validate value and return a cleaned version of it, or throw a ValidationError if validation fails.
+ The thrown ValidationError instance will subsequently be passed to render() to display the
+ error message; nested blocks therefore need to wrap child validations like this:
+ https://docs.djangoproject.com/en/dev/ref/forms/validation/#raising-multiple-errors
+
+ NB The ValidationError must have an error_list property (which can be achieved by passing a
+ list or an individual error message to its constructor), NOT an error_dict -
+ Django has problems nesting ValidationErrors with error_dicts.
+ """
+ return value
+
+ def to_python(self, value):
+ """
+ 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).
+ """
+ return value
+
+ def get_prep_value(self, value):
+ """
+ The reverse of to_python; convert the python value into JSON-serialisable form.
+ """
+ return value
+
+ def render(self, value):
+ """
+ Return a text rendering of 'value', suitable for display on templates. By default, this will
+ use a template if a 'template' property is specified on the block, and fall back on render_basic
+ otherwise.
+ """
+ template = getattr(self.meta, 'template', None)
+ if template:
+ return render_to_string(template, {'self': value})
+ else:
+ return self.render_basic(value)
+
+ def render_basic(self, value):
+ """
+ Return a text rendering of 'value', suitable for display on templates. render() will fall back on
+ this if the block does not define a 'template' property.
+ """
+ return force_text(value)
+
+ def get_searchable_content(self, value):
+ """
+ Returns a list of strings containing text content within this block to be used in a search engine.
+ """
+ return []
+
+ def deconstruct(self):
+ # adapted from django.utils.deconstruct.deconstructible
+ module_name = self.__module__
+ name = self.__class__.__name__
+
+ # Make sure it's actually there and not an inner class
+ module = import_module(module_name)
+ if not hasattr(module, name):
+ raise ValueError(
+ "Could not find object %s in %s.\n"
+ "Please note that you cannot serialize things like inner "
+ "classes. Please move the object into the main module "
+ "body to use migrations.\n"
+ % (name, module_name))
+
+ # if the module defines a DECONSTRUCT_ALIASES dictionary, see if the class has an entry in there;
+ # if so, use that instead of the real path
+ try:
+ path = module.DECONSTRUCT_ALIASES[self.__class__]
+ except (AttributeError, KeyError):
+ path = '%s.%s' % (module_name, name)
+
+ return (
+ path,
+ self._constructor_args[0],
+ self._constructor_args[1],
+ )
+
+ def __eq__(self, other):
+ """
+ The deep_deconstruct method in django.db.migrations.autodetector.MigrationAutodetector does not
+ recurse into arbitrary lists and dicts. As a result, when it is passed a field such as:
+ StreamField([
+ ('heading', CharBlock()),
+ ])
+ the CharBlock object will be left in its constructed form. This causes problems when
+ MigrationAutodetector compares two separate instances of the StreamField from different project
+ states: since the CharBlocks are different objects, it will report a change where there isn't one.
+
+ To prevent this, we implement the equality operator on Block instances such that the two CharBlocks
+ are reported as equal. Since block objects are intended to be immutable with the exception of
+ set_name(), it is sufficient to compare the 'name' property and the constructor args/kwargs of the
+ two block objects. The 'deconstruct' method provides a convenient way to access the latter.
+ """
+
+ if not isinstance(other, Block):
+ # if the other object isn't a block at all, it clearly isn't equal.
+ return False
+
+ # Note that we do not require the two blocks to be of the exact same class. This is because
+ # we may wish the following blocks to be considered equal:
+ #
+ # class FooBlock(StructBlock):
+ # first_name = CharBlock()
+ # surname = CharBlock()
+ #
+ # class BarBlock(StructBlock):
+ # first_name = CharBlock()
+ # surname = CharBlock()
+ #
+ # FooBlock() == BarBlock() == StructBlock([('first_name', CharBlock()), ('surname': CharBlock())])
+ #
+ # For this to work, StructBlock will need to ensure that 'deconstruct' returns the same signature
+ # in all of these cases, including reporting StructBlock as the path:
+ #
+ # FooBlock().deconstruct() == (
+ # 'wagtail.wagtailcore.blocks.StructBlock',
+ # [('first_name', CharBlock()), ('surname': CharBlock())],
+ # {}
+ # )
+ #
+ # This has the bonus side effect that the StructBlock field definition gets frozen into
+ # the migration, rather than leaving the migration vulnerable to future changes to FooBlock / BarBlock
+ # in models.py.
+
+ return (self.name == other.name) and (self.deconstruct() == other.deconstruct())
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ # Making block instances hashable in a way that's consistent with __eq__ is non-trivial, because
+ # self.deconstruct() is liable to contain unhashable data (e.g. lists and dicts). So let's set
+ # Block to be explicitly unhashable - Python 3 will do this automatically when defining __eq__,
+ # but Python 2 won't, and we'd like the behaviour to be consistent on both.
+ __hash__ = None
+
+
+class BoundBlock(object):
+ def __init__(self, block, value, prefix=None, errors=None):
+ self.block = block
+ self.value = value
+ self.prefix = prefix
+ self.errors = errors
+
+ def render_form(self):
+ return self.block.render_form(self.value, self.prefix, errors=self.errors)
+
+ def render(self):
+ return self.block.render(self.value)
+
+
+class DeclarativeSubBlocksMetaclass(BaseBlock):
+ """
+ Metaclass that collects sub-blocks declared on the base classes.
+ (cheerfully stolen from https://github.com/django/django/blob/master/django/forms/forms.py)
+ """
+ def __new__(mcs, name, bases, attrs):
+ # Collect sub-blocks from current class.
+ current_blocks = []
+ for key, value in list(attrs.items()):
+ if isinstance(value, Block):
+ current_blocks.append((key, value))
+ value.set_name(key)
+ attrs.pop(key)
+ current_blocks.sort(key=lambda x: x[1].creation_counter)
+ attrs['declared_blocks'] = collections.OrderedDict(current_blocks)
+
+ new_class = (super(DeclarativeSubBlocksMetaclass, mcs)
+ .__new__(mcs, name, bases, attrs))
+
+ # Walk through the MRO.
+ declared_blocks = collections.OrderedDict()
+ for base in reversed(new_class.__mro__):
+ # Collect sub-blocks from base class.
+ if hasattr(base, 'declared_blocks'):
+ declared_blocks.update(base.declared_blocks)
+
+ # Field shadowing.
+ for attr, value in base.__dict__.items():
+ if value is None and attr in declared_blocks:
+ declared_blocks.pop(attr)
+
+ new_class.base_blocks = declared_blocks
+ new_class.declared_blocks = declared_blocks
+
+ return new_class
+
+
+# ========================
+# django.forms integration
+# ========================
+
+class BlockWidget(forms.Widget):
+ """Wraps a block object as a widget so that it can be incorporated into a Django form"""
+ def __init__(self, block_def, attrs=None):
+ super(BlockWidget, self).__init__(attrs=attrs)
+ self.block_def = block_def
+
+ def render_with_errors(self, name, value, attrs=None, errors=None):
+ bound_block = self.block_def.bind(value, prefix=name, errors=errors)
+ js_initializer = self.block_def.js_initializer()
+ if js_initializer:
+ js_snippet = """
+
+ """ % (js_initializer, name)
+ else:
+ js_snippet = ''
+ return mark_safe(bound_block.render_form() + js_snippet)
+
+ def render(self, name, value, attrs=None):
+ return self.render_with_errors(name, value, attrs=attrs, errors=None)
+
+ @property
+ def media(self):
+ return self.block_def.all_media()
+
+ def value_from_datadict(self, data, files, name):
+ return self.block_def.value_from_datadict(data, files, name)
+
+
+class BlockField(forms.Field):
+ """Wraps a block object as a form field so that it can be incorporated into a Django form"""
+ def __init__(self, block=None, **kwargs):
+ if block is None:
+ raise ImproperlyConfigured("BlockField was not passed a 'block' object")
+ self.block = block
+
+ if 'widget' not in kwargs:
+ kwargs['widget'] = BlockWidget(block)
+
+ super(BlockField, self).__init__(**kwargs)
+
+ def clean(self, value):
+ return self.block.clean(value)
+
+
+DECONSTRUCT_ALIASES = {
+ Block: 'wagtail.wagtailcore.blocks.Block',
+}
diff --git a/wagtail/wagtailcore/blocks/field_block.py b/wagtail/wagtailcore/blocks/field_block.py
index 2cf01eea0..11199110d 100644
--- a/wagtail/wagtailcore/blocks/field_block.py
+++ b/wagtail/wagtailcore/blocks/field_block.py
@@ -7,9 +7,11 @@ from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import mark_safe
-from wagtail.wagtailcore.blocks import Block
from wagtail.wagtailcore.rich_text import expand_db_html
+from .base import Block
+
+
class FieldBlock(Block):
class Meta:
default = None
diff --git a/wagtail/wagtailcore/blocks/list_block.py b/wagtail/wagtailcore/blocks/list_block.py
index 57778e2f7..5faec3066 100644
--- a/wagtail/wagtailcore/blocks/list_block.py
+++ b/wagtail/wagtailcore/blocks/list_block.py
@@ -7,13 +7,15 @@ from django.template.loader import render_to_string
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
-from wagtail.wagtailcore.blocks import Block
from wagtail.wagtailcore.utils import escape_script
+from .base import Block
from .utils import js_dict
+
__all__ = ['ListBlock']
+
class ListBlock(Block):
class Meta:
# Default to a list consisting of one empty child item (using None to trigger the child's empty / default rendering)
diff --git a/wagtail/wagtailcore/blocks/stream_block.py b/wagtail/wagtailcore/blocks/stream_block.py
index 873b1f74d..3e46163eb 100644
--- a/wagtail/wagtailcore/blocks/stream_block.py
+++ b/wagtail/wagtailcore/blocks/stream_block.py
@@ -12,13 +12,15 @@ 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 .base import Block, DeclarativeSubBlocksMetaclass, BoundBlock
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
diff --git a/wagtail/wagtailcore/blocks/struct_block.py b/wagtail/wagtailcore/blocks/struct_block.py
index c39c8b75b..f4e37066f 100644
--- a/wagtail/wagtailcore/blocks/struct_block.py
+++ b/wagtail/wagtailcore/blocks/struct_block.py
@@ -11,8 +11,7 @@ from django.utils.html import format_html, format_html_join
import six
-from wagtail.wagtailcore.blocks import Block, DeclarativeSubBlocksMetaclass
-
+from .base import Block, DeclarativeSubBlocksMetaclass
from .utils import js_dict