diff --git a/Jenkinsfile b/Jenkinsfile index d22b2ea..897e4e2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -121,6 +121,7 @@ pipeline { */ "Plone5 & Python3": { node(label: 'docker') { + sh '''docker pull eeacms/plone-test:5-python3''' sh '''docker run -i --rm --name="$BUILD_TAG-python3" -e GIT_BRANCH="$BRANCH_NAME" -e ADDONS="$GIT_NAME" -e DEVELOP="src/$GIT_NAME" -e GIT_CHANGE_ID="$CHANGE_ID" eeacms/plone-test:5-python3 -v -vv -s $GIT_NAME''' } }, diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index ea73ded..aa8ccff 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -1,6 +1,11 @@ Changelog ========= +3.0 - (2023-08-30) +--------------------------- +* Feature: Add image_scales to catalog and update list of scales to registry + [nileshgulia1 - refs #254889] + 2.3 - (2023-07-31) --------------------------- * Docs: Update sonarqube tags diff --git a/eea/volto/policy/configure.zcml b/eea/volto/policy/configure.zcml index 2dc0823..126f8b6 100644 --- a/eea/volto/policy/configure.zcml +++ b/eea/volto/policy/configure.zcml @@ -1,5 +1,6 @@ @@ -14,4 +15,8 @@ + + + + diff --git a/eea/volto/policy/image_scales/__init__.py b/eea/volto/policy/image_scales/__init__.py new file mode 100644 index 0000000..2db3b13 --- /dev/null +++ b/eea/volto/policy/image_scales/__init__.py @@ -0,0 +1 @@ +""" Image scales fallback for Plone 5 """ diff --git a/eea/volto/policy/image_scales/adapters.py b/eea/volto/policy/image_scales/adapters.py new file mode 100644 index 0000000..b05b66c --- /dev/null +++ b/eea/volto/policy/image_scales/adapters.py @@ -0,0 +1,175 @@ +# pylint: disable=ungrouped-imports +""" +ImageScales +""" +from Acquisition import aq_inner +from plone.dexterity.interfaces import IDexterityContent +from plone.dexterity.utils import iterSchemata +from zope.component import adapter +from zope.component import queryMultiAdapter +from zope.interface import implementer +from zope.interface import Interface +from zope.schema import getFields +from plone.namedfile.interfaces import INamedImageField +from plone.registry.interfaces import IRegistry +from zope.component import getMultiAdapter + +from zope.component import getUtility +from zope.interface.interfaces import ComponentLookupError + +from eea.volto.policy.image_scales.interfaces import ( + IImageScalesAdapter, + IImageScalesFieldAdapter, + IImagingSchema, +) + + +@implementer(IImageScalesAdapter) +@adapter(IDexterityContent, Interface) +class ImageScales(object): + """ + Adapter for getting image scales + """ + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + obj = aq_inner(self.context) + res = {} + for schema in iterSchemata(self.context): + for name, field in getFields(schema).items(): + # serialize the field + serializer = queryMultiAdapter( + (field, obj, self.request), IImageScalesFieldAdapter + ) + if serializer: + scales = serializer() + if scales: + res[name] = scales + return res + + +def _split_scale_info(allowed_size): + """ + get desired attr(name,width,height) from scale names + """ + name, dims = allowed_size.split(" ") + width, height = list(map(int, dims.split(":"))) + return name, width, height + + +def _get_scale_infos(): + """Returns list of (name, width, height) of the available image scales.""" + if IImagingSchema is None: + return [] + registry = getUtility(IRegistry) + imaging_settings = registry.forInterface( + IImagingSchema, prefix="plone", omit=("picture_variants") + ) + allowed_sizes = imaging_settings.allowed_sizes + return [_split_scale_info(size) for size in allowed_sizes] + + +@implementer(IImageScalesFieldAdapter) +@adapter(INamedImageField, IDexterityContent, Interface) +class ImageFieldScales(object): + """ + Image scale serializer + """ + + def __init__(self, field, context, request): + self.context = context + self.request = request + self.field = field + + def __call__(self): + image = self.field.get(self.context) + if not image: + return None + + # Get the @@images view once and store it, so all methods can use it. + try: + self.images_view = getMultiAdapter( + (self.context, self.request), name="images" + ) + except ComponentLookupError: + # Seen in plone.app.caching.tests.test_profile_with_caching_proxy. + # If we cannot find the images view, there is nothing for us to do. + return None + width, height = image.getImageSize() + url = self.get_original_image_url(self.field.__name__, width, height) + scales = self.get_scales(self.field, width, height) + + return [ + { + "filename": image.filename, + "content-type": image.contentType, + "size": image.getSize(), + "download": self._scale_view_from_url(url), + "width": width, + "height": height, + "scales": scales, + } + ] + + def get_scales(self, field, width, height): + """Get a dictionary of available scales for a particular image field, + with the actual dimensions (aspect ratio of the original image). + """ + scales = {} + + for name, actual_width, actual_height in _get_scale_infos(): + if actual_width > width: + # The width of the scale is larger than the original width. + # Scaling would simply return the original (or perhaps a copy + # with the same size). We do not need this scale. + # If we *do* want this, we should call the scale method with + # mode="cover", so it scales up. + continue + + # Get the scale info without actually generating the scale, + # nor any old-style HiDPI scales. + scale = self.images_view.scale( + field.__name__, + width=actual_width, + height=actual_height, + ) + if scale is None: + # If we cannot get a scale, it is probably a corrupt image. + continue + + url = scale.url + actual_width = scale.width + actual_height = scale.height + + scales[name] = { + "download": self._scale_view_from_url(url), + "width": actual_width, + "height": actual_height, + } + + return scales + + def get_original_image_url(self, fieldname, width, height): + """ + get image url from scale + """ + scale = self.images_view.scale( + fieldname, + width=width, + height=height, + ) + # Corrupt images may not have a scale. + return scale.url if scale else None + + def _scale_view_from_url(self, url): + """ + flatten to scale url + """ + # The "download" information for scales is the path to + # "@@images/foo-scale" only. + # The full URL to the scale is rendered by the scaling adapter at + # runtime to make sure they are correct in virtual hostings. + return url.replace(self.context.absolute_url(), "").lstrip("/") diff --git a/eea/volto/policy/image_scales/configure.zcml b/eea/volto/policy/image_scales/configure.zcml new file mode 100644 index 0000000..01ea9b2 --- /dev/null +++ b/eea/volto/policy/image_scales/configure.zcml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/eea/volto/policy/image_scales/indexer.py b/eea/volto/policy/image_scales/indexer.py new file mode 100644 index 0000000..08b10fe --- /dev/null +++ b/eea/volto/policy/image_scales/indexer.py @@ -0,0 +1,31 @@ +# pylint: disable=ungrouped-imports +""" +Indexer +""" + +from persistent.dict import PersistentDict +from plone.dexterity.interfaces import IDexterityContent +from plone.indexer.decorator import indexer +from zope.component import queryMultiAdapter +from zope.globalrequest import getRequest + +from eea.volto.policy.image_scales.interfaces import IImageScalesAdapter + + +@indexer(IDexterityContent) +def image_scales(obj): + """ + Indexer used to store in metadata the image scales of the object. + """ + adapter = queryMultiAdapter((obj, getRequest()), IImageScalesAdapter) + if not adapter: + # Raising an AttributeError does the right thing, + # making sure nothing is saved in the catalog. + raise AttributeError + try: + scales = adapter() + except TypeError: + scales = {} + if not scales: + raise AttributeError + return PersistentDict(scales) diff --git a/eea/volto/policy/image_scales/interfaces.py b/eea/volto/policy/image_scales/interfaces.py new file mode 100644 index 0000000..678f898 --- /dev/null +++ b/eea/volto/policy/image_scales/interfaces.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +"""Module where all interfaces, events and exceptions live.""" +import json +from plone import schema +from zope.interface import Interface +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary +from eea.volto.policy import EEAMessageFactory as _ + + +class IImageScalesAdapter(Interface): + """Return a list of image scales for the given context.""" + + def __init__(self, context, request): + """Adapts context and the request.""" + + def __call__(self): + """Call IImageScalesFieldAdapter on all fields.""" + + +class IImageScalesFieldAdapter(Interface): + """Adapter from field to image_scales. + + This is called by an IImageScalesAdapter. + Default expectation is that there will be adapters for image fields + and not for others. But adapters for text fields or relation fields + are imaginable. + """ + + def __init__(self, field, context, request): + """Adapts field, context and request.""" + + def __call__(self): + """Returns JSON compatible python data.""" + + +class IImagingSchema(Interface): + """ + Schema for Images Controlpanel + """ + + allowed_sizes = schema.List( + title=_("Allowed image sizes"), + description=_( + "Specify all allowed maximum image dimensions, one per line. The " + "required format is <name> <width>:<height>." + ), + value_type=schema.TextLine(), + default=[ + "huge 1600:65536", + "great 1200:65536", + "larger 1000:65536", + "large 800:65536", + "teaser 600:65536", + "preview 400:65536", + "mini 200:65536", + "thumb 128:128", + "tile 64:64", + "icon 32:32", + "listing 16:16", + ], + missing_value=[], + required=False, + ) + + quality = schema.Int( + title=_("Scaled image quality"), + description=_( + "A value for the quality of scaled images, from 1 " + "(lowest) to 95 (highest). A value of 0 will mean " + "plone.scaling's default will be used, which is " + "currently 88." + ), + min=0, + max=95, + default=88, + ) + + highpixeldensity_scales = schema.Choice( + title=_("High pixel density mode"), + description=_(""), + default="disabled", + vocabulary=SimpleVocabulary( + [ + SimpleTerm("disabled", "disabled", "Disabled"), + SimpleTerm("2x", "2x", "Enabled (2x)"), + SimpleTerm("3x", "3x", "Enabled (2x, 3x)"), + ] + ), + ) + + quality_2x = schema.Int( + title=_("Image quality at 2x"), + description=_( + "A value for the quality of 2x high pixel density images, from 1 " + "(lowest) to 95 (highest). A value of 0 will mean " + "plone.scaling's default will be used, which is " + "currently 62." + ), + min=0, + max=95, + default=62, + ) + + quality_3x = schema.Int( + title=_("Image quality at 3x"), + description=_( + "A value for the quality of 3x high pixel density images, from 1 " + "(lowest) to 95 (highest). A value of 0 will mean " + "plone.scaling's default will be used, which is " + "currently 51." + ), + min=0, + max=95, + default=51, + ) + + picture_variants = schema.JSONField( + title=_("Picture variants"), + description=_( + "Enter a JSON-formatted picture variants configuration." + ), + schema=json.dumps( + { + "title": "Image srcset definition", + "type": "object", + "additionalProperties": {"$ref": "#/$defs/srcset"}, + "$defs": { + "srcset": { + "type": "object", + "properties": { + "title": { + "type": "string", + }, + "preview": { + "type": "string", + }, + "hideInEditor": { + "type": "boolean", + }, + "sourceset": { + "type": "array", + "items": { + "type": "object", + "properties": { + "scale": { + "type": "string", + }, + "media": { + "type": "string", + }, + "additionalScales": { + "type": "array", + }, + }, + "additionalProperties": False, + "required": ["scale"], + }, + }, + }, + "additionalProperties": False, + "required": ["title", "sourceset"], + }, + }, + } + ), + default={ + "large": { + "title": "Large", + "sourceset": [ + { + "scale": "larger", + "additionalScales": [ + "preview", + "teaser", + "large", + "great", + "huge", + ], + }, + ], + }, + "medium": { + "title": "Medium", + "sourceset": [ + { + "scale": "teaser", + "additionalScales": [ + "preview", + "large", + "larger", + "great", + ], + }, + ], + }, + "small": { + "title": "Small", + "sourceset": [ + { + "scale": "preview", + "additionalScales": ["large", "larger"], + }, + ], + }, + }, + required=True, + ) + + image_captioning = schema.Bool( + title=_("image_captioning_title", "Enable image captioning"), + description=_( + "image_captioning_description", + "Enable automatic image captioning for images set in the richtext" + "editor based on the description of images.", + ), + default=True, + required=False, + ) diff --git a/eea/volto/policy/profiles/default/catalog.xml b/eea/volto/policy/profiles/default/catalog.xml new file mode 100644 index 0000000..70d8232 --- /dev/null +++ b/eea/volto/policy/profiles/default/catalog.xml @@ -0,0 +1,4 @@ + + + + diff --git a/eea/volto/policy/profiles/default/metadata.xml b/eea/volto/policy/profiles/default/metadata.xml index 38ddfac..e4ba0c6 100644 --- a/eea/volto/policy/profiles/default/metadata.xml +++ b/eea/volto/policy/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 2.0 + 3.0 profile-plone.volto:default diff --git a/eea/volto/policy/profiles/default/registry.xml b/eea/volto/policy/profiles/default/registry.xml new file mode 100644 index 0000000..1b10e2c --- /dev/null +++ b/eea/volto/policy/profiles/default/registry.xml @@ -0,0 +1,23 @@ + + + + + icon 32:32 + tile 64:64 + thumb 128:128 + mini 200:65536 + preview 400:65536 + teaser 600:65536 + large 800:65536 + larger 1000:65536 + great 1200:65536 + tiny 24:24 + small 48:48 + medium 60:60 + big 80:80 + huge 1920:65536 + + + diff --git a/eea/volto/policy/upgrades/configure.zcml b/eea/volto/policy/upgrades/configure.zcml index dfd5e54..f8a59ea 100644 --- a/eea/volto/policy/upgrades/configure.zcml +++ b/eea/volto/policy/upgrades/configure.zcml @@ -27,4 +27,16 @@ + + + + + + diff --git a/eea/volto/policy/version.txt b/eea/volto/policy/version.txt index bb576db..9f55b2c 100644 --- a/eea/volto/policy/version.txt +++ b/eea/volto/policy/version.txt @@ -1 +1 @@ -2.3 +3.0