diff --git a/wagtail_localize/models.py b/wagtail_localize/models.py index 6b0bf90f..efc1c285 100644 --- a/wagtail_localize/models.py +++ b/wagtail_localize/models.py @@ -36,6 +36,7 @@ get_serializable_data_for_fields, model_from_serializable_data, ) +from wagtail import VERSION as WAGTAIL_VERSION from wagtail.core import blocks from wagtail.core.fields import StreamField from wagtail.core.models import ( @@ -1514,6 +1515,10 @@ def get_field_path_from_stream_block(stream_value, path_components): ] + get_field_path_from_stream_block( block.value, path_components[1:] ) + elif isinstance( + block_def, blocks.ListBlock + ) and WAGTAIL_VERSION >= (2, 16): + return [block.block_type, path_components[1]] else: return [block.block_type] diff --git a/wagtail_localize/segments/extract.py b/wagtail_localize/segments/extract.py index 5cbf5c09..56d2ddd7 100644 --- a/wagtail_localize/segments/extract.py +++ b/wagtail_localize/segments/extract.py @@ -2,6 +2,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models from modelcluster.fields import ParentalKey +from wagtail import VERSION as WAGTAIL_VERSION from wagtail.core import blocks from wagtail.core.fields import RichTextField, StreamField from wagtail.core.models import Page, TranslatableMixin @@ -40,7 +41,7 @@ def __init__(self, field, include_overridables=False): self.field = field self.include_overridables = include_overridables - def handle_block(self, block_type, block_value): + def handle_block(self, block_type, block_value, raw_value=None): # Need to check if the app is installed before importing EmbedBlock # See: https://github.com/wagtail/wagtail-localize/issues/309 if apps.is_installed("wagtail.embeds"): @@ -96,7 +97,7 @@ def handle_block(self, block_type, block_value): return self.handle_struct_block(block_value) elif isinstance(block_type, blocks.ListBlock): - return self.handle_list_block(block_value) + return self.handle_list_block(block_value, raw_value) elif isinstance(block_type, blocks.StreamBlock): return self.handle_stream_block(block_value) @@ -132,17 +133,34 @@ def handle_struct_block(self, struct_block): return segments - def handle_list_block(self, list_block): - # TODO - return [] + def handle_list_block(self, list_block, raw_value): + segments = [] + if WAGTAIL_VERSION >= (2, 16): + # Wagtail 2.16 changes ListBlock values to be ListValue objects (i.e. {'value': '', 'id': ''}) + # and will automatically convert from the simple list format used before. However that requires + # the block to be saved. bound_blocks will return ListValue objects, so we need to check that the + # stored value is the new format before extracting segments, othewise the block ids will continue + # to change. + has_block_format = list_block.list_block._item_is_in_block_format( + raw_value["value"][0] + ) + if has_block_format: + for block in list_block.bound_blocks: + segments.extend( + segment.wrap(block.id) + for segment in self.handle_block(block.block, block.value) + ) + return segments def handle_stream_block(self, stream_block): segments = [] - for block in stream_block: + for index, block in enumerate(stream_block): segments.extend( segment.wrap(block.id) - for segment in self.handle_block(block.block, block.value) + for segment in self.handle_block( + block.block, block.value, raw_value=stream_block.raw_data[index] + ) ) return segments diff --git a/wagtail_localize/segments/ingest.py b/wagtail_localize/segments/ingest.py index fa8fed01..99dd2aa8 100644 --- a/wagtail_localize/segments/ingest.py +++ b/wagtail_localize/segments/ingest.py @@ -2,6 +2,7 @@ from django.apps import apps from django.db import models +from wagtail import VERSION as WAGTAIL_VERSION from wagtail.core import blocks from wagtail.core.fields import RichTextField, StreamField from wagtail.core.rich_text import RichText @@ -202,8 +203,20 @@ def handle_struct_block(self, struct_block, segments): return struct_block def handle_list_block(self, list_block, segments): - # TODO - pass + if WAGTAIL_VERSION >= (2, 16): + segments_by_block = defaultdict(list) + + for segment in segments: + block_uuid, segment = segment.unwrap() + segments_by_block[block_uuid].append(segment) + + for block_index, block in enumerate(list_block.bound_blocks): + block_segments = segments_by_block[block.id] + list_block.bound_blocks[block_index].value = self.handle_block( + block.block, block.value, block_segments + ) + + return list_block def get_stream_block_child_data(self, stream_block, block_uuid): for stream_child in stream_block: diff --git a/wagtail_localize/segments/tests/test_segment_extraction.py b/wagtail_localize/segments/tests/test_segment_extraction.py index 26e7b77f..8713dd8a 100644 --- a/wagtail_localize/segments/tests/test_segment_extraction.py +++ b/wagtail_localize/segments/tests/test_segment_extraction.py @@ -3,6 +3,7 @@ from django.core.exceptions import ImproperlyConfigured from django.test import TestCase +from wagtail import VERSION as WAGTAIL_VERSION from wagtail.core.blocks import StreamValue from wagtail.core.models import Page, Site @@ -12,7 +13,10 @@ StringSegmentValue, TemplateSegmentValue, ) -from wagtail_localize.segments.extract import extract_segments +from wagtail_localize.segments.extract import ( + StreamFieldSegmentExtractor, + extract_segments, +) from wagtail_localize.strings import StringValue from wagtail_localize.test.models import ( TestChildObject, @@ -336,24 +340,48 @@ def test_structblock(self): ], ) - @unittest.expectedFailure # Not supported (probably won't ever be due to lack of path stability) + @unittest.skipUnless( + WAGTAIL_VERSION >= (2, 16), + "ListBlocks are supported starting with Wagtail 2.16", + ) def test_listblock(self): block_id = uuid.uuid4() page = make_test_page_with_streamfield_block( - str(block_id), "test_listblock", ["Test content", "Some more test content"] + str(block_id), + "test_listblock", + [ + { + "type": "item", + "value": "Test content", + "id": "11111111-1111-1111-1111-111111111111", + }, + { + "type": "item", + "value": "Some more test content", + "id": "22222222-2222-2222-2222-222222222222", + }, + ], ) + expected_segments = [ + StringSegmentValue(f"test_streamfield.{block_id}.{item.id}", item.value) + for item in page.test_streamfield[0].value.bound_blocks + ] segments = extract_segments(page) + self.assertEqual(segments, expected_segments) - self.assertEqual( - segments, - [ - StringSegmentValue(f"test_streamfield.{block_id}", "Test content"), - StringSegmentValue( - f"test_streamfield.{block_id}", "Some more test content" - ), - ], + @unittest.skipUnless( + WAGTAIL_VERSION >= (2, 16), + "ListBlocks are supported starting with Wagtail 2.16", + ) + def test_listblock_not_extracted_when_not_in_block_format(self): + page = make_test_page_with_streamfield_block( + uuid.uuid4(), "test_listblock", ["Test content", "Some more test content"] ) + segments = StreamFieldSegmentExtractor( + page.test_streamfield + ).handle_stream_block(page.test_streamfield) + self.assertEqual(segments, []) def test_nestedstreamblock(self): block_id = uuid.uuid4() diff --git a/wagtail_localize/segments/tests/test_segment_ingestion.py b/wagtail_localize/segments/tests/test_segment_ingestion.py index 7280b17f..ce12eebe 100644 --- a/wagtail_localize/segments/tests/test_segment_ingestion.py +++ b/wagtail_localize/segments/tests/test_segment_ingestion.py @@ -567,28 +567,31 @@ def test_structblock(self): ], ) - @unittest.expectedFailure # Not supported (probably won't ever be due to lack of path stability) + @unittest.skipUnless( + WAGTAIL_VERSION >= (2, 16), "ListBlocks are supported starting Wagtail 2.16" + ) def test_listblock(self): block_id = uuid.uuid4() page = make_test_page_with_streamfield_block( str(block_id), "test_listblock", ["Test content", "Some more test content"] ) - translated_page = page.copy_for_translation(self.locale) + translated_strings = ["Tester le contenu", "Encore du contenu de test"] + block_ids = [ + item.id for item in translated_page.test_streamfield[0].value.bound_blocks + ] + expected_segments = [ + StringSegmentValue( + f"test_streamfield.{block_id}.{item_id}", + translated_strings[index], + order=index, + ) + for index, item_id in enumerate(block_ids) + ] + ingest_segments( - page, - translated_page, - self.src_locale, - self.locale, - [ - StringSegmentValue( - f"test_streamfield.{block_id}", "Tester le contenu", order=0 - ), - StringSegmentValue( - f"test_streamfield.{block_id}", "Encore du contenu de test", order=1 - ), - ], + page, translated_page, self.src_locale, self.locale, expected_segments ) translated_page.save() @@ -600,7 +603,18 @@ def test_listblock(self): { "id": str(block_id), "type": "test_listblock", - "value": ["Tester le contenu", "Encore du contenu de test"], + "value": [ + { + "type": "item", + "value": "Tester le contenu", + "id": block_ids[0], + }, + { + "type": "item", + "value": "Encore du contenu de test", + "id": block_ids[1], + }, + ], } ], )