Skip to content

Commit

Permalink
Add support for Wagtail 6.3 (and ImageBlock) (#840)
Browse files Browse the repository at this point in the history
* Add Wagtail 6.3 / Python 3.13 to the tox matrix
* Add support for `ImageBlock`
  • Loading branch information
zerolab authored Dec 19, 2024
1 parent caf14b8 commit 2168241
Show file tree
Hide file tree
Showing 11 changed files with 1,002 additions and 13 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ env:
TOX_TESTENV_PASSENV: FORCE_COLOR
PIP_DISABLE_PIP_VERSION_CHECK: '1'
PIP_NO_PYTHON_VERSION_WARNING: '1'
PYTHON_LATEST: '3.11'
PYTHON_LATEST: '3.13'

jobs:
test-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.10', '3.11', '3.12']
python: ['3.10', '3.11', '3.12', '3.13']
database: ['sqlite']

steps:
Expand Down Expand Up @@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.9', '3.10', '3.11', '3.12']
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
database: ['postgres']

services:
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
- test-postgres
strategy:
matrix:
python: ['3.12']
python: ['3.13']

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ __pycache__/

.DS_Store
.ruff_cache
.coverage.*
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
Expand Down Expand Up @@ -58,7 +59,7 @@ testing = [
"coverage>=7.0,<8.0"
]
linting = [
"pre-commit>=3.4,<4"
"pre-commit>=4.0,<5"
]

[project.urls]
Expand Down
18 changes: 10 additions & 8 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
min_version = 4.11

envlist =
python{3.9,3.10,3.11,3.12}-django4.2-wagtail{5.2,6.1,6.2}-postgres15
python{3.10,3.11,3.12}-django5.0-wagtail{5.2,6.1,6.2}-postgres15
python{3.12}-django5.1-wagtail{6.2}-postgres15
python{3.9,3.10,3.11,3.12}-django4.2-wagtail{5.2,6.2,6.3}-postgres15
python{3.10,3.11,3.12}-django5.0-wagtail{5.2,6.2,6.3}-postgres15
python{3.13}-django5.1-wagtail{6.3}-postgres15
# note: we're running a subset of the test with sqlite
python3.10-django4.2-wagtail{5.2}-sqlite
python3.11-django5.0-wagtail{6.1}-sqlite
python3.12-django5.1-wagtail{6.2}-sqlite
python3.11-django4.2-wagtail{5.2}-sqlite
python3.12-django5.0-wagtail{6.2}-sqlite
python3.13-django5.1-wagtail{6.3}-sqlite

[gh-actions]
python =
3.9: python3.9
3.10: python3.10
3.11: python3.11
3.12: python3.12
3.13: python3.13

[gh-actions:env]
DATABASE =
Expand All @@ -36,6 +37,7 @@ setenv =
PYTHONDEVMODE = 1
# use the Python 3.12+ sys.monitoring
python3.12: COVERAGE_CORE=sysmon
python3.13: COVERAGE_CORE=sysmon

extras = testing

Expand All @@ -45,8 +47,8 @@ deps =
django5.1: Django>=5.1,<5.2

wagtail5.2: wagtail>=5.2.2,<5.3
wagtail6.1: wagtail>=6.1,<6.2
wagtail6.2: wagtail>=6.2,<6.3
wagtail6.3: wagtail>=6.3,<6.4
wagtailmain: git+https://github.com/wagtail/wagtail.git

postgres: psycopg2>=2.9
Expand Down Expand Up @@ -94,7 +96,7 @@ setenv =
INTERACTIVE = 1

[testenv:migrations]
basepython = python3.11
basepython = python3.12

# always generate with the min supported versions
deps =
Expand Down
13 changes: 13 additions & 0 deletions wagtail_localize/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import polib

from django.apps import apps
from django.conf import settings
from django.contrib.admin.utils import quote
from django.contrib.contenttypes.models import ContentType
Expand Down Expand Up @@ -36,6 +37,7 @@
get_serializable_data_for_fields,
model_from_serializable_data,
)
from wagtail import VERSION as WAGTAIL_VERSION
from wagtail import blocks
from wagtail.blocks.list_block import ListValue
from wagtail.coreutils import find_available_slug
Expand Down Expand Up @@ -1559,6 +1561,17 @@ def get_field_path_from_streamfield_block(value, path_components):
block_def = value.stream_block.child_blocks[block_type]
block_value = block.value

if WAGTAIL_VERSION >= (6, 3) and apps.is_installed(
"wagtail.images"
):
from wagtail.images.blocks import ImageBlock

if isinstance(block_def, ImageBlock):
# the path components are ["the_image_block_field_name", "alt_text"]
# so there is no need for further processing as this will return
# ["image_block", "alt_text"]
return [block_type] + path_components[1:]

if isinstance(
block_def,
(blocks.StructBlock, blocks.StreamBlock, blocks.ListBlock),
Expand Down
37 changes: 37 additions & 0 deletions wagtail_localize/segments/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 import blocks
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page, TranslatableMixin
Expand Down Expand Up @@ -52,6 +53,14 @@ def handle_block(self, block_type, block_value, raw_value=None):
else:
return []

if WAGTAIL_VERSION >= (6, 3) and apps.is_installed("wagtail.images"):
from wagtail.images.blocks import ImageBlock

if isinstance(block_type, ImageBlock):
return self.handle_image_block(
block_type, block_value, raw_value=raw_value
)

if hasattr(block_type, "get_translatable_segments"):
return block_type.get_translatable_segments(block_value)

Expand Down Expand Up @@ -181,6 +190,34 @@ def handle_list_block(self, list_block, raw_value=None):
)
return segments

def handle_image_block(self, block, image_block_value, raw_value=None):
"""
Handles the Wagtail 6.3+ ImageBlock, which is a specialized StructBlock with
image (ImageChooserBlock), alt_text (CharBlock) and decorative (BooleanBlock).
However, unlike a StructBlock, it deconstructs to an Image instance, rather than
the usual StructValue, so we need add special handling.
"""
segments = []

for field_name, block_type in block.child_blocks.items():
try:
block_raw_value = raw_value["value"].get(field_name)
block_value = (
image_block_value if field_name == "image" else block_raw_value
)
except (KeyError, TypeError):
# e.g. raw_value is None, or is that from chooser
block_raw_value = None
block_value = None
segments.extend(
segment.wrap(field_name)
for segment in self.handle_block(
block_type, block_value, raw_value=block_raw_value
)
)

return segments

def handle_stream_block(self, stream_block):
segments = []

Expand Down
32 changes: 32 additions & 0 deletions wagtail_localize/segments/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.apps import apps
from django.db import models
from wagtail import VERSION as WAGTAIL_VERSION
from wagtail import blocks
from wagtail.fields import RichTextField, StreamField
from wagtail.rich_text import RichText
Expand Down Expand Up @@ -129,6 +130,12 @@ def handle_block(self, block_type, block_value, segments):
if isinstance(segment, OverridableSegmentValue):
return EmbedValue(segment.data)

if WAGTAIL_VERSION >= (6, 3) and apps.is_installed("wagtail.images"):
from wagtail.images.blocks import ImageBlock

if isinstance(block_type, ImageBlock):
return self.handle_image_block(block_type, block_value, segments)

if hasattr(block_type, "restore_translated_segments"):
return block_type.restore_translated_segments(block_value, segments)

Expand Down Expand Up @@ -211,6 +218,31 @@ def handle_list_block(self, list_block, segments):

return list_block

def handle_image_block(self, block, image_block_value, segments):
"""
The Wagtail 6.3+ ImageBlock deconstructs to an Image instance with the
contextual alt text / decorative values set based on the ImageBlock selection.
"""
segments_by_field = defaultdict(list)

for segment in segments:
field_name, segment = segment.unwrap()
segments_by_field[field_name].append(segment)

# ImageBlock field -> Image field.
field_map = {"alt_text": "contextual_alt_text", "decorative": "decorative"}
for field_name, segments in segments_by_field.items():
if segments:
block_type = block.child_blocks[field_name]
value = self.handle_block(
block_type,
getattr(image_block_value, field_map[field_name]),
segments,
)
setattr(image_block_value, field_map[field_name], value)

return image_block_value

def get_stream_block_child_data(self, stream_block, block_uuid):
for stream_child in stream_block:
if stream_child.id == block_uuid:
Expand Down
34 changes: 34 additions & 0 deletions wagtail_localize/segments/tests/test_segment_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from wagtail import VERSION as WAGTAIL_VERSION
from wagtail.blocks import StreamValue
from wagtail.images import get_image_model
from wagtail.images.tests.utils import get_test_image_file
from wagtail.models import Page, Site

from wagtail_localize.segments import (
Expand Down Expand Up @@ -343,6 +346,37 @@ def test_structblock(self):
],
)

@unittest.skipUnless(
WAGTAIL_VERSION >= (6, 3), "ImageBlock was added in Wagtail 6.3"
)
def test_imageblock(self):
block_id = uuid.uuid4()
test_image = get_image_model().objects.create(
title="Test image", file=get_test_image_file()
)
page = make_test_page_with_streamfield_block(
str(block_id),
"test_imageblock",
{
"image": test_image.pk,
"decorative": False,
"alt_text": "Test alt content",
},
)

segments = extract_segments(page)
self.assertEqual(
segments,
[
OverridableSegmentValue(
f"test_streamfield.{block_id}.image", test_image.pk
),
StringSegmentValue(
f"test_streamfield.{block_id}.alt_text", "Test alt content"
),
],
)

def test_listblock(self):
block_id = uuid.uuid4()
page = make_test_page_with_streamfield_block(
Expand Down
51 changes: 51 additions & 0 deletions wagtail_localize/segments/tests/test_segment_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import uuid

from django.test import TestCase
from wagtail import VERSION as WAGTAIL_VERSION
from wagtail.blocks import StreamValue
from wagtail.images import get_image_model
from wagtail.images.tests.utils import get_test_image_file
from wagtail.models import Locale, Page

from wagtail_localize.fields import copy_synchronised_fields
Expand Down Expand Up @@ -516,6 +519,7 @@ def test_blockquoteblock(self):

def test_structblock(self):
block_id = uuid.uuid4()

page = make_test_page_with_streamfield_block(
str(block_id),
"test_structblock",
Expand Down Expand Up @@ -556,6 +560,53 @@ def test_structblock(self):
],
)

@unittest.skipUnless(
WAGTAIL_VERSION >= (6, 3), "ImageBlock was added in Wagtail 6.3"
)
def test_imageblock(self):
block_id = uuid.uuid4()
test_image = get_image_model().objects.create(
title="Test image", file=get_test_image_file()
)
page = make_test_page_with_streamfield_block(
str(block_id),
"test_imageblock",
{"image": test_image.pk, "alt_text": "Some test alt text"},
)

translated_page = page.copy_for_translation(self.locale)

ingest_segments(
page,
translated_page,
self.src_locale,
self.locale,
[
StringSegmentValue(
f"test_streamfield.{block_id}.alt_text",
"Tester le alt_text contenu",
),
],
)

translated_page.save()
translated_page.refresh_from_db()

self.assertEqual(
list(translated_page.test_streamfield.raw_data),
[
{
"id": str(block_id),
"type": "test_imageblock",
"value": {
"image": 1,
"alt_text": "Tester le alt_text contenu",
"decorative": False,
},
}
],
)

def test_listblock(self):
block_id = uuid.uuid4()
page = make_test_page_with_streamfield_block(
Expand Down
Loading

0 comments on commit 2168241

Please sign in to comment.