Added generic content renderer feature
authorThierry Florac <thierry.florac@onf.fr>
Thu, 15 Feb 2018 15:08:29 +0100
changeset 395 2a39b333a585
parent 394 1ebcb03e9bff
child 396 a6c3d1974420
Added generic content renderer feature
src/pyams_content/component/gallery/__init__.py
src/pyams_content/component/gallery/interfaces/__init__.py
src/pyams_content/component/gallery/renderer.py
src/pyams_content/component/gallery/zmi/__init__.py
src/pyams_content/component/gallery/zmi/paragraph.py
src/pyams_content/component/gallery/zmi/renderer.py
src/pyams_content/component/illustration/__init__.py
src/pyams_content/component/illustration/interfaces/__init__.py
src/pyams_content/component/illustration/renderer.py
src/pyams_content/component/illustration/zmi/__init__.py
src/pyams_content/component/illustration/zmi/interfaces.py
src/pyams_content/component/illustration/zmi/paragraph.py
src/pyams_content/component/illustration/zmi/renderer.py
src/pyams_content/component/paragraph/zmi/html.py
src/pyams_content/features/renderer/__init__.py
src/pyams_content/features/renderer/interfaces/__init__.py
src/pyams_content/features/renderer/zmi/__init__.py
src/pyams_content/features/renderer/zmi/templates/renderer-input.pt
src/pyams_content/features/renderer/zmi/widget.py
--- a/src/pyams_content/component/gallery/__init__.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/gallery/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -16,8 +16,8 @@
 # import standard library
 
 # import interfaces
-from pyams_content.component.gallery.interfaces import IGallery, IGalleryTarget, \
-    GALLERY_CONTAINER_KEY, IGalleryRenderer
+from pyams_content.component.gallery.interfaces import IBaseGallery, IGallery, IGalleryTarget, \
+    GALLERY_CONTAINER_KEY
 from pyams_content.component.paragraph import IBaseParagraph
 from pyams_content.features.checker.interfaces import IContentChecker
 from pyams_content.shared.common.interfaces import IWfSharedContent
@@ -31,18 +31,16 @@
 # import packages
 from pyams_catalog.utils import index_object
 from pyams_content.features.checker import BaseContentChecker
+from pyams_content.features.renderer import RenderedContentMixin
 from pyams_utils.adapter import adapter_config, ContextAdapter
 from pyams_utils.container import BTreeOrderedContainer
-from pyams_utils.request import check_request
 from pyams_utils.traversing import get_parent
-from pyams_utils.vocabulary import vocabulary_config
 from pyramid.events import subscriber
 from pyramid.threadlocal import get_current_registry
 from zope.interface import implementer
 from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
 from zope.location import locate
 from zope.schema.fieldproperty import FieldProperty
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
 
 from pyams_content import _
 
@@ -52,7 +50,7 @@
 #
 
 @implementer(IGallery)
-class Gallery(BTreeOrderedContainer):
+class Gallery(BTreeOrderedContainer, RenderedContentMixin):
     """Gallery persistent class"""
 
     title = FieldProperty(IGallery['title'])
@@ -179,18 +177,3 @@
 def GalleryTargetContentChecker(context):
     gallery = IGallery(context)
     return IContentChecker(gallery, None)
-
-
-@vocabulary_config(name='PyAMS gallery renderers')
-class GalleryRendererVocabulary(SimpleVocabulary):
-    """Gallery renderer utilities vocabulary"""
-
-    def __init__(self, context=None):
-        request = check_request()
-        translate = request.localizer.translate
-        registry = request.registry
-        context = Gallery()
-        terms = [SimpleTerm(name, title=translate(adapter.label))
-                 for name, adapter in sorted(registry.getAdapters((context, request), IGalleryRenderer),
-                                             key=lambda x: x[1].weight)]
-        super(GalleryRendererVocabulary, self).__init__(terms)
--- a/src/pyams_content/component/gallery/interfaces/__init__.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/gallery/interfaces/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -17,15 +17,15 @@
 
 # import interfaces
 from pyams_content.component.paragraph.interfaces import IBaseParagraph
+from pyams_content.features.renderer.interfaces import IRenderedContent
 from zope.container.interfaces import IOrderedContainer
-from zope.contentprovider.interfaces import IContentProvider
 
 # import packages
 from pyams_file.schema import MediaField, AudioField
 from pyams_i18n.schema import I18nTextLineField, I18nTextField
 from zope.annotation.interfaces import IAttributeAnnotatable
 from zope.container.constraints import contains, containers
-from zope.interface import Interface, Attribute
+from zope.interface import Interface
 from zope.schema import Bool, TextLine, Choice
 
 from pyams_content import _
@@ -34,11 +34,15 @@
 GALLERY_CONTAINER_KEY = 'pyams_content.gallery'
 
 
-class IGalleryFile(Interface):
-    """Gallery file marker interface"""
+class IGalleryItem(Interface):
+    """Gallery item base interface"""
 
     containers('.IGallery')
 
+
+class IGalleryFile(IGalleryItem):
+    """Gallery file marker interface"""
+
     title = I18nTextLineField(title=_("Legend"),
                               required=False)
 
@@ -83,7 +87,7 @@
                    default=True)
 
 
-class IBaseGallery(IOrderedContainer, IAttributeAnnotatable):
+class IBaseGallery(IOrderedContainer, IAttributeAnnotatable, IRenderedContent):
     """Base gallery interface"""
 
     title = I18nTextLineField(title=_("Title"),
@@ -113,13 +117,7 @@
 class IGallery(IBaseGallery):
     """Gallery interface"""
 
-    contains(IGalleryFile)
-
-
-class IGalleryRenderer(IContentProvider):
-    """Gallery renderer utility interface"""
-
-    label = Attribute("Renderer label")
+    contains(IGalleryItem)
 
 
 class IGalleryTarget(IAttributeAnnotatable):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/renderer.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IBaseGallery
+from pyams_content.features.renderer.interfaces import IContentRenderer
+
+# import packages
+from pyams_content.component.gallery import Gallery
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@vocabulary_config(name='PyAMS gallery renderers')
+class GalleryRendererVocabulary(SimpleVocabulary):
+    """Gallery renderer utilities vocabulary"""
+
+    def __init__(self, context=None):
+        request = check_request()
+        translate = request.localizer.translate
+        registry = request.registry
+        if not IBaseGallery.providedBy(context):
+            context = Gallery()
+        terms = [SimpleTerm(name, title=translate(adapter.label))
+                 for name, adapter in sorted(registry.getAdapters((context, request), IContentRenderer),
+                                             key=lambda x: x[1].weight)]
+        super(GalleryRendererVocabulary, self).__init__(terms)
--- a/src/pyams_content/component/gallery/zmi/__init__.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/gallery/zmi/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -20,8 +20,9 @@
 from io import BytesIO
 
 # import interfaces
-from pyams_content.component.gallery.interfaces import IGallery, IGalleryRenderer
+from pyams_content.component.gallery.interfaces import IGallery
 from pyams_content.component.gallery.zmi.interfaces import IGalleryMediasAddFields, IGalleryContentsView
+from pyams_content.features.renderer.interfaces import IContentRenderer
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_file.interfaces import IFileInfo
 from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager
@@ -32,10 +33,12 @@
 
 # import packages
 from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
+from pyams_content.features.renderer.zmi import BaseContentRenderer
+from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
-from pyams_template.template import template_config, get_view_template
-from pyams_utils.adapter import adapter_config, ContextRequestAdapter
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
 from pyams_utils.url import absolute_url
 from pyams_viewlet.viewlet import viewlet_config, Viewlet
 from pyams_zmi.form import AdminDialogEditForm, AdminDialogDisplayForm
@@ -61,6 +64,8 @@
     icon_css_class = 'fa fa-fw fa-picture-o'
 
     fields = field.Fields(IGallery).omit('__parent__', '__file__')
+    fields['renderer'].widgetFactory = RendererFieldWidget
+
     ajax_handler = 'properties.json'
     edit_permission = MANAGE_CONTENT_PERMISSION
 
@@ -154,28 +159,6 @@
 
 
 #
-# Gallery renderers
-#
-
-class BaseGalleryRenderer(ContextRequestAdapter):
-    """Base gallery renderer"""
-
-    def update(self):
-        pass
-
-    render = get_view_template()
-
-
-@adapter_config(name='default', context=(IGallery, IPyAMSLayer), provides=IGalleryRenderer)
-@template_config(template='templates/renderer-default.pt', layer=IPyAMSLayer)
-class DefaultGalleryRenderer(BaseGalleryRenderer):
-    """Default gallery renderer"""
-
-    label = _("Default gallery renderer")
-    weight = 1
-
-
-#
 # Gallery medias downloader
 #
 
--- a/src/pyams_content/component/gallery/zmi/paragraph.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/gallery/zmi/paragraph.py	Thu Feb 15 15:08:29 2018 +0100
@@ -15,10 +15,8 @@
 
 # import standard library
 
-from datetime import datetime
-
 # import interfaces
-from pyams_content.component.gallery.interfaces import IGalleryParagraph, IBaseGallery, IGalleryRenderer
+from pyams_content.component.gallery.interfaces import IGalleryParagraph, IBaseGallery
 from pyams_content.component.gallery.zmi.interfaces import IGalleryContentsView
 from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
     IParagraphSummary
@@ -35,6 +33,7 @@
 from pyams_content.component.gallery.paragraph import Gallery
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
     BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.viewlet.toolbar import ToolbarAction
@@ -100,6 +99,8 @@
     icon_css_class = 'fa fa-fw fa-picture-o'
 
     fields = field.Fields(IGalleryParagraph).omit('__parent__', '__name__', 'visible')
+    fields['renderer'].widgetFactory = RendererFieldWidget
+
     ajax_handler = 'properties.json'
     edit_permission = MANAGE_CONTENT_PERMISSION
 
@@ -192,8 +193,7 @@
 
     def __init__(self, context, request):
         super(GalleryParagraphSummary, self).__init__(context, request)
-        self.renderer = request.registry.queryMultiAdapter((context, request), IGalleryRenderer,
-                                                           name=self.context.renderer)
+        self.renderer = self.context.get_renderer(request)
 
     language = None
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/renderer.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery import IGallery
+from pyams_content.features.renderer.interfaces import IContentRenderer
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.features.renderer.zmi import BaseContentRenderer
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+
+from pyams_content import _
+
+
+class BaseGalleryRenderer(BaseContentRenderer):
+    """Base gallery renderer"""
+
+
+@adapter_config(name='default', context=(IGallery, IPyAMSLayer), provides=IContentRenderer)
+@template_config(template='templates/renderer-default.pt', layer=IPyAMSLayer)
+class DefaultGalleryRenderer(BaseGalleryRenderer):
+    """Default gallery renderer"""
+
+    label = _("Default gallery renderer")
+    weight = 1
--- a/src/pyams_content/component/illustration/__init__.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/illustration/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -16,7 +16,7 @@
 # import standard library
 
 # import interfaces
-from pyams_content.component.illustration.interfaces import IIllustrationRenderer, IIllustration, IIllustrationTarget, \
+from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationTarget, \
     ILLUSTRATION_KEY
 from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE
 from pyams_file.interfaces import IFileInfo, IImage, IResponsiveImage
@@ -29,12 +29,12 @@
 # import packages
 from persistent import Persistent
 from pyams_content.features.checker import BaseContentChecker
+from pyams_content.features.renderer import RenderedContentMixin
 from pyams_i18n.property import I18nFileProperty
 from pyams_utils.adapter import adapter_config, ContextAdapter
 from pyams_utils.registry import query_utility, get_utility
 from pyams_utils.request import check_request
 from pyams_utils.traversing import get_parent
-from pyams_utils.vocabulary import vocabulary_config
 from pyramid.events import subscriber
 from pyramid.threadlocal import get_current_registry
 from zope.container.contained import Contained
@@ -42,13 +42,12 @@
 from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent
 from zope.location import locate
 from zope.schema.fieldproperty import FieldProperty
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
 
 from pyams_content import _
 
 
 @implementer(IIllustration)
-class Illustration(Persistent, Contained):
+class Illustration(Persistent, Contained, RenderedContentMixin):
     """Illustration persistent class"""
 
     title = FieldProperty(IIllustration['title'])
@@ -67,7 +66,7 @@
     @data.setter
     def data(self, value):
         self._data = value
-        for data in self._data.values():
+        for data in (self._data or {}).values():
             if IImage.providedBy(data):
                 alsoProvides(data, IResponsiveImage)
 
@@ -181,18 +180,3 @@
     illustration = IIllustration(context, None)
     if illustration is not None:
         return IContentChecker(illustration)
-
-
-@vocabulary_config(name='PyAMS illustration renderers')
-class IllustrationRendererVocabulary(SimpleVocabulary):
-    """Illustration renderer utilities vocabulary"""
-
-    def __init__(self, context=None):
-        request = check_request()
-        translate = request.localizer.translate
-        registry = request.registry
-        context = Illustration()
-        terms = [SimpleTerm(name, title=translate(adapter.label))
-                 for name, adapter in sorted(registry.getAdapters((context, request), IIllustrationRenderer),
-                                             key=lambda x: x[1].weight)]
-        super(IllustrationRendererVocabulary, self).__init__(terms)
--- a/src/pyams_content/component/illustration/interfaces/__init__.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/illustration/interfaces/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -17,12 +17,11 @@
 
 # import interfaces
 from pyams_content.component.paragraph.interfaces import IBaseParagraph
+from pyams_content.features.renderer.interfaces import IRenderedContent
 from pyams_i18n.schema import I18nTextLineField, I18nTextField, I18nThumbnailMediaField
 from zope.annotation.interfaces import IAttributeAnnotatable
-from zope.contentprovider.interfaces import IContentProvider
 
 # import packages
-from zope.interface import Interface, Attribute
 from zope.schema import Choice, TextLine
 
 from pyams_content import _
@@ -35,7 +34,7 @@
 ILLUSTRATION_KEY = 'pyams_content.illustration'
 
 
-class IIllustration(Interface):
+class IIllustration(IRenderedContent):
     """Illustration paragraph"""
 
     title = I18nTextLineField(title=_("Legend"),
@@ -63,7 +62,8 @@
 
     renderer = Choice(title=_("Illustration template"),
                       description=_("Presentation template used for illustration"),
-                      vocabulary='PyAMS illustration renderers')
+                      vocabulary='PyAMS illustration renderers',
+                      default='hidden')
 
     language = Choice(title=_("Language"),
                       description=_("File's content language"),
@@ -75,12 +75,6 @@
     """Illustration target marker interface"""
 
 
-class IIllustrationRenderer(IContentProvider):
-    """Illustration renderer utility interface"""
-
-    label = Attribute("Renderer label")
-
-
 class IIllustrationParagraph(IIllustration, IBaseParagraph):
     """Illustration paragraph"""
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/renderer.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer.interfaces import IContentRenderer
+
+# import packages
+from pyams_content.component.illustration import Illustration, IIllustration
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@vocabulary_config(name='PyAMS illustration renderers')
+class IllustrationRendererVocabulary(SimpleVocabulary):
+    """Illustration renderer utilities vocabulary"""
+
+    def __init__(self, context=None):
+        request = check_request()
+        translate = request.localizer.translate
+        registry = request.registry
+        if not IIllustration.providedBy(context):
+            context = Illustration()
+        terms = [SimpleTerm(name, title=translate(adapter.label))
+                 for name, adapter in sorted(registry.getAdapters((context, request), IContentRenderer),
+                                             key=lambda x: x[1].weight)]
+        super(IllustrationRendererVocabulary, self).__init__(terms)
--- a/src/pyams_content/component/illustration/zmi/__init__.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/illustration/zmi/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -16,10 +16,9 @@
 # import standard library
 
 # import interfaces
-from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationRenderer, IIllustrationTarget
+from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationTarget
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphContainerTable, IParagraphTitleToolbar
 from pyams_form.interfaces.form import IInnerSubForm, IWidgetsPrefixViewletsManager
-from pyams_i18n.interfaces import II18n
 from pyams_skin.layer import IPyAMSLayer
 from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
 from pyams_utils.interfaces.data import IObjectData
@@ -27,11 +26,12 @@
 from transaction.interfaces import ITransactionManager
 
 # import packages
+from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_content.skin import pyams_content
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_skin.viewlet.toolbar import JsToolbarAction
-from pyams_template.template import get_view_template, template_config
-from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
 from pyams_utils.fanstatic import get_resource_path
 from pyams_viewlet.viewlet import viewlet_config, Viewlet
 from pyams_zmi.form import InnerAdminEditForm
@@ -42,63 +42,6 @@
 
 
 #
-# Illustration renderers
-#
-
-class BaseIllustrationRenderer(ContextRequestAdapter):
-    """Base illustration renderer"""
-
-    language = None
-
-    def update(self):
-        i18n = II18n(self.context)
-        if self.language:
-            self.legend = i18n.get_attribute('alt_title', self.language, request=self.request)
-        else:
-            self.legend = i18n.query_attribute('alt_title', request=self.request)
-
-    render = get_view_template()
-
-
-@adapter_config(name='hidden', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
-class HiddenIllustrationRenderer(BaseIllustrationRenderer):
-    """Hidden illustration renderer"""
-
-    label = _("Hidden illustration")
-    weight = -999
-
-    def render(self):
-        return ''
-
-
-@adapter_config(name='default', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
-@template_config(template='templates/renderer-default.pt', layer=IPyAMSLayer)
-class DefaultIllustrationRenderer(BaseIllustrationRenderer):
-    """Default illustration renderer"""
-
-    label = _("Centered illustration")
-    weight = 1
-
-
-@adapter_config(name='left+zoom', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
-@template_config(template='templates/renderer-left.pt', layer=IPyAMSLayer)
-class LeftIllustrationWithZoomRenderer(BaseIllustrationRenderer):
-    """Illustrtaion renderer with small image and zoom"""
-
-    label = _("Small illustration on the left with zoom")
-    weight = 2
-
-
-@adapter_config(name='right+zoom', context=(IIllustration, IPyAMSLayer), provides=IIllustrationRenderer)
-@template_config(template='templates/renderer-right.pt', layer=IPyAMSLayer)
-class RightIllustrationWithZoomRenderer(BaseIllustrationRenderer):
-    """Illustrtaion renderer with small image and zoom"""
-
-    label = _("Small illustration on the right with zoom")
-    weight = 3
-
-
-#
 # Illustration properties inner edit form
 #
 
@@ -138,6 +81,8 @@
     legend_class = 'illustration switcher no-y-padding padding-right-10 pull-left width-auto'
 
     fields = field.Fields(IIllustration).omit('__parent__', '__name__')
+    fields['renderer'].widgetFactory = RendererFieldWidget
+
     hide_widgets_prefix_div = True
     weight = 10
 
@@ -155,7 +100,8 @@
 
     def get_ajax_output(self, changes):
         output = super(IllustrationPropertiesInnerEditForm, self).get_ajax_output(changes)
-        if 'data' in changes.get(IIllustration, ()):
+        illustration_changes = changes.get(IIllustration, ())
+        if ('data' in illustration_changes) or ('renderer' in illustration_changes):
             # we have to commit transaction to be able to handle blobs...
             ITransactionManager(self.context).get().commit()
             form = IllustrationPropertiesInnerEditForm(self.context, self.request)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/interfaces.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+from zope.schema import Bool
+
+from pyams_content import _
+
+
+class IIllustrationWithZoomSettings(Interface):
+    """Illustration with zoom interface"""
+
+    zoom_on_click = Bool(title=_("Zoom on click?"),
+                         description=_("If 'yes', a click on illustration thumbnail is required to zoom"),
+                         required=True,
+                         default=True)
--- a/src/pyams_content/component/illustration/zmi/paragraph.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/illustration/zmi/paragraph.py	Thu Feb 15 15:08:29 2018 +0100
@@ -18,7 +18,7 @@
 # import interfaces
 from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, \
     IParagraphContainer, IParagraphSummary
-from pyams_content.component.illustration.interfaces import IIllustrationRenderer, IIllustration, IIllustrationParagraph
+from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationParagraph
 from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor, IParagraphContainerView
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
@@ -32,6 +32,7 @@
 from pyams_content.component.illustration.paragraph import Illustration
 from pyams_content.component.paragraph.zmi import BaseParagraphAJAXAddForm, BaseParagraphAJAXEditForm, \
     BaseParagraphAddMenu, BaseParagraphPropertiesEditForm
+from pyams_content.features.renderer.zmi.widget import RendererFieldWidget
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_utils.adapter import adapter_config
 from pyams_viewlet.viewlet import viewlet_config, BaseContentProvider
@@ -99,6 +100,8 @@
     icon_css_class = 'fa fa-fw fa-file-image-o'
 
     fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__', 'visible')
+    fields['renderer'].widgetFactory = RendererFieldWidget
+
     ajax_handler = 'properties.json'
     edit_permission = MANAGE_CONTENT_PERMISSION
 
@@ -191,8 +194,7 @@
 
     def __init__(self, context, request):
         super(IllustrationSummary, self).__init__(context, request)
-        self.renderer = request.registry.queryMultiAdapter((context, request), IIllustrationRenderer,
-                                                           name=self.context.renderer)
+        self.renderer = self.context.get_renderer()
 
     language = None
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/zmi/renderer.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,119 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from persistent import Persistent
+
+# import interfaces
+from pyams_content.component.illustration.interfaces import IIllustration
+from pyams_content.component.illustration.zmi.interfaces import IIllustrationWithZoomSettings
+from pyams_i18n.interfaces import II18n
+from pyams_content.features.renderer.interfaces import IContentRenderer
+from pyams_skin.layer import IPyAMSLayer
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from pyams_content.features.renderer.zmi import BaseContentRenderer
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from zope.interface import implementer
+from zope.location import locate, Location
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+#
+# Illustration renderers
+#
+
+class BaseIllustrationRenderer(BaseContentRenderer):
+    """Base illustration renderer"""
+
+    language = None
+
+    def update(self):
+        i18n = II18n(self.context)
+        if self.language:
+            self.legend = i18n.get_attribute('alt_title', self.language, request=self.request)
+        else:
+            self.legend = i18n.query_attribute('alt_title', request=self.request)
+
+
+@adapter_config(name='hidden', context=(IIllustration, IPyAMSLayer), provides=IContentRenderer)
+class HiddenIllustrationRenderer(BaseIllustrationRenderer):
+    """Hidden illustration renderer"""
+
+    label = _("Hidden illustration")
+    weight = -999
+
+    def render(self):
+        return ''
+
+
+@adapter_config(name='default', context=(IIllustration, IPyAMSLayer), provides=IContentRenderer)
+@template_config(template='templates/renderer-default.pt', layer=IPyAMSLayer)
+class DefaultIllustrationRenderer(BaseIllustrationRenderer):
+    """Default illustration renderer"""
+
+    label = _("Centered illustration")
+    weight = 1
+
+
+@adapter_config(name='left+zoom', context=(IIllustration, IPyAMSLayer), provides=IContentRenderer)
+@template_config(template='templates/renderer-left.pt', layer=IPyAMSLayer)
+class LeftIllustrationWithZoomRenderer(BaseIllustrationRenderer):
+    """Illustration renderer with small image and zoom"""
+
+    label = _("Small illustration on the left with zoom")
+    weight = 2
+
+    target_interface = IIllustrationWithZoomSettings
+
+
+@adapter_config(name='right+zoom', context=(IIllustration, IPyAMSLayer), provides=IContentRenderer)
+@template_config(template='templates/renderer-right.pt', layer=IPyAMSLayer)
+class RightIllustrationWithZoomRenderer(BaseIllustrationRenderer):
+    """Illustration renderer with small image and zoom"""
+
+    label = _("Small illustration on the right with zoom")
+    weight = 3
+
+    target_interface = IIllustrationWithZoomSettings
+
+
+#
+# Illustration renderer with zoom settings
+#
+
+ILLUSTRATION_ZOOM_RENDERER_SETTINGS_KEY = 'pyams_content.illustration.renderer:zoom'
+
+
+@implementer(IIllustrationWithZoomSettings)
+class IllustrationZoomSettings(Persistent, Location):
+    """Illustration zoom renderer settings"""
+
+    zoom_on_click = FieldProperty(IIllustrationWithZoomSettings['zoom_on_click'])
+
+
+@adapter_config(context=IIllustration, provides=IIllustrationWithZoomSettings)
+def IllustrationWithZoomSettingsFactory(context):
+    """Illustration zoom renderer settings factory"""
+    annotations = IAnnotations(context)
+    settings = annotations.get(ILLUSTRATION_ZOOM_RENDERER_SETTINGS_KEY)
+    if settings is None:
+        settings = annotations[ILLUSTRATION_ZOOM_RENDERER_SETTINGS_KEY] = IllustrationZoomSettings()
+        locate(settings, context)
+    return settings
--- a/src/pyams_content/component/paragraph/zmi/html.py	Sun Feb 11 12:11:05 2018 +0100
+++ b/src/pyams_content/component/paragraph/zmi/html.py	Thu Feb 15 15:08:29 2018 +0100
@@ -18,7 +18,7 @@
 # import interfaces
 from pyams_content.component.association.interfaces import IAssociationTarget
 from pyams_content.component.association.zmi.interfaces import IAssociationsParentForm
-from pyams_content.component.illustration.interfaces import IIllustration, IIllustrationRenderer
+from pyams_content.component.illustration.interfaces import IIllustration
 from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
     IParagraphSummary
 from pyams_content.component.paragraph.interfaces.html import IHTMLParagraph, IRawParagraph
@@ -354,9 +354,7 @@
         self.illustration = IIllustration(self.context)
         if self.illustration.data:
             registry = get_current_registry()
-            renderer = self.illustration_renderer = registry.queryMultiAdapter((self.illustration, self.request),
-                                                                               IIllustrationRenderer,
-                                                                               name=self.illustration.renderer)
+            renderer = self.illustration_renderer = self.illustration.get_renderer()
             if renderer is not None:
                 renderer.update()
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/renderer/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer.interfaces import IRenderedContent, IContentRenderer, IRendererSettings
+
+# import packages
+from pyams_utils.adapter import adapter_config
+from pyams_utils.request import check_request
+from zope.interface import implementer
+
+
+@implementer(IRenderedContent)
+class RenderedContentMixin(object):
+    """Renderer mixin interface"""
+
+    renderer = None
+
+    def get_renderer(self, request=None):
+        if request is None:
+            request = check_request()
+        return request.registry.queryMultiAdapter((self, request), IContentRenderer, name=self.renderer)
+
+
+@adapter_config(context=IRenderedContent, provides=IContentRenderer)
+def RenderedContentRendererFactory(context):
+    """Rendered content renderer factory"""
+    return context.get_renderer()
+
+
+@adapter_config(context=IRenderedContent, provides=IRendererSettings)
+def RenderedContentRendererSettingsFactory(context):
+    """Rendered content renderer settings factory"""
+    renderer = IContentRenderer(context)
+    if renderer.target_interface is None:
+        return None
+    return renderer.target_interface(context)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/renderer/interfaces/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,44 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation import IAttributeAnnotatable
+from zope.contentprovider.interfaces import IContentProvider
+
+# import packages
+from zope.interface import Interface, Attribute
+
+
+class IRenderedContent(IAttributeAnnotatable):
+    """Generic interface for any rendered content"""
+
+    renderer = Attribute("Selected renderer name")
+
+    def get_renderer(self, request=None):
+        """Get selected renderer implementation"""
+
+
+class IContentRenderer(IContentProvider):
+    """Content renderer interface"""
+
+    label = Attribute("Renderer label")
+
+    target_interface = Attribute("Renderer target interface")
+
+
+class IRendererSettings(Interface):
+    """Base renderer settings interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/renderer/zmi/__init__.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,74 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer.interfaces import IRenderedContent, IContentRenderer, IRendererSettings
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_template.template import get_view_template
+from pyams_utils.adapter import ContextRequestAdapter
+from pyams_zmi.form import AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@implementer(IContentRenderer)
+class BaseContentRenderer(ContextRequestAdapter):
+    """Base content renderer"""
+
+    target_interface = None
+
+    @property
+    def settings(self):
+        if self.target_interface is None:
+            return None
+        return IRendererSettings(self.context)
+
+    render = get_view_template()
+
+
+@pagelet_config(name='renderer-properties.html', context=IRenderedContent, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class RendererPropertiesEditForm(AdminDialogEditForm):
+    """Renderer properties edit form"""
+
+    legend = _("Edit renderer properties")
+    icon_css_class = 'fa fa-fw fa-pencil-square-o'
+
+    @property
+    def fields(self):
+        renderer = IContentRenderer(self.context)
+        return field.Fields(renderer.target_interface or Interface)
+
+    ajax_handler = 'renderer-properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def getContent(self):
+        return IRendererSettings(self.context)
+
+
+@view_config(name='renderer-properties.json', context=IRenderedContent, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class RendererPropertiesAJAXEditForm(AJAXEditForm, RendererPropertiesEditForm):
+    """Renderer properties edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/renderer/zmi/templates/renderer-input.pt	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,43 @@
+<label class="input bordered with-icon" i18n:domain="pyams_security">
+	<tal:var define="render view/show_renderer_properties">
+		<i class="icon-append fa fa-fw fa-pencil-square-o hint align-base opaque"
+			title="Edit renderer properties" i18n:attributes="title"
+			data-ams-hint-gravity="se" data-toggle="modal"
+			tal:condition="render"
+			tal:attributes="data-ams-url extension:absolute_url(context, 'renderer-properties.html');"></i>
+		<i class="icon-append fa fa-fw fa-pencil-square-o opacity-50"
+			tal:condition="not:render"></i>
+	</tal:var>
+	<div class="select2-parent">
+		<select class="select2"
+				tal:attributes='id view/id;
+								name string:${view/name}:list;
+								class string:${view/klass} select2;
+								style view/style;
+								title view/title;
+								lang view/lang;
+								onclick view/onclick;
+								ondblclick view/ondblclick;
+								onmousedown view/onmousedown;
+								onmouseup view/onmouseup;
+								onmouseover view/onmouseover;
+								onmousemove view/onmousemove;
+								onmouseout view/onmouseout;
+								onkeypress view/onkeypress;
+								onkeydown view/onkeydown;
+								onkeyup view/onkeyup;
+								disabled view/disabled;
+								tabindex view/tabindex;
+								onfocus view/onfocus;
+								onblur view/onblur;
+								onchange view/onchange;
+								multiple view/multiple;
+								size view/size;
+								data-ams-data extension:object_data(view);'>
+			<option tal:repeat="entry view/items"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="python:view.get_content(entry) if hasattr(view, 'get_content') else entry['content']"></option>
+		</select>
+	</div>
+</label>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/renderer/zmi/widget.py	Thu Feb 15 15:08:29 2018 +0100
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.renderer.interfaces import IContentRenderer
+from pyams_form.interfaces.form import IFormLayer
+from z3c.form.interfaces import INPUT_MODE
+
+# import packages
+from pyams_form.widget import Select2Widget, widgettemplate_config
+from z3c.form.widget import FieldWidget
+
+
+@widgettemplate_config(mode=INPUT_MODE, template='templates/renderer-input.pt', layer=IFormLayer)
+class RendererWidget(Select2Widget):
+    """Illustration renderer selection widget"""
+
+    @property
+    def show_renderer_properties(self):
+        renderer = IContentRenderer(self.context)
+        return (renderer is not None) and (renderer.target_interface is not None)
+
+
+def RendererFieldWidget(field, request):
+    return FieldWidget(field, RendererWidget(request))