diff --git a/wagtail/wagtailadmin/blocks.py b/wagtail/wagtailadmin/blocks.py
new file mode 100644
index 000000000..8a4d3bc16
--- /dev/null
+++ b/wagtail/wagtailadmin/blocks.py
@@ -0,0 +1,649 @@
+import re
+from collections import OrderedDict
+
+from django.core.exceptions import ValidationError
+from django.utils.html import format_html, format_html_join
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
+from django.utils.encoding import python_2_unicode_compatible
+from django.template.loader import render_to_string
+from django.forms import Media
+from django.forms.utils import ErrorList
+
+import six
+
+# helpers for Javascript expression formatting
+
+def indent(string, depth=1):
+ """indent all non-empty lines of string by 'depth' 4-character tabs"""
+ return re.sub(r'(^|\n)([^\n]+)', '\g<1>' + (' ' * depth) + '\g<2>', string)
+
+def js_dict(d):
+ """
+ Return a Javascript expression string for the dict 'd'.
+ Keys are assumed to be strings consisting only of JS-safe characters, and will be quoted but not escaped;
+ values are assumed to be valid Javascript expressions and will be neither escaped nor quoted (but will be
+ wrapped in parentheses, in case some awkward git decides to use the comma operator...)
+ """
+ dict_items = [
+ indent("'%s': (%s)" % (k, v))
+ for (k, v) in d.items()
+ ]
+ return "{\n%s\n}" % ',\n'.join(dict_items)
+
+# =========================================
+# Top-level superclasses and helper objects
+# =========================================
+
+class Block(object):
+ creation_counter = 0
+
+ """
+ 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 = set()
+
+ def all_blocks(self):
+ """
+ Return a set consisting of self and all block objects that are direct or indirect dependencies
+ of this block
+ """
+ result = set([self])
+ for dep in self.dependencies:
+ result |= dep.all_blocks()
+ return result
+
+ def all_media(self):
+ media = 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):
+ if 'default' in kwargs:
+ self.default = kwargs['default'] # if not specified, leave as the class-level default
+ self.label = kwargs.get('label', None)
+
+ # 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
+
+ def set_name(self, name):
+ self.name = name
+
+ # if we don't have a label already, generate one from name
+ if self.label is None:
+ self.label = capfirst(name.replace('_', ' '))
+
+ @property
+ def media(self):
+ return 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", TextInput(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=''):
+ """
+ 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, error=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, prefix, value, error=error)
+
+ 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.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 renderable(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
+ 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
+
+
+class BoundBlock(object):
+ def __init__(self, block, prefix, value, error=None):
+ self.block = block
+ self.prefix = prefix
+ self.value = value
+ self.error = error
+
+ def render_form(self):
+ return self.block.render_form(self.value, self.prefix, error=self.error)
+
+
+# ==========
+# Text input
+# ==========
+
+class TextInputBlock(Block):
+ default = ''
+
+ def render_form(self, value, prefix='', error=None):
+ if self.label:
+ return format_html(
+ """ """,
+ prefix=prefix, label=self.label, value=value
+ )
+ else:
+ return format_html(
+ """""",
+ prefix=prefix, label=self.label, value=value
+ )
+
+ def value_from_datadict(self, data, files, prefix):
+ return data.get(prefix, '')
+
+
+# ===========
+# Field block
+# ===========
+
+class FieldBlock(Block):
+ default = None
+
+ def __init__(self, field, **kwargs):
+ super(FieldBlock, self).__init__(**kwargs)
+ self.field = field
+
+ def render_form(self, value, prefix='', error=None):
+ widget = self.field.widget
+
+ widget_html = widget.render(prefix, value, {'id': prefix})
+
+ if error:
+ error_html = str(ErrorList(error.error_list))
+ else:
+ error_html = ''
+
+ if self.label:
+ label_html = format_html(
+ """ """,
+ label_id=widget.id_for_label(prefix), label=self.label
+ )
+ else:
+ label_html = ''
+
+ return mark_safe(error_html + label_html + widget_html)
+
+ def value_from_datadict(self, data, files, prefix):
+ return self.field.widget.value_from_datadict(data, files, prefix)
+
+ def clean(self, value):
+ return self.field.clean(value)
+
+# =======
+# Chooser
+# =======
+
+class ChooserBlock(Block):
+ default = None
+
+ @property
+ def media(self):
+ return Media(js=['wagtailadmin/js/blocks/chooser.js'])
+
+ def js_initializer(self):
+ return "Chooser('%s')" % self.definition_prefix
+
+ def render_form(self, value, prefix='', error=None):
+ if self.label:
+ return format_html(
+ """ """,
+ label=self.label, prefix=prefix
+ )
+ else:
+ return format_html(
+ """""",
+ prefix=prefix
+ )
+
+ def value_from_datadict(self, data, files, prefix):
+ return 123
+
+
+# ===========
+# StructBlock
+# ===========
+
+class BaseStructBlock(Block):
+ default = {}
+ template = "wagtailadmin/blocks/struct.html"
+
+ def __init__(self, local_blocks=None, **kwargs):
+ super(BaseStructBlock, 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.child_js_initializers = {}
+ for name, block in self.child_blocks.items():
+ js_initializer = block.js_initializer()
+ if js_initializer is not None:
+ self.child_js_initializers[name] = js_initializer
+
+ self.dependencies = set(self.child_blocks.values())
+
+ def js_initializer(self):
+ # skip JS setup entirely if no children have js_initializers
+ if not self.child_js_initializers:
+ return None
+
+ return "StructBlock(%s)" % js_dict(self.child_js_initializers)
+
+ @property
+ def media(self):
+ return Media(js=['wagtailadmin/js/blocks/struct.js'])
+
+ def render_form(self, value, prefix='', error=None):
+ child_renderings = [
+ block.render_form(value.get(name, block.default), prefix="%s-%s" % (prefix, name),
+ error=error.params.get(name) if error else None)
+ for name, block in self.child_blocks.items()
+ ]
+
+ list_items = format_html_join('\n', "
{0}
", [
+ [child_rendering]
+ for child_rendering in child_renderings
+ ])
+
+ if self.label:
+ return format_html("