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