Initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user