Initial commit
This commit is contained in:
7
env/lib/python3.10/site-packages/wagtail/blocks/__init__.py
vendored
Normal file
7
env/lib/python3.10/site-packages/wagtail/blocks/__init__.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Import block types defined in submodules into the wagtail.blocks namespace
|
||||
from .base import * # NOQA: F403
|
||||
from .field_block import * # NOQA: F403
|
||||
from .list_block import * # NOQA: F403
|
||||
from .static_block import * # NOQA: F403
|
||||
from .stream_block import * # NOQA: F403
|
||||
from .struct_block import * # NOQA: F403
|
||||
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/base.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/base.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/definition_lookup.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/definition_lookup.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/field_block.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/field_block.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/list_block.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/list_block.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/static_block.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/static_block.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/stream_block.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/stream_block.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/struct_block.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/__pycache__/struct_block.cpython-310.pyc
vendored
Normal file
Binary file not shown.
705
env/lib/python3.10/site-packages/wagtail/blocks/base.py
vendored
Normal file
705
env/lib/python3.10/site-packages/wagtail/blocks/base.py
vendored
Normal file
@@ -0,0 +1,705 @@
|
||||
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 <label> elements that refer to this block,
|
||||
when the given field prefix is in use. Return None if no 'for' attribute should be used.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
"""
|
||||
Flag used to determine whether labels for this block should display a 'required' asterisk.
|
||||
False by default, since Block does not provide any validation of its own - it's up to subclasses
|
||||
to define what required-ness means.
|
||||
"""
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def canonical_module_path(self):
|
||||
"""
|
||||
Return the module path string that should be used to refer to this block in migrations.
|
||||
"""
|
||||
# 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:
|
||||
return module.DECONSTRUCT_ALIASES[self.__class__]
|
||||
except (AttributeError, KeyError):
|
||||
return f"{module_name}.{name}"
|
||||
|
||||
def deconstruct(self):
|
||||
return (
|
||||
self.canonical_module_path,
|
||||
self._constructor_args[0],
|
||||
self._constructor_args[1],
|
||||
)
|
||||
|
||||
def deconstruct_with_lookup(self, lookup):
|
||||
"""
|
||||
Like `deconstruct`, but with a `wagtail.blocks.definition_lookup.BlockDefinitionLookupBuilder`
|
||||
object available so that any block instances within the definition can be added to the lookup
|
||||
table to obtain an ID (potentially shared with other matching block definitions, thus reducing
|
||||
the overall definition size) to be used in place of the block. The resulting deconstructed form
|
||||
returned here can then be restored into a block object using `Block.construct_from_lookup`.
|
||||
"""
|
||||
# In the base implementation, no substitutions happen, so we ignore the lookup and just call
|
||||
# deconstruct
|
||||
return self.deconstruct()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Implement equality on block objects so that two blocks with matching definitions are considered
|
||||
equal. Block objects are intended to be immutable with the exception of set_name() and any meta
|
||||
attributes identified in MUTABLE_META_ATTRIBUTES, so checking these along with the result of
|
||||
deconstruct (which captures the constructor arguments) is sufficient to identify (valid) differences.
|
||||
|
||||
This was implemented as a workaround for a Django <1.9 bug and is quite possibly not used by Wagtail
|
||||
any more, but has been retained as it provides a sensible definition of equality (and there's no
|
||||
reason to break it).
|
||||
"""
|
||||
|
||||
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.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()
|
||||
and all(
|
||||
getattr(self.meta, attr, None) == getattr(other.meta, attr, None)
|
||||
for attr in self.MUTABLE_META_ATTRIBUTES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class BoundBlock:
|
||||
def __init__(self, block, value, prefix=None, errors=None):
|
||||
self.block = block
|
||||
self.value = value
|
||||
self.prefix = prefix
|
||||
self.errors = errors
|
||||
|
||||
def render(self, context=None):
|
||||
return self.block.render(self.value, context=context)
|
||||
|
||||
def render_as_block(self, context=None):
|
||||
"""
|
||||
Alias for render; the include_block tag will specifically check for the presence of a method
|
||||
with this name. (This is because {% include_block %} is just as likely to be invoked on a bare
|
||||
value as a BoundBlock. If we looked for a `render` method instead, we'd run the risk of finding
|
||||
an unrelated method that just happened to have that name - for example, when called on a
|
||||
PageChooserBlock it could end up calling page.render.
|
||||
"""
|
||||
return self.block.render(self.value, context=context)
|
||||
|
||||
def id_for_label(self):
|
||||
return self.block.id_for_label(self.prefix)
|
||||
|
||||
def __str__(self):
|
||||
"""Render the value according to the block's native rendering"""
|
||||
return self.block.render(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "<block {}: {!r}>".format(
|
||||
self.block.name or type(self.block).__name__,
|
||||
self.value,
|
||||
)
|
||||
|
||||
|
||||
class DeclarativeSubBlocksMetaclass(BaseBlock):
|
||||
"""
|
||||
Metaclass that collects sub-blocks declared on the base classes.
|
||||
(cheerfully stolen from https://github.com/django/django/blob/main/django/forms/forms.py)
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
# Collect sub-blocks declared on the current class.
|
||||
# These are available on the class as `declared_blocks`
|
||||
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().__new__(mcs, name, bases, attrs)
|
||||
|
||||
# Walk through the MRO, collecting all inherited sub-blocks, to make
|
||||
# the combined `base_blocks`.
|
||||
base_blocks = collections.OrderedDict()
|
||||
for base in reversed(new_class.__mro__):
|
||||
# Collect sub-blocks from base class.
|
||||
if hasattr(base, "declared_blocks"):
|
||||
base_blocks.update(base.declared_blocks)
|
||||
|
||||
# Field shadowing.
|
||||
for attr, value in base.__dict__.items():
|
||||
if value is None and attr in base_blocks:
|
||||
base_blocks.pop(attr)
|
||||
new_class.base_blocks = base_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().__init__(attrs=attrs)
|
||||
self.block_def = block_def
|
||||
self._js_context = None
|
||||
self._block_json = None
|
||||
|
||||
def _build_block_json(self):
|
||||
try:
|
||||
self._js_context = JSContext()
|
||||
self._block_json = json.dumps(self._js_context.pack(self.block_def))
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise ValueError("Error while serializing block definition: %s" % e) from e
|
||||
|
||||
@property
|
||||
def js_context(self):
|
||||
if self._js_context is None:
|
||||
self._build_block_json()
|
||||
|
||||
return self._js_context
|
||||
|
||||
@property
|
||||
def block_json(self):
|
||||
if self._block_json is None:
|
||||
self._build_block_json()
|
||||
|
||||
return self._block_json
|
||||
|
||||
def id_for_label(self, prefix):
|
||||
# Delegate the job of choosing a label ID to the top-level block.
|
||||
# (In practice, the top-level block will typically be a StreamBlock, which returns None.)
|
||||
return self.block_def.id_for_label(prefix)
|
||||
|
||||
def render_with_errors(self, name, value, attrs=None, errors=None, renderer=None):
|
||||
value_json = json.dumps(self.block_def.get_form_state(value))
|
||||
|
||||
if errors:
|
||||
# errors is expected to be an ErrorList consisting of a single validation error
|
||||
error = errors.as_data()[0]
|
||||
error_json = json.dumps(get_error_json_data(error))
|
||||
else:
|
||||
error_json = json.dumps(None)
|
||||
|
||||
return format_html(
|
||||
"""
|
||||
<div id="{id}" data-block data-controller="w-block" data-w-block-data-value="{block_json}" data-w-block-arguments-value="[{value_json},{error_json}]"></div>
|
||||
""",
|
||||
id=name,
|
||||
block_json=self.block_json,
|
||||
value_json=value_json,
|
||||
error_json=error_json,
|
||||
)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
return self.render_with_errors(
|
||||
name, value, attrs=attrs, errors=None, renderer=renderer
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
return self.js_context.media + forms.Media(
|
||||
js=[
|
||||
# needed for initBlockWidget, although these will almost certainly be
|
||||
# pulled in by the block adapters too
|
||||
versioned_static("wagtailadmin/js/telepath/telepath.js"),
|
||||
versioned_static("wagtailadmin/js/telepath/blocks.js"),
|
||||
],
|
||||
css={
|
||||
"all": [
|
||||
versioned_static("wagtailadmin/css/panels/streamfield.css"),
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return self.block_def.value_from_datadict(data, files, name)
|
||||
|
||||
def value_omitted_from_data(self, data, files, name):
|
||||
return self.block_def.value_omitted_from_data(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().__init__(**kwargs)
|
||||
|
||||
def clean(self, value):
|
||||
return self.block.clean(value)
|
||||
|
||||
def has_changed(self, initial_value, data_value):
|
||||
return self.block.get_prep_value(initial_value) != self.block.get_prep_value(
|
||||
data_value
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_help_icon():
|
||||
return render_to_string(
|
||||
"wagtailadmin/shared/icon.html", {"name": "help", "classname": "default"}
|
||||
)
|
||||
|
||||
|
||||
def get_error_json_data(error):
|
||||
"""
|
||||
Translate a ValidationError instance raised against a block (which may potentially be a
|
||||
ValidationError subclass specialised for a particular block type) into a JSON-serialisable dict
|
||||
consisting of one or both of:
|
||||
messages: a list of error message strings to be displayed against the block
|
||||
blockErrors: a structure specific to the block type, containing further error objects in this
|
||||
format to be displayed against this block's children
|
||||
"""
|
||||
if hasattr(error, "as_json_data"):
|
||||
return error.as_json_data()
|
||||
else:
|
||||
return {"messages": error.messages}
|
||||
|
||||
|
||||
def get_error_list_json_data(error_list):
|
||||
"""
|
||||
Flatten an ErrorList instance containing any number of ValidationErrors
|
||||
(which may themselves contain multiple messages) into a list of error message strings.
|
||||
This does not consider any other properties of ValidationError other than `message`,
|
||||
so should not be used where ValidationError subclasses with nested block errors may be
|
||||
present.
|
||||
(In terms of StreamBlockValidationError et al: it's valid for use on non_block_errors
|
||||
but not block_errors)
|
||||
"""
|
||||
return list(itertools.chain(*(err.messages for err in error_list.as_data())))
|
||||
|
||||
|
||||
DECONSTRUCT_ALIASES = {
|
||||
Block: "wagtail.blocks.Block",
|
||||
}
|
||||
85
env/lib/python3.10/site-packages/wagtail/blocks/definition_lookup.py
vendored
Normal file
85
env/lib/python3.10/site-packages/wagtail/blocks/definition_lookup.py
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
from collections import defaultdict
|
||||
from importlib import import_module
|
||||
|
||||
|
||||
class BlockDefinitionLookup:
|
||||
"""
|
||||
A utility for constructing StreamField Block objects in migrations, starting from
|
||||
a compact representation that avoids repeating the same definition whenever a
|
||||
block is re-used in multiple places over the block definition tree.
|
||||
|
||||
The underlying data is a dict of block definitions, such as:
|
||||
```
|
||||
{
|
||||
0: ("wagtail.blocks.CharBlock", [], {"required": True}),
|
||||
1: ("wagtail.blocks.RichTextBlock", [], {}),
|
||||
2: ("wagtail.blocks.StreamBlock", [
|
||||
[
|
||||
("heading", 0),
|
||||
("paragraph", 1),
|
||||
],
|
||||
], {}),
|
||||
}
|
||||
```
|
||||
|
||||
where each definition is a tuple of (module_path, args, kwargs) similar to that
|
||||
returned by `deconstruct` - with the difference that any block objects appearing
|
||||
in args / kwargs may be substituted with an index into the lookup table that
|
||||
points to that block's definition. Any block class that wants to support such
|
||||
substitutions should implement a static/class method
|
||||
`construct_from_lookup(lookup, *args, **kwargs)`, where `lookup` is
|
||||
the `BlockDefinitionLookup` instance. The method should return a block instance
|
||||
constructed from the provided arguments (after performing any lookups).
|
||||
"""
|
||||
|
||||
def __init__(self, blocks):
|
||||
self.blocks = blocks
|
||||
self.block_classes = {}
|
||||
|
||||
def get_block(self, index):
|
||||
path, args, kwargs = self.blocks[index]
|
||||
try:
|
||||
cls = self.block_classes[path]
|
||||
except KeyError:
|
||||
module_name, class_name = path.rsplit(".", 1)
|
||||
module = import_module(module_name)
|
||||
cls = self.block_classes[path] = getattr(module, class_name)
|
||||
|
||||
return cls.construct_from_lookup(self, *args, **kwargs)
|
||||
|
||||
|
||||
class BlockDefinitionLookupBuilder:
|
||||
"""
|
||||
Helper for constructing the lookup data used by BlockDefinitionLookup
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.blocks = []
|
||||
|
||||
# Lookup table mapping the deconstructed tuple forms of blocks (as obtained from
|
||||
# `block.deconstruct_with_lookup`) to their index in the `blocks` list. These
|
||||
# tuples can be compared for equality, but not hashed, so we cannot use them as
|
||||
# dict keys; instead, we index them on the first tuple element (the module path)
|
||||
# and maintain a list of (index, deconstructed_tuple) pairs for each one.
|
||||
self.block_indexes_by_type = defaultdict(list)
|
||||
|
||||
def add_block(self, block):
|
||||
"""
|
||||
Add a block to the lookup table, returning an index that can be used to refer to it
|
||||
"""
|
||||
deconstructed = block.deconstruct_with_lookup(self)
|
||||
|
||||
# Check if we've already seen this block definition
|
||||
block_indexes = self.block_indexes_by_type[deconstructed[0]]
|
||||
for index, existing_deconstructed in block_indexes:
|
||||
if existing_deconstructed == deconstructed:
|
||||
return index
|
||||
|
||||
# If not, add it to the lookup table
|
||||
index = len(self.blocks)
|
||||
self.blocks.append(deconstructed)
|
||||
block_indexes.append((index, deconstructed))
|
||||
return index
|
||||
|
||||
def get_lookup_as_dict(self):
|
||||
return dict(enumerate(self.blocks))
|
||||
982
env/lib/python3.10/site-packages/wagtail/blocks/field_block.py
vendored
Normal file
982
env/lib/python3.10/site-packages/wagtail/blocks/field_block.py
vendored
Normal file
@@ -0,0 +1,982 @@
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Model
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
||||
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.translation import gettext as _
|
||||
|
||||
from wagtail.admin.staticfiles import versioned_static
|
||||
from wagtail.coreutils import camelcase_to_underscore, resolve_model_string
|
||||
from wagtail.rich_text import (
|
||||
RichText,
|
||||
RichTextMaxLengthValidator,
|
||||
extract_references_from_rich_text,
|
||||
get_text_for_indexing,
|
||||
)
|
||||
from wagtail.telepath import Adapter, register
|
||||
|
||||
from .base import Block
|
||||
|
||||
try:
|
||||
from django.utils.choices import CallableChoiceIterator
|
||||
except ImportError:
|
||||
# DJANGO_VERSION < 5.0
|
||||
from django.forms.fields import CallableChoiceIterator
|
||||
|
||||
|
||||
class FieldBlock(Block):
|
||||
"""A block that wraps a Django form field"""
|
||||
|
||||
def id_for_label(self, prefix):
|
||||
return self.field.widget.id_for_label(prefix)
|
||||
|
||||
def value_from_form(self, value):
|
||||
"""
|
||||
The value that we get back from the form field might not be the type
|
||||
that this block works with natively; for example, the block may want to
|
||||
wrap a simple value such as a string in an object that provides a fancy
|
||||
HTML rendering (e.g. EmbedBlock).
|
||||
|
||||
We therefore provide this method to perform any necessary conversion
|
||||
from the form field value to the block's native value. As standard,
|
||||
this returns the form field value unchanged.
|
||||
"""
|
||||
return value
|
||||
|
||||
def value_for_form(self, value):
|
||||
"""
|
||||
Reverse of value_from_form; convert a value of this block's native value type
|
||||
to one that can be rendered by the form field
|
||||
"""
|
||||
return value
|
||||
|
||||
def value_from_datadict(self, data, files, prefix):
|
||||
return self.value_from_form(
|
||||
self.field.widget.value_from_datadict(data, files, prefix)
|
||||
)
|
||||
|
||||
def value_omitted_from_data(self, data, files, prefix):
|
||||
return self.field.widget.value_omitted_from_data(data, files, prefix)
|
||||
|
||||
def clean(self, value):
|
||||
# We need an annoying value_for_form -> value_from_form round trip here to account for
|
||||
# the possibility that the form field is set up to validate a different value type to
|
||||
# the one this block works with natively
|
||||
return self.value_from_form(self.field.clean(self.value_for_form(value)))
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
# a FieldBlock is required if and only if its underlying form field is required
|
||||
return self.field.required
|
||||
|
||||
def get_form_state(self, value):
|
||||
return self.field.widget.format_value(
|
||||
self.field.prepare_value(self.value_for_form(value))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
default = None
|
||||
|
||||
|
||||
class FieldBlockAdapter(Adapter):
|
||||
js_constructor = "wagtail.blocks.FieldBlock"
|
||||
|
||||
def js_args(self, block):
|
||||
classname = [
|
||||
"w-field",
|
||||
f"w-field--{camelcase_to_underscore(block.field.__class__.__name__)}",
|
||||
f"w-field--{camelcase_to_underscore(block.field.widget.__class__.__name__)}",
|
||||
]
|
||||
|
||||
form_classname = getattr(block.meta, "form_classname", "")
|
||||
if form_classname:
|
||||
classname.append(form_classname)
|
||||
|
||||
# Provided for backwards compatibility. Replaced with 'form_classname'
|
||||
legacy_classname = getattr(block.meta, "classname", "")
|
||||
if legacy_classname:
|
||||
classname.append(legacy_classname)
|
||||
|
||||
meta = {
|
||||
"label": block.label,
|
||||
"required": block.required,
|
||||
"icon": block.meta.icon,
|
||||
"classname": " ".join(classname),
|
||||
"showAddCommentButton": getattr(
|
||||
block.field.widget, "show_add_comment_button", True
|
||||
),
|
||||
"strings": {"ADD_COMMENT": _("Add Comment")},
|
||||
}
|
||||
if block.field.help_text:
|
||||
meta["helpText"] = block.field.help_text
|
||||
|
||||
if hasattr(block, "max_length") and block.max_length is not None:
|
||||
meta["maxLength"] = block.max_length
|
||||
|
||||
return [
|
||||
block.name,
|
||||
block.field.widget,
|
||||
meta,
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
return forms.Media(
|
||||
js=[
|
||||
versioned_static("wagtailadmin/js/telepath/blocks.js"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
register(FieldBlockAdapter(), FieldBlock)
|
||||
|
||||
|
||||
class CharBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
max_length=None,
|
||||
min_length=None,
|
||||
validators=(),
|
||||
search_index=True,
|
||||
**kwargs,
|
||||
):
|
||||
# CharField's 'label' and 'initial' parameters are not exposed, as Block handles that functionality natively
|
||||
# (via 'label' and 'default')
|
||||
self.search_index = search_index
|
||||
self.field = forms.CharField(
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
max_length=max_length,
|
||||
min_length=min_length,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
return [force_str(value)] if self.search_index else []
|
||||
|
||||
|
||||
class TextBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
rows=1,
|
||||
max_length=None,
|
||||
min_length=None,
|
||||
search_index=True,
|
||||
validators=(),
|
||||
**kwargs,
|
||||
):
|
||||
self.field_options = {
|
||||
"required": required,
|
||||
"help_text": help_text,
|
||||
"max_length": max_length,
|
||||
"min_length": min_length,
|
||||
"validators": validators,
|
||||
}
|
||||
self.rows = rows
|
||||
self.search_index = search_index
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@cached_property
|
||||
def field(self):
|
||||
from wagtail.admin.widgets import AdminAutoHeightTextInput
|
||||
|
||||
field_kwargs = {"widget": AdminAutoHeightTextInput(attrs={"rows": self.rows})}
|
||||
field_kwargs.update(self.field_options)
|
||||
return forms.CharField(**field_kwargs)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
return [force_str(value)] if self.search_index else []
|
||||
|
||||
class Meta:
|
||||
icon = "pilcrow"
|
||||
|
||||
|
||||
class BlockQuoteBlock(TextBlock):
|
||||
def render_basic(self, value, context=None):
|
||||
if value:
|
||||
return format_html("<blockquote>{0}</blockquote>", value)
|
||||
else:
|
||||
return ""
|
||||
|
||||
class Meta:
|
||||
icon = "openquote"
|
||||
|
||||
|
||||
class FloatBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
max_value=None,
|
||||
min_value=None,
|
||||
validators=(),
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.field = forms.FloatField(
|
||||
required=required,
|
||||
max_value=max_value,
|
||||
min_value=min_value,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
icon = "decimal"
|
||||
|
||||
|
||||
class DecimalBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
max_value=None,
|
||||
min_value=None,
|
||||
max_digits=None,
|
||||
decimal_places=None,
|
||||
validators=(),
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.field = forms.DecimalField(
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
max_value=max_value,
|
||||
min_value=min_value,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
else:
|
||||
return Decimal(value)
|
||||
|
||||
class Meta:
|
||||
icon = "decimal"
|
||||
|
||||
|
||||
class RegexBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
regex,
|
||||
required=True,
|
||||
help_text=None,
|
||||
max_length=None,
|
||||
min_length=None,
|
||||
error_messages=None,
|
||||
validators=(),
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.field = forms.RegexField(
|
||||
regex=regex,
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
max_length=max_length,
|
||||
min_length=min_length,
|
||||
error_messages=error_messages,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
icon = "regex"
|
||||
|
||||
|
||||
class URLBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
max_length=None,
|
||||
min_length=None,
|
||||
validators=(),
|
||||
**kwargs,
|
||||
):
|
||||
self.field = forms.URLField(
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
max_length=max_length,
|
||||
min_length=min_length,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
class Meta:
|
||||
icon = "link-external"
|
||||
|
||||
|
||||
class BooleanBlock(FieldBlock):
|
||||
def __init__(self, required=True, help_text=None, **kwargs):
|
||||
# NOTE: As with forms.BooleanField, the default of required=True means that the checkbox
|
||||
# must be ticked to pass validation (i.e. it's equivalent to an "I agree to the terms and
|
||||
# conditions" box). To get the conventional yes/no behaviour, you must explicitly pass
|
||||
# required=False.
|
||||
self.field = forms.BooleanField(required=required, help_text=help_text)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_form_state(self, value):
|
||||
# Bypass widget.format_value, because CheckboxInput uses that to prepare the "value"
|
||||
# attribute (as distinct from the "checked" attribute that represents the actual checkbox
|
||||
# state, which it handles in get_context).
|
||||
return bool(value)
|
||||
|
||||
class Meta:
|
||||
icon = "tick-inverse"
|
||||
|
||||
|
||||
class DateBlock(FieldBlock):
|
||||
def __init__(
|
||||
self, required=True, help_text=None, format=None, validators=(), **kwargs
|
||||
):
|
||||
self.field_options = {
|
||||
"required": required,
|
||||
"help_text": help_text,
|
||||
"validators": validators,
|
||||
}
|
||||
try:
|
||||
self.field_options["input_formats"] = kwargs.pop("input_formats")
|
||||
except KeyError:
|
||||
pass
|
||||
self.format = format
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@cached_property
|
||||
def field(self):
|
||||
from wagtail.admin.widgets import AdminDateInput
|
||||
|
||||
field_kwargs = {
|
||||
"widget": AdminDateInput(format=self.format),
|
||||
}
|
||||
field_kwargs.update(self.field_options)
|
||||
return forms.DateField(**field_kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
# Serialising to JSON uses DjangoJSONEncoder, which converts date/time objects to strings.
|
||||
# The reverse does not happen on decoding, because there's no way to know which strings
|
||||
# should be decoded; we have to convert strings back to dates here instead.
|
||||
if value is None or isinstance(value, datetime.date):
|
||||
return value
|
||||
else:
|
||||
return parse_date(value)
|
||||
|
||||
class Meta:
|
||||
icon = "date"
|
||||
|
||||
|
||||
class TimeBlock(FieldBlock):
|
||||
def __init__(
|
||||
self, required=True, help_text=None, format=None, validators=(), **kwargs
|
||||
):
|
||||
self.field_options = {
|
||||
"required": required,
|
||||
"help_text": help_text,
|
||||
"validators": validators,
|
||||
}
|
||||
self.format = format
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@cached_property
|
||||
def field(self):
|
||||
from wagtail.admin.widgets import AdminTimeInput
|
||||
|
||||
field_kwargs = {"widget": AdminTimeInput(format=self.format)}
|
||||
field_kwargs.update(self.field_options)
|
||||
return forms.TimeField(**field_kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, datetime.time):
|
||||
return value
|
||||
else:
|
||||
return parse_time(value)
|
||||
|
||||
class Meta:
|
||||
icon = "time"
|
||||
|
||||
|
||||
class DateTimeBlock(FieldBlock):
|
||||
def __init__(
|
||||
self, required=True, help_text=None, format=None, validators=(), **kwargs
|
||||
):
|
||||
self.field_options = {
|
||||
"required": required,
|
||||
"help_text": help_text,
|
||||
"validators": validators,
|
||||
}
|
||||
self.format = format
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@cached_property
|
||||
def field(self):
|
||||
from wagtail.admin.widgets import AdminDateTimeInput
|
||||
|
||||
field_kwargs = {
|
||||
"widget": AdminDateTimeInput(format=self.format),
|
||||
}
|
||||
field_kwargs.update(self.field_options)
|
||||
return forms.DateTimeField(**field_kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None or isinstance(value, datetime.datetime):
|
||||
return value
|
||||
else:
|
||||
return parse_datetime(value)
|
||||
|
||||
class Meta:
|
||||
icon = "date"
|
||||
|
||||
|
||||
class EmailBlock(FieldBlock):
|
||||
def __init__(self, required=True, help_text=None, validators=(), **kwargs):
|
||||
self.field = forms.EmailField(
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
class Meta:
|
||||
icon = "mail"
|
||||
|
||||
|
||||
class IntegerBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
min_value=None,
|
||||
max_value=None,
|
||||
validators=(),
|
||||
**kwargs,
|
||||
):
|
||||
self.field = forms.IntegerField(
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
validators=validators,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
class Meta:
|
||||
icon = "placeholder"
|
||||
|
||||
|
||||
class BaseChoiceBlock(FieldBlock):
|
||||
choices = ()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
choices=None,
|
||||
default=None,
|
||||
required=True,
|
||||
help_text=None,
|
||||
search_index=True,
|
||||
widget=None,
|
||||
validators=(),
|
||||
**kwargs,
|
||||
):
|
||||
self._required = required
|
||||
self._default = default
|
||||
self.search_index = search_index
|
||||
|
||||
if choices is None:
|
||||
# no choices specified, so pick up the choice defined at the class level
|
||||
choices = self.choices
|
||||
|
||||
if callable(choices):
|
||||
# Support of callable choices. Wrap the callable in an iterator so that we can
|
||||
# handle this consistently with ordinary choice lists;
|
||||
# however, the `choices` constructor kwarg as reported by deconstruct() should
|
||||
# remain as the callable
|
||||
choices_for_constructor = choices
|
||||
choices = CallableChoiceIterator(choices)
|
||||
else:
|
||||
# Cast as a list
|
||||
choices_for_constructor = choices = list(choices)
|
||||
|
||||
# keep a copy of all kwargs (including our normalised choices list) for deconstruct()
|
||||
# Note: we omit the `widget` kwarg, as widgets do not provide a serialization method
|
||||
# for migrations, and they are unlikely to be useful within the frozen ORM anyhow
|
||||
self._constructor_kwargs = kwargs.copy()
|
||||
self._constructor_kwargs["choices"] = choices_for_constructor
|
||||
if required is not True:
|
||||
self._constructor_kwargs["required"] = required
|
||||
if help_text is not None:
|
||||
self._constructor_kwargs["help_text"] = help_text
|
||||
|
||||
# We will need to modify the choices list to insert a blank option, if there isn't
|
||||
# one already. We have to do this at render time in the case of callable choices - so rather
|
||||
# than having separate code paths for static vs dynamic lists, we'll _always_ pass a callable
|
||||
# to ChoiceField to perform this step at render time.
|
||||
|
||||
callable_choices = self._get_callable_choices(choices)
|
||||
self.field = self.get_field(
|
||||
choices=callable_choices,
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
validators=validators,
|
||||
widget=widget,
|
||||
)
|
||||
super().__init__(default=default, **kwargs)
|
||||
|
||||
def _get_callable_choices(self, choices, blank_choice=True):
|
||||
"""
|
||||
Return a callable that we can pass into `forms.ChoiceField`, which will provide the
|
||||
choices list with the addition of a blank choice (if blank_choice=True and one does not
|
||||
already exist).
|
||||
"""
|
||||
|
||||
def choices_callable():
|
||||
# Variable choices could be an instance of CallableChoiceIterator, which may be wrapping
|
||||
# something we don't want to evaluate multiple times (e.g. a database query). Cast as a
|
||||
# list now to prevent it getting evaluated twice (once while searching for a blank choice,
|
||||
# once while rendering the final ChoiceField).
|
||||
local_choices = list(choices)
|
||||
|
||||
# If blank_choice=False has been specified, return the choices list as is
|
||||
if not blank_choice:
|
||||
return local_choices
|
||||
|
||||
# Else: if choices does not already contain a blank option, insert one
|
||||
# (to match Django's own behaviour for modelfields:
|
||||
# https://github.com/django/django/blob/1.7.5/django/db/models/fields/__init__.py#L732-744)
|
||||
has_blank_choice = False
|
||||
for v1, v2 in local_choices:
|
||||
if isinstance(v2, (list, tuple)):
|
||||
# this is a named group, and v2 is the value list
|
||||
has_blank_choice = any(value in ("", None) for value, label in v2)
|
||||
if has_blank_choice:
|
||||
break
|
||||
else:
|
||||
# this is an individual choice; v1 is the value
|
||||
if v1 in ("", None):
|
||||
has_blank_choice = True
|
||||
break
|
||||
|
||||
if not has_blank_choice:
|
||||
return BLANK_CHOICE_DASH + local_choices
|
||||
|
||||
return local_choices
|
||||
|
||||
return choices_callable
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
|
||||
|
||||
class ChoiceBlock(BaseChoiceBlock):
|
||||
def get_field(self, **kwargs):
|
||||
return forms.ChoiceField(**kwargs)
|
||||
|
||||
def _get_callable_choices(self, choices, blank_choice=None):
|
||||
# If we have a default choice and the field is required, we don't need to add a blank option.
|
||||
if blank_choice is None:
|
||||
blank_choice = not (self._default and self._required)
|
||||
return super()._get_callable_choices(choices, blank_choice=blank_choice)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Always deconstruct ChoiceBlock instances as if they were plain ChoiceBlocks with their
|
||||
choice list passed in the constructor, even if they are actually subclasses. This allows
|
||||
users to define subclasses of ChoiceBlock in their models.py, with specific choice lists
|
||||
passed in, without references to those classes ending up frozen into migrations.
|
||||
"""
|
||||
return ("wagtail.blocks.ChoiceBlock", [], self._constructor_kwargs)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
# Return the display value as the searchable value
|
||||
if not self.search_index:
|
||||
return []
|
||||
text_value = force_str(value)
|
||||
for k, v in self.field.choices:
|
||||
if isinstance(v, (list, tuple)):
|
||||
# This is an optgroup, so look inside the group for options
|
||||
for k2, v2 in v:
|
||||
if value == k2 or text_value == force_str(k2):
|
||||
return [force_str(k), force_str(v2)]
|
||||
else:
|
||||
if value == k or text_value == force_str(k):
|
||||
return [force_str(v)]
|
||||
return [] # Value was not found in the list of choices
|
||||
|
||||
|
||||
class MultipleChoiceBlock(BaseChoiceBlock):
|
||||
def get_field(self, **kwargs):
|
||||
return forms.MultipleChoiceField(**kwargs)
|
||||
|
||||
def _get_callable_choices(self, choices, blank_choice=False):
|
||||
"""Override to default blank choice to False"""
|
||||
return super()._get_callable_choices(choices, blank_choice=blank_choice)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Always deconstruct MultipleChoiceBlock instances as if they were plain
|
||||
MultipleChoiceBlocks with their choice list passed in the constructor,
|
||||
even if they are actually subclasses. This allows users to define
|
||||
subclasses of MultipleChoiceBlock in their models.py, with specific choice
|
||||
lists passed in, without references to those classes ending up frozen
|
||||
into migrations.
|
||||
"""
|
||||
return ("wagtail.blocks.MultipleChoiceBlock", [], self._constructor_kwargs)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
# Return the display value as the searchable value
|
||||
if not self.search_index:
|
||||
return []
|
||||
content = []
|
||||
text_value = force_str(value)
|
||||
for k, v in self.field.choices:
|
||||
if isinstance(v, (list, tuple)):
|
||||
# This is an optgroup, so look inside the group for options
|
||||
for k2, v2 in v:
|
||||
if value == k2 or text_value == force_str(k2):
|
||||
content.append(force_str(k))
|
||||
content.append(force_str(v2))
|
||||
else:
|
||||
if value == k or text_value == force_str(k):
|
||||
content.append(force_str(v))
|
||||
return content
|
||||
|
||||
|
||||
class RichTextBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
editor="default",
|
||||
features=None,
|
||||
max_length=None,
|
||||
validators=(),
|
||||
search_index=True,
|
||||
**kwargs,
|
||||
):
|
||||
if max_length is not None:
|
||||
validators = list(validators) + [
|
||||
RichTextMaxLengthValidator(max_length),
|
||||
]
|
||||
self.field_options = {
|
||||
"required": required,
|
||||
"help_text": help_text,
|
||||
"validators": validators,
|
||||
}
|
||||
self.max_length = max_length
|
||||
self.editor = editor
|
||||
self.features = features
|
||||
self.search_index = search_index
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
# convert a source-HTML string from the JSONish representation
|
||||
# to a RichText object
|
||||
return RichText(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
# convert a RichText object back to a source-HTML string to go into
|
||||
# the JSONish representation
|
||||
return value.source
|
||||
|
||||
def normalize(self, value):
|
||||
if isinstance(value, RichText):
|
||||
return value
|
||||
return RichText(value)
|
||||
|
||||
@cached_property
|
||||
def field(self):
|
||||
from wagtail.admin.rich_text import get_rich_text_editor_widget
|
||||
|
||||
return forms.CharField(
|
||||
widget=get_rich_text_editor_widget(self.editor, features=self.features),
|
||||
**self.field_options,
|
||||
)
|
||||
|
||||
def value_for_form(self, value):
|
||||
# Rich text editors take the source-HTML string as input (and takes care
|
||||
# of expanding it for the purposes of the editor)
|
||||
return value.source
|
||||
|
||||
def value_from_form(self, value):
|
||||
# Rich text editors return a source-HTML string; convert to a RichText object
|
||||
return RichText(value)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
if not self.search_index:
|
||||
return []
|
||||
source = force_str(value.source)
|
||||
# Strip HTML tags to prevent search backend from indexing them
|
||||
return [get_text_for_indexing(source)]
|
||||
|
||||
def extract_references(self, value):
|
||||
# Extracts any references to images/pages/embeds
|
||||
yield from extract_references_from_rich_text(force_str(value.source))
|
||||
|
||||
class Meta:
|
||||
icon = "pilcrow"
|
||||
|
||||
|
||||
class RawHTMLBlock(FieldBlock):
|
||||
def __init__(
|
||||
self,
|
||||
required=True,
|
||||
help_text=None,
|
||||
max_length=None,
|
||||
min_length=None,
|
||||
validators=(),
|
||||
**kwargs,
|
||||
):
|
||||
self.field = forms.CharField(
|
||||
required=required,
|
||||
help_text=help_text,
|
||||
max_length=max_length,
|
||||
min_length=min_length,
|
||||
validators=validators,
|
||||
widget=forms.Textarea,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_default(self):
|
||||
return self.normalize(self.meta.default or "")
|
||||
|
||||
def to_python(self, value):
|
||||
return mark_safe(value)
|
||||
|
||||
def normalize(self, value):
|
||||
return mark_safe(value)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
# explicitly convert to a plain string, just in case we're using some serialisation method
|
||||
# that doesn't cope with SafeString values correctly
|
||||
return str(value) + ""
|
||||
|
||||
def value_for_form(self, value):
|
||||
# need to explicitly mark as unsafe, or it'll output unescaped HTML in the textarea
|
||||
return str(value) + ""
|
||||
|
||||
def value_from_form(self, value):
|
||||
return mark_safe(value)
|
||||
|
||||
class Meta:
|
||||
icon = "code"
|
||||
|
||||
|
||||
class ChooserBlock(FieldBlock):
|
||||
def __init__(self, required=True, help_text=None, validators=(), **kwargs):
|
||||
self._required = required
|
||||
self._help_text = help_text
|
||||
self._validators = validators
|
||||
super().__init__(**kwargs)
|
||||
|
||||
"""Abstract superclass for fields that implement a chooser interface (page, image, snippet etc)"""
|
||||
|
||||
@cached_property
|
||||
def model_class(self):
|
||||
return resolve_model_string(self.target_model)
|
||||
|
||||
@cached_property
|
||||
def field(self):
|
||||
return forms.ModelChoiceField(
|
||||
queryset=self.model_class.objects.all(),
|
||||
widget=self.widget,
|
||||
required=self._required,
|
||||
validators=self._validators,
|
||||
help_text=self._help_text,
|
||||
)
|
||||
|
||||
def to_python(self, value):
|
||||
# the incoming serialised value should be None or an ID
|
||||
if value is None:
|
||||
return value
|
||||
else:
|
||||
try:
|
||||
return self.model_class.objects.get(pk=value)
|
||||
except self.model_class.DoesNotExist:
|
||||
return None
|
||||
|
||||
def bulk_to_python(self, values):
|
||||
"""Return the model instances for the given list of primary keys.
|
||||
|
||||
The instances must be returned in the same order as the values and keep None values.
|
||||
"""
|
||||
objects = self.model_class.objects.in_bulk(values)
|
||||
return [
|
||||
objects.get(id) for id in values
|
||||
] # Keeps the ordering the same as in values.
|
||||
|
||||
def get_prep_value(self, value):
|
||||
# the native value (a model instance or None) should serialise to a PK or None
|
||||
if value is None:
|
||||
return None
|
||||
else:
|
||||
return value.pk
|
||||
|
||||
def value_from_form(self, value):
|
||||
# ModelChoiceField sometimes returns an ID, and sometimes an instance; we want the instance
|
||||
if value is None or isinstance(value, self.model_class):
|
||||
return value
|
||||
else:
|
||||
try:
|
||||
return self.model_class.objects.get(pk=value)
|
||||
except self.model_class.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_form_state(self, value):
|
||||
return self.widget.get_value_data(value)
|
||||
|
||||
def clean(self, value):
|
||||
# ChooserBlock works natively with model instances as its 'value' type (because that's what you
|
||||
# want to work with when doing front-end templating), but ModelChoiceField.clean expects an ID
|
||||
# as the input value (and returns a model instance as the result). We don't want to bypass
|
||||
# ModelChoiceField.clean entirely (it might be doing relevant validation, such as checking page
|
||||
# type) so we convert our instance back to an ID here. It means we have a wasted round-trip to
|
||||
# the database when ModelChoiceField.clean promptly does its own lookup, but there's no easy way
|
||||
# around that...
|
||||
if isinstance(value, self.model_class):
|
||||
value = value.pk
|
||||
return super().clean(value)
|
||||
|
||||
def extract_references(self, value):
|
||||
if value is not None and issubclass(self.model_class, Model):
|
||||
yield self.model_class, str(value.pk), "", ""
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
|
||||
|
||||
class PageChooserBlock(ChooserBlock):
|
||||
def __init__(
|
||||
self, page_type=None, can_choose_root=False, target_model=None, **kwargs
|
||||
):
|
||||
# We cannot simply deprecate 'target_model' in favour of 'page_type'
|
||||
# as it would force developers to update their old migrations.
|
||||
# Mapping the old 'target_model' to the new 'page_type' kwarg instead.
|
||||
if target_model:
|
||||
page_type = target_model
|
||||
|
||||
if page_type:
|
||||
# Convert single string/model into a list
|
||||
if not isinstance(page_type, (list, tuple)):
|
||||
page_type = [page_type]
|
||||
else:
|
||||
page_type = []
|
||||
|
||||
self.page_type = page_type
|
||||
self.can_choose_root = can_choose_root
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@cached_property
|
||||
def target_model(self):
|
||||
"""
|
||||
Defines the model used by the base ChooserBlock for ID <-> instance
|
||||
conversions. If a single page type is specified in target_model,
|
||||
we can use that to get the more specific instance "for free"; otherwise
|
||||
use the generic Page model.
|
||||
"""
|
||||
if len(self.target_models) == 1:
|
||||
return self.target_models[0]
|
||||
|
||||
return resolve_model_string("wagtailcore.Page")
|
||||
|
||||
@cached_property
|
||||
def target_models(self):
|
||||
target_models = []
|
||||
|
||||
for target_model in self.page_type:
|
||||
target_models.append(resolve_model_string(target_model))
|
||||
|
||||
return target_models
|
||||
|
||||
@cached_property
|
||||
def widget(self):
|
||||
from wagtail.admin.widgets import AdminPageChooser
|
||||
|
||||
return AdminPageChooser(
|
||||
target_models=self.target_models, can_choose_root=self.can_choose_root
|
||||
)
|
||||
|
||||
def get_form_state(self, value):
|
||||
value_data = self.widget.get_value_data(value)
|
||||
if value_data is None:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"id": value_data["id"],
|
||||
"parentId": value_data["parent_id"],
|
||||
"adminTitle": value_data["display_title"],
|
||||
"editUrl": value_data["edit_url"],
|
||||
}
|
||||
|
||||
def render_basic(self, value, context=None):
|
||||
if value:
|
||||
return format_html('<a href="{0}">{1}</a>', value.url, value.title)
|
||||
else:
|
||||
return ""
|
||||
|
||||
def deconstruct(self):
|
||||
name, args, kwargs = super().deconstruct()
|
||||
|
||||
if "target_model" in kwargs or "page_type" in kwargs:
|
||||
target_models = []
|
||||
|
||||
for target_model in self.target_models:
|
||||
opts = target_model._meta
|
||||
target_models.append(f"{opts.app_label}.{opts.object_name}")
|
||||
|
||||
kwargs.pop("target_model", None)
|
||||
kwargs["page_type"] = target_models
|
||||
|
||||
return name, args, kwargs
|
||||
|
||||
class Meta:
|
||||
icon = "doc-empty-inverse"
|
||||
|
||||
|
||||
# Ensure that the blocks defined here get deconstructed as wagtailcore.blocks.FooBlock
|
||||
# rather than wagtailcore.blocks.field.FooBlock
|
||||
block_classes = [
|
||||
FieldBlock,
|
||||
CharBlock,
|
||||
URLBlock,
|
||||
RichTextBlock,
|
||||
RawHTMLBlock,
|
||||
ChooserBlock,
|
||||
PageChooserBlock,
|
||||
TextBlock,
|
||||
BooleanBlock,
|
||||
DateBlock,
|
||||
TimeBlock,
|
||||
DateTimeBlock,
|
||||
ChoiceBlock,
|
||||
MultipleChoiceBlock,
|
||||
EmailBlock,
|
||||
IntegerBlock,
|
||||
FloatBlock,
|
||||
DecimalBlock,
|
||||
RegexBlock,
|
||||
BlockQuoteBlock,
|
||||
]
|
||||
DECONSTRUCT_ALIASES = {cls: "wagtail.blocks.%s" % cls.__name__ for cls in block_classes}
|
||||
__all__ = [cls.__name__ for cls in block_classes]
|
||||
495
env/lib/python3.10/site-packages/wagtail/blocks/list_block.py
vendored
Normal file
495
env/lib/python3.10/site-packages/wagtail/blocks/list_block.py
vendored
Normal file
@@ -0,0 +1,495 @@
|
||||
import uuid
|
||||
from collections.abc import Mapping, MutableSequence
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms.utils import ErrorList
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin.staticfiles import versioned_static
|
||||
from wagtail.telepath import Adapter, register
|
||||
|
||||
from .base import (
|
||||
Block,
|
||||
BoundBlock,
|
||||
get_error_json_data,
|
||||
get_error_list_json_data,
|
||||
get_help_icon,
|
||||
)
|
||||
|
||||
__all__ = ["ListBlock", "ListBlockValidationError"]
|
||||
|
||||
|
||||
class ListBlockValidationError(ValidationError):
|
||||
def __init__(self, block_errors=None, non_block_errors=None):
|
||||
# non_block_errors may be passed here as an ErrorList, a plain list (of strings or
|
||||
# ValidationErrors), or None.
|
||||
# Normalise it to be an ErrorList, which provides an as_data() method that consistently
|
||||
# returns a flat list of ValidationError objects.
|
||||
# (note: iterating over ErrorList itself appears to give a list of message strings,
|
||||
# but doesn't correctly account for ValidationErrors containing multiple messages)
|
||||
# (note 2: items in this list are expected to be plain ValidationError instances; there is
|
||||
# no special treatment of subclasses such as StructBlockValidationError)
|
||||
self.non_block_errors = ErrorList(non_block_errors)
|
||||
|
||||
# block_errors may be passed here as a dict whose keys are the indexes of the child blocks
|
||||
# with errors, or a list (corresponding to the block value's elements, with None for child
|
||||
# blocks with no errors)
|
||||
# Items in this list / dict may be:
|
||||
# - a ValidationError instance (potentially a subclass such as StructBlockValidationError)
|
||||
# - an ErrorList containing a single ValidationError
|
||||
# - a plain list containing a single ValidationError
|
||||
# All representations will be normalised to a dict of ValidationError instances,
|
||||
# which is also the preferred format for the original argument to be in.
|
||||
|
||||
# normalise to a dict
|
||||
if block_errors is None:
|
||||
block_errors_dict = {}
|
||||
elif isinstance(block_errors, Mapping):
|
||||
block_errors_dict = block_errors
|
||||
elif isinstance(block_errors, list):
|
||||
block_errors_dict = {
|
||||
index: val for index, val in enumerate(block_errors) if val is not None
|
||||
}
|
||||
else:
|
||||
raise ValueError(
|
||||
"Expected dict or list for block_errors, got %r" % block_errors
|
||||
)
|
||||
|
||||
# normalise items to ValidationError instances
|
||||
self.block_errors = {}
|
||||
for index, val in block_errors_dict.items():
|
||||
if isinstance(val, ErrorList):
|
||||
self.block_errors[index] = val.as_data()[0]
|
||||
elif isinstance(val, list):
|
||||
self.block_errors[index] = val[0]
|
||||
else:
|
||||
self.block_errors[index] = val
|
||||
|
||||
super().__init__("Validation error in ListBlock")
|
||||
|
||||
def as_json_data(self):
|
||||
result = {}
|
||||
if self.non_block_errors:
|
||||
result["messages"] = get_error_list_json_data(self.non_block_errors)
|
||||
if self.block_errors:
|
||||
result["blockErrors"] = {
|
||||
index: get_error_json_data(error)
|
||||
for index, error in self.block_errors.items()
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ListValue(MutableSequence):
|
||||
"""
|
||||
The native data type used by ListBlock. Behaves as a list of values, but also provides
|
||||
a bound_blocks property giving access to block IDs
|
||||
"""
|
||||
|
||||
class ListChild(BoundBlock):
|
||||
# a wrapper for list values that keeps track of the associated block type and ID
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.original_id = kwargs.pop("id", None)
|
||||
self.id = self.original_id or str(uuid.uuid4())
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_prep_value(self):
|
||||
return {
|
||||
"type": "item",
|
||||
"value": self.block.get_prep_value(self.value),
|
||||
"id": self.id,
|
||||
}
|
||||
|
||||
def __init__(self, list_block, values=None, bound_blocks=None):
|
||||
self.list_block = list_block
|
||||
|
||||
if bound_blocks is not None:
|
||||
self.bound_blocks = bound_blocks
|
||||
elif values is not None:
|
||||
self.bound_blocks = [
|
||||
ListValue.ListChild(self.list_block.child_block, value)
|
||||
for value in values
|
||||
]
|
||||
else:
|
||||
self.bound_blocks = []
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.bound_blocks[i].value
|
||||
|
||||
def __setitem__(self, i, item):
|
||||
self.bound_blocks[i] = ListValue.ListChild(self.list_block.child_block, item)
|
||||
|
||||
def __delitem__(self, i):
|
||||
del self.bound_blocks[i]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.bound_blocks)
|
||||
|
||||
def insert(self, i, item):
|
||||
self.bound_blocks.insert(
|
||||
i, ListValue.ListChild(self.list_block.child_block, item)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ListValue: {[bb.value for bb in self.bound_blocks]!r}>"
|
||||
|
||||
|
||||
class ListBlock(Block):
|
||||
def __init__(self, child_block, search_index=True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.search_index = search_index
|
||||
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
|
||||
|
||||
if not hasattr(self.meta, "default"):
|
||||
# Default to a list consisting of one empty (i.e. default-valued) child item
|
||||
self.meta.default = [self.child_block.get_default()]
|
||||
|
||||
# If a subclass of ListBlock overrides __init__, we cannot assume that the first argument is
|
||||
# the child block, and thus we cannot rely on the conversion applied in construct_from_lookup /
|
||||
# deconstruct_with_lookup to be valid. We set a flag attribute on the __init__ method so that
|
||||
# we can spot this case.
|
||||
__init__.has_child_block_arg = True
|
||||
|
||||
@classmethod
|
||||
def construct_from_lookup(cls, lookup, *args, **kwargs):
|
||||
if getattr(cls.__init__, "has_child_block_arg", False):
|
||||
if args and isinstance(args[0], int):
|
||||
child_block = lookup.get_block(args[0])
|
||||
args = (child_block, *args[1:])
|
||||
else:
|
||||
child_block_kwarg = kwargs.get("child_block")
|
||||
if isinstance(child_block_kwarg, int):
|
||||
child_block = lookup.get_block(child_block_kwarg)
|
||||
kwargs["child_block"] = child_block
|
||||
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
def value_from_datadict(self, data, files, prefix):
|
||||
count = int(data["%s-count" % prefix])
|
||||
child_blocks_with_indexes = []
|
||||
for i in range(0, count):
|
||||
if data["%s-%d-deleted" % (prefix, i)]:
|
||||
continue
|
||||
child_blocks_with_indexes.append(
|
||||
(
|
||||
int(data["%s-%d-order" % (prefix, i)]),
|
||||
ListValue.ListChild(
|
||||
self.child_block,
|
||||
self.child_block.value_from_datadict(
|
||||
data, files, "%s-%d-value" % (prefix, i)
|
||||
),
|
||||
id=data.get("%s-%d-id" % (prefix, i)),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
child_blocks_with_indexes.sort()
|
||||
return ListValue(self, bound_blocks=[b for (i, b) in child_blocks_with_indexes])
|
||||
|
||||
def value_omitted_from_data(self, data, files, prefix):
|
||||
return ("%s-count" % prefix) not in data
|
||||
|
||||
def clean(self, value):
|
||||
# value is expected to be a ListValue, but if it's been assigned through external code it might
|
||||
# be a plain list; normalise it to a ListValue
|
||||
value = self.normalize(value)
|
||||
|
||||
result = []
|
||||
block_errors = {}
|
||||
non_block_errors = ErrorList()
|
||||
for index, bound_block in enumerate(value.bound_blocks):
|
||||
try:
|
||||
result.append(
|
||||
ListValue.ListChild(
|
||||
self.child_block,
|
||||
self.child_block.clean(bound_block.value),
|
||||
id=bound_block.id,
|
||||
)
|
||||
)
|
||||
except ValidationError as e:
|
||||
block_errors[index] = e
|
||||
|
||||
if self.meta.min_num is not None and self.meta.min_num > len(value):
|
||||
non_block_errors.append(
|
||||
ValidationError(
|
||||
_("The minimum number of items is %(min_num)d")
|
||||
% {"min_num": self.meta.min_num}
|
||||
)
|
||||
)
|
||||
|
||||
if self.meta.max_num is not None and self.meta.max_num < len(value):
|
||||
non_block_errors.append(
|
||||
ValidationError(
|
||||
_("The maximum number of items is %(max_num)d")
|
||||
% {"max_num": self.meta.max_num}
|
||||
)
|
||||
)
|
||||
|
||||
if block_errors or non_block_errors:
|
||||
raise ListBlockValidationError(
|
||||
block_errors=block_errors, non_block_errors=non_block_errors
|
||||
)
|
||||
|
||||
return ListValue(self, bound_blocks=result)
|
||||
|
||||
def normalize(self, value):
|
||||
if isinstance(value, ListValue):
|
||||
return value
|
||||
elif isinstance(value, list):
|
||||
return ListValue(
|
||||
self, values=[self.child_block.normalize(x) for x in value]
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Cannot handle {value!r} (type {type(value)!r}) as a value of a ListBlock"
|
||||
)
|
||||
|
||||
def empty_value(self):
|
||||
return ListValue(self, values=[])
|
||||
|
||||
def _item_is_in_block_format(self, item):
|
||||
# check a list item retrieved from the database JSON representation to see whether it follows
|
||||
# the new format (https://github.com/wagtail/rfcs/blob/main/text/065-listblock.md) for a list item
|
||||
return (
|
||||
isinstance(item, dict)
|
||||
and "id" in item
|
||||
and "value" in item
|
||||
and item.get("type") == "item"
|
||||
)
|
||||
|
||||
def to_python(self, value):
|
||||
# 'value' is a list of child block values; use bulk_to_python to convert them all in one go
|
||||
|
||||
# get a list of the child block values; this will be the 'value' item of the dict if the list item
|
||||
# is in the new block format, or the list item itself if in the old format
|
||||
raw_values = [
|
||||
item["value"] if self._item_is_in_block_format(item) else item
|
||||
for item in value
|
||||
]
|
||||
converted_values = self.child_block.bulk_to_python(raw_values)
|
||||
bound_blocks = []
|
||||
for i, item in enumerate(value):
|
||||
if self._item_is_in_block_format(item):
|
||||
list_item_id = item["id"]
|
||||
else:
|
||||
list_item_id = None
|
||||
bound_blocks.append(
|
||||
ListValue.ListChild(
|
||||
self.child_block, converted_values[i], id=list_item_id
|
||||
)
|
||||
)
|
||||
return ListValue(self, bound_blocks=bound_blocks)
|
||||
|
||||
def bulk_to_python(self, values):
|
||||
# 'values' is a list of lists of child block values; concatenate them into one list so that
|
||||
# we can make a single call to child_block.bulk_to_python
|
||||
|
||||
lengths = []
|
||||
raw_values = []
|
||||
for list_stream in values:
|
||||
lengths.append(len(list_stream))
|
||||
for list_child in list_stream:
|
||||
if self._item_is_in_block_format(list_child):
|
||||
raw_values.append(list_child["value"])
|
||||
else:
|
||||
raw_values.append(list_child)
|
||||
|
||||
converted_values = self.child_block.bulk_to_python(raw_values)
|
||||
|
||||
# split converted_values back into sub-lists of the original lengths
|
||||
result = []
|
||||
offset = 0
|
||||
values = list(values)
|
||||
for i, sublist_len in enumerate(lengths):
|
||||
bound_blocks = []
|
||||
for j in range(sublist_len):
|
||||
if self._item_is_in_block_format(values[i][j]):
|
||||
list_item_id = values[i][j]["id"]
|
||||
else:
|
||||
list_item_id = None
|
||||
bound_blocks.append(
|
||||
ListValue.ListChild(
|
||||
self.child_block, converted_values[offset + j], id=list_item_id
|
||||
)
|
||||
)
|
||||
|
||||
result.append(ListValue(self, bound_blocks=bound_blocks))
|
||||
offset += sublist_len
|
||||
|
||||
return result
|
||||
|
||||
def get_prep_value(self, value):
|
||||
# value is expected to be a ListValue, but if it's been assigned through external code it might
|
||||
# be a plain list; normalise it to a ListValue
|
||||
if not isinstance(value, ListValue):
|
||||
value = ListValue(self, values=value)
|
||||
|
||||
prep_value = []
|
||||
|
||||
for item in value.bound_blocks:
|
||||
# Convert the native value back into raw JSONish data
|
||||
if not item.id:
|
||||
item.id = str(uuid.uuid4())
|
||||
prep_value.append(item.get_prep_value())
|
||||
return prep_value
|
||||
|
||||
def get_form_state(self, value):
|
||||
# value is expected to be a ListValue, but if it's been assigned through external code it might
|
||||
# be a plain list; normalise it to a ListValue
|
||||
if not isinstance(value, ListValue):
|
||||
value = ListValue(self, values=value)
|
||||
|
||||
return [
|
||||
{
|
||||
"value": self.child_block.get_form_state(block.value),
|
||||
"id": block.id,
|
||||
}
|
||||
for block in value.bound_blocks
|
||||
]
|
||||
|
||||
def get_api_representation(self, value, context=None):
|
||||
# recursively call get_api_representation on children and return as a list
|
||||
return [
|
||||
self.child_block.get_api_representation(item, context=context)
|
||||
for item in value
|
||||
]
|
||||
|
||||
def render_basic(self, value, context=None):
|
||||
children = format_html_join(
|
||||
"\n",
|
||||
"<li>{0}</li>",
|
||||
[
|
||||
(self.child_block.render(child_value, context=context),)
|
||||
for child_value in value
|
||||
],
|
||||
)
|
||||
return format_html("<ul>{0}</ul>", children)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
if not self.search_index:
|
||||
return []
|
||||
content = []
|
||||
for child_value in value:
|
||||
content.extend(self.child_block.get_searchable_content(child_value))
|
||||
|
||||
return content
|
||||
|
||||
def extract_references(self, value):
|
||||
for child in value.bound_blocks:
|
||||
for (
|
||||
model,
|
||||
object_id,
|
||||
model_path,
|
||||
content_path,
|
||||
) in child.block.extract_references(child.value):
|
||||
model_path = f"item.{model_path}" if model_path else "item"
|
||||
content_path = (
|
||||
f"{child.id}.{content_path}" if content_path else child.id
|
||||
)
|
||||
yield model, object_id, model_path, content_path
|
||||
|
||||
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.
|
||||
"""
|
||||
if path_elements:
|
||||
id, *remaining_elements = path_elements
|
||||
for child in value.bound_blocks:
|
||||
if child.id == id:
|
||||
return child.block.get_block_by_content_path(
|
||||
child.value, remaining_elements
|
||||
)
|
||||
else:
|
||||
# an empty path refers to the list as a whole
|
||||
return self.bind(value)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super().check(**kwargs)
|
||||
errors.extend(self.child_block.check(**kwargs))
|
||||
return errors
|
||||
|
||||
def deconstruct_with_lookup(self, lookup):
|
||||
path, args, kwargs = super().deconstruct_with_lookup(lookup)
|
||||
if getattr(self.__init__, "has_child_block_arg", False):
|
||||
if args and isinstance(args[0], Block):
|
||||
block_id = lookup.add_block(args[0])
|
||||
args = (block_id, *args[1:])
|
||||
else:
|
||||
child_block = kwargs.get("child_block")
|
||||
if isinstance(child_block, Block):
|
||||
block_id = lookup.add_block(child_block)
|
||||
kwargs["child_block"] = block_id
|
||||
|
||||
return path, args, kwargs
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
form_classname = None
|
||||
min_num = None
|
||||
max_num = None
|
||||
collapsed = False
|
||||
|
||||
MUTABLE_META_ATTRIBUTES = ["min_num", "max_num"]
|
||||
|
||||
|
||||
class ListBlockAdapter(Adapter):
|
||||
js_constructor = "wagtail.blocks.ListBlock"
|
||||
|
||||
def js_args(self, block):
|
||||
meta = {
|
||||
"label": block.label,
|
||||
"icon": block.meta.icon,
|
||||
"classname": block.meta.form_classname,
|
||||
"collapsed": block.meta.collapsed,
|
||||
"strings": {
|
||||
"MOVE_UP": _("Move up"),
|
||||
"MOVE_DOWN": _("Move down"),
|
||||
"DUPLICATE": _("Duplicate"),
|
||||
"DELETE": _("Delete"),
|
||||
"ADD": _("Add"),
|
||||
},
|
||||
}
|
||||
help_text = getattr(block.meta, "help_text", None)
|
||||
if help_text:
|
||||
meta["helpText"] = help_text
|
||||
meta["helpIcon"] = get_help_icon()
|
||||
|
||||
if block.meta.min_num is not None:
|
||||
meta["minNum"] = block.meta.min_num
|
||||
|
||||
if block.meta.max_num is not None:
|
||||
meta["maxNum"] = block.meta.max_num
|
||||
|
||||
return [
|
||||
block.name,
|
||||
block.child_block,
|
||||
block.child_block.get_form_state(block.child_block.get_default()),
|
||||
meta,
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
return forms.Media(
|
||||
js=[
|
||||
versioned_static("wagtailadmin/js/telepath/blocks.js"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
register(ListBlockAdapter(), ListBlock)
|
||||
|
||||
|
||||
DECONSTRUCT_ALIASES = {
|
||||
ListBlock: "wagtail.blocks.ListBlock",
|
||||
}
|
||||
0
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__pycache__/operations.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__pycache__/operations.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__pycache__/utils.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/blocks/migrations/__pycache__/utils.cpython-310.pyc
vendored
Normal file
Binary file not shown.
352
env/lib/python3.10/site-packages/wagtail/blocks/migrations/migrate_operation.py
vendored
Normal file
352
env/lib/python3.10/site-packages/wagtail/blocks/migrations/migrate_operation.py
vendored
Normal file
@@ -0,0 +1,352 @@
|
||||
import json
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from django.db.models import JSONField, F, Q, Subquery, OuterRef
|
||||
from django.db.models.functions import Cast
|
||||
from django.db.migrations import RunPython
|
||||
from django.utils.functional import cached_property
|
||||
from wagtail.blocks import StreamValue
|
||||
from wagtail.blocks.migrations import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MigrateStreamData(RunPython):
|
||||
"""Subclass of RunPython for streamfield data migration operations"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_name,
|
||||
model_name,
|
||||
field_name,
|
||||
operations_and_block_paths,
|
||||
revisions_from=None,
|
||||
chunk_size=1024,
|
||||
**kwargs,
|
||||
):
|
||||
"""MigrateStreamData constructor
|
||||
|
||||
Args:
|
||||
app_name (str): Name of the app.
|
||||
model_name (str): Name of the model.
|
||||
field_name (str): Name of the streamfield.
|
||||
operations_and_block_paths (:obj:`list` of :obj:`tuple` of (:obj:`operation`, :obj:`str`)):
|
||||
List of operations and corresponding block paths to apply.
|
||||
revisions_from (:obj:`datetime`, optional): Only revisions created from this date
|
||||
onwards will be updated. Passing `None` updates all revisions. Defaults to `None`.
|
||||
Note that live and latest revisions will be updated regardless of what value this
|
||||
takes.
|
||||
chunk_size (:obj:`int`, optional): chunk size for queryset.iterator and bulk_update.
|
||||
Defaults to 1024.
|
||||
**kwargs: atomic, elidable, hints for superclass RunPython can be given
|
||||
|
||||
Example:
|
||||
Renaming a block named `field1` to `block1`::
|
||||
MigrateStreamData(
|
||||
app_name="blog",
|
||||
model_name="BlogPage",
|
||||
field_name="content",
|
||||
operations_and_block_paths=[
|
||||
(RenameStreamChildrenOperation(old_name="field1", new_name="block1"), ""),
|
||||
],
|
||||
revisions_from=datetime.date(2022, 7, 25)
|
||||
),
|
||||
"""
|
||||
|
||||
self.app_name = app_name
|
||||
self.model_name = model_name
|
||||
self.field_name = field_name
|
||||
self.operations_and_block_paths = operations_and_block_paths
|
||||
self.revisions_from = revisions_from
|
||||
self.chunk_size = chunk_size
|
||||
|
||||
# TODO add reverse code when needed, will probably need another input (reversible?)
|
||||
# super class kwargs - atomic,elidable,hints
|
||||
super().__init__(
|
||||
code=self.migrate_stream_data_forward,
|
||||
reverse_code=lambda *args: None,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def deconstruct(self):
|
||||
_, args, kwargs = super().deconstruct()
|
||||
kwargs["app_name"] = self.app_name
|
||||
kwargs["model_name"] = self.model_name
|
||||
kwargs["field_name"] = self.field_name
|
||||
kwargs["operations_and_block_paths"] = self.operations_and_block_paths
|
||||
kwargs["revisions_from"] = self.revisions_from
|
||||
kwargs["chunk_size"] = self.chunk_size
|
||||
|
||||
return (self.__class__.__qualname__, args, kwargs)
|
||||
|
||||
@property
|
||||
def migration_name_fragment(self):
|
||||
# We are using an OrderedDict here to essentially get the functionality of an ordered set
|
||||
# so that names generated will be consistent.
|
||||
fragments = OrderedDict(
|
||||
(op.operation_name_fragment, None)
|
||||
for op, _ in self.operations_and_block_paths
|
||||
)
|
||||
return "_".join(fragments.keys())
|
||||
|
||||
def migrate_stream_data_forward(self, apps, schema_editor):
|
||||
model = apps.get_model(self.app_name, self.model_name)
|
||||
|
||||
# Here we can't directly check the wagtail version, rather we need to check the wagtail
|
||||
# version at the project state when the migration is being applied
|
||||
try:
|
||||
apps.get_model("wagtailcore", "Revision")
|
||||
revision_query_maker = DefaultRevisionQueryMaker(
|
||||
apps, model, self.revisions_from
|
||||
)
|
||||
except LookupError:
|
||||
revision_query_maker = Wagtail3RevisionQueryMaker(
|
||||
apps, model, self.revisions_from
|
||||
)
|
||||
|
||||
model_queryset = model.objects.annotate(
|
||||
raw_content=Cast(F(self.field_name), JSONField())
|
||||
).all()
|
||||
|
||||
updated_model_instances_buffer = []
|
||||
for instance in model_queryset.iterator(chunk_size=self.chunk_size):
|
||||
if instance.raw_content is None:
|
||||
continue
|
||||
|
||||
revision_query_maker.append_instance_data_for_revision_query(instance)
|
||||
|
||||
raw_data = instance.raw_content
|
||||
for operation, block_path_str in self.operations_and_block_paths:
|
||||
try:
|
||||
raw_data = utils.apply_changes_to_raw_data(
|
||||
raw_data=raw_data,
|
||||
block_path_str=block_path_str,
|
||||
operation=operation,
|
||||
streamfield=getattr(model, self.field_name),
|
||||
)
|
||||
# - TODO add a return value to util to know if changes were made
|
||||
# - TODO save changed only
|
||||
except utils.InvalidBlockDefError as e:
|
||||
raise utils.InvalidBlockDefError(instance=instance) from e
|
||||
|
||||
stream_block = getattr(instance, self.field_name).stream_block
|
||||
setattr(
|
||||
instance,
|
||||
self.field_name,
|
||||
StreamValue(stream_block, raw_data, is_lazy=True),
|
||||
)
|
||||
updated_model_instances_buffer.append(instance)
|
||||
|
||||
if len(updated_model_instances_buffer) == self.chunk_size:
|
||||
model.objects.bulk_update(
|
||||
updated_model_instances_buffer, [self.field_name]
|
||||
)
|
||||
updated_model_instances_buffer = []
|
||||
|
||||
if len(updated_model_instances_buffer) > 0:
|
||||
# For any remaining chunks
|
||||
model.objects.bulk_update(updated_model_instances_buffer, [self.field_name])
|
||||
|
||||
# For models without revisions
|
||||
if not revision_query_maker.has_revisions:
|
||||
return
|
||||
|
||||
revision_queryset = revision_query_maker.get_revision_queryset()
|
||||
|
||||
updated_revisions_buffer = []
|
||||
for revision in revision_queryset.iterator(chunk_size=self.chunk_size):
|
||||
|
||||
raw_data = json.loads(revision.content[self.field_name])
|
||||
for operation, block_path_str in self.operations_and_block_paths:
|
||||
try:
|
||||
raw_data = utils.apply_changes_to_raw_data(
|
||||
raw_data=raw_data,
|
||||
block_path_str=block_path_str,
|
||||
operation=operation,
|
||||
streamfield=getattr(model, self.field_name),
|
||||
)
|
||||
except utils.InvalidBlockDefError as e:
|
||||
if not revision_query_maker.get_is_live_or_latest_revision(
|
||||
revision
|
||||
):
|
||||
logger.exception(
|
||||
utils.InvalidBlockDefError(
|
||||
revision=revision, instance=instance
|
||||
)
|
||||
)
|
||||
continue
|
||||
else:
|
||||
raise utils.InvalidBlockDefError(
|
||||
revision=revision, instance=instance
|
||||
) from e
|
||||
# - TODO add a return value to util to know if changes were made
|
||||
# - TODO save changed only
|
||||
|
||||
revision.content[self.field_name] = json.dumps(raw_data)
|
||||
updated_revisions_buffer.append(revision)
|
||||
|
||||
if len(updated_revisions_buffer) == self.chunk_size:
|
||||
revision_query_maker.bulk_update(updated_revisions_buffer)
|
||||
updated_revisions_buffer = []
|
||||
|
||||
if len(updated_revisions_buffer) > 0:
|
||||
revision_query_maker.bulk_update(updated_revisions_buffer)
|
||||
|
||||
|
||||
class AbstractRevisionQueryMaker:
|
||||
"""Helper class for making the revision query needed for the data migration"""
|
||||
|
||||
def __init__(self, apps, model, revisions_from):
|
||||
self.apps = apps
|
||||
self.model = model
|
||||
self.revisions_from = revisions_from
|
||||
self.RevisionModel = self.get_revision_model()
|
||||
self.has_revisions = self.get_has_revisions()
|
||||
if self.has_revisions:
|
||||
# latest or live revision ids may be available directly from the instance. In that case
|
||||
# we can keep track of them here.
|
||||
self.instance_field_revision_ids = set()
|
||||
|
||||
def get_revision_model(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_has_revisions(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def append_instance_data_for_revision_query(self, instance):
|
||||
raise NotImplementedError
|
||||
|
||||
def _make_revision_query(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_revision_queryset(self):
|
||||
revision_query = self._make_revision_query()
|
||||
return self.RevisionModel.objects.filter(revision_query)
|
||||
|
||||
def bulk_update(self, data):
|
||||
self.RevisionModel.objects.bulk_update(data, ["content"])
|
||||
|
||||
def get_is_live_or_latest_revision(self, revision):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Wagtail3RevisionQueryMaker(AbstractRevisionQueryMaker):
|
||||
"""Revision Query maker to support Wagtail 3"""
|
||||
|
||||
def __init__(self, apps, model, revisions_from):
|
||||
self.page_ids = []
|
||||
|
||||
super().__init__(apps, model, revisions_from)
|
||||
|
||||
def get_revision_model(self):
|
||||
return self.apps.get_model("wagtailcore", "PageRevision")
|
||||
|
||||
def get_has_revisions(self):
|
||||
return issubclass(self.model, self.apps.get_model("wagtailcore", "Page"))
|
||||
|
||||
def append_instance_data_for_revision_query(self, instance):
|
||||
if self.has_revisions:
|
||||
self.page_ids.append(instance.id)
|
||||
self.instance_field_revision_ids.add(instance.live_revision_id)
|
||||
|
||||
def _make_revision_query(self):
|
||||
if self.revisions_from is not None:
|
||||
# All revisions created after the given date.
|
||||
revision_query = Q(
|
||||
created_at__gte=self.revisions_from,
|
||||
page_id__in=self.page_ids,
|
||||
)
|
||||
# All live revisions.
|
||||
revision_query = revision_query | Q(id__in=self.instance_field_revision_ids)
|
||||
# All latest revisions. For each revision, we check if it is the revision with the
|
||||
# last `created_at` from all revisions with its `page_id`.
|
||||
revision_query = revision_query | Q(
|
||||
id__in=Subquery(
|
||||
self.RevisionModel.objects.filter(page_id=OuterRef("page_id"))
|
||||
.order_by("-created_at", "-id")
|
||||
.values_list("id", flat=True)[:1]
|
||||
),
|
||||
page_id__in=self.page_ids,
|
||||
)
|
||||
return revision_query
|
||||
|
||||
# otherwise query all revisions for the page
|
||||
else:
|
||||
return Q(page_id__in=self.page_ids)
|
||||
|
||||
def get_is_live_or_latest_revision(self, revision):
|
||||
if revision.id in self.instance_field_revision_ids:
|
||||
return True
|
||||
return revision.id in self._latest_revision_ids
|
||||
|
||||
@cached_property
|
||||
def _latest_revision_ids(self):
|
||||
return self.RevisionModel.objects.filter(
|
||||
id__in=Subquery(
|
||||
self.RevisionModel.objects.filter(page_id=OuterRef("page_id"))
|
||||
.order_by("-created_at", "-id")
|
||||
.values_list("id", flat=True)[:1]
|
||||
),
|
||||
page_id__in=self.page_ids,
|
||||
).values_list("id", flat=True)
|
||||
|
||||
|
||||
class DefaultRevisionQueryMaker(AbstractRevisionQueryMaker):
|
||||
"""Revision Query Maker for Wagtail 4+"""
|
||||
|
||||
def __init__(self, apps, model, revisions_from):
|
||||
self.has_live_revisions = False
|
||||
self.has_latest_revisions = False
|
||||
|
||||
super().__init__(apps, model, revisions_from)
|
||||
|
||||
def get_revision_model(self):
|
||||
return self.apps.get_model("wagtailcore", "Revision")
|
||||
|
||||
def get_has_revisions(self):
|
||||
# We check if the models have a field `latest_revision` and make sure it points to the
|
||||
# Revision model. This relation is there on models with `RevisionMixin`.
|
||||
self.has_latest_revisions = (
|
||||
hasattr(self.model, "latest_revision")
|
||||
and self.model.latest_revision.field.remote_field.model
|
||||
is self.RevisionModel
|
||||
)
|
||||
# Again, check for `live_revision`. This relation is there on models with `DraftStateMixin`.
|
||||
self.has_live_revisions = (
|
||||
hasattr(self.model, "live_revision")
|
||||
and self.model.live_revision.field.remote_field.model is self.RevisionModel
|
||||
)
|
||||
return self.has_latest_revisions or self.has_live_revisions
|
||||
|
||||
def append_instance_data_for_revision_query(self, instance):
|
||||
if self.has_revisions:
|
||||
# From wagtail 4 onwards, there can be non page models which may have live or latest
|
||||
# revisions, but not necessarily having both at the same time.
|
||||
if self.has_latest_revisions:
|
||||
self.instance_field_revision_ids.add(instance.latest_revision_id)
|
||||
|
||||
if self.has_live_revisions:
|
||||
self.instance_field_revision_ids.add(instance.live_revision_id)
|
||||
|
||||
def _make_revision_query(self):
|
||||
ContentType = self.apps.get_model("contenttypes", "ContentType")
|
||||
contenttype_id = ContentType.objects.get_for_model(self.model).id
|
||||
|
||||
# if revisions_from is given, then query only the revisions created after that
|
||||
# datetime (and the latest and live revisions if they are not after revisions_from)
|
||||
if self.revisions_from is not None:
|
||||
# All revisions created after the given date.
|
||||
revision_query = Q(
|
||||
created_at__gte=self.revisions_from,
|
||||
content_type_id=contenttype_id,
|
||||
)
|
||||
# All live and latest revisions
|
||||
revision_query = revision_query | Q(id__in=self.instance_field_revision_ids)
|
||||
return revision_query
|
||||
|
||||
# otherwise query all revisions for the model
|
||||
else:
|
||||
return Q(content_type_id=contenttype_id)
|
||||
|
||||
def get_is_live_or_latest_revision(self, revision):
|
||||
return revision.id in self.instance_field_revision_ids
|
||||
332
env/lib/python3.10/site-packages/wagtail/blocks/migrations/operations.py
vendored
Normal file
332
env/lib/python3.10/site-packages/wagtail/blocks/migrations/operations.py
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from wagtail.blocks.migrations.utils import formatted_list_child_generator
|
||||
from django.utils.deconstruct import deconstructible
|
||||
|
||||
|
||||
class BaseBlockOperation(ABC):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def apply(self, block_value):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def operation_name_fragment(self):
|
||||
pass
|
||||
|
||||
|
||||
@deconstructible
|
||||
class RenameStreamChildrenOperation(BaseBlockOperation):
|
||||
"""Renames all StreamBlock children of the given type
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StreamBlock
|
||||
which contains the blocks to be renamed, not the block being renamed.
|
||||
|
||||
Attributes:
|
||||
old_name (str): name of the child block type to be renamed
|
||||
new_name (str): new name to rename to
|
||||
"""
|
||||
|
||||
def __init__(self, old_name, new_name):
|
||||
super().__init__()
|
||||
self.old_name = old_name
|
||||
self.new_name = new_name
|
||||
|
||||
def apply(self, block_value):
|
||||
mapped_block_value = []
|
||||
for child_block in block_value:
|
||||
if child_block["type"] == self.old_name:
|
||||
mapped_block_value.append({**child_block, "type": self.new_name})
|
||||
else:
|
||||
mapped_block_value.append(child_block)
|
||||
return mapped_block_value
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"rename_{self.old_name}_to_{self.new_name}"
|
||||
|
||||
|
||||
@deconstructible
|
||||
class RenameStructChildrenOperation(BaseBlockOperation):
|
||||
"""Renames all StructBlock children of the given type
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StructBlock
|
||||
which contains the blocks to be renamed, not the block being renamed.
|
||||
|
||||
Attributes:
|
||||
old_name (str): name of the child block type to be renamed
|
||||
new_name (str): new name to rename to
|
||||
"""
|
||||
|
||||
def __init__(self, old_name, new_name):
|
||||
super().__init__()
|
||||
self.old_name = old_name
|
||||
self.new_name = new_name
|
||||
|
||||
def apply(self, block_value):
|
||||
mapped_block_value = {}
|
||||
for child_key, child_value in block_value.items():
|
||||
if child_key == self.old_name:
|
||||
mapped_block_value[self.new_name] = child_value
|
||||
else:
|
||||
mapped_block_value[child_key] = child_value
|
||||
return mapped_block_value
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"rename_{self.old_name}_to_{self.new_name}"
|
||||
|
||||
|
||||
@deconstructible
|
||||
class RemoveStreamChildrenOperation(BaseBlockOperation):
|
||||
"""Removes all StreamBlock children of the given type
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StreamBlock
|
||||
which contains the blocks to be removed, not the block being removed.
|
||||
|
||||
Attributes:
|
||||
name (str): name of the child block type to be removed
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
||||
def apply(self, block_value):
|
||||
return [
|
||||
child_block
|
||||
for child_block in block_value
|
||||
if child_block["type"] != self.name
|
||||
]
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"remove_{self.name}"
|
||||
|
||||
|
||||
@deconstructible
|
||||
class RemoveStructChildrenOperation(BaseBlockOperation):
|
||||
"""Removes all StructBlock children of the given type
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StructBlock
|
||||
which contains the blocks to be removed, not the block being removed.
|
||||
|
||||
Attributes:
|
||||
name (str): name of the child block type to be removed
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
||||
def apply(self, block_value):
|
||||
return {
|
||||
child_key: child_value
|
||||
for child_key, child_value in block_value.items()
|
||||
if child_key != self.name
|
||||
}
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"remove_{self.name}"
|
||||
|
||||
|
||||
class StreamChildrenToListBlockOperation(BaseBlockOperation):
|
||||
"""Combines StreamBlock children of the given type into a new ListBlock
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StreamBlock
|
||||
which contains the blocks to be combined, not the child block itself.
|
||||
|
||||
Attributes:
|
||||
block_name (str): name of the child block type to be combined
|
||||
list_block_name (str): name of the new ListBlock type
|
||||
"""
|
||||
|
||||
def __init__(self, block_name, list_block_name):
|
||||
super().__init__()
|
||||
self.block_name = block_name
|
||||
self.list_block_name = list_block_name
|
||||
self.temp_blocks = []
|
||||
|
||||
def apply(self, block_value):
|
||||
mapped_block_value = []
|
||||
for child_block in block_value:
|
||||
if child_block["type"] == self.block_name:
|
||||
self.temp_blocks.append(child_block)
|
||||
else:
|
||||
mapped_block_value.append(child_block)
|
||||
|
||||
self.map_temp_blocks_to_list_items()
|
||||
|
||||
if self.temp_blocks:
|
||||
new_list_block = {"type": self.list_block_name, "value": self.temp_blocks}
|
||||
mapped_block_value.append(new_list_block)
|
||||
|
||||
return mapped_block_value
|
||||
|
||||
def map_temp_blocks_to_list_items(self):
|
||||
new_temp_blocks = []
|
||||
for block in self.temp_blocks:
|
||||
new_temp_blocks.append({**block, "type": "item"})
|
||||
self.temp_blocks = new_temp_blocks
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"{self.block_name}_to_list_block_{self.list_block_name}"
|
||||
|
||||
|
||||
class StreamChildrenToStreamBlockOperation(BaseBlockOperation):
|
||||
"""Combines StreamBlock children of the given types into a new StreamBlock
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StreamBlock
|
||||
which contains the blocks to be combined, not the child block itself.
|
||||
|
||||
Attributes:
|
||||
block_names (:obj:`list` of :obj:`str`): names of the child block types to be combined
|
||||
stream_block_name (str): name of the new StreamBlock type
|
||||
"""
|
||||
|
||||
def __init__(self, block_names, stream_block_name):
|
||||
super().__init__()
|
||||
self.block_names = block_names
|
||||
self.stream_block_name = stream_block_name
|
||||
|
||||
def apply(self, block_value):
|
||||
mapped_block_value = []
|
||||
stream_value = []
|
||||
|
||||
for child_block in block_value:
|
||||
if child_block["type"] in self.block_names:
|
||||
stream_value.append(child_block)
|
||||
else:
|
||||
mapped_block_value.append(child_block)
|
||||
|
||||
if stream_value:
|
||||
new_stream_block = {"type": self.stream_block_name, "value": stream_value}
|
||||
mapped_block_value.append(new_stream_block)
|
||||
|
||||
return mapped_block_value
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return "{}_to_stream_block".format("_".join(self.block_names))
|
||||
|
||||
|
||||
class AlterBlockValueOperation(BaseBlockOperation):
|
||||
"""Alters the value of each block to the given value
|
||||
|
||||
Attributes:
|
||||
new_value : new value to change to
|
||||
"""
|
||||
|
||||
def __init__(self, new_value):
|
||||
super().__init__()
|
||||
self.new_value = new_value
|
||||
|
||||
def apply(self, block_value):
|
||||
return self.new_value
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return "alter_block_value"
|
||||
|
||||
|
||||
class StreamChildrenToStructBlockOperation(BaseBlockOperation):
|
||||
"""Move each StreamBlock child of the given type inside a new StructBlock
|
||||
|
||||
A new StructBlock will be created as a child of the parent StreamBlock for each child block of
|
||||
the given type, and then that child block will be moved from the parent StreamBlocks children
|
||||
inside the new StructBlock as a child of that StructBlock.
|
||||
|
||||
Example:
|
||||
Consider the following StreamField definition::
|
||||
|
||||
mystream = StreamField([("char1", CharBlock()) ...], ...)
|
||||
|
||||
Then the stream data would look like the following::
|
||||
|
||||
[
|
||||
...
|
||||
{ "type": "char1", "value": "Value1", ... },
|
||||
{ "type": "char1", "value": "Value2", ... },
|
||||
...
|
||||
]
|
||||
|
||||
And if we define the operation like this::
|
||||
|
||||
StreamChildrenToStructBlockOperation("char1", "struct1")
|
||||
|
||||
Our altered stream data would look like this::
|
||||
|
||||
[
|
||||
...
|
||||
{ "type": "struct1", "value": { "char1": "Value1" } },
|
||||
{ "type": "struct1", "value": { "char1": "Value2" } },
|
||||
...
|
||||
]
|
||||
|
||||
Note:
|
||||
The `block_path_str` when using this operation should point to the parent StreamBlock
|
||||
which contains the blocks to be combined, not the child block itself.
|
||||
|
||||
Note:
|
||||
Block ids are not preserved here since the new blocks are structurally different than the
|
||||
previous blocks.
|
||||
|
||||
Attributes:
|
||||
block_names (str): names of the child block types to be combined
|
||||
struct_block_name (str): name of the new StructBlock type
|
||||
"""
|
||||
|
||||
def __init__(self, block_name, struct_block_name):
|
||||
super().__init__()
|
||||
self.block_name = block_name
|
||||
self.struct_block_name = struct_block_name
|
||||
|
||||
def apply(self, block_value):
|
||||
mapped_block_value = []
|
||||
for child_block in block_value:
|
||||
if child_block["type"] == self.block_name:
|
||||
mapped_block_value.append(
|
||||
{
|
||||
**child_block,
|
||||
"type": self.struct_block_name,
|
||||
"value": {self.block_name: child_block["value"]},
|
||||
}
|
||||
)
|
||||
else:
|
||||
mapped_block_value.append(child_block)
|
||||
return mapped_block_value
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"{self.block_name}_to_struct_block_{self.struct_block_name}"
|
||||
|
||||
|
||||
class ListChildrenToStructBlockOperation(BaseBlockOperation):
|
||||
def __init__(self, block_name):
|
||||
super().__init__()
|
||||
self.block_name = block_name
|
||||
|
||||
def apply(self, block_value):
|
||||
mapped_block_value = []
|
||||
|
||||
# In case there is data from the old list format (wagtail < 2.16), we use the generator
|
||||
# to convert them into the new list format
|
||||
for child_block in formatted_list_child_generator(block_value):
|
||||
mapped_block_value.append(
|
||||
{**child_block, "value": {self.block_name: child_block["value"]}}
|
||||
)
|
||||
return mapped_block_value
|
||||
|
||||
@property
|
||||
def operation_name_fragment(self):
|
||||
return f"list_block_items_to_{self.block_name}"
|
||||
291
env/lib/python3.10/site-packages/wagtail/blocks/migrations/utils.py
vendored
Normal file
291
env/lib/python3.10/site-packages/wagtail/blocks/migrations/utils.py
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
from wagtail.blocks import ListBlock, StreamBlock, StructBlock
|
||||
|
||||
|
||||
class InvalidBlockDefError(Exception):
|
||||
"""Exception for invalid block definitions"""
|
||||
|
||||
def __init__(self, *args, instance=None, revision=None, **kwargs):
|
||||
# in the case of a revision pass both instance and revision
|
||||
self.instance = instance
|
||||
self.revision = revision
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
message = ""
|
||||
if self.instance is not None:
|
||||
message += "Invalid block def in {} object ({})".format(
|
||||
self.instance.__class__.__name__, self.instance.id
|
||||
)
|
||||
if self.revision is not None:
|
||||
message += " for revision id ({}) created at {}".format(
|
||||
self.revision.id,
|
||||
self.revision.created_at,
|
||||
)
|
||||
if self.args:
|
||||
message += "\n"
|
||||
|
||||
message += super().__str__()
|
||||
return message
|
||||
|
||||
|
||||
def should_alter_block(block_name, block_path):
|
||||
# If the block is not at the start of `block_path`, then neither it nor its children are
|
||||
# blocks that we need to alter.
|
||||
return block_name == block_path[0]
|
||||
|
||||
|
||||
def map_block_value(block_value, block_def, block_path, operation, **kwargs):
|
||||
"""
|
||||
Maps the value of a block.
|
||||
|
||||
Args:
|
||||
block_value:
|
||||
The value of the block. This would be a list or dict of children for structural blocks.
|
||||
block_def:
|
||||
The definition of the block.
|
||||
block_path:
|
||||
A '.' separated list of names of the blocks from the current block (not included) to
|
||||
the nested block of which the value will be passed to the operation.
|
||||
operation:
|
||||
An Operation class instance (extends `BaseBlockOperation`), which has an `apply` method
|
||||
for mapping values.
|
||||
|
||||
Returns:
|
||||
mapped_value:
|
||||
"""
|
||||
|
||||
# If the `block_path` length is 0, that means we've reached the end of the block path, that
|
||||
# is, the block where we need to apply the operation. Note that we are asking the user to
|
||||
# pass "item" as part of the block path for list children, so it won't give rise to any
|
||||
# problems here.
|
||||
if len(block_path) == 0:
|
||||
return operation.apply(block_value)
|
||||
|
||||
# Depending on whether the block is a ListBlock, StructBlock or StreamBlock we call a
|
||||
# different function to alter its children.
|
||||
|
||||
if isinstance(block_def, StreamBlock):
|
||||
return map_stream_block_value(
|
||||
block_value,
|
||||
operation=operation,
|
||||
block_def=block_def,
|
||||
block_path=block_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
elif isinstance(block_def, ListBlock):
|
||||
return map_list_block_value(
|
||||
block_value,
|
||||
operation=operation,
|
||||
block_def=block_def,
|
||||
block_path=block_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
elif isinstance(block_def, StructBlock):
|
||||
return map_struct_block_value(
|
||||
block_value,
|
||||
operation=operation,
|
||||
block_def=block_def,
|
||||
block_path=block_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unexpected Structural Block: {block_value}")
|
||||
|
||||
|
||||
def map_stream_block_value(stream_block_value, block_def, block_path, **kwargs):
|
||||
"""
|
||||
Maps each child block in a StreamBlock value.
|
||||
|
||||
Args:
|
||||
stream_block_value:
|
||||
The value of the StreamBlock, a list of child blocks
|
||||
block_def:
|
||||
The definition of the StreamBlock
|
||||
block_path:
|
||||
A '.' separated list of names of the blocks from the current block (not included) to
|
||||
the nested block of which the value will be passed to the operation.
|
||||
|
||||
Returns
|
||||
mapped_value:
|
||||
The value of the StreamBlock after mapping all the children.
|
||||
"""
|
||||
|
||||
mapped_value = []
|
||||
for child_block in stream_block_value:
|
||||
|
||||
if not should_alter_block(child_block["type"], block_path):
|
||||
mapped_value.append(child_block)
|
||||
|
||||
else:
|
||||
try:
|
||||
child_block_def = block_def.child_blocks[child_block["type"]]
|
||||
except KeyError:
|
||||
raise InvalidBlockDefError(
|
||||
"No current block def named {}".format(child_block["type"])
|
||||
)
|
||||
mapped_child_value = map_block_value(
|
||||
child_block["value"],
|
||||
block_def=child_block_def,
|
||||
block_path=block_path[1:],
|
||||
**kwargs,
|
||||
)
|
||||
mapped_value.append({**child_block, "value": mapped_child_value})
|
||||
|
||||
return mapped_value
|
||||
|
||||
|
||||
def map_struct_block_value(struct_block_value, block_def, block_path, **kwargs):
|
||||
"""
|
||||
Maps each child block in a StructBlock value.
|
||||
|
||||
Args:
|
||||
stream_block_value:
|
||||
The value of the StructBlock, a dict of child blocks
|
||||
block_def:
|
||||
The definition of the StructBlock
|
||||
block_path:
|
||||
A '.' separated list of names of the blocks from the current block (not included) to
|
||||
the nested block of which the value will be passed to the operation.
|
||||
|
||||
Returns
|
||||
mapped_value:
|
||||
The value of the StructBlock after mapping all the children.
|
||||
"""
|
||||
|
||||
mapped_value = {}
|
||||
for key, child_value in struct_block_value.items():
|
||||
|
||||
if not should_alter_block(key, block_path):
|
||||
mapped_value[key] = child_value
|
||||
|
||||
else:
|
||||
try:
|
||||
child_block_def = block_def.child_blocks[key]
|
||||
except KeyError:
|
||||
raise InvalidBlockDefError(f"No current block def named {key}")
|
||||
altered_child_value = map_block_value(
|
||||
child_value,
|
||||
block_def=child_block_def,
|
||||
block_path=block_path[1:],
|
||||
**kwargs,
|
||||
)
|
||||
mapped_value[key] = altered_child_value
|
||||
|
||||
return mapped_value
|
||||
|
||||
|
||||
def map_list_block_value(list_block_value, block_def, block_path, **kwargs):
|
||||
"""
|
||||
Maps each child block in a ListBlock value.
|
||||
|
||||
Args:
|
||||
stream_block_value:
|
||||
The value of the ListBlock, a list of child blocks
|
||||
block_def:
|
||||
The definition of the ListBlock
|
||||
block_path:
|
||||
A '.' separated list of names of the blocks from the current block (not included) to
|
||||
the nested block of which the value will be passed to the operation.
|
||||
|
||||
Returns
|
||||
mapped_value:
|
||||
The value of the ListBlock after mapping all the children.
|
||||
"""
|
||||
|
||||
mapped_value = []
|
||||
# In case data is in old list format
|
||||
for child_block in formatted_list_child_generator(list_block_value):
|
||||
|
||||
mapped_child_value = map_block_value(
|
||||
child_block["value"],
|
||||
block_def=block_def.child_block,
|
||||
block_path=block_path[1:],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
mapped_value.append({**child_block, "value": mapped_child_value})
|
||||
|
||||
return mapped_value
|
||||
|
||||
|
||||
def formatted_list_child_generator(list_block_value):
|
||||
is_old_format = False
|
||||
if not isinstance(list_block_value[0], dict):
|
||||
is_old_format = True
|
||||
elif "type" not in list_block_value[0] or list_block_value[0]["type"] != "item":
|
||||
is_old_format = True
|
||||
|
||||
for child in list_block_value:
|
||||
if not is_old_format:
|
||||
yield child
|
||||
else:
|
||||
yield {"type": "item", "value": child}
|
||||
|
||||
|
||||
def apply_changes_to_raw_data(
|
||||
raw_data, block_path_str, operation, streamfield, **kwargs
|
||||
):
|
||||
"""
|
||||
Applies changes to raw stream data
|
||||
|
||||
Args:
|
||||
raw_data:
|
||||
The current stream data (a list of top level blocks)
|
||||
block_path_str:
|
||||
A '.' separated list of names of the blocks from the top level block to the nested
|
||||
block of which the value will be passed to the operation.
|
||||
|
||||
eg:- 'simplestream.struct1' would point to,
|
||||
[..., { type: simplestream, value: [..., { type: struct1, value: {...} }] }]
|
||||
|
||||
NOTE: If we're directly applying changes on the top level stream block, then this will
|
||||
be "".
|
||||
|
||||
NOTE: When the path contains a ListBlock child, 'item' must be added to the block as
|
||||
the name of said child.
|
||||
|
||||
eg:- 'list1.item.stream1' where the list child is a StructBlock would point to,
|
||||
[
|
||||
...,
|
||||
{
|
||||
type: list1,
|
||||
value: [
|
||||
{
|
||||
type: item,
|
||||
value: { ..., stream1: [...] }
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
]
|
||||
operation:
|
||||
A subclass of `operations.BaseBlockOperation`. It will have the `apply` method
|
||||
for applying changes to the matching block values.
|
||||
streamfield:
|
||||
The streamfield for which data is being migrated. This is used to get the definitions
|
||||
of the blocks.
|
||||
|
||||
Returns:
|
||||
altered_raw_data:
|
||||
"""
|
||||
|
||||
if block_path_str == "":
|
||||
# If block_path_str is "", we're directly applying the operation on the top level
|
||||
# streamblock.
|
||||
block_path = []
|
||||
else:
|
||||
block_path = block_path_str.split(".")
|
||||
block_def = streamfield.field.stream_block
|
||||
|
||||
altered_raw_data = map_block_value(
|
||||
raw_data,
|
||||
block_def=block_def,
|
||||
block_path=block_path,
|
||||
operation=operation,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return altered_raw_data
|
||||
70
env/lib/python3.10/site-packages/wagtail/blocks/static_block.py
vendored
Normal file
70
env/lib/python3.10/site-packages/wagtail/blocks/static_block.py
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
from django import forms
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import SafeString
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin.staticfiles import versioned_static
|
||||
from wagtail.telepath import Adapter, register
|
||||
|
||||
from .base import Block
|
||||
|
||||
__all__ = ["StaticBlock"]
|
||||
|
||||
|
||||
class StaticBlock(Block):
|
||||
"""
|
||||
A block that just 'exists' and has no fields.
|
||||
"""
|
||||
|
||||
def get_admin_text(self):
|
||||
if self.meta.admin_text is None:
|
||||
if self.label:
|
||||
return _("%(label)s: this block has no options.") % {
|
||||
"label": self.label
|
||||
}
|
||||
else:
|
||||
return _("This block has no options.")
|
||||
|
||||
return self.meta.admin_text
|
||||
|
||||
def value_from_datadict(self, data, files, prefix):
|
||||
return None
|
||||
|
||||
def normalize(self, value):
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
admin_text = None
|
||||
default = None
|
||||
|
||||
|
||||
class StaticBlockAdapter(Adapter):
|
||||
js_constructor = "wagtail.blocks.StaticBlock"
|
||||
|
||||
def js_args(self, block):
|
||||
admin_text = block.get_admin_text()
|
||||
|
||||
if isinstance(admin_text, SafeString):
|
||||
text_or_html = "html"
|
||||
else:
|
||||
text_or_html = "text"
|
||||
|
||||
return [
|
||||
block.name,
|
||||
{
|
||||
text_or_html: admin_text,
|
||||
"icon": block.meta.icon,
|
||||
"label": block.label,
|
||||
},
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
return forms.Media(
|
||||
js=[
|
||||
versioned_static("wagtailadmin/js/telepath/blocks.js"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
register(StaticBlockAdapter(), StaticBlock)
|
||||
868
env/lib/python3.10/site-packages/wagtail/blocks/stream_block.py
vendored
Normal file
868
env/lib/python3.10/site-packages/wagtail/blocks/stream_block.py
vendored
Normal file
@@ -0,0 +1,868 @@
|
||||
import itertools
|
||||
import json
|
||||
import uuid
|
||||
from collections import OrderedDict, defaultdict
|
||||
from collections.abc import Mapping, MutableSequence
|
||||
from pickle import PickleError
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.fields import _load_field
|
||||
from django.forms.utils import ErrorList
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html_join
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin.staticfiles import versioned_static
|
||||
from wagtail.telepath import Adapter, register
|
||||
|
||||
from .base import (
|
||||
Block,
|
||||
BoundBlock,
|
||||
DeclarativeSubBlocksMetaclass,
|
||||
get_error_json_data,
|
||||
get_error_list_json_data,
|
||||
get_help_icon,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BaseStreamBlock",
|
||||
"StreamBlock",
|
||||
"StreamValue",
|
||||
"StreamBlockValidationError",
|
||||
]
|
||||
|
||||
|
||||
class StreamBlockValidationError(ValidationError):
|
||||
def __init__(self, block_errors=None, non_block_errors=None):
|
||||
# non_block_errors may be passed here as an ErrorList, a plain list (of strings or
|
||||
# ValidationErrors), or None.
|
||||
# Normalise it to be an ErrorList, which provides an as_data() method that consistently
|
||||
# returns a flat list of ValidationError objects.
|
||||
self.non_block_errors = ErrorList(non_block_errors)
|
||||
|
||||
# block_errors may be passed here as None, or a dict keyed by the indexes of the child blocks
|
||||
# with errors.
|
||||
# Items in this list / dict may be:
|
||||
# - a ValidationError instance (potentially a subclass such as StructBlockValidationError)
|
||||
# - an ErrorList containing a single ValidationError
|
||||
# - a plain list containing a single ValidationError
|
||||
# All representations will be normalised to a dict of ValidationError instances,
|
||||
# which is also the preferred format for the original argument to be in.
|
||||
self.block_errors = {}
|
||||
if block_errors is None:
|
||||
pass
|
||||
else:
|
||||
for index, val in block_errors.items():
|
||||
if isinstance(val, ErrorList):
|
||||
self.block_errors[index] = val.as_data()[0]
|
||||
elif isinstance(val, list):
|
||||
self.block_errors[index] = val[0]
|
||||
else:
|
||||
self.block_errors[index] = val
|
||||
|
||||
super().__init__("Validation error in StreamBlock")
|
||||
|
||||
def as_json_data(self):
|
||||
result = {}
|
||||
if self.non_block_errors:
|
||||
result["messages"] = get_error_list_json_data(self.non_block_errors)
|
||||
if self.block_errors:
|
||||
result["blockErrors"] = {
|
||||
index: get_error_json_data(error)
|
||||
for (index, error) in self.block_errors.items()
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class BaseStreamBlock(Block):
|
||||
def __init__(self, local_blocks=None, search_index=True, **kwargs):
|
||||
self._constructor_kwargs = kwargs
|
||||
self.search_index = search_index
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
|
||||
self.child_blocks = self.base_blocks.copy()
|
||||
if local_blocks:
|
||||
for name, block in local_blocks:
|
||||
block.set_name(name)
|
||||
self.child_blocks[name] = block
|
||||
|
||||
@classmethod
|
||||
def construct_from_lookup(cls, lookup, child_blocks, **kwargs):
|
||||
if child_blocks:
|
||||
child_blocks = [
|
||||
(name, lookup.get_block(index)) for name, index in child_blocks
|
||||
]
|
||||
return cls(child_blocks, **kwargs)
|
||||
|
||||
def empty_value(self, raw_text=None):
|
||||
return StreamValue(self, [], raw_text=raw_text)
|
||||
|
||||
def sorted_child_blocks(self):
|
||||
"""Child blocks, sorted in to their groups."""
|
||||
return sorted(
|
||||
self.child_blocks.values(), key=lambda child_block: child_block.meta.group
|
||||
)
|
||||
|
||||
def grouped_child_blocks(self):
|
||||
"""
|
||||
The available child block types of this stream block, organised into groups according to
|
||||
their meta.group attribute.
|
||||
Returned as an iterable of (group_name, list_of_blocks) tuples
|
||||
"""
|
||||
return itertools.groupby(
|
||||
self.sorted_child_blocks(), key=lambda child_block: child_block.meta.group
|
||||
)
|
||||
|
||||
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)]
|
||||
try:
|
||||
child_block = self.child_blocks[block_type_name]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
values_with_indexes.append(
|
||||
(
|
||||
int(data["%s-%d-order" % (prefix, i)]),
|
||||
block_type_name,
|
||||
child_block.value_from_datadict(
|
||||
data, files, "%s-%d-value" % (prefix, i)
|
||||
),
|
||||
data.get("%s-%d-id" % (prefix, i)),
|
||||
)
|
||||
)
|
||||
|
||||
values_with_indexes.sort()
|
||||
return StreamValue(
|
||||
self,
|
||||
[
|
||||
(child_block_type_name, value, block_id)
|
||||
for (
|
||||
index,
|
||||
child_block_type_name,
|
||||
value,
|
||||
block_id,
|
||||
) in values_with_indexes
|
||||
],
|
||||
)
|
||||
|
||||
def value_omitted_from_data(self, data, files, prefix):
|
||||
return ("%s-count" % prefix) not in data
|
||||
|
||||
@property
|
||||
def required(self):
|
||||
return self.meta.required
|
||||
|
||||
def clean(self, value):
|
||||
cleaned_data = []
|
||||
errors = {}
|
||||
non_block_errors = ErrorList()
|
||||
for i, child in enumerate(value): # child is a StreamChild instance
|
||||
try:
|
||||
cleaned_data.append(
|
||||
(child.block.name, child.block.clean(child.value), child.id)
|
||||
)
|
||||
except ValidationError as e:
|
||||
errors[i] = e
|
||||
|
||||
if self.meta.min_num is not None and self.meta.min_num > len(value):
|
||||
non_block_errors.append(
|
||||
ValidationError(
|
||||
_("The minimum number of items is %(min_num)d")
|
||||
% {"min_num": self.meta.min_num}
|
||||
)
|
||||
)
|
||||
elif self.required and len(value) == 0:
|
||||
non_block_errors.append(ValidationError(_("This field is required.")))
|
||||
|
||||
if self.meta.max_num is not None and self.meta.max_num < len(value):
|
||||
non_block_errors.append(
|
||||
ValidationError(
|
||||
_("The maximum number of items is %(max_num)d")
|
||||
% {"max_num": self.meta.max_num}
|
||||
)
|
||||
)
|
||||
|
||||
if self.meta.block_counts:
|
||||
block_counts = defaultdict(int)
|
||||
for item in value:
|
||||
block_counts[item.block_type] += 1
|
||||
|
||||
for block_name, min_max in self.meta.block_counts.items():
|
||||
block = self.child_blocks[block_name]
|
||||
max_num = min_max.get("max_num", None)
|
||||
min_num = min_max.get("min_num", None)
|
||||
block_count = block_counts[block_name]
|
||||
if min_num is not None and min_num > block_count:
|
||||
non_block_errors.append(
|
||||
ValidationError(
|
||||
"{}: {}".format(
|
||||
block.label,
|
||||
_("The minimum number of items is %(min_num)d")
|
||||
% {"min_num": min_num},
|
||||
)
|
||||
)
|
||||
)
|
||||
if max_num is not None and max_num < block_count:
|
||||
non_block_errors.append(
|
||||
ValidationError(
|
||||
"{}: {}".format(
|
||||
block.label,
|
||||
_("The maximum number of items is %(max_num)d")
|
||||
% {"max_num": max_num},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if errors or non_block_errors:
|
||||
# The message here is arbitrary - outputting error messages is delegated to the child blocks,
|
||||
# which only involves the 'params' list
|
||||
raise StreamBlockValidationError(
|
||||
block_errors=errors, non_block_errors=non_block_errors
|
||||
)
|
||||
|
||||
return StreamValue(self, cleaned_data)
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, StreamValue):
|
||||
return value
|
||||
elif isinstance(value, str) and value:
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except ValueError:
|
||||
# value is not valid JSON; most likely, this field was previously a
|
||||
# rich text field before being migrated to StreamField, and the data
|
||||
# was left intact in the migration. Return an empty stream instead
|
||||
# (but keep the raw text available as an attribute, so that it can be
|
||||
# used to migrate that data to StreamField)
|
||||
return self.empty_value(raw_text=value)
|
||||
|
||||
if not value:
|
||||
return self.empty_value()
|
||||
|
||||
# ensure value is a list and not some other kind of iterable
|
||||
value = list(value)
|
||||
|
||||
if isinstance(value[0], dict):
|
||||
# value is in JSONish representation - a dict with 'type' and 'value' keys.
|
||||
# This is passed to StreamValue to be expanded lazily - but first we reject any unrecognised
|
||||
# block types from the list
|
||||
return StreamValue(
|
||||
self,
|
||||
[
|
||||
child_data
|
||||
for child_data in value
|
||||
if child_data["type"] in self.child_blocks
|
||||
],
|
||||
is_lazy=True,
|
||||
)
|
||||
else:
|
||||
# See if it looks like the standard non-smart representation of a
|
||||
# StreamField value: a list of (block_name, value) tuples
|
||||
try:
|
||||
[None for (x, y) in value]
|
||||
except (TypeError, ValueError) as exc:
|
||||
# Give up trying to make sense of the value
|
||||
raise TypeError(
|
||||
f"Cannot handle {value!r} (type {type(value)!r}) as a value of a StreamBlock"
|
||||
) from exc
|
||||
|
||||
# Test succeeded, so return as a StreamValue-ified version of that value
|
||||
return StreamValue(
|
||||
self,
|
||||
[
|
||||
(k, self.child_blocks[k].normalize(v))
|
||||
for k, v in value
|
||||
if k in self.child_blocks
|
||||
],
|
||||
)
|
||||
|
||||
def bulk_to_python(self, values):
|
||||
# 'values' is a list of streams, each stream being a list of dicts with 'type', 'value' and
|
||||
# optionally 'id'.
|
||||
# We will iterate over these streams, constructing:
|
||||
# 1) a set of per-child-block lists ('child_inputs'), to be sent to each child block's
|
||||
# bulk_to_python method in turn (giving us 'child_outputs')
|
||||
# 2) a 'block map' of each stream, telling us the type and id of each block and the index we
|
||||
# need to look up in the corresponding child_outputs list to obtain its final value
|
||||
|
||||
child_inputs = defaultdict(list)
|
||||
block_maps = []
|
||||
|
||||
for stream in values:
|
||||
block_map = []
|
||||
for block_dict in stream:
|
||||
block_type = block_dict["type"]
|
||||
|
||||
if block_type not in self.child_blocks:
|
||||
# skip any blocks with an unrecognised type
|
||||
continue
|
||||
|
||||
child_input_list = child_inputs[block_type]
|
||||
child_index = len(child_input_list)
|
||||
child_input_list.append(block_dict["value"])
|
||||
block_map.append((block_type, block_dict.get("id"), child_index))
|
||||
|
||||
block_maps.append(block_map)
|
||||
|
||||
# run each list in child_inputs through the relevant block's bulk_to_python
|
||||
# to obtain child_outputs
|
||||
child_outputs = {
|
||||
block_type: self.child_blocks[block_type].bulk_to_python(child_input_list)
|
||||
for block_type, child_input_list in child_inputs.items()
|
||||
}
|
||||
|
||||
# for each stream, go through the block map, picking out the appropriately-indexed
|
||||
# value from the relevant list in child_outputs
|
||||
return [
|
||||
StreamValue(
|
||||
self,
|
||||
[
|
||||
(block_type, child_outputs[block_type][child_index], id)
|
||||
for block_type, id, child_index in block_map
|
||||
],
|
||||
is_lazy=False,
|
||||
)
|
||||
for block_map in block_maps
|
||||
]
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if not value:
|
||||
# Falsy values (including None, empty string, empty list, and
|
||||
# empty StreamValue) become an empty stream
|
||||
return []
|
||||
else:
|
||||
# value is a StreamValue - delegate to its get_prep_value() method
|
||||
# (which has special-case handling for lazy StreamValues to avoid useless
|
||||
# round-trips to the full data representation and back)
|
||||
return value.get_prep_value()
|
||||
|
||||
def normalize(self, value):
|
||||
return self.to_python(value)
|
||||
|
||||
def get_form_state(self, value):
|
||||
if not value:
|
||||
return []
|
||||
else:
|
||||
return [
|
||||
{
|
||||
"type": child.block.name,
|
||||
"value": child.block.get_form_state(child.value),
|
||||
"id": child.id,
|
||||
}
|
||||
for child in value
|
||||
]
|
||||
|
||||
def get_api_representation(self, value, context=None):
|
||||
if value is None:
|
||||
# treat None as identical to an empty stream
|
||||
return []
|
||||
|
||||
return [
|
||||
{
|
||||
"type": child.block.name,
|
||||
"value": child.block.get_api_representation(
|
||||
child.value, context=context
|
||||
),
|
||||
"id": child.id,
|
||||
}
|
||||
for child in value # child is a StreamChild instance
|
||||
]
|
||||
|
||||
def render_basic(self, value, context=None):
|
||||
return format_html_join(
|
||||
"\n",
|
||||
'<div class="block-{1}">{0}</div>',
|
||||
[(child.render(context=context), child.block_type) for child in value],
|
||||
)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
if not self.search_index:
|
||||
return []
|
||||
content = []
|
||||
|
||||
for child in value:
|
||||
content.extend(child.block.get_searchable_content(child.value))
|
||||
|
||||
return content
|
||||
|
||||
def extract_references(self, value):
|
||||
for child in value:
|
||||
for (
|
||||
model,
|
||||
object_id,
|
||||
model_path,
|
||||
content_path,
|
||||
) in child.block.extract_references(child.value):
|
||||
model_path = (
|
||||
f"{child.block_type}.{model_path}"
|
||||
if model_path
|
||||
else child.block_type
|
||||
)
|
||||
content_path = (
|
||||
f"{child.id}.{content_path}" if content_path else child.id
|
||||
)
|
||||
yield model, object_id, model_path, content_path
|
||||
|
||||
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.
|
||||
"""
|
||||
if path_elements:
|
||||
id, *remaining_elements = path_elements
|
||||
for child in value:
|
||||
if child.id == id:
|
||||
return child.block.get_block_by_content_path(
|
||||
child.value, remaining_elements
|
||||
)
|
||||
else:
|
||||
# an empty path refers to the stream as a whole
|
||||
return self.bind(value)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Always deconstruct StreamBlock instances as if they were plain StreamBlocks with all of the
|
||||
field definitions passed to the constructor - even if in reality this is a subclass of StreamBlock
|
||||
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.blocks.StreamBlock"
|
||||
args = [list(self.child_blocks.items())]
|
||||
kwargs = self._constructor_kwargs
|
||||
return (path, args, kwargs)
|
||||
|
||||
def deconstruct_with_lookup(self, lookup):
|
||||
path = "wagtail.blocks.StreamBlock"
|
||||
args = [
|
||||
[
|
||||
(name, lookup.add_block(block))
|
||||
for name, block in self.child_blocks.items()
|
||||
]
|
||||
]
|
||||
kwargs = self._constructor_kwargs
|
||||
return (path, args, kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super().check(**kwargs)
|
||||
for name, child_block in self.child_blocks.items():
|
||||
errors.extend(child_block.check(**kwargs))
|
||||
errors.extend(child_block._check_name(**kwargs))
|
||||
|
||||
return errors
|
||||
|
||||
class Meta:
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
default = []
|
||||
required = True
|
||||
form_classname = None
|
||||
min_num = None
|
||||
max_num = None
|
||||
block_counts = {}
|
||||
collapsed = False
|
||||
|
||||
MUTABLE_META_ATTRIBUTES = [
|
||||
"required",
|
||||
"min_num",
|
||||
"max_num",
|
||||
"block_counts",
|
||||
"collapsed",
|
||||
]
|
||||
|
||||
|
||||
class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass):
|
||||
pass
|
||||
|
||||
|
||||
class StreamValue(MutableSequence):
|
||||
"""
|
||||
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).
|
||||
"""
|
||||
|
||||
class StreamChild(BoundBlock):
|
||||
"""
|
||||
Iterating over (or indexing into) a StreamValue returns instances of StreamChild.
|
||||
These are wrappers for the individual data items in the stream, extending BoundBlock
|
||||
(which keeps track of the data item's corresponding Block definition object, and provides
|
||||
the `render` method to render itself with a template) with an `id` property (a UUID
|
||||
assigned to the item - this is managed by the enclosing StreamBlock and is not a property
|
||||
of blocks in general) and a `block_type` property.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.id = kwargs.pop("id")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@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 get_prep_value(self):
|
||||
return {
|
||||
"type": self.block_type,
|
||||
"value": self.block.get_prep_value(self.value),
|
||||
"id": self.id,
|
||||
}
|
||||
|
||||
def _as_tuple(self):
|
||||
if self.id:
|
||||
return (self.block.name, self.value, self.id)
|
||||
else:
|
||||
return (self.block.name, self.value)
|
||||
|
||||
class RawDataView(MutableSequence):
|
||||
"""
|
||||
Internal helper class to present the stream data in raw JSONish format. For backwards
|
||||
compatibility with old code that manipulated StreamValue.stream_data, this is considered
|
||||
mutable to some extent, with the proviso that once the BoundBlock representation has been
|
||||
accessed, any changes to fields within raw data will not propagate back to the BoundBlock
|
||||
and will not be saved back when calling get_prep_value.
|
||||
"""
|
||||
|
||||
def __init__(self, stream_value):
|
||||
self.stream_value = stream_value
|
||||
|
||||
def __getitem__(self, i):
|
||||
item = self.stream_value._raw_data[i]
|
||||
if item is None:
|
||||
# reconstruct raw data from the bound block
|
||||
item = self.stream_value._bound_blocks[i].get_prep_value()
|
||||
self.stream_value._raw_data[i] = item
|
||||
|
||||
return item
|
||||
|
||||
def __len__(self):
|
||||
return len(self.stream_value._raw_data)
|
||||
|
||||
def __setitem__(self, i, item):
|
||||
self.stream_value._raw_data[i] = item
|
||||
# clear the cached bound_block for this item
|
||||
self.stream_value._bound_blocks[i] = None
|
||||
|
||||
def __delitem__(self, i):
|
||||
# same as deletion on the stream itself - delete both the raw and bound_block data
|
||||
del self.stream_value[i]
|
||||
|
||||
def insert(self, i, item):
|
||||
self.stream_value._raw_data.insert(i, item)
|
||||
self.stream_value._bound_blocks.insert(i, None)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(list(self))
|
||||
|
||||
class BlockNameLookup(Mapping):
|
||||
"""
|
||||
Dict-like object returned from `blocks_by_name`, for looking up a stream's blocks by name.
|
||||
Uses lazy evaluation on access, so that we're not redundantly constructing StreamChild
|
||||
instances for blocks of different names.
|
||||
"""
|
||||
|
||||
def __init__(self, stream_value, find_all=True):
|
||||
self.stream_value = stream_value
|
||||
self.block_names = stream_value.stream_block.child_blocks.keys()
|
||||
self.find_all = (
|
||||
find_all # whether to return all results rather than just the first
|
||||
)
|
||||
|
||||
def __getitem__(self, block_name):
|
||||
result = [] if self.find_all else None
|
||||
|
||||
if block_name not in self.block_names:
|
||||
# skip the search and return an empty result
|
||||
return result
|
||||
|
||||
for i in range(len(self.stream_value)):
|
||||
# Skip over blocks that have not yet been instantiated from _raw_data and are of
|
||||
# different names to the one we're looking for
|
||||
if (
|
||||
self.stream_value._bound_blocks[i] is None
|
||||
and self.stream_value._raw_data[i]["type"] != block_name
|
||||
):
|
||||
continue
|
||||
|
||||
block = self.stream_value[i]
|
||||
if block.block_type == block_name:
|
||||
if self.find_all:
|
||||
result.append(block)
|
||||
else:
|
||||
return block
|
||||
|
||||
return result
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.block_names
|
||||
|
||||
def __len__(self):
|
||||
return len(self.block_names)
|
||||
|
||||
def __init__(self, stream_block, stream_data, is_lazy=False, raw_text=None):
|
||||
"""
|
||||
Construct a StreamValue linked to the given StreamBlock,
|
||||
with child values given in stream_data.
|
||||
|
||||
Passing is_lazy=True means that stream_data is raw JSONish data as stored
|
||||
in the database, and needs to be converted to native values
|
||||
(using block.to_python()) when accessed. In this mode, stream_data is a
|
||||
list of dicts, each containing 'type' and 'value' keys.
|
||||
|
||||
Passing is_lazy=False means that stream_data consists of immediately usable
|
||||
native values. In this mode, stream_data is a list of (type_name, value)
|
||||
or (type_name, value, id) tuples.
|
||||
|
||||
raw_text exists solely as a way of representing StreamField content that is
|
||||
not valid JSON; this may legitimately occur if an existing text field is
|
||||
migrated to a StreamField. In this situation we return a blank StreamValue
|
||||
with the raw text accessible under the `raw_text` attribute, so that migration
|
||||
code can be rewritten to convert it as desired.
|
||||
"""
|
||||
self.stream_block = (
|
||||
stream_block # the StreamBlock object that handles this value
|
||||
)
|
||||
self.is_lazy = is_lazy
|
||||
self.raw_text = raw_text
|
||||
|
||||
if is_lazy:
|
||||
# store raw stream data in _raw_data; on retrieval it will be converted to a native
|
||||
# value (via block.to_python) and wrapped as a StreamValue, and cached in _bound_blocks.
|
||||
self._raw_data = stream_data
|
||||
self._bound_blocks = [None] * len(stream_data)
|
||||
else:
|
||||
# store native stream data in _bound_blocks; on serialization it will be converted to
|
||||
# a JSON-ish representation via block.get_prep_value.
|
||||
self._raw_data = [None] * len(stream_data)
|
||||
self._bound_blocks = [
|
||||
self._construct_stream_child(item) for item in stream_data
|
||||
]
|
||||
|
||||
def _construct_stream_child(self, item):
|
||||
"""
|
||||
Create a StreamChild instance from a (type, value, id) or (type, value) tuple,
|
||||
or return item if it's already a StreamChild
|
||||
"""
|
||||
if isinstance(item, StreamValue.StreamChild):
|
||||
return item
|
||||
|
||||
try:
|
||||
type_name, value, block_id = item
|
||||
except ValueError:
|
||||
type_name, value = item
|
||||
block_id = None
|
||||
|
||||
block_def = self.stream_block.child_blocks[type_name]
|
||||
return StreamValue.StreamChild(block_def, value, id=block_id)
|
||||
|
||||
def __getitem__(self, i):
|
||||
if isinstance(i, slice):
|
||||
start, stop, step = i.indices(len(self._bound_blocks))
|
||||
return [self[j] for j in range(start, stop, step)]
|
||||
|
||||
if self._bound_blocks[i] is None:
|
||||
raw_value = self._raw_data[i]
|
||||
self._prefetch_blocks(raw_value["type"])
|
||||
|
||||
return self._bound_blocks[i]
|
||||
|
||||
def __setitem__(self, i, item):
|
||||
self._bound_blocks[i] = self._construct_stream_child(item)
|
||||
|
||||
def __delitem__(self, i):
|
||||
del self._bound_blocks[i]
|
||||
del self._raw_data[i]
|
||||
|
||||
def insert(self, i, item):
|
||||
self._bound_blocks.insert(i, self._construct_stream_child(item))
|
||||
self._raw_data.insert(i, None)
|
||||
|
||||
@cached_property
|
||||
def raw_data(self):
|
||||
return StreamValue.RawDataView(self)
|
||||
|
||||
def _prefetch_blocks(self, type_name):
|
||||
"""
|
||||
Populate _bound_blocks with all items in this stream of type `type_name` that exist in
|
||||
_raw_data but do not already exist in _bound_blocks.
|
||||
|
||||
Fetching is done via the block's bulk_to_python method, so that database lookups are
|
||||
batched into a single query where possible.
|
||||
"""
|
||||
child_block = self.stream_block.child_blocks[type_name]
|
||||
# create a mapping of all the child blocks matching the given block type,
|
||||
# mapping (index within the stream) => (raw block value)
|
||||
raw_values = OrderedDict(
|
||||
(i, raw_item["value"])
|
||||
for i, raw_item in enumerate(self._raw_data)
|
||||
if raw_item["type"] == type_name and self._bound_blocks[i] is None
|
||||
)
|
||||
# pass the raw block values to bulk_to_python as a list
|
||||
converted_values = child_block.bulk_to_python(raw_values.values())
|
||||
|
||||
# reunite the converted values with their stream indexes, along with the block ID
|
||||
# if one exists
|
||||
for i, value in zip(raw_values.keys(), converted_values):
|
||||
self._bound_blocks[i] = StreamValue.StreamChild(
|
||||
child_block, value, id=self._raw_data[i].get("id")
|
||||
)
|
||||
|
||||
def get_prep_value(self):
|
||||
prep_value = []
|
||||
|
||||
for i, item in enumerate(self._bound_blocks):
|
||||
if item:
|
||||
# Convert the native value back into raw JSONish data
|
||||
if not item.id:
|
||||
item.id = str(uuid.uuid4())
|
||||
|
||||
prep_value.append(item.get_prep_value())
|
||||
else:
|
||||
# item has not been converted to a BoundBlock, so its _raw_data entry is
|
||||
# still usable (but ensure it has an ID before returning it)
|
||||
|
||||
raw_item = self._raw_data[i]
|
||||
if not raw_item.get("id"):
|
||||
raw_item["id"] = str(uuid.uuid4())
|
||||
|
||||
prep_value.append(raw_item)
|
||||
|
||||
return prep_value
|
||||
|
||||
def blocks_by_name(self, block_name=None):
|
||||
lookup = StreamValue.BlockNameLookup(self, find_all=True)
|
||||
if block_name:
|
||||
return lookup[block_name]
|
||||
else:
|
||||
return lookup
|
||||
|
||||
def first_block_by_name(self, block_name=None):
|
||||
lookup = StreamValue.BlockNameLookup(self, find_all=False)
|
||||
if block_name:
|
||||
return lookup[block_name]
|
||||
else:
|
||||
return lookup
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, StreamValue) or len(other) != len(self):
|
||||
return False
|
||||
|
||||
# scan both lists for non-matching items
|
||||
for i in range(0, len(self)):
|
||||
if self._bound_blocks[i] is None and other._bound_blocks[i] is None:
|
||||
# compare raw values as a shortcut to save the conversion step
|
||||
if self._raw_data[i] != other._raw_data[i]:
|
||||
return False
|
||||
else:
|
||||
this_item = self[i]
|
||||
other_item = other[i]
|
||||
if (
|
||||
this_item.block_type != other_item.block_type
|
||||
or this_item.id != other_item.id
|
||||
or this_item.value != other_item.value
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __len__(self):
|
||||
return len(self._bound_blocks)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{type(self).__name__} {list(self)!r}>"
|
||||
|
||||
def render_as_block(self, context=None):
|
||||
return self.stream_block.render(self, context=context)
|
||||
|
||||
def __html__(self):
|
||||
return self.stream_block.render(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.__html__()
|
||||
|
||||
@staticmethod
|
||||
def _deserialize_pickle_value(app_label, model_name, field_name, field_value):
|
||||
"""Returns StreamValue from pickled data"""
|
||||
field = _load_field(app_label, model_name, field_name)
|
||||
return field.to_python(field_value)
|
||||
|
||||
def __reduce__(self):
|
||||
try:
|
||||
stream_field = self._stream_field
|
||||
except AttributeError:
|
||||
raise PickleError(
|
||||
"StreamValue can only be pickled if it is associated with a StreamField"
|
||||
)
|
||||
|
||||
return (
|
||||
self._deserialize_pickle_value,
|
||||
(
|
||||
stream_field.model._meta.app_label,
|
||||
stream_field.model._meta.object_name,
|
||||
stream_field.name,
|
||||
self.get_prep_value(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class StreamBlockAdapter(Adapter):
|
||||
js_constructor = "wagtail.blocks.StreamBlock"
|
||||
|
||||
def js_args(self, block):
|
||||
meta = {
|
||||
"label": block.label,
|
||||
"required": block.required,
|
||||
"icon": block.meta.icon,
|
||||
"classname": block.meta.form_classname,
|
||||
"maxNum": block.meta.max_num,
|
||||
"minNum": block.meta.min_num,
|
||||
"blockCounts": block.meta.block_counts,
|
||||
"collapsed": block.meta.collapsed,
|
||||
"strings": {
|
||||
"MOVE_UP": _("Move up"),
|
||||
"MOVE_DOWN": _("Move down"),
|
||||
"DUPLICATE": _("Duplicate"),
|
||||
"DELETE": _("Delete"),
|
||||
"ADD": _("Add"),
|
||||
},
|
||||
}
|
||||
help_text = getattr(block.meta, "help_text", None)
|
||||
if help_text:
|
||||
meta["helpText"] = help_text
|
||||
meta["helpIcon"] = get_help_icon()
|
||||
|
||||
return [
|
||||
block.name,
|
||||
block.grouped_child_blocks(),
|
||||
{
|
||||
name: child_block.get_form_state(child_block.get_default())
|
||||
for name, child_block in block.child_blocks.items()
|
||||
},
|
||||
meta,
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
return forms.Media(
|
||||
js=[
|
||||
versioned_static("wagtailadmin/js/telepath/blocks.js"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
register(StreamBlockAdapter(), StreamBlock)
|
||||
435
env/lib/python3.10/site-packages/wagtail/blocks/struct_block.py
vendored
Normal file
435
env/lib/python3.10/site-packages/wagtail/blocks/struct_block.py
vendored
Normal file
@@ -0,0 +1,435 @@
|
||||
import collections
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms.utils import ErrorList
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from wagtail.admin.staticfiles import versioned_static
|
||||
from wagtail.telepath import Adapter, register
|
||||
|
||||
from .base import (
|
||||
Block,
|
||||
BoundBlock,
|
||||
DeclarativeSubBlocksMetaclass,
|
||||
get_error_json_data,
|
||||
get_error_list_json_data,
|
||||
get_help_icon,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BaseStructBlock",
|
||||
"StructBlock",
|
||||
"StructValue",
|
||||
"StructBlockValidationError",
|
||||
]
|
||||
|
||||
|
||||
class StructBlockValidationError(ValidationError):
|
||||
def __init__(self, block_errors=None, non_block_errors=None):
|
||||
# non_block_errors may be passed here as an ErrorList, a plain list (of strings or
|
||||
# ValidationErrors), or None.
|
||||
# Normalise it to be an ErrorList, which provides an as_data() method that consistently
|
||||
# returns a flat list of ValidationError objects.
|
||||
self.non_block_errors = ErrorList(non_block_errors)
|
||||
|
||||
# block_errors may be passed here as None, or a dict keyed by the names of the child blocks
|
||||
# with errors.
|
||||
# Items in this list / dict may be:
|
||||
# - a ValidationError instance (potentially a subclass such as StructBlockValidationError)
|
||||
# - an ErrorList containing a single ValidationError
|
||||
# - a plain list containing a single ValidationError
|
||||
# All representations will be normalised to a dict of ValidationError instances,
|
||||
# which is also the preferred format for the original argument to be in.
|
||||
self.block_errors = {}
|
||||
if block_errors is None:
|
||||
pass
|
||||
else:
|
||||
for name, val in block_errors.items():
|
||||
if isinstance(val, ErrorList):
|
||||
self.block_errors[name] = val.as_data()[0]
|
||||
elif isinstance(val, list):
|
||||
self.block_errors[name] = val[0]
|
||||
else:
|
||||
self.block_errors[name] = val
|
||||
|
||||
super().__init__("Validation error in StructBlock")
|
||||
|
||||
def as_json_data(self):
|
||||
result = {}
|
||||
if self.non_block_errors:
|
||||
result["messages"] = get_error_list_json_data(self.non_block_errors)
|
||||
if self.block_errors:
|
||||
result["blockErrors"] = {
|
||||
name: get_error_json_data(error)
|
||||
for (name, error) in self.block_errors.items()
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class StructValue(collections.OrderedDict):
|
||||
"""A class that generates a StructBlock value from provided sub-blocks"""
|
||||
|
||||
def __init__(self, block, *args):
|
||||
super().__init__(*args)
|
||||
self.block = block
|
||||
|
||||
def __html__(self):
|
||||
return self.block.render(self)
|
||||
|
||||
def render_as_block(self, context=None):
|
||||
return self.block.render(self, context=context)
|
||||
|
||||
@cached_property
|
||||
def bound_blocks(self):
|
||||
return collections.OrderedDict(
|
||||
[
|
||||
(name, block.bind(self.get(name)))
|
||||
for name, block in self.block.child_blocks.items()
|
||||
]
|
||||
)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self.block,), None, None, iter(self.items()))
|
||||
|
||||
|
||||
class PlaceholderBoundBlock(BoundBlock):
|
||||
"""
|
||||
Provides a render_form method that outputs a block placeholder, for use in custom form_templates
|
||||
"""
|
||||
|
||||
def render_form(self):
|
||||
return format_html('<div data-structblock-child="{}"></div>', self.block.name)
|
||||
|
||||
|
||||
class BaseStructBlock(Block):
|
||||
def __init__(self, local_blocks=None, search_index=True, **kwargs):
|
||||
self._constructor_kwargs = kwargs
|
||||
self.search_index = search_index
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
|
||||
self.child_blocks = self.base_blocks.copy()
|
||||
if local_blocks:
|
||||
for name, block in local_blocks:
|
||||
block.set_name(name)
|
||||
self.child_blocks[name] = block
|
||||
|
||||
@classmethod
|
||||
def construct_from_lookup(cls, lookup, child_blocks, **kwargs):
|
||||
if child_blocks:
|
||||
child_blocks = [
|
||||
(name, lookup.get_block(index)) for name, index in child_blocks
|
||||
]
|
||||
return cls(child_blocks, **kwargs)
|
||||
|
||||
def get_default(self):
|
||||
"""
|
||||
Any default value passed in the constructor or self.meta is going to be a dict
|
||||
rather than a StructValue; for consistency, we need to convert it to a StructValue
|
||||
for StructBlock to work with
|
||||
"""
|
||||
|
||||
return self.normalize(
|
||||
{
|
||||
name: self.meta.default[name]
|
||||
if name in self.meta.default
|
||||
else block.get_default()
|
||||
for name, block in self.child_blocks.items()
|
||||
}
|
||||
)
|
||||
|
||||
def value_from_datadict(self, data, files, prefix):
|
||||
return self._to_struct_value(
|
||||
[
|
||||
(
|
||||
name,
|
||||
block.value_from_datadict(data, files, f"{prefix}-{name}"),
|
||||
)
|
||||
for name, block in self.child_blocks.items()
|
||||
]
|
||||
)
|
||||
|
||||
def value_omitted_from_data(self, data, files, prefix):
|
||||
return all(
|
||||
block.value_omitted_from_data(data, files, f"{prefix}-{name}")
|
||||
for name, block in self.child_blocks.items()
|
||||
)
|
||||
|
||||
def clean(self, value):
|
||||
result = [] # build up a list of (name, value) tuples to be passed to the StructValue constructor
|
||||
errors = {}
|
||||
for name, val in value.items():
|
||||
try:
|
||||
result.append((name, self.child_blocks[name].clean(val)))
|
||||
except ValidationError as e:
|
||||
errors[name] = e
|
||||
|
||||
if errors:
|
||||
raise StructBlockValidationError(errors)
|
||||
|
||||
return self._to_struct_value(result)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Recursively call to_python on children and return as a StructValue"""
|
||||
return self._to_struct_value(
|
||||
[
|
||||
(
|
||||
name,
|
||||
(
|
||||
child_block.to_python(value[name])
|
||||
if name in value
|
||||
else child_block.get_default()
|
||||
),
|
||||
# NB the result of get_default is NOT passed through to_python, as it's expected
|
||||
# to be in the block's native type already
|
||||
)
|
||||
for name, child_block in self.child_blocks.items()
|
||||
]
|
||||
)
|
||||
|
||||
def bulk_to_python(self, values):
|
||||
# values is a list of dicts; split this into a series of per-subfield lists so that we can
|
||||
# call bulk_to_python on each subfield
|
||||
|
||||
values_by_subfield = {}
|
||||
for name, child_block in self.child_blocks.items():
|
||||
# We need to keep track of which dicts actually have an item for this field, as missing
|
||||
# values will be populated with child_block.get_default(); this is expected to be a
|
||||
# value in the block's native type, and should therefore not undergo conversion via
|
||||
# bulk_to_python.
|
||||
indexes = []
|
||||
raw_values = []
|
||||
for i, val in enumerate(values):
|
||||
if name in val:
|
||||
indexes.append(i)
|
||||
raw_values.append(val[name])
|
||||
|
||||
converted_values = child_block.bulk_to_python(raw_values)
|
||||
# create a mapping from original index to converted value
|
||||
converted_values_by_index = dict(zip(indexes, converted_values))
|
||||
|
||||
# now loop over all list indexes, falling back on the default for any indexes not in
|
||||
# the mapping, to arrive at the final list for this subfield
|
||||
values_by_subfield[name] = []
|
||||
for i in range(0, len(values)):
|
||||
try:
|
||||
converted_value = converted_values_by_index[i]
|
||||
except KeyError:
|
||||
converted_value = child_block.get_default()
|
||||
|
||||
values_by_subfield[name].append(converted_value)
|
||||
|
||||
# now form the final list of StructValues, with each one constructed by taking the
|
||||
# appropriately-indexed item from all of the per-subfield lists
|
||||
return [
|
||||
self._to_struct_value(
|
||||
{name: values_by_subfield[name][i] for name in self.child_blocks.keys()}
|
||||
)
|
||||
for i in range(0, len(values))
|
||||
]
|
||||
|
||||
def _to_struct_value(self, block_items):
|
||||
"""Return a Structvalue representation of the sub-blocks in this block"""
|
||||
return self.meta.value_class(self, block_items)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
"""Recursively call get_prep_value on children and return as a plain dict"""
|
||||
return {
|
||||
name: self.child_blocks[name].get_prep_value(val)
|
||||
for name, val in value.items()
|
||||
}
|
||||
|
||||
def normalize(self, value):
|
||||
if isinstance(value, self.meta.value_class):
|
||||
return value
|
||||
|
||||
return self._to_struct_value(
|
||||
{k: self.child_blocks[k].normalize(v) for k, v in value.items()}
|
||||
)
|
||||
|
||||
def get_form_state(self, value):
|
||||
return {
|
||||
name: self.child_blocks[name].get_form_state(val)
|
||||
for name, val in value.items()
|
||||
}
|
||||
|
||||
def get_api_representation(self, value, context=None):
|
||||
"""Recursively call get_api_representation on children and return as a plain dict"""
|
||||
return {
|
||||
name: self.child_blocks[name].get_api_representation(val, context=context)
|
||||
for name, val in value.items()
|
||||
}
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
if not self.search_index:
|
||||
return []
|
||||
content = []
|
||||
|
||||
for name, block in self.child_blocks.items():
|
||||
content.extend(
|
||||
block.get_searchable_content(value.get(name, block.get_default()))
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
def extract_references(self, value):
|
||||
for name, block in self.child_blocks.items():
|
||||
for model, object_id, model_path, content_path in block.extract_references(
|
||||
value.get(name, block.get_default())
|
||||
):
|
||||
model_path = f"{name}.{model_path}" if model_path else name
|
||||
content_path = f"{name}.{content_path}" if content_path else name
|
||||
yield model, object_id, model_path, content_path
|
||||
|
||||
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.
|
||||
"""
|
||||
if path_elements:
|
||||
name, *remaining_elements = path_elements
|
||||
try:
|
||||
child_block = self.child_blocks[name]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
child_value = value.get(name, child_block.get_default())
|
||||
return child_block.get_block_by_content_path(
|
||||
child_value, remaining_elements
|
||||
)
|
||||
else:
|
||||
# an empty path refers to the struct as a whole
|
||||
return self.bind(value)
|
||||
|
||||
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.blocks.StructBlock"
|
||||
args = [list(self.child_blocks.items())]
|
||||
kwargs = self._constructor_kwargs
|
||||
return (path, args, kwargs)
|
||||
|
||||
def deconstruct_with_lookup(self, lookup):
|
||||
path = "wagtail.blocks.StructBlock"
|
||||
args = [
|
||||
[
|
||||
(name, lookup.add_block(block))
|
||||
for name, block in self.child_blocks.items()
|
||||
]
|
||||
]
|
||||
kwargs = self._constructor_kwargs
|
||||
return (path, args, kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super().check(**kwargs)
|
||||
for name, child_block in self.child_blocks.items():
|
||||
errors.extend(child_block.check(**kwargs))
|
||||
errors.extend(child_block._check_name(**kwargs))
|
||||
|
||||
return errors
|
||||
|
||||
def render_basic(self, value, context=None):
|
||||
return format_html(
|
||||
"<dl>\n{}\n</dl>",
|
||||
format_html_join("\n", " <dt>{}</dt>\n <dd>{}</dd>", value.items()),
|
||||
)
|
||||
|
||||
def render_form_template(self):
|
||||
# Support for custom form_template options in meta. Originally form_template would have been
|
||||
# invoked once for each occurrence of this block in the stream data, but this rendering now
|
||||
# happens client-side, so we need to turn the Django template into one that can be used by
|
||||
# the client-side code. This is done by rendering it up-front with placeholder objects as
|
||||
# child blocks - these return <div data-structblock-child="first-name"></div> from their
|
||||
# render_form_method.
|
||||
# The change to client-side rendering means that the `value` and `errors` arguments on
|
||||
# `get_form_context` no longer receive real data; these are passed the block's default value
|
||||
# and None respectively.
|
||||
context = self.get_form_context(
|
||||
self.get_default(), prefix="__PREFIX__", errors=None
|
||||
)
|
||||
return mark_safe(render_to_string(self.meta.form_template, context))
|
||||
|
||||
def get_form_context(self, value, prefix="", errors=None):
|
||||
return {
|
||||
"children": collections.OrderedDict(
|
||||
[
|
||||
(
|
||||
name,
|
||||
PlaceholderBoundBlock(
|
||||
block, value.get(name), prefix=f"{prefix}-{name}"
|
||||
),
|
||||
)
|
||||
for name, block in self.child_blocks.items()
|
||||
]
|
||||
),
|
||||
"help_text": getattr(self.meta, "help_text", None),
|
||||
"classname": self.meta.form_classname,
|
||||
"block_definition": self,
|
||||
"prefix": prefix,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
default = {}
|
||||
form_classname = "struct-block"
|
||||
form_template = None
|
||||
value_class = StructValue
|
||||
label_format = None
|
||||
# No icon specified here, because that depends on the purpose that the
|
||||
# block is being used for. Feel encouraged to specify an icon in your
|
||||
# descendant block type
|
||||
icon = "placeholder"
|
||||
|
||||
|
||||
class StructBlock(BaseStructBlock, metaclass=DeclarativeSubBlocksMetaclass):
|
||||
pass
|
||||
|
||||
|
||||
class StructBlockAdapter(Adapter):
|
||||
js_constructor = "wagtail.blocks.StructBlock"
|
||||
|
||||
def js_args(self, block):
|
||||
meta = {
|
||||
"label": block.label,
|
||||
"required": block.required,
|
||||
"icon": block.meta.icon,
|
||||
"classname": block.meta.form_classname,
|
||||
}
|
||||
|
||||
help_text = getattr(block.meta, "help_text", None)
|
||||
if help_text:
|
||||
meta["helpText"] = help_text
|
||||
meta["helpIcon"] = get_help_icon()
|
||||
|
||||
if block.meta.form_template:
|
||||
meta["formTemplate"] = block.render_form_template()
|
||||
|
||||
if block.meta.label_format:
|
||||
meta["labelFormat"] = block.meta.label_format
|
||||
|
||||
return [
|
||||
block.name,
|
||||
block.child_blocks.values(),
|
||||
meta,
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
return forms.Media(
|
||||
js=[
|
||||
versioned_static("wagtailadmin/js/telepath/blocks.js"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
register(StructBlockAdapter(), StructBlock)
|
||||
Reference in New Issue
Block a user