Skip to content

Commit

Permalink
Add to generate Plone image scales on save when environment variable …
Browse files Browse the repository at this point in the history
…PLONE_SCALE_GENERATE_ON_SAVE=1 is set
  • Loading branch information
datakurre committed Jul 7, 2021
1 parent 757a57a commit 83fc204
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
6 changes: 6 additions & 0 deletions plone/formwidget/namedfile/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
<include package="z3c.form" />
<include package="plone.namedfile" />

<adapter factory=".datamanager.NamedImageAttributeField" />
<adapter factory=".converter.NamedDataConverter" />
<adapter factory=".converter.Base64Converter" />
<adapter factory=".validator.NamedFileWidgetValidator" />
<adapter factory=".utils.FileUploadTemporaryStorage" />

<subscriber
for="ZPublisher.interfaces.IPubSuccess"
handler=".datamanager.plone_scale_generate_on_save"
/>

<class class=".widget.NamedFileWidget">
<require
permission="zope.Public"
Expand Down
111 changes: 111 additions & 0 deletions plone/formwidget/namedfile/datamanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
from plone.formwidget.namedfile.interfaces import IScaleGenerateOnSave
from plone.namedfile.field import INamedImageField
from z3c.form.datamanager import AttributeField
from ZODB.POSException import ConflictError
from zope.annotation.interfaces import IAnnotations
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.component import getUtility
from zope.globalrequest import getRequest
from zope.interface import Interface
from zope.interface import alsoProvides

import logging
import os
import transaction

try:
from Products.CMFPlone.factory import _IMREALLYPLONE5 # noqa
except ImportError:
PLONE_5 = False # pragma: no cover
else:
PLONE_5 = True # pragma: no cover


ANNOTATION_KEY = "plone.formwidget.namedfile.scale"
ENVIRONMENT_KEY = "PLONE_SCALE_GENERATE_ON_SAVE"

logger = logging.getLogger(__name__)


@adapter(Interface, INamedImageField)
class NamedImageAttributeField(AttributeField):

def __init__(self, *args, **kwargs):
super(NamedImageAttributeField, self).__init__(*args, **kwargs)
flag = (os.environ.get(ENVIRONMENT_KEY) or "").lower()
self.scale_generate_on_save = flag in ["1", "true", "yes", "on"]

def set(self, value):
"""See z3c.form.interfaces.IDataManager"""
super(NamedImageAttributeField, self).set(value)
if self.scale_generate_on_save:
schedule_plone_scale_generate_on_save(
self.context, getRequest(), self.field.__name__)


def schedule_plone_scale_generate_on_save(context, request, fieldname):
annotations = IAnnotations(request, None)
if annotations is not None:
annotations.setdefault(ANNOTATION_KEY, [])
annotations[ANNOTATION_KEY].append((context, fieldname))
alsoProvides(request, IScaleGenerateOnSave)


def plone_scale_generate_on_save(event):
if not IScaleGenerateOnSave.providedBy(event.request):
return
annotations = IAnnotations(event.request, None)
if annotations is None:
return
for context, fieldname in annotations.get(ANNOTATION_KEY) or []:
try:
images = getMultiAdapter((context, event.request), name="images")
try:
scales = get_scale_infos()
except ImportError:
continue
t = transaction.get()
for name, actual_width, actual_height in scales:
images.scale(fieldname, scale=name)
image = getattr(context, fieldname, None)
if image: # REST API requires this scale to refer the original
width, height = image.getImageSize()
images.scale(fieldname,
width=width, height=height, direction="thumbnail")
msg = "/".join(filter(bool, ["/".join(context.getPhysicalPath()),
"@@images", fieldname]))
t.note(msg)
t.commit()
except ConflictError:
msg = "/".join(filter(bool, ["/".join(context.getPhysicalPath()),
"@@images", fieldname]))
logger.warning("ConflictError. Scale not generated on save: " + msg)


def get_scale_infos():
"""Returns a list of (name, width, height) 3-tuples of the
available image scales.
"""
from Products.CMFCore.interfaces import IPropertiesTool
if PLONE_5:
from plone.registry.interfaces import IRegistry

registry = getUtility(IRegistry)
from Products.CMFPlone.interfaces import IImagingSchema

imaging_settings = registry.forInterface(IImagingSchema, prefix="plone")
allowed_sizes = imaging_settings.allowed_sizes

else:
ptool = getUtility(IPropertiesTool)
image_properties = ptool.imaging_properties
allowed_sizes = image_properties.getProperty("allowed_sizes")

def split_scale_info(allowed_size):
name, dims = allowed_size.split(" ")
width, height = list(map(int, dims.split(":")))
return name, width, height

return [split_scale_info(size) for size in allowed_sizes]
6 changes: 6 additions & 0 deletions plone/formwidget/namedfile/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ class IFileUploadTemporaryStorage(Interface):

def cleanup():
"""Removes stale temporary uploads from the upload storage"""


class IScaleGenerateOnSave(Interface):
"""Marker interface on request for creating scales
when NamedImageField has been used to save a new image file on content.
"""
64 changes: 64 additions & 0 deletions plone/formwidget/namedfile/tests.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,78 @@
from DateTime import DateTime
from OFS.SimpleItem import SimpleItem
from ZPublisher.pubevents import PubSuccess
from plone.formwidget.namedfile.datamanager import NamedImageAttributeField
from plone.formwidget.namedfile.interfaces import IScaleGenerateOnSave
from plone.formwidget.namedfile.testing import FUNCTIONAL_TESTING
from plone.formwidget.namedfile.testing import INTEGRATION_TESTING
from plone.namedfile.field import NamedImage as NamedImageField
from plone.namedfile.file import NamedImage
from plone.namedfile.interfaces import IImageScaleTraversable
from plone.namedfile.tests import getFile
from plone.scale.storage import AnnotationStorage
from plone.testing import layered
from z3c.form.interfaces import IDataManager
from zope.annotation import IAttributeAnnotatable
from zope.component import getMultiAdapter
from zope.event import notify
from zope.interface import implementer

import doctest
import re
import six
import unittest


class IHasImage(IImageScaleTraversable):
image = NamedImageField()


@implementer(IAttributeAnnotatable, IHasImage)
class DummyContent(SimpleItem):
image = None
modified = DateTime
id = __name__ = "item"
title = "foo"

def Title(self):
return self.title


class ScaleGenerateOnSaveTests(unittest.TestCase):

layer = FUNCTIONAL_TESTING

def setUp(self):
item = DummyContent()
self.layer["app"]._setOb("item", item)
self.item = self.layer["app"].item
self.request = self.layer["request"]

def test_not_generate_scales_on_save(self):
self.assertEqual(len(AnnotationStorage(self.item).storage), 0)
dm = getMultiAdapter((self.item, IHasImage["image"]), IDataManager)
self.assertIsInstance(dm, NamedImageAttributeField)
self.assertFalse(dm.scale_generate_on_save)
dm.set(NamedImage(getFile("image.png"), "image/png", "image.png"))
self.assertFalse(IScaleGenerateOnSave.providedBy(self.request))
notify(PubSuccess(self.request))
self.assertEqual(len(AnnotationStorage(self.item).storage), 0)

def test_generate_scales_on_save(self):
self.assertEqual(len(AnnotationStorage(self.item).storage), 0)
dm = getMultiAdapter((self.item, IHasImage["image"]), IDataManager)
self.assertIsInstance(dm, NamedImageAttributeField)
self.assertFalse(dm.scale_generate_on_save)
dm.scale_generate_on_save = True
dm.set(NamedImage(getFile("image.png"), "image/png", "image.png"))
self.assertTrue(IScaleGenerateOnSave.providedBy(self.request))
notify(PubSuccess(self.request))
self.assertGreater(len(AnnotationStorage(self.item).storage), 0)


def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(ScaleGenerateOnSaveTests))
suite.addTest(
layered(
doctest.DocFileSuite(
Expand Down

0 comments on commit 83fc204

Please sign in to comment.