import collections import itertools import json import re import warnings 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.coreutils import accepts_kwarg from wagtail.telepath import JSContext from wagtail.utils.deprecation import RemovedInWagtail70Warning __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 TEMPLATE_VAR = "value" 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 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(self.meta.default) 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. """ 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 on meta class. This extraction was added to make dynamic templates possible if you override this method value contains the current value of the block, allowing overridden methods to select the proper template based on the actual block value. """ 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. """ args = {"context": context} if accepts_kwarg(self.get_template, "value"): args["value"] = value else: warnings.warn( f"{self.__class__.__name__}.get_template should accept a 'value' argument as first argument", RemovedInWagtail70Warning, ) template = self.get_template(**args) 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_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