diff --git a/wagtail/wagtailcore/blocks/__init__.py b/wagtail/wagtailcore/blocks/__init__.py index 47862b546..8f95e70d9 100644 --- a/wagtail/wagtailcore/blocks/__init__.py +++ b/wagtail/wagtailcore/blocks/__init__.py @@ -6,20 +6,15 @@ from __future__ import absolute_import, unicode_literals import collections from importlib import import_module -from django.core.exceptions import ValidationError, ImproperlyConfigured -from django.utils.html import format_html, format_html_join +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, python_2_unicode_compatible -from django.utils.functional import cached_property +from django.utils.encoding import force_text from django.template.loader import render_to_string from django import forms -from django.forms.utils import ErrorList import six -from .utils import js_dict - # ========================================= # Top-level superclasses and helper objects @@ -318,152 +313,6 @@ class BoundBlock(object): return self.block.render(self.value) -# =========== -# StructBlock -# =========== - -class BaseStructBlock(Block): - class Meta: - default = {} - template = "wagtailadmin/blocks/struct.html" - - def __init__(self, local_blocks=None, **kwargs): - self._constructor_kwargs = 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 = 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 forms.Media(js=['wagtailadmin/js/blocks/struct.js']) - - def render_form(self, value, prefix='', errors=None): - if errors: - if len(errors) > 1: - # We rely on StructBlock.clean throwing a single ValidationError with a specially crafted - # 'params' attribute that we can pull apart and distribute to the child blocks - raise TypeError('StructBlock.render_form unexpectedly received multiple errors') - error_dict = errors.as_data()[0].params - else: - error_dict = {} - - child_renderings = [ - block.render_form(value.get(name, block.meta.default), prefix="%s-%s" % (prefix, name), - errors=error_dict.get(name)) - for name, block in self.child_blocks.items() - ] - - list_items = format_html_join('\n', "
  • {0}
  • ", [ - [child_rendering] - for child_rendering in child_renderings - ]) - - - # Can these be rendered with a template? - if self.label: - return format_html('
    ', self.label, list_items) - else: - return format_html('
    ', 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] = ErrorList([e]) - - if errors: - # The message here is arbitrary - StructBlock.render_form will suppress it - # and delegate the errors contained in the 'params' dict to the child blocks instead - 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() - ]) - - def get_searchable_content(self, value): - content = [] - - for name, block in self.child_blocks.items(): - content.extend(block.get_searchable_content(value.get(name, block.meta.default))) - - return content - - def deconstruct(self): - """ - Always deconstruct StructBlock instances as if they were plain StructBlocks with all of the - field definitions passed to the constructor - even if in reality this is a subclass of StructBlock - 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.StructBlock' - args = [self.child_blocks.items()] - kwargs = self._constructor_kwargs - return (path, args, kwargs) - - -@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. @@ -500,9 +349,6 @@ class DeclarativeSubBlocksMetaclass(BaseBlock): return new_class -class StructBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStructBlock)): - pass - # ======================== # django.forms integration @@ -559,5 +405,6 @@ class BlockField(forms.Field): # Import block types defined in submodules into the wagtail.wagtailcore.blocks namespace from .field_block import * # NOQA +from .struct_block import * # NOQA from .list_block import * # NOQA from .stream_block import * # NOQA diff --git a/wagtail/wagtailcore/blocks/struct_block.py b/wagtail/wagtailcore/blocks/struct_block.py new file mode 100644 index 000000000..c39c8b75b --- /dev/null +++ b/wagtail/wagtailcore/blocks/struct_block.py @@ -0,0 +1,165 @@ +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.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property +from django.utils.html import format_html, format_html_join + +import six + +from wagtail.wagtailcore.blocks import Block, DeclarativeSubBlocksMetaclass + +from .utils import js_dict + + +__all__ = ['BaseStructBlock', 'StructBlock', 'StructValue'] + + +class BaseStructBlock(Block): + class Meta: + default = {} + template = "wagtailadmin/blocks/struct.html" + + def __init__(self, local_blocks=None, **kwargs): + self._constructor_kwargs = 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 = 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 forms.Media(js=['wagtailadmin/js/blocks/struct.js']) + + def render_form(self, value, prefix='', errors=None): + if errors: + if len(errors) > 1: + # We rely on StructBlock.clean throwing a single ValidationError with a specially crafted + # 'params' attribute that we can pull apart and distribute to the child blocks + raise TypeError('StructBlock.render_form unexpectedly received multiple errors') + error_dict = errors.as_data()[0].params + else: + error_dict = {} + + child_renderings = [ + block.render_form(value.get(name, block.meta.default), prefix="%s-%s" % (prefix, name), + errors=error_dict.get(name)) + for name, block in self.child_blocks.items() + ] + + list_items = format_html_join('\n', "
  • {0}
  • ", [ + [child_rendering] + for child_rendering in child_renderings + ]) + + + # Can these be rendered with a template? + if self.label: + return format_html('
    ', self.label, list_items) + else: + return format_html('
    ', 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] = ErrorList([e]) + + if errors: + # The message here is arbitrary - StructBlock.render_form will suppress it + # and delegate the errors contained in the 'params' dict to the child blocks instead + 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() + ]) + + def get_searchable_content(self, value): + content = [] + + for name, block in self.child_blocks.items(): + content.extend(block.get_searchable_content(value.get(name, block.meta.default))) + + return content + + def deconstruct(self): + """ + Always deconstruct StructBlock instances as if they were plain StructBlocks with all of the + field definitions passed to the constructor - even if in reality this is a subclass of StructBlock + 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.StructBlock' + args = [self.child_blocks.items()] + kwargs = self._constructor_kwargs + return (path, args, kwargs) + + +class StructBlock(six.with_metaclass(DeclarativeSubBlocksMetaclass, BaseStructBlock)): + pass + + +@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() + ])