Changes inner forms base class
authorThierry Florac <thierry.florac@onf.fr>
Mon, 18 Jan 2016 16:08:07 +0100
changeset 7 cbc55162b64e
parent 6 b952eda9bb42
child 8 4a2b5dd3d69b
Changes inner forms base class
src/pyams_content/component/extfile/zmi/container.py
src/pyams_content/component/gallery/zmi/__init__.py
src/pyams_content/component/gallery/zmi/container.py
src/pyams_content/component/links/zmi/container.py
src/pyams_content/component/paragraph/__init__.py
src/pyams_content/component/paragraph/interfaces/__init__.py
src/pyams_content/component/paragraph/zmi/__init__.py
src/pyams_content/component/paragraph/zmi/container.py
src/pyams_content/component/paragraph/zmi/html.py
src/pyams_content/component/paragraph/zmi/illustration.py
src/pyams_content/component/paragraph/zmi/interfaces.py
src/pyams_content/component/paragraph/zmi/summary.py
src/pyams_content/component/theme/interfaces/__init__.py
src/pyams_content/component/theme/portlet.py
src/pyams_content/component/theme/zmi/portlet.py
--- a/src/pyams_content/component/extfile/zmi/container.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/extfile/zmi/container.py	Mon Jan 18 16:08:07 2016 +0100
@@ -251,3 +251,13 @@
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
 class ExtFileLinksContainerAJAXEditForm(AJAXEditForm, ExtFileLinksContainerLinksEditForm):
     """External file links container edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'files' in changes.get(IExtFileLinksContainer, ()):
+            return {'status': 'success',
+                    'event': 'PyAMS_content.changed_item',
+                    'event_options': {'object_type': 'extfiles_container',
+                                      'object_name': self.context.__name__,
+                                      'nb_files': len(IExtFileLinksContainer(self.context).files or ())}}
+        else:
+            return super(ExtFileLinksContainerAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/gallery/zmi/__init__.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/gallery/zmi/__init__.py	Mon Jan 18 16:08:07 2016 +0100
@@ -59,7 +59,7 @@
 
 
 @pagelet_config(name='add-gallery.html', context=IGalleryContainerTarget, layer=IPyAMSLayer,
-                permission='pyams.ManageContent')
+                permission=MANAGE_CONTENT_PERMISSION)
 class GalleryAddForm(AdminDialogAddForm):
     """Gallery add form"""
 
--- a/src/pyams_content/component/gallery/zmi/container.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/gallery/zmi/container.py	Mon Jan 18 16:08:07 2016 +0100
@@ -114,6 +114,20 @@
         return super(GalleryContainerTable, self).render()
 
 
+@adapter_config(name='manage', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
+class GalleryContainerManageColumn(ActionColumn):
+    """Gallery container manage column"""
+
+    icon_class = 'fa fa-fw fa-camera'
+    icon_hint = _("Display gallery contents")
+
+    url = 'contents.html'
+    target = None
+    modal_target = True
+
+    weight = 5
+
+
 @adapter_config(name='name', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
 class GalleryContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
     """Galleries container name column"""
@@ -138,20 +152,6 @@
         return len(obj)
 
 
-@adapter_config(name='manage', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
-class GalleryContainerManageColumn(ActionColumn):
-    """Gallery container manage column"""
-
-    icon_class = 'fa fa-fw fa-camera'
-    icon_hint = _("Display gallery contents")
-
-    url = 'contents.html'
-    target = None
-    modal_target = True
-
-    weight = 30
-
-
 @adapter_config(name='trash', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
 class GalleryContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
     """Galleries container trash column"""
@@ -196,3 +196,13 @@
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
 class GalleryLinksContainerAJAXEditForm(AJAXEditForm, GalleryLinksContainerLinksEditForm):
     """Galleries links container edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'galleries' in changes.get(IGalleryLinksContainer, ()):
+            return {'status': 'success',
+                    'event': 'PyAMS_content.changed_item',
+                    'event_options': {'object_type': 'galleries_container',
+                                      'object_name': self.context.__name__,
+                                      'nb_galleries': len(IGalleryLinksContainer(self.context).galleries or ())}}
+        else:
+            return super(GalleryLinksContainerAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/links/zmi/container.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/links/zmi/container.py	Mon Jan 18 16:08:07 2016 +0100
@@ -209,3 +209,13 @@
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
 class LinkLinksContainerAJAXEditForm(AJAXEditForm, LinkLinksContainerLinksEditForm):
     """Links links container edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'links' in changes.get(ILinkLinksContainer, ()):
+            return {'status': 'success',
+                    'event': 'PyAMS_content.changed_item',
+                    'event_options': {'object_type': 'links_container',
+                                      'object_name': self.context.__name__,
+                                      'nb_links': len(ILinkLinksContainer(self.context).links or ())}}
+        else:
+            return super(LinkLinksContainerAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/paragraph/__init__.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/__init__.py	Mon Jan 18 16:08:07 2016 +0100
@@ -37,6 +37,7 @@
 class BaseParagraph(Persistent, Contained):
     """Base paragraph persistent class"""
 
+    visible = FieldProperty(IBaseParagraph['visible'])
     title = FieldProperty(IBaseParagraph['title'])
 
 
--- a/src/pyams_content/component/paragraph/interfaces/__init__.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/interfaces/__init__.py	Mon Jan 18 16:08:07 2016 +0100
@@ -25,7 +25,7 @@
 from pyams_i18n.schema import I18nTextLineField, I18nHTMLField
 from zope.container.constraints import containers, contains
 from zope.interface import Interface, Attribute
-from zope.schema import Choice
+from zope.schema import Bool, Choice
 
 from pyams_content import _
 
@@ -38,6 +38,11 @@
 
     containers('.IParagraphContainer')
 
+    visible = Bool(title=_("Visible?"),
+                   description=_("Is this paragraph visible in front-office?"),
+                   required=True,
+                   default=True)
+
     title = I18nTextLineField(title=_("Title"),
                               description=_("Paragraph title"),
                               required=False)
--- a/src/pyams_content/component/paragraph/zmi/__init__.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/zmi/__init__.py	Mon Jan 18 16:08:07 2016 +0100
@@ -16,5 +16,27 @@
 # import standard library
 
 # import interfaces
+from pyams_content.component.paragraph.interfaces import IBaseParagraph
+from pyams_i18n.interfaces import II18n
 
 # import packages
+from pyams_form.form import AJAXEditForm
+
+
+class BaseParagraphAJAXEditForm(AJAXEditForm):
+    """Base paragraph AJAX edit form"""
+
+    def get_ajax_output(self, changes):
+        updated = changes.get(IBaseParagraph, ())
+        if ('title' in updated) or ('visible' in updated):
+            return {'status': 'success',
+                    'event': 'PyAMS_content.changed_item',
+                    'event_options': {'object_type': 'paragraph',
+                                      'object_name': self.context.__name__,
+                                      'title': II18n(self.context).query_attribute('title', request=self.request),
+                                      'visible': self.context.visible}}
+        if 'title' in changes.get(IBaseParagraph, ()):
+            return {'status': 'reload',
+                    'location': '#paragraphs.html'}
+        else:
+            return super(BaseParagraphAJAXEditForm, self).get_ajax_output(changes)
--- a/src/pyams_content/component/paragraph/zmi/container.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/zmi/container.py	Mon Jan 18 16:08:07 2016 +0100
@@ -9,6 +9,7 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
+from pyams_form.interfaces.form import IFormSecurityContext
 
 __docformat__ = 'restructuredtext'
 
@@ -18,10 +19,11 @@
 
 # import interfaces
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_content.component.extfile.interfaces import IExtFileLinksContainerTarget
-from pyams_content.component.gallery.interfaces import IGalleryLinksContainerTarget
-from pyams_content.component.links.interfaces import ILinkLinksContainerTarget
-from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer
+from pyams_content.component.extfile.interfaces import IExtFileLinksContainerTarget, IExtFileLinksContainer
+from pyams_content.component.gallery.interfaces import IGalleryLinksContainerTarget, IGalleryLinksContainer
+from pyams_content.component.links.interfaces import ILinkLinksContainerTarget, ILinkLinksContainer
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, IBaseParagraph
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
 from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces import IInnerPage, IPageHeader
 from pyams_skin.layer import IPyAMSLayer
@@ -35,11 +37,12 @@
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, ActionColumn
+from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, ActionColumn, JsActionColumn
 from pyams_skin.viewlet.menu import MenuItem
 from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter
 from pyams_utils.url import absolute_url
+from pyramid.exceptions import NotFound
 from pyramid.view import view_config
 from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.view import AdminView
@@ -64,7 +67,8 @@
 # Paragraphs container view
 #
 
-@pagelet_config(name='paragraphs.html', context=IParagraphContainerTarget, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@pagelet_config(name='paragraphs.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
 @template_config(template='templates/container.pt', layer=IPyAMSLayer)
 @implementer(IInnerPage)
 class ParagraphContainerView(AdminView):
@@ -103,10 +107,12 @@
     @property
     def data_attributes(self):
         attributes = super(ParagraphContainerTable, self).data_attributes
-        del attributes['tr']['data-ams-url']
-        del attributes['tr']['data-toggle']
         attributes['table'] = {'id': self.id,
+                               'data-ams-plugins': 'pyams_content',
+                               'data-ams-plugin-pyams_content-src':
+                                   '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
                                'data-ams-location': absolute_url(IParagraphContainer(self.context), self.request),
+                               'data-ams-tablednd-drag-handle': 'td.sorter',
                                'data-ams-tablednd-drop-target': 'set-paragraphs-order.json'}
         return attributes
 
@@ -121,19 +127,58 @@
         return super(ParagraphContainerTable, self).render()
 
 
-@adapter_config(name='properties', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+@adapter_config(name='sorter', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
                 provides=IColumn)
-class ParagraphContainerPropertiesColumn(ActionColumn):
-    """Paragraphs container properties column"""
+class ParagraphContainerSorterColumn(ProtectedFormObjectMixin, ActionColumn):
+    """Paragraphs container sorter column"""
+
+    cssClasses = {'th': 'action',
+                  'td': 'action sorter'}
+
+    icon_class = 'fa fa-fw fa-sort'
+    icon_hint = _("Click and drag to sort paragraphs...")
+
+    url = '#'
+    weight = 1
 
-    icon_class = 'fa fa-fw fa-edit'
-    icon_hint = _("Paragraph properties")
+    def get_url(self, item):
+        return '#'
+
 
-    url = 'properties.html'
-    modal_target = True
+@adapter_config(name='show-hide', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerShowHideColumn(ProtectedFormObjectMixin, JsActionColumn):
+    """Paragraphs container visibility switcher column"""
+
+    cssClasses = {'th': 'action',
+                  'td': 'action switcher'}
+
+    icon_class = 'fa fa-fw fa-eye'
+    icon_hint = _("Switch paragraph visibility")
+
+    url = 'PyAMS_content.paragraphs.switchVisibility'
 
     weight = 5
 
+    def get_icon(self, item):
+        if item.visible:
+            icon_class = 'fa fa-fw fa-eye'
+        else:
+            icon_class = 'fa fa-fw fa-eye-slash text-danger'
+        return '<i class="{icon_class}"></i>'.format(icon_class=icon_class)
+
+    def renderCell(self, item):
+        if self.permission and not self.request.has_permission(self.permission, context=item):
+            return self.get_icon(item)
+        else:
+            return super(ParagraphContainerShowHideColumn, self).renderCell(item)
+
+
+@adapter_config(context=ParagraphContainerShowHideColumn, provides=IFormSecurityContext)
+def ShowHideColumnSecurityContextFactory(column):
+    """Show/hide column security context factory"""
+    return column.table.context
+
 
 @adapter_config(name='files', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
                 provides=IColumn)
@@ -143,6 +188,8 @@
     icon_class = 'fa fa-fw fa-file-text-o'
     icon_hint = _("External files")
 
+    cssClasses = {'td': 'action extfiles nowrap'}
+
     url = 'extfile-links.html'
     modal_target = True
 
@@ -151,7 +198,14 @@
     def renderCell(self, item):
         if not IExtFileLinksContainerTarget.providedBy(item):
             return ''
-        return super(ParagraphContainerExtFileLinksColumn, self).renderCell(item)
+        action = '{action} <span class="count">{{count}}</span>'.format(
+            action=super(ParagraphContainerExtFileLinksColumn, self).renderCell(item))
+        length = len(IExtFileLinksContainer(item).files or ())
+        if length:
+            action = action.format(count='({0})'.format(length))
+        else:
+            action = action.format(count='')
+        return action
 
 
 @adapter_config(name='links', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
@@ -162,6 +216,8 @@
     icon_class = 'fa fa-fw fa-link'
     icon_hint = _("Useful links")
 
+    cssClasses = {'td': 'action links nowrap'}
+
     url = 'link-links.html'
     modal_target = True
 
@@ -170,7 +226,14 @@
     def renderCell(self, item):
         if not ILinkLinksContainerTarget.providedBy(item):
             return ''
-        return super(ParagraphContainerLinkLinksColumn, self).renderCell(item)
+        action = '{action} <span class="count">{{count}}</span>'.format(
+            action=super(ParagraphContainerLinkLinksColumn, self).renderCell(item))
+        length = len(ILinkLinksContainer(item).links or ())
+        if length:
+            action = action.format(count='({0})'.format(length))
+        else:
+            action = action.format(count='')
+        return action
 
 
 @adapter_config(name='gallery', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
@@ -181,6 +244,8 @@
     icon_class = 'fa fa-fw fa-picture-o'
     icon_hint = _("Images galleries")
 
+    cssClasses = {'td': 'action galleries nowrap'}
+
     url = 'gallery-links.html'
     modal_target = True
 
@@ -189,7 +254,14 @@
     def renderCell(self, item):
         if not IGalleryLinksContainerTarget.providedBy(item):
             return ''
-        return super(ParagraphContainerGalleryLinksColumn, self).renderCell(item)
+        action = '{action} <span class="count">{{count}}</span>'.format(
+            action=super(ParagraphContainerGalleryLinksColumn, self).renderCell(item))
+        length = len(IGalleryLinksContainer(item).galleries or ())
+        if length:
+            action = action.format(count='({0})'.format(length))
+        else:
+            action = action.format(count='')
+        return action
 
 
 @adapter_config(name='name', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
@@ -201,6 +273,25 @@
 
     weight = 50
 
+    def renderHeadCell(self):
+        return '<span class="small hint" title="{title}" data-ams-hint-gravity="e"' \
+               '      data-ams-stop-propagation="true"' \
+               '      data-ams-click-handler="PyAMS_content.paragraphs.switchAllEditors">' \
+               '    <i class="fa fa-plus-square-o"></i>' \
+               '</span> '.format(
+                    title=self.request.localizer.translate(_("Click to open/close all paragraphs editors"))) + \
+               super(ParagraphContainerTitleColumn, self).renderHeadCell()
+
+    def renderCell(self, item):
+        return '<div><span class="small hint" title="{title}" data-ams-hint-gravity="e"' \
+               '      data-ams-stop-propagation="true" ' \
+               '      data-ams-click-handler="PyAMS_content.paragraphs.switchEditor">' \
+               '    <i class="fa fa-plus-square-o"></i>' \
+               '</span> '.format(
+                    title=self.request.localizer.translate(_("Click to open/close paragraph editor"))) + \
+               '<span class="title">{0}</span>'.format(super(ParagraphContainerTitleColumn, self).renderCell(item)) + \
+               '</div><div class="inner-table-form editor margin-x-10 margin-bottom-0"></div>'
+
     def getValue(self, obj):
         return II18n(obj).query_attribute('title', request=self.request) or '--'
 
@@ -237,3 +328,50 @@
     order = list(map(str, json.loads(request.params.get('names'))))
     container.updateOrder(order)
     return {'status': 'success'}
+
+
+@view_config(name='set-paragraph-visibility.json', context=IParagraphContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_paragraph_visibility(request):
+    """Set paragraph visibility"""
+    container = IParagraphContainer(request.context)
+    paragraph = container.get(str(request.params.get('object_name')))
+    if paragraph is None:
+        raise NotFound()
+    paragraph = IBaseParagraph(paragraph)
+    paragraph.visible = not paragraph.visible
+    return {'visible': paragraph.visible}
+
+
+@view_config(name='get-paragraph-editor.json', context=IParagraphContainer, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def get_paragraph_editor(request):
+    """Get paragraph editor"""
+    container = IParagraphContainer(request.context)
+    paragraph = container.get(str(request.params.get('object_name')))
+    if paragraph is None:
+        raise NotFound()
+    registry = request.registry
+    editor = registry.queryMultiAdapter((paragraph, request), IParagraphInnerEditor)
+    if editor is None:
+        editor = registry.queryAdapter(paragraph, IParagraphInnerEditor)
+    if editor is not None:
+        editor.update()
+        return editor.render()
+
+
+@view_config(name='get-paragraphs-editors.json', context=IParagraphContainer, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def get_paragraphs_editors(request):
+    """Get all paragraphs inner editors"""
+    container = IParagraphContainer(request.context)
+    registry = request.registry
+    result = {}
+    for key, paragraph in container.items():
+        editor = registry.queryMultiAdapter((paragraph, request), IParagraphInnerEditor)
+        if editor is None:
+            editor = registry.queryAdapter(paragraph, IParagraphInnerEditor)
+        if editor is not None:
+            editor.update()
+            result[key] = editor.render()
+    return result
--- a/src/pyams_content/component/paragraph/zmi/html.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/zmi/html.py	Mon Jan 18 16:08:07 2016 +0100
@@ -9,8 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_template.template import template_config, ViewTemplate, get_view_template
 
 __docformat__ = 'restructuredtext'
 
@@ -19,25 +17,32 @@
 
 # import interfaces
 from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IHTMLParagraph, \
-    IParagraphContainer, IBaseParagraph, IParagraphSummary
+    IParagraphContainer, IParagraphSummary
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
 from pyams_i18n.interfaces import II18n
 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
 from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import INPUT_MODE
 
 # import packages
 from pyams_content.component.paragraph.html import HTMLParagraph
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
 from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
-from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.form import AJAXAddForm
 from pyams_form.security import ProtectedFormObjectMixin
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config, get_view_template
 from pyams_utils.adapter import adapter_config, ContextRequestAdapter
 from pyams_utils.traversing import get_parent
-from pyams_viewlet.viewlet import viewlet_config, ContentProvider
+from pyams_viewlet.viewlet import viewlet_config
 from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
 from pyramid.view import view_config
-from z3c.form import field
+from z3c.form import field, button
+from zope.interface import implementer
 
 from pyams_content import _
 
@@ -111,25 +116,38 @@
 
     fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__')
     ajax_handler = 'properties.json'
-    edit_permission = 'pyams.ManageContent'
+    edit_permission = MANAGE_CONTENT_PERMISSION
 
     def updateWidgets(self, prefix=None):
         super(HTMLParagraphPropertiesEditForm, self).updateWidgets(prefix)
+        for lang in self.widgets['body'].langs:
+            widget = self.widgets['body'].widgets[lang]
+            widget.id = '{id}_{name}'.format(id=widget.id, name=self.context.__name__)
         self.widgets['body'].label_css_class = 'textarea'
 
 
+@adapter_config(context=(IHTMLParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm)
+class HTMLParagraphInnerEditForm(HTMLParagraphPropertiesEditForm):
+    """HTML paragraph inner edit form"""
+
+    legend = None
+    main_group_legend = _("HTML paragraph properties")
+    main_group_class = 'inner'
+
+    @property
+    def buttons(self):
+        if self.mode == INPUT_MODE:
+            return button.Buttons(IEditFormButtons)
+        else:
+            return button.Buttons()
+
+
 @view_config(name='properties.json', context=IHTMLParagraph, request_type=IPyAMSLayer,
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class HTMLParagraphPropertiesAJAXEditForm(AJAXEditForm, HTMLParagraphPropertiesEditForm):
+class HTMLParagraphPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, HTMLParagraphPropertiesEditForm):
     """HTML paragraph properties edit form, JSON renderer"""
 
-    def get_ajax_output(self, changes):
-        if 'title' in changes.get(IBaseParagraph, ()):
-            return {'status': 'reload',
-                    'location': '#paragraphs.html'}
-        else:
-            return super(HTMLParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
-
 
 #
 # HTML paragraph summary
--- a/src/pyams_content/component/paragraph/zmi/illustration.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/zmi/illustration.py	Mon Jan 18 16:08:07 2016 +0100
@@ -9,25 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from pyramid.view import view_config
-from pyams_content.component.paragraph.illustration import Illustration
-from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IIllustrationParagraph, \
-    IParagraphContainer, IBaseParagraph, IParagraphSummary, IIllustrationRenderer
-from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
-from pyams_content.shared.common.interfaces import IWfSharedContent
-from pyams_form.form import AJAXAddForm, AJAXEditForm
-from pyams_form.security import ProtectedFormObjectMixin
-from pyams_i18n.interfaces import II18n
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
-from pyams_skin.layer import IPyAMSLayer
-from pyams_skin.viewlet.toolbar import ToolbarMenuItem
-from pyams_template.template import template_config, get_view_template
-from pyams_utils.adapter import ContextRequestAdapter, adapter_config
-from pyams_utils.traversing import get_parent
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
 
 __docformat__ = 'restructuredtext'
 
@@ -35,9 +16,33 @@
 # import standard library
 
 # import interfaces
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IIllustrationParagraph, \
+    IParagraphContainer, IParagraphSummary, IIllustrationRenderer
+from pyams_content.component.paragraph.zmi.interfaces import IParagraphInnerEditor
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IInnerForm, IEditFormButtons
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import INPUT_MODE
 
 # import packages
-from z3c.form import field
+from pyams_content.component.paragraph.illustration import Illustration
+from pyams_content.component.paragraph.zmi import BaseParagraphAJAXEditForm
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_form.form import AJAXAddForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config, get_view_template
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer
 
 from pyams_content import _
 
@@ -106,18 +111,28 @@
     edit_permission = MANAGE_CONTENT_PERMISSION
 
 
+@adapter_config(context=(IIllustrationParagraph, IPyAMSLayer), provides=IParagraphInnerEditor)
+@implementer(IInnerForm)
+class IllustrationInnerEditForm(IllustrationPropertiesEditForm):
+    """Illustration inner edit form"""
+
+    legend = None
+    main_group_legend = _("Illustration properties")
+    main_group_class = 'inner'
+
+    @property
+    def buttons(self):
+        if self.mode == INPUT_MODE:
+            return button.Buttons(IEditFormButtons)
+        else:
+            return button.Buttons()
+
+
 @view_config(name='properties.json', context=IIllustrationParagraph, request_type=IPyAMSLayer,
              permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class IllustrationPropertiesAJAXEditForm(AJAXEditForm, IllustrationPropertiesEditForm):
+class IllustrationPropertiesAJAXEditForm(BaseParagraphAJAXEditForm, IllustrationPropertiesEditForm):
     """HTML paragraph properties edit form, JSON renderer"""
 
-    def get_ajax_output(self, changes):
-        if 'title' in changes.get(IBaseParagraph, ()):
-            return {'status': 'reload',
-                    'location': '#paragraphs.html'}
-        else:
-            return super(IllustrationPropertiesAJAXEditForm, self).get_ajax_output(changes)
-
 
 #
 # Illustration summary
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/interfaces.py	Mon Jan 18 16:08:07 2016 +0100
@@ -0,0 +1,25 @@
+#
+# 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_form.interfaces.form import IInnerForm
+
+# import packages
+
+
+class IParagraphInnerEditor(IInnerForm):
+    """Paragraph inner editor form interface"""
--- a/src/pyams_content/component/paragraph/zmi/summary.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/paragraph/zmi/summary.py	Mon Jan 18 16:08:07 2016 +0100
@@ -9,18 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
-    IParagraphSummary
-from pyams_content.shared.common.interfaces.zmi import IInnerSummaryView
-from pyams_content.shared.common.zmi.summary import SharedContentSummaryForm
-from pyams_form.form import InnerDisplayForm
-from pyams_form.interfaces.form import IInnerTabForm
-from pyams_i18n.interfaces import II18nManager
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_skin.layer import IPyAMSLayer
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
 
 __docformat__ = 'restructuredtext'
 
@@ -28,8 +16,20 @@
 # import standard library
 
 # import interfaces
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
+    IParagraphSummary
+from pyams_content.shared.common.interfaces.zmi import IInnerSummaryView
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_i18n.interfaces import II18nManager
+from pyams_skin.layer import IPyAMSLayer
 
 # import packages
+from pyams_content.shared.common.zmi.summary import SharedContentSummaryForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.form import InnerAdminDisplayForm
 from z3c.form import field
 from zope.interface import implementer, Interface
 
@@ -39,7 +39,7 @@
 @adapter_config(name='paragraphs-summary',
                 context=(IParagraphContainerTarget, IPyAMSLayer, SharedContentSummaryForm),
                 provides=IInnerTabForm)
-class ParagraphsContainerSummary(InnerDisplayForm):
+class ParagraphsContainerSummary(InnerAdminDisplayForm):
     """Paragraphs container summary"""
 
     weight = 20
--- a/src/pyams_content/component/theme/interfaces/__init__.py	Thu Oct 15 15:42:01 2015 +0200
+++ b/src/pyams_content/component/theme/interfaces/__init__.py	Mon Jan 18 16:08:07 2016 +0100
@@ -46,3 +46,14 @@
 
 class IThemesTarget(Interface):
     """Themes target interface"""
+
+
+PORTLET_SETTINGS_THEMES_KEY = 'pyams_content.themes.settings'
+
+
+class IPortletThemesSettings(Interface):
+    """Interface for portlet settings managing themes"""
+
+
+class IPortletThemesSettingsTarget(Interface):
+    """Marker interface for portlet settings managing themes"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/portlet.py	Mon Jan 18 16:08:07 2016 +0100
@@ -0,0 +1,53 @@
+#
+# 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.theme.interfaces import IPortletThemesSettings, IPortletThemesSettingsTarget, \
+    PORTLET_SETTINGS_THEMES_KEY
+from zope.annotation.interfaces import IAnnotations
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.location import locate
+
+
+@implementer(IPortletThemesSettings)
+class PortletThemesSettings(Persistent, Contained):
+    """Portlet themes settings"""
+
+
+@adapter_config(context=IPortletThemesSettingsTarget, provides=IPortletThemesSettings)
+def PortletThemesSettingsFactory(context):
+    """Portlet themes settings adapter"""
+    annotations = IAnnotations(context)
+    settings = annotations.get(PORTLET_SETTINGS_THEMES_KEY)
+    if settings is None:
+        settings = annotations[PORTLET_SETTINGS_THEMES_KEY] = PortletThemesSettings()
+        locate(settings, context, '++themes++')
+    return settings
+
+
+@adapter_config(name='themes', context=IPortletThemesSettingsTarget, provides=ITraversable)
+class ThemesPortletsSettingsTraverser(ContextAdapter):
+    """++themes++ portlet settings adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return IPortletThemesSettings(self.context)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/zmi/portlet.py	Mon Jan 18 16:08:07 2016 +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 pyams_content.component.theme.interfaces import IPortletThemesSettingsTarget
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_portal.zmi.portlet import PortletSettingsEditor
+from pyams_utils.adapter import adapter_config
+from pyams_zmi.form import InnerAdminEditForm
+from z3c.form import field
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@adapter_config(name='themes', context=(IPortletThemesSettingsTarget, IPyAMSLayer, PortletSettingsEditor),
+                provides=IInnerTabForm)
+class PortletSettingsThemesEditor(InnerAdminEditForm):
+    """Portlet settings for themes"""
+
+    id = 'themes_form'
+    tab_label = _("Themes")
+    legend = None
+
+    fields = field.Fields(Interface)
+
+    weight = 50