wagtail/wagtail/images/blocks.py

280 wiersze
9.4 KiB
Python

from django import forms
from django.core.exceptions import ValidationError
from django.template.loader import render_to_string
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from wagtail.admin.compare import BlockComparison, StructBlockComparison
from wagtail.blocks import BooleanBlock, CharBlock, ChooserBlock, StructBlock
from wagtail.blocks.struct_block import StructBlockAdapter, StructBlockValidationError
from wagtail.images.models import AbstractImage
from wagtail.telepath import register
from .shortcuts import get_rendition_or_not_found
class ImageChooserBlock(ChooserBlock):
@cached_property
def target_model(self):
from wagtail.images import get_image_model
return get_image_model()
@cached_property
def widget(self):
from wagtail.images.widgets import AdminImageChooser
return AdminImageChooser()
def render_basic(self, value, context=None):
if value:
return get_rendition_or_not_found(value, "original").img_tag()
else:
return ""
def get_comparison_class(self):
return ImageChooserBlockComparison
class Meta:
icon = "image"
class ImageChooserBlockComparison(BlockComparison):
def htmlvalue(self, val):
return render_to_string(
"wagtailimages/widgets/compare.html",
{
"image_a": val,
"image_b": val,
},
)
def htmldiff(self):
return render_to_string(
"wagtailimages/widgets/compare.html",
{
"image_a": self.val_a,
"image_b": self.val_b,
},
)
class ImageBlock(StructBlock):
"""
An usage of ImageChooserBlock with support for alt text.
For backward compatibility, this block overrides necessary methods to change
the StructValue to be an Image model instance, making it compatible in
places where ImageChooserBlock was used.
"""
image = ImageChooserBlock(required=True)
decorative = BooleanBlock(
default=False, required=False, label=_("Image is decorative")
)
alt_text = CharBlock(required=False, label=_("Alt text"))
def __init__(self, required=True, **kwargs):
super().__init__(
[
("image", ImageChooserBlock(required=required)),
(
"decorative",
BooleanBlock(
default=False, required=False, label=_("Image is decorative")
),
),
("alt_text", CharBlock(required=False, label=_("Alt text"))),
],
**kwargs,
)
def deconstruct(self):
"""
For typical StructBlock subclasses, it makes sense for the deconstructed block object to be a basic StructBlock
with the child blocks passed to the constructor (because this is largely functionally identical to the
subclass, and avoids embedding a reference to a potentially-volatile custom class in migrations).
This is not the case for ImageBlock, as it overrides enough of StructBlock's behaviour that a basic StructBlock
is not a suitable substitute - and also has an incompatible constructor signature (as we don't want to support
passing child blocks to it).
Therefore, we opt out of the standard StructBlock deconstruction behaviour here, and always
deconstruct an ImageBlock as an ImageBlock.
"""
return ("wagtail.images.blocks.ImageBlock", [], self._constructor_kwargs)
def deconstruct_with_lookup(self, lookup):
return self.deconstruct()
@classmethod
def construct_from_lookup(cls, lookup, *args, **kwargs):
return cls(**kwargs)
def get_searchable_content(self, value):
if not self.search_index or not value:
return []
return self.child_blocks["alt_text"].get_searchable_content(
value.contextual_alt_text
)
def _struct_value_to_image(self, struct_value):
image = struct_value.get("image")
decorative = struct_value.get("decorative")
if image:
# If the image is decorative, set alt_text to an empty string
image.contextual_alt_text = (
"" if decorative else struct_value.get("alt_text")
)
image.decorative = decorative
return image
def _image_to_struct_value(self, image):
return {
"image": image,
"alt_text": image and image.contextual_alt_text,
"decorative": image and image.decorative,
}
def to_python(self, value):
# For backward compatibility with ImageChooserBlock
if isinstance(value, int):
image = self.child_blocks["image"].to_python(value)
struct_value = {"image": image, "decorative": False, "alt_text": None}
else:
struct_value = super().to_python(value)
return self._struct_value_to_image(struct_value)
def bulk_to_python(self, values):
values = list(values)
if values and all(value is None or isinstance(value, int) for value in values):
# `values` looks like a list of image IDs and/or None values (as we might encounter
# if an ImageChooserBlock has been changed to an ImageBlock with no data migration)
image_values = self.child_blocks["image"].bulk_to_python(values)
struct_values = [
{
"image": image,
"decorative": False,
"alt_text": None,
}
for image in image_values
]
else:
# Treat `values` as the standard ImageBlock representation - a (possibly empty) list of
# dicts containing `image`, `decorative` and `alt_text` keys to be handled by the
# StructBlock superclass
struct_values = super().bulk_to_python(values)
return [
self._struct_value_to_image(struct_value) for struct_value in struct_values
]
def value_from_datadict(self, data, files, prefix):
struct_value = super().value_from_datadict(data, files, prefix)
return self._struct_value_to_image(struct_value)
def clean(self, value):
try:
self.child_blocks["image"].clean(value)
except ValidationError as e:
raise StructBlockValidationError(
block_errors={"image": e},
)
if value and not value.contextual_alt_text and not value.decorative:
raise StructBlockValidationError(
block_errors={
"alt_text": ValidationError(
_(
"Please add some alt text for your image or mark it as decorative"
)
)
}
)
return value
def normalize(self, value):
if value is None or isinstance(value, AbstractImage):
return value
else:
struct_value = super().normalize(value)
return self._struct_value_to_image(struct_value)
def get_form_context(self, value, prefix="", errors=None):
dict_value = {
"image": value,
"alt_text": value and value.contextual_alt_text,
"decorative": value and value.decorative,
}
context = super().get_form_context(dict_value, prefix=prefix, errors=errors)
context["suggested_alt_text"] = value
return context
def get_form_state(self, value):
return {
"image": self.child_blocks["image"].get_form_state(value),
"alt_text": value and value.contextual_alt_text,
"decorative": value and value.decorative,
}
def get_prep_value(self, value):
return {
"image": self.child_blocks["image"].get_prep_value(value),
"alt_text": value and value.contextual_alt_text,
"decorative": value and value.decorative,
}
def extract_references(self, value):
return self.child_blocks["image"].extract_references(value)
def get_comparison_class(self):
return ImageBlockComparison
def get_api_representation(self, value, context=None):
return super().get_api_representation(
self._image_to_struct_value(value), context=context
)
def render_basic(self, value, context=None):
return self.child_blocks["image"].render_basic(value, context=context)
class Meta:
icon = "image"
template = "wagtailimages/widgets/image.html"
class ImageBlockAdapter(StructBlockAdapter):
js_constructor = "wagtail.images.blocks.ImageBlock"
@cached_property
def media(self):
structblock_media = super().media
return forms.Media(
js=structblock_media._js + ["wagtailimages/js/image-block.js"],
css=structblock_media._css,
)
register(ImageBlockAdapter(), ImageBlock)
class ImageBlockComparison(StructBlockComparison):
def __init__(self, block, exists_a, exists_b, val_a, val_b):
super().__init__(
block,
exists_a,
exists_b,
block._image_to_struct_value(val_a),
block._image_to_struct_value(val_b),
)
def htmlvalue(self, val):
if isinstance(val, AbstractImage):
return super().htmlvalue(self.block._image_to_struct_value(val))
else:
return super().htmlvalue(val)