mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-12 09:13:14 +00:00
849 lines
32 KiB
Python
849 lines
32 KiB
Python
from __future__ import unicode_literals
|
|
# this 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 re
|
|
import collections
|
|
|
|
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 force_text, python_2_unicode_compatible
|
|
from django.utils.deconstruct import deconstructible
|
|
from django.utils.functional import cached_property
|
|
from django.template.loader import render_to_string
|
|
from django.forms import Media, CharField
|
|
from django.forms.utils import ErrorList
|
|
|
|
import six
|
|
|
|
from wagtail.wagtailcore.utils import escape_script
|
|
from wagtail.wagtailcore.rich_text import expand_db_html
|
|
|
|
# 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 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
|
|
|
|
|
|
@deconstructible
|
|
class Block(six.with_metaclass(BaseBlock, object)):
|
|
name = ''
|
|
creation_counter = 0
|
|
|
|
class Meta:
|
|
label = None
|
|
icon = "streamfield-block-placeholder"
|
|
|
|
"""
|
|
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):
|
|
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
|
|
|
|
def set_name(self, name):
|
|
self.name = name
|
|
|
|
def get_label(self):
|
|
return self.meta.label or self.name
|
|
|
|
@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 <script type="text/template"></script> 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=None, 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, value, prefix=prefix, 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.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)
|
|
|
|
|
|
class BoundBlock(object):
|
|
def __init__(self, block, value, prefix=None, error=None):
|
|
self.block = block
|
|
self.value = value
|
|
self.prefix = prefix
|
|
self.error = error
|
|
|
|
def render_form(self):
|
|
return self.block.render_form(self.value, self.prefix, error=self.error)
|
|
|
|
def render(self):
|
|
return self.block.render(self.value)
|
|
|
|
|
|
# ==========
|
|
# Text input
|
|
# ==========
|
|
|
|
class TextInputBlock(Block):
|
|
class Meta:
|
|
default = ''
|
|
|
|
def render_form(self, value, prefix='', error=None):
|
|
if self.get_label():
|
|
return format_html(
|
|
"""<label for="{prefix}">{label}</label> <input type="text" name="{prefix}" id="{prefix}" value="{value}">""",
|
|
prefix=prefix, label=self.get_label(), value=value
|
|
)
|
|
else:
|
|
return format_html(
|
|
"""<input type="text" name="{prefix}" id="{prefix}" value="{value}">""",
|
|
prefix=prefix, label=self.get_label(), value=value
|
|
)
|
|
|
|
def value_from_datadict(self, data, files, prefix):
|
|
return data.get(prefix, '')
|
|
|
|
|
|
# ===========
|
|
# Field block
|
|
# ===========
|
|
|
|
# FIXME: form field instances are not deconstructible for migrations. Need some other way to refer to
|
|
# them in the initialiser, in the case that FieldBlock appears inline within a StreamField definition.
|
|
# (Referring to them by class would probably work; it's unlikely that any parameter passed to them
|
|
# would affect anything you're doing in migrations)
|
|
|
|
class FieldBlock(Block):
|
|
class Meta:
|
|
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.get_label():
|
|
label_html = format_html(
|
|
"""<label for={label_id}>{label}</label> """,
|
|
label_id=widget.id_for_label(prefix), label=self.get_label()
|
|
)
|
|
else:
|
|
label_html = ''
|
|
|
|
widget_html = widget.render(prefix, value, {'id': prefix, 'placeholder': self.label})
|
|
|
|
#if error:
|
|
# error_html = str(ErrorList(error.error_list))
|
|
#else:
|
|
# error_html = ''
|
|
|
|
return render_to_string('wagtailadmin/block_forms/field.html', {
|
|
'widget': widget_html,
|
|
'label_tag': label_html,
|
|
'field': self.field,
|
|
'errors': error.error_list if error else [], # TODO: should this be ErrorList(error.error_list)?
|
|
})
|
|
|
|
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)
|
|
|
|
class CharBlock(FieldBlock):
|
|
def __init__(self, **kwargs):
|
|
super(CharBlock, self).__init__(CharField(), **kwargs)
|
|
# TODO: some kwargs, such as max_length, and *possibly* things like help_text, should be passed to
|
|
# the CharField constructor. Figure out a system for doing this
|
|
|
|
class RichTextBlock(FieldBlock):
|
|
def __init__(self, **kwargs):
|
|
from wagtail.wagtailcore.fields import RichTextArea
|
|
super(RichTextBlock, self).__init__(CharField(widget=RichTextArea), **kwargs)
|
|
|
|
def render_basic(self, value):
|
|
return mark_safe('<div class="rich-text">' + expand_db_html(value) + '</div>')
|
|
|
|
# =======
|
|
# Chooser
|
|
# =======
|
|
|
|
class ChooserBlock(Block):
|
|
class Meta:
|
|
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.get_label():
|
|
return format_html(
|
|
"""<label>{label}</label> <input type="button" id="{prefix}-button" value="Choose a thing">""",
|
|
label=self.get_label(), prefix=prefix
|
|
)
|
|
else:
|
|
return format_html(
|
|
"""<input type="button" id="{prefix}-button" value="Choose a thing">""",
|
|
prefix=prefix
|
|
)
|
|
|
|
def value_from_datadict(self, data, files, prefix):
|
|
return 123
|
|
|
|
|
|
# ===========
|
|
# StructBlock
|
|
# ===========
|
|
|
|
class BaseStructBlock(Block):
|
|
class Meta:
|
|
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.meta.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', "<li>{0}</li>", [
|
|
[child_rendering]
|
|
for child_rendering in child_renderings
|
|
])
|
|
|
|
|
|
# Can these be rendered with a template?
|
|
if self.get_label():
|
|
return format_html('<div class="struct-block"><label>{0}</label> <ul>{1}</ul></div>', self.get_label(), list_items)
|
|
else:
|
|
return format_html('<div class="struct-block"><ul>{0}</ul></div>', list_items)
|
|
|
|
def value_from_datadict(self, data, files, prefix):
|
|
return dict([
|
|
(name, block.value_from_datadict(data, files, '%s-%s' % (prefix, name)))
|
|
for name, block in self.child_blocks.items()
|
|
])
|
|
|
|
def clean(self, value):
|
|
result = {}
|
|
errors = {}
|
|
for name, val in value.items():
|
|
try:
|
|
result[name] = self.child_blocks[name].clean(val)
|
|
except ValidationError as e:
|
|
errors[name] = e
|
|
|
|
if errors:
|
|
# The message here is arbitrary - outputting error messages is delegated to the child blocks,
|
|
# which only involves the 'params' dict
|
|
raise ValidationError('Validation error in StructBlock', params=errors)
|
|
|
|
return result
|
|
|
|
def to_python(self, value):
|
|
# recursively call to_python on children and return as a StructValue
|
|
return StructValue(self, [
|
|
(
|
|
name,
|
|
child_block.to_python(value.get(name, child_block.meta.default))
|
|
)
|
|
for name, child_block in self.child_blocks.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()
|
|
])
|
|
|
|
@python_2_unicode_compatible # provide equivalent __unicode__ and __str__ methods on Py2
|
|
class StructValue(collections.OrderedDict):
|
|
def __init__(self, block, *args):
|
|
super(StructValue, self).__init__(*args)
|
|
self.block = block
|
|
|
|
def __str__(self):
|
|
return self.block.render(self)
|
|
|
|
@cached_property
|
|
def bound_blocks(self):
|
|
return collections.OrderedDict([
|
|
(name, block.bind(self.get(name)))
|
|
for name, block in self.block.child_blocks.items()
|
|
])
|
|
|
|
|
|
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
|
|
|
|
class StructBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStructBlock)):
|
|
pass
|
|
|
|
|
|
# =========
|
|
# ListBlock
|
|
# =========
|
|
|
|
class ListBlock(Block):
|
|
class Meta:
|
|
default = []
|
|
|
|
def __init__(self, child_block, **kwargs):
|
|
super(ListBlock, self).__init__(**kwargs)
|
|
|
|
if isinstance(child_block, type):
|
|
# child_block was passed as a class, so convert it to a block instance
|
|
self.child_block = child_block()
|
|
else:
|
|
self.child_block = child_block
|
|
|
|
self.dependencies = set([self.child_block])
|
|
self.child_js_initializer = self.child_block.js_initializer()
|
|
|
|
@property
|
|
def media(self):
|
|
return Media(js=['wagtailadmin/js/blocks/sequence.js', 'wagtailadmin/js/blocks/list.js'])
|
|
|
|
def render_list_member(self, value, prefix, index, error=None):
|
|
"""
|
|
Render the HTML for a single list item in the form. This consists of an <li> wrapper, hidden fields
|
|
to manage ID/deleted state, delete/reorder buttons, and the child block's own form HTML.
|
|
"""
|
|
child = self.child_block.bind(value, prefix="%s-value" % prefix, error=error)
|
|
return render_to_string('wagtailadmin/block_forms/list_member.html', {
|
|
'prefix': prefix,
|
|
'child': child,
|
|
'index': index,
|
|
})
|
|
|
|
def html_declarations(self):
|
|
# generate the HTML to be used when adding a new item to the list;
|
|
# this is the output of render_list_member as rendered with the prefix '__PREFIX__'
|
|
# (to be replaced dynamically when adding the new item) and the child block's default value
|
|
# as its value.
|
|
list_member_html = self.render_list_member(self.child_block.meta.default, '__PREFIX__', '')
|
|
|
|
return format_html(
|
|
'<script type="text/template" id="{0}-newmember">{1}</script>',
|
|
self.definition_prefix, mark_safe(escape_script(list_member_html))
|
|
)
|
|
|
|
def js_initializer(self):
|
|
opts = {'definitionPrefix': "'%s'" % self.definition_prefix}
|
|
|
|
if self.child_js_initializer:
|
|
opts['childInitializer'] = self.child_js_initializer
|
|
|
|
return "ListBlock(%s)" % js_dict(opts)
|
|
|
|
def render_form(self, value, prefix='', error=None):
|
|
list_members_html = [
|
|
self.render_list_member(child_val, "%s-%d" % (prefix, i), i,
|
|
error=error.params[i] if error else None)
|
|
for (i, child_val) in enumerate(value)
|
|
]
|
|
|
|
return render_to_string('wagtailadmin/block_forms/list.html', {
|
|
'label': self.get_label(),
|
|
'prefix': prefix,
|
|
'list_members_html': list_members_html,
|
|
})
|
|
|
|
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
|
|
values_with_indexes.append(
|
|
(
|
|
data['%s-%d-order' % (prefix, i)],
|
|
self.child_block.value_from_datadict(data, files, '%s-%d-value' % (prefix, i))
|
|
)
|
|
)
|
|
|
|
values_with_indexes.sort()
|
|
return [v for (i, v) in values_with_indexes]
|
|
|
|
def clean(self, value):
|
|
result = []
|
|
errors = []
|
|
for child_val in value:
|
|
try:
|
|
result.append(self.child_block.clean(child_val))
|
|
except ValidationError as e:
|
|
errors.append(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 ListBlock', params=errors)
|
|
|
|
return result
|
|
|
|
def to_python(self, value):
|
|
# recursively call to_python on children and return as a list
|
|
return [
|
|
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
|
|
]
|
|
|
|
def render_basic(self, value):
|
|
children = format_html_join('\n', '<li>{0}</li>',
|
|
[(self.child_block.render(child_value),) for child_value in value]
|
|
)
|
|
return format_html("<ul>{0}</ul>", children)
|
|
|
|
|
|
# ===========
|
|
# 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):
|
|
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 = set(self.child_blocks.values())
|
|
|
|
def render_list_member(self, block_type_name, value, prefix, index, error=None):
|
|
"""
|
|
Render the HTML for a single list item. This consists of an <li> 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, error=error)
|
|
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', '<script type="text/template" id="{0}-newmember-{1}">{2}</script>',
|
|
[
|
|
(
|
|
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 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='', error=None):
|
|
list_members_html = [
|
|
self.render_list_member(child.block.name, child.value, "%s-%d" % (prefix, i), i,
|
|
error=error.params[i] if error else None)
|
|
for (i, child) in enumerate(value)
|
|
]
|
|
|
|
return render_to_string('wagtailadmin/block_forms/stream.html', {
|
|
'label': self.get_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)]
|
|
child_block = self.child_blocks[block_type_name]
|
|
|
|
values_with_indexes.append(
|
|
(
|
|
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.clean(child.value), child.block.name)
|
|
)
|
|
except ValidationError as e:
|
|
errors.append(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
|
|
])
|
|
|
|
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', '<div class="block-{1}">{0}</div>',
|
|
[(child, child.block_type) for child in value]
|
|
)
|
|
|
|
|
|
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)
|