import collections import itertools import json import re from functools import lru_cache from importlib import import_module from django import forms from django.core import checks from django.core.exceptions import ImproperlyConfigured from django.template.loader import render_to_string from django.utils.encoding import force_str from django.utils.functional import cached_property from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.text import capfirst from wagtail.admin.staticfiles import versioned_static from wagtail.telepath import JSContext from wagtail.utils.templates import template_is_overridden __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().__new__(mcs, name, bases, attrs) # Get all the Meta classes from all the bases meta_class_bases = [meta_class] + [ getattr(base, "_meta_class", None) for base in bases ] meta_class_bases = tuple(filter(bool, meta_class_bases)) cls._meta_class = type(str(name + "Meta"), meta_class_bases, {}) return cls class Block(metaclass=BaseBlock): name = "" creation_counter = 0 definition_registry = {} TEMPLATE_VAR = "value" DEFAULT_PREVIEW_TEMPLATE = "wagtailcore/shared/block_preview.html" class Meta: label = None icon = "placeholder" classname = None group = "" # Attributes of Meta which can legally be modified after the block has been instantiated. # Used to implement __eq__. label is not included here, despite it technically being mutable via # set_name, since its value must originate from either the constructor arguments or set_name, # both of which are captured by the equality test, so checking label as well would be redundant. MUTABLE_META_ATTRIBUTES = [] 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().__new__(cls) obj._constructor_args = (args, kwargs) return obj def __init__(self, **kwargs): if "classname" in self._constructor_args[1]: # Adding this so that migrations are not triggered # when form_classname is used instead of classname # in the initialisation of the FieldBlock classname = self._constructor_args[1].pop("classname") self._constructor_args[1].setdefault("form_classname", classname) 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 Block.definition_registry[self.definition_prefix] = self self.label = self.meta.label or "" @classmethod def construct_from_lookup(cls, lookup, *args, **kwargs): """ See `wagtail.blocks.definition_lookup.BlockDefinitionLookup`. Construct a block instance from the provided arguments, using the given BlockDefinitionLookup object to perform any necessary lookups. """ # In the base implementation, no lookups take place - args / kwargs are passed # on to the constructor as-is return cls(*args, **kwargs) def set_name(self, name): self.name = name if not self.meta.label: self.label = capfirst(force_str(name).replace("_", " ")) def set_meta_options(self, opts): """ Update this block's meta options (out of the ones designated as mutable) from the given dict. Used by the StreamField constructor to pass on kwargs that are to be handled by the block, since the block object has already been created by that point, e.g.: body = StreamField(SomeStreamBlock(), max_num=5) """ for attr, value in opts.items(): if attr in self.MUTABLE_META_ATTRIBUTES: setattr(self.meta, attr, value) else: raise TypeError( "set_meta_options received unexpected option: %r" % attr ) def value_from_datadict(self, data, files, prefix): raise NotImplementedError("%s.value_from_datadict" % self.__class__) def value_omitted_from_data(self, data, files, name): """ Used only for top-level blocks wrapped by BlockWidget (i.e.: typically only StreamBlock) to inform ModelForm logic on Django >=1.10.2 whether the field is absent from the form submission (and should therefore revert to the field default). """ return name not in data 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 get_default(self): """ Return this block's default value (conventionally found in self.meta.default), converted to the value type expected by this block. This caters for the case where that value type is not something that can be expressed statically at model definition time (e.g. something like StructValue which incorporates a pointer back to the block definition object). """ return self.normalize(getattr(self.meta, "default", None)) 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; the ValidationError must therefore include all detail necessary to perform that rendering, such as identifying the specific child block(s) with errors, in the case of nested blocks. (It is suggested that you use the 'params' attribute for this; using error_list / error_dict is unreliable because Django tends to hack around with these when nested.) """ return value def normalize(self, value): """ Given a value for any acceptable type for this block (e.g. string or RichText for a RichTextBlock; dict or StructValue for a StructBlock), return a value of the block's native type (e.g. RichText for RichTextBlock, StructValue for StructBlock). In simple cases this will return the value unchanged. """ 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). For blocks that are usable at the top level of a StreamField, this must also accept any type accepted by normalize. (This is because Django calls `Field.to_python` from `Field.clean`.) """ return value def bulk_to_python(self, values): """ Apply the to_python conversion to a list of values. The default implementation simply iterates over the list; subclasses may optimise this, e.g. by combining database lookups into a single query. """ return [self.to_python(value) for value in values] def get_prep_value(self, value): """ The reverse of to_python; convert the python value into JSON-serialisable form. """ return value def get_form_state(self, value): """ Convert a python value for this block into a JSON-serialisable representation containing all the data needed to present the value in a form field, to be received by the block's client-side component. Examples of where this conversion is not trivial include rich text (where it needs to be supplied in a format that the editor can process, e.g. ContentState for Draftail) and page / image / document choosers (where it needs to include all displayed data for the selected item, such as title or thumbnail). """ return value def get_context(self, value, parent_context=None): """ Return a dict of context variables (derived from the block ``value`` and combined with the ``parent_context``) to be used as the template context when rendering this value through a template. See :ref:`the usage example ` for more details. """ context = parent_context or {} context.update( { "self": value, self.TEMPLATE_VAR: value, } ) return context def get_template(self, value=None, context=None): """ Return the template to use for rendering the block if specified. This method allows for dynamic templates based on the block instance and a given ``value``. See :ref:`the usage example ` for more details. """ return getattr(self.meta, "template", None) def render(self, value, context=None): """ Return a text rendering of 'value', suitable for display on templates. By default, this will use a template (with the passed context, supplemented by the result of get_context) if a 'template' property is specified on the block, and fall back on render_basic otherwise. """ template = self.get_template(value, context=context) if not template: return self.render_basic(value, context=context) if context is None: new_context = self.get_context(value) else: new_context = self.get_context(value, parent_context=dict(context)) return mark_safe(render_to_string(template, new_context)) def get_preview_context(self, value, parent_context=None): """ Return a dict of context variables to be used as the template context when rendering the block's preview. The ``value`` argument is the value returned by :meth:`get_preview_value`. The ``parent_context`` argument contains the following variables: - ``request``: The current request object. - ``block_def``: The block instance. - ``block_class``: The block class. - ``bound_block``: A ``BoundBlock`` instance representing the block and its value. If :ref:`the global preview template ` is used, the block will be rendered as the main content using ``{% include_block %}``, which in turn uses :meth:`get_context`. As a result, the context returned by this method will be available as the ``parent_context`` for ``get_context()`` when the preview is rendered. """ # NOTE: see StreamFieldBlockPreview.base_context for the context variables # that can be documented. return parent_context or {} def get_preview_template(self, value, context=None): """ Return the template to use for rendering the block's preview. The ``value`` argument is the value returned by :meth:`get_preview_value`. The ``context`` argument contains the variables listed for the ``parent_context`` argument of :meth:`get_preview_context` above (and not the context returned by that method itself). Note that the preview template is used to render a complete HTML page of the preview, not just an HTML fragment for the block. The method returns the ``preview_template`` attribute from the block's options if provided, and falls back to :ref:`the global preview template ` otherwise. If the global preview template is used, the block will be rendered as the main content using ``{% include_block %}``, which in turn uses :meth:`get_template`. """ return ( getattr(self.meta, "preview_template", None) or self.DEFAULT_PREVIEW_TEMPLATE ) def get_preview_value(self): """ Return the placeholder value that will be used for rendering the block's preview. By default, the value is the ``preview_value`` from the block's options if provided, otherwise the ``default`` is used as fallback. This method can be overridden to provide a dynamic preview value, such as from the database. """ if hasattr(self.meta, "preview_value"): return self.normalize(self.meta.preview_value) return self.get_default() @cached_property def _has_default(self): return getattr(self.meta, "default", None) is not None @cached_property def is_previewable(self): """ Determine whether the block is previewable in the block picker. By default, it automatically detects when a custom template is used or the :ref:`the global preview template ` is overridden and a preview value is provided. If the block is previewable by other means, override this property to return ``True``. To turn off previews for the block, set it to ``False``. """ has_specific_template = ( hasattr(self.meta, "preview_template") or self.__class__.get_preview_template is not Block.get_preview_template ) has_preview_value = ( hasattr(self.meta, "preview_value") or self._has_default or self.__class__.get_preview_context is not Block.get_preview_context or self.__class__.get_preview_value is not Block.get_preview_value ) has_global_template = template_is_overridden( self.DEFAULT_PREVIEW_TEMPLATE, "templates", ) return has_specific_template or (has_preview_value and has_global_template) def get_description(self): """ Return the description of the block to be shown to editors as part of the preview. For :ref:`field block types `, it will fall back to ``help_text`` if not provided. """ return getattr(self.meta, "description", "") def get_api_representation(self, value, context=None): """ Can be used to customise the API response and defaults to the value returned by get_prep_value. """ return self.get_prep_value(value) def render_basic(self, value, context=None): """ 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_str(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 extract_references(self, value): return [] def get_block_by_content_path(self, value, path_elements): """ Given a list of elements from a content path, retrieve the block at that path as a BoundBlock object, or None if the path does not correspond to a valid block. """ # In the base case, where a block has no concept of children, the only valid path is # the empty one (which refers to the current block). if path_elements: return None else: return self.bind(value) def check(self, **kwargs): """ Hook for the Django system checks framework - returns a list of django.core.checks.Error objects indicating validity errors in the block """ return [] def _check_name(self, **kwargs): """ Helper method called by container blocks as part of the system checks framework, to validate that this block's name is a valid identifier. (Not called universally, because not all blocks need names) """ errors = [] if not self.name: errors.append( checks.Error( "Block name %r is invalid" % self.name, hint="Block name cannot be empty", obj=kwargs.get("field", self), id="wagtailcore.E001", ) ) if " " in self.name: errors.append( checks.Error( "Block name %r is invalid" % self.name, hint="Block names cannot contain spaces", obj=kwargs.get("field", self), id="wagtailcore.E001", ) ) if "-" in self.name: errors.append( checks.Error( "Block name %r is invalid" % self.name, "Block names cannot contain dashes", obj=kwargs.get("field", self), id="wagtailcore.E001", ) ) if self.name and self.name[0].isdigit(): errors.append( checks.Error( "Block name %r is invalid" % self.name, "Block names cannot begin with a digit", obj=kwargs.get("field", self), id="wagtailcore.E001", ) ) if not errors and not re.match(r"^[_a-zA-Z][_a-zA-Z0-9]*$", self.name): errors.append( checks.Error( "Block name %r is invalid" % self.name, "Block names should follow standard Python conventions for " "variable names: alphanumeric and underscores, and cannot " "begin with a digit", obj=kwargs.get("field", self), id="wagtailcore.E001", ) ) return errors def id_for_label(self, prefix): """ Return the ID to be used as the 'for' attribute of