Rewritten base portal engine!
authorThierry Florac <thierry.florac@onf.fr>
Mon, 18 Jan 2016 18:09:46 +0100
changeset 5 670b7956c689
parent 4 a5f118662d87
child 6 f88ccd965f2d
Rewritten base portal engine!
src/pyams_portal/__init__.py
src/pyams_portal/interfaces/__init__.py
src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.mo
src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.po
src/pyams_portal/locales/pyams_portal.pot
src/pyams_portal/page.py
src/pyams_portal/portlet.py
src/pyams_portal/portlets/content/__init__.py
src/pyams_portal/portlets/content/content.pt
src/pyams_portal/portlets/content/interfaces.py
src/pyams_portal/portlets/context/__init__.py
src/pyams_portal/portlets/context/context.pt
src/pyams_portal/portlets/context/interfaces.py
src/pyams_portal/portlets/image/__init__.py
src/pyams_portal/portlets/image/interfaces.py
src/pyams_portal/resources/css/portal.min.css
src/pyams_portal/resources/js/portal.js
src/pyams_portal/resources/js/portal.min.js
src/pyams_portal/slot.py
src/pyams_portal/template.py
src/pyams_portal/workflow.py
src/pyams_portal/zmi/container.py
src/pyams_portal/zmi/interfaces.py
src/pyams_portal/zmi/layout.py
src/pyams_portal/zmi/page.py
src/pyams_portal/zmi/portlet.py
src/pyams_portal/zmi/portlets/content.py
src/pyams_portal/zmi/portlets/context.py
src/pyams_portal/zmi/portlets/image.py
src/pyams_portal/zmi/portlets/templates/image-preview.pt
src/pyams_portal/zmi/template.py
src/pyams_portal/zmi/template/__init__.py
src/pyams_portal/zmi/template/config.py
src/pyams_portal/zmi/template/page.py
src/pyams_portal/zmi/template/templates/config.pt
src/pyams_portal/zmi/template/workflow.py
src/pyams_portal/zmi/templates/layout.pt
src/pyams_portal/zmi/templates/portlet.pt
--- a/src/pyams_portal/__init__.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/__init__.py	Mon Jan 18 18:09:46 2016 +0100
@@ -19,6 +19,7 @@
 from pyramid.i18n import TranslationStringFactory
 _ = TranslationStringFactory('pyams_portal')
 
+from pyams_portal.interfaces import MANAGE_TEMPLATE_PERMISSION
 from pyams_utils.interfaces import VIEW_PERMISSION, VIEW_SYSTEM_PERMISSION
 
 
@@ -29,10 +30,10 @@
     include_package(config)
 
     # register custom permissions
-    config.register_permission({'id': 'portal.templates.manage',
+    config.register_permission({'id': MANAGE_TEMPLATE_PERMISSION,
                                 'title': _("Manage portal templates")})
 
     # register custom roles
     config.register_role({'id': 'portal.TemplatesManager',
                           'title': _("Portal templates manager"),
-                          'permissions': {'portal.templates.manage', VIEW_PERMISSION, VIEW_SYSTEM_PERMISSION}})
+                          'permissions': {MANAGE_TEMPLATE_PERMISSION, VIEW_PERMISSION, VIEW_SYSTEM_PERMISSION}})
--- a/src/pyams_portal/interfaces/__init__.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/interfaces/__init__.py	Mon Jan 18 18:09:46 2016 +0100
@@ -16,10 +16,10 @@
 # import standard library
 
 # import interfaces
-from pyams_workflow.interfaces import IWorkflowManagedContent
 from zope.annotation.interfaces import IAttributeAnnotatable
 from zope.container.interfaces import IContainer
 from zope.contentprovider.interfaces import IContentProvider
+from zope.location.interfaces import ILocation, IContained
 
 # import packages
 from pyams_security.schema import PermissionField
@@ -31,21 +31,33 @@
 from pyams_portal import _
 
 
+MANAGE_TEMPLATE_PERMISSION = 'pyams_portal.manage_template'
+
+
+#
+# Portlet interfaces
+#
+
 class IPortlet(Interface):
-    """Portlet interface"""
+    """Portlet utility interface
+
+    Portlets are registered utilities providing IPortlet
+    """
 
     name = Attribute("Portlet internal name")
 
     label = Attribute("Portlet visible name")
 
     permission = PermissionField(title="Portlet permission",
-                                 description="Permission required to display permission",
+                                 description="Permission required to display portlet",
                                  required=False)
 
-    toolbar_image = Attribute("Porlet toolbar image")
+    toolbar_image = Attribute("Portlet toolbar image")
 
     toolbar_css_class = Attribute("Portlet toolbar CSS class")
 
+    settings_class = Attribute("Portlet settings class")
+
 
 class IPortletAddingInfo(Interface):
     """Portlet adding info interface"""
@@ -59,35 +71,53 @@
                        vocabulary='PyAMS template slots')
 
 
-class IPortletConfiguration(Interface):
-    """Portlet configuration interface"""
+class IPortletSettings(ILocation, IAttributeAnnotatable):
+    """Portlet settings interface
+
+    Portlet settings is parented to it's configuration
+    """
+
+    configuration = Attribute("Settings parent configuration")
+
+    visible = Bool(title=_("Visible portlet?"),
+                   description=_("Select 'no' to hide this portlet..."),
+                   required=True,
+                   default=True)
+
 
-    template = Attribute("Template to which this configuration applis")
+PORTLETS_CONFIGURATION_KEY = 'pyams_portal.portlets'
+
+
+class IPortletConfiguration(ILocation):
+    """Portlet common configuration interface
 
-    slot_name = TextLine(title=_("Slot name"),
-                         description=_("Slot name to which this configuration applies"),
-                         required=True)
+    This is generic configuration settings common to all portlets.
+    Portlet configuration is parented to:
+     - it's template if parent is the template
+     - it's context if parent is a portal context
+    """
+
+    portlet_id = Int(title="Portlet ID",
+                     required=True)
 
     portlet_name = Attribute("Portlet name")
 
-    position = Int(title=_("Position"),
-                   description=_("Portlet position inside slot"),
-                   required=True,
-                   min=0)
-
-    visible = Bool(title=_("Visible portlet?"),
-                   description=_("Select 'no' to hide this portlet. This will not break configuration inheritance..."),
-                   required=True,
-                   default=True)
-
     can_inherit = Attribute("Can inherit parent configuration?")
 
+    parent = Attribute("Portlet configuration parent")
+
     inherit_parent = Bool(title=_("Inherit parent configuration?"),
-                          description=_("This option is only available if context's parent is using the same template "
-                                        "and if this portlet is also present in the same slot..."),
+                          description=_("This option is only available if context's parent is using the same "
+                                        "template..."),
                           required=True,
                           default=True)
 
+    settings = Object(title="Portlet local settings",
+                      schema=IPortletSettings,
+                      readonly=True)
+
+    editor_settings = Attribute("Editor settings")
+
 
 class IPortletContentProvider(IContentProvider):
     """Portlet content provider"""
@@ -115,6 +145,26 @@
     """
 
 
+class IPortalPortletsConfiguration(IContained):
+    """Portal template portlet configuration interface"""
+
+    def get_portlet_configuration(self, portlet_id):
+        """Get portlet configuration for given slot"""
+
+    def set_portlet_configuration(self, portlet_id, config):
+        """Set portlet configuration"""
+
+    def delete_portlet_configuration(self, portlet_id):
+        """Delete portlet configuration"""
+
+
+#
+# Slot interfaces
+#
+
+PORTAL_SLOTS_KEY = 'pyams_portal.slots'
+
+
 class ISlot(Interface):
     """Portal template slot interface"""
 
@@ -133,19 +183,14 @@
 
     slot_name = TextLine(title="Slot name")
 
+    portlet_ids = PersistentList(title="Portlet IDs",
+                                 value_type=Int())
+
     visible = Bool(title=_("Visible slot?"),
-                   description=_("Select 'no' to hide this slot. This will not break configuration inheritance..."),
+                   description=_("Select 'no' to hide this slot..."),
                    required=True,
                    default=True)
 
-    can_inherit = Attribute("Can inherit parent configuration?")
-
-    inherit_parent = Bool(title=_("Inherit parent configuration?"),
-                          description=_("This option is only available if context's parent template is using a "
-                                        "template containing the same slot..."),
-                          required=True,
-                          default=True)
-
     xs_width = Int(title=_("Extra small device width"),
                    description=_("Slot width, in columns count, on extra small devices (phones...); "
                                  "set to 0 to hide the portlet"),
@@ -192,9 +237,14 @@
     """Slot renderer"""
 
 
-class IPortalTemplateConfiguration(Interface):
+#
+# Template configuration interfaces
+#
+
+class IPortalTemplateConfiguration(IContained):
     """Portal template configuration interface"""
 
+    # Rows configuration
     rows = Int(title="Rows count",
                required=True,
                default=1,
@@ -209,6 +259,7 @@
     def delete_row(self, row_id):
         """Delete template row"""
 
+    # Slots configuration
     slot_names = PersistentList(title="Slot names",
                                 value_type=TextLine())
 
@@ -216,14 +267,6 @@
                                 key_type=Int(),  # row index
                                 value_type=PersistentList(value_type=TextLine()))  # slot name
 
-    slots = PersistentDict(title="Slots portlets",
-                           description="List of slots associated with a given template",
-                           key_type=Int(),  # row index
-                           value_type=PersistentDict(key_type=TextLine(),  # slot name
-                                                     value_type=PersistentList(value_type=Choice(
-                                                         vocabulary='PyAMS portal portlets')),  # portlet names
-                                                     required=False))
-
     slot_config = PersistentDict(title="Slots configuration",
                                  key_type=TextLine(),  # slot name
                                  value_type=Object(schema=ISlotConfiguration),
@@ -253,33 +296,39 @@
     def delete_slot(self, slot_name):
         """Delete template slot"""
 
-
-# class IPortalPortletsConfiguration(Interface):
-#     """Portal template portlets configuration interface"""
-#
-    portlet_config = PersistentDict(title="Portlet configuration",
-                                    key_type=TextLine(),  # slot name
-                                    value_type=PersistentDict(key_type=Int(min=0),  # portlet position inside slot
-                                                              value_type=Object(schema=IPortletConfiguration)),
-                                    required=False)
-
+    # Portlets configuration
     def add_portlet(self, portlet_name, slot_name):
         """Add portlet to givben slot"""
 
-    def set_portlet_order(self, order):
-        """Set template portlets order"""
+    def get_portlet_slot(self, portlet_id):
+        """Get row ID and slot for given portlet"""
 
-    def get_portlet_configuration(self, slot_name, position):
-        """Get portlet configuration for given slot"""
+    def set_portlet_order(self, slot_name, order):
+        """Set template portlets order"""
 
     def delete_portlet(self, slot_name, position):
         """Delete template portlet"""
 
 
-class IPortalTemplate(IAttributeAnnotatable):
+#
+# Portal templates interfaces
+#
+
+TEMPLATE_CONFIGURATION_KEY = 'pyams_portal.template'
+
+TEMPLATE_CONTAINER_CONFIGURATION_KEY = 'pyams_portal.container.configuration'
+
+PORTAL_PAGE_KEY = 'pyams_portal.page'
+
+
+class ILocalTemplateHandler(IAttributeAnnotatable):
+    """Base interface for local template handler"""
+
+
+class IPortalTemplate(ILocalTemplateHandler):
     """Portal template interface
 
-    A portal template is a named utility providing a name and a set of slots.
+    A portal template is a named utility providing a name and a set of slots containing portlets
     """
 
     name = TextLine(title=_("Template name"),
@@ -287,24 +336,28 @@
                     required=True)
 
 
-class IPortalWfTemplate(IWorkflowManagedContent):
-    """Workflow managed portal template interface"""
-
-
 class IPortalTemplateContainer(IContainer, IAttributeAnnotatable):
     """Portal template container interface"""
 
     contains(IPortalTemplate)
 
+    last_portlet_id = Int(title="Last portlet ID",
+                          required=True,
+                          default=1,
+                          min=0)
+
+    def get_portlet_id(self):
+        """Get new portlet ID"""
+
 
 class IPortalTemplateContainerConfiguration(Interface):
     """Portal templates container configuration"""
 
-    selected_portlets = List(title=_("Selected portlets"),
-                             description=_("These portlets will be directly available in templates configuration "
-                                           "page toolbar"),
-                             value_type=Choice(vocabulary="PyAMS portal portlets"),
-                             required=False)
+    toolbar_portlets = List(title=_("Toolbar portlets"),
+                            description=_("These portlets will be directly available in templates configuration "
+                                          "page toolbar"),
+                            value_type=Choice(vocabulary="PyAMS portal portlets"),
+                            required=False)
 
 
 class IPortalTemplateRenderer(IContentProvider):
@@ -320,11 +373,13 @@
 
     The page is the highest configuration level.
     It defines which template is used (a shared or local one), which gives
-    the slots list.
+    the slot and portlet lists.
     """
 
     can_inherit = Attribute("Can inherit parent template?")
 
+    parent = Attribute("Parent from which to inherit, the real parent or the source template")
+
     inherit_parent = Bool(title=_("Inherit parent template?"),
                           description=_("Should we reuse parent template?"),
                           required=True,
@@ -347,7 +402,7 @@
             raise Invalid(_("You must choose to use a local template or select a shared one!"))
 
     local_template = Object(title=_("Local template"),
-                            schema=IPortalWfTemplate,
+                            schema=IPortalTemplate,
                             required=False)
 
     template = Attribute("Used template")
Binary file src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.mo has changed
--- a/src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.po	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/locales/fr/LC_MESSAGES/pyams_portal.po	Mon Jan 18 18:09:46 2016 +0100
@@ -5,7 +5,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2015-05-12 13:59+0200\n"
+"POT-Creation-Date: 2016-01-06 11:55+0100\n"
 "PO-Revision-Date: 2015-05-12 12:10+0200\n"
 "Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
 "Language-Team: French <traduc@traduc.org>\n"
@@ -16,312 +16,202 @@
 "Generated-By: Lingua 3.10.dev0\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
-#: src/pyams_portal/workflow.py:41
-msgid "Draft"
-msgstr "Brouillon"
-
-#: src/pyams_portal/workflow.py:42
-msgid "Published"
-msgstr "Publié"
-
-#: src/pyams_portal/workflow.py:43
-msgid "Retired"
-msgstr "Retiré"
-
-#: src/pyams_portal/workflow.py:44
-msgid "Archived"
-msgstr "Archivé"
-
-#: src/pyams_portal/workflow.py:45
-msgid "Deleted"
-msgstr "Supprimé"
-
-#: src/pyams_portal/workflow.py:103
-msgid "Initialize"
-msgstr "Initialiser"
-
-#: src/pyams_portal/workflow.py:108
-msgid "Publish..."
-msgstr "Publier..."
-
-#: src/pyams_portal/workflow.py:116
-msgid ""
-"This content is currently in DRAFT mode.\n"
-"                                               Publishing it will make it "
-"publicly visible."
-msgstr ""
-"Ce modèle est actuellement en mode BROUILLON.\n"
-"En le publiant, vous le rendrez visible."
-
-#: src/pyams_portal/workflow.py:120
-msgid "Retire..."
-msgstr "Retirer..."
-
-#: src/pyams_portal/workflow.py:128
-msgid ""
-"This content is actually published.\n"
-"                                                 You can retire it to make "
-"it invisible, but contents using this\n"
-"                                                 template won't be visible "
-"anymore!"
-msgstr ""
-"Ce modèle est actuellement publié.\n"
-"Vous pouvez le retirer pour le rendre invisible, mais les contenus qui "
-"utilisent ce modèle ne seront plus consultables !"
-
-#: src/pyams_portal/workflow.py:133 src/pyams_portal/workflow.py:181
-msgid "Create new version..."
-msgstr "Créer une nouvelle version..."
-
-#: src/pyams_portal/workflow.py:143
-msgid "Re-publish..."
-msgstr "Re-publier..."
-
-#: src/pyams_portal/workflow.py:151
-msgid ""
-"This content was published and retired.\n"
-"                                                 You can re-publish it to "
-"make it visible again."
-msgstr ""
-"Ce modèle a été publié puis retiré.\n"
-"Vous pouvez le re-publier pour le rendre à nouveau disponible."
-
-#: src/pyams_portal/workflow.py:155 src/pyams_portal/workflow.py:168
-msgid "Archive..."
-msgstr "Archiver..."
-
-#: src/pyams_portal/workflow.py:163
-msgid ""
-"This content is currently published.\n"
-"                                                  If it is archived, it will "
-"not be possible to make it visible again\n"
-"                                                  except by creating a new "
-"version!"
-msgstr ""
-"Ce modèle est actuellement publié.\n"
-"S'il est archivé, il ne sera plus possible de le rendre à nouveau "
-"disponible, sauf en créant une nouvelle version."
-
-#: src/pyams_portal/workflow.py:176
-msgid ""
-"This content has been published but is currently retired.\n"
-"                                                If it is archived, it will "
-"not be possible to make it visible again\n"
-"                                                except by creating a new "
-"version!"
-msgstr ""
-"Ce contenu a été publié mais est actuellement retiré.\n"
-"S'il est archivé, il ne sera plus possible de le rendre à nouveau "
-"disponible, sauf en créant une nouvelle version."
-
-#: src/pyams_portal/workflow.py:191
-msgid "Delete..."
-msgstr "Supprimer..."
-
-#: src/pyams_portal/workflow.py:199
-msgid ""
-"This content has never been published.\n"
-"                                    It can be removed and definitely deleted."
-msgstr ""
-"Ce modèle n'a jamais été publié.\n"
-"Vous pouvez donc le supprimer définitivement."
-
-#: src/pyams_portal/__init__.py:31
+#: src/pyams_portal/__init__.py:34
 msgid "Manage portal templates"
 msgstr "Gérer les modèles de présentation"
 
-#: src/pyams_portal/__init__.py:35
+#: src/pyams_portal/__init__.py:38
 msgid "Portal templates manager"
 msgstr "Gestionnaire des modèles"
 
-#: src/pyams_portal/zmi/portlet.py:41
-msgid "Edit portlet configuration"
-msgstr "Modifier la configuration d'un modèle"
+#: src/pyams_portal/zmi/portlet.py:49
+msgid "Edit portlet settings"
+msgstr "Propriétés du composant"
+
+#: src/pyams_portal/zmi/portlet.py:85
+msgid "Main properties"
+msgstr "Propriétés"
 
-#: src/pyams_portal/zmi/portlet.py:38
+#: src/pyams_portal/zmi/portlet.py:64
+msgid "Override parent settings"
+msgstr "Remplacer le paramétrage du parent"
+
+#: src/pyams_portal/zmi/portlet.py:66
+msgid "Override template settings"
+msgstr "Remplacer le paramétrage du modèle"
+
+#: src/pyams_portal/zmi/portlet.py:46
 #, python-format
 msgid "« {0} »  portal template - {1}"
 msgstr "Modèle de présentation « {0} »  - {1}"
 
-#: src/pyams_portal/zmi/template/config.py:61
-msgid "Properties"
-msgstr "Propriétés"
-
-#: src/pyams_portal/zmi/template/config.py:72
-msgid "Portal template configuration"
-msgstr "Configuration d'un modèle"
+#: src/pyams_portal/zmi/page.py:60
+msgid "Presentation"
+msgstr "Présentation"
 
-#: src/pyams_portal/zmi/template/config.py:120
-msgid "Portlets configuration"
-msgstr "Configuration des portlets"
+#: src/pyams_portal/zmi/page.py:75
+msgid "Edit template configuration"
+msgstr "Choix du modèle de présentation"
 
-#: src/pyams_portal/zmi/template/config.py:133
-msgid "Add row..."
-msgstr "Ajouter une ligne..."
+#: src/pyams_portal/zmi/page.py:119
+msgid "Template properties"
+msgstr "Configuration du modèle"
 
-#: src/pyams_portal/zmi/template/config.py:175
-msgid "Add slot..."
-msgstr "Ajouter un slot..."
-
-#: src/pyams_portal/zmi/template/config.py:191
-msgid "Add slot"
-msgstr "Ajout d'un slot"
+#: src/pyams_portal/zmi/template.py:83
+msgid "Add template"
+msgstr "Ajouter un modèle"
 
-#: src/pyams_portal/zmi/template/config.py:265
-msgid "Edit slot properties"
-msgstr "Propriétés d'un slot"
+#: src/pyams_portal/zmi/template.py:93 src/pyams_portal/zmi/container.py:78
+msgid "Portal templates"
+msgstr "Modèles de présentation"
 
-#: src/pyams_portal/zmi/template/config.py:333
-msgid "Add portlet..."
-msgstr "Ajouter un composant..."
+#: src/pyams_portal/zmi/template.py:94
+msgid "Add shared template"
+msgstr "Ajout d'un modèle de présentation"
 
-#: src/pyams_portal/zmi/template/config.py:349
-msgid "Add portlet"
-msgstr "Ajouter un composant"
-
-#: src/pyams_portal/zmi/template/config.py:209
-#: src/pyams_portal/zmi/template/__init__.py:269
+#: src/pyams_portal/zmi/template.py:118 src/pyams_portal/zmi/layout.py:246
 msgid "Specified name is already used!"
 msgstr "Le nom indiqué est déjà utilisé !"
 
-#: src/pyams_portal/zmi/template/config.py:118
-#: src/pyams_portal/zmi/template/config.py:189
-#: src/pyams_portal/zmi/template/config.py:347
+#: src/pyams_portal/zmi/template.py:62 src/pyams_portal/zmi/layout.py:220
+#: src/pyams_portal/zmi/layout.py:382
 #, python-format
 msgid "« {0} »  portal template"
 msgstr "Modèle de présentation « {0} »"
 
-#: src/pyams_portal/zmi/template/config.py:262
+#: src/pyams_portal/zmi/layout.py:78
+msgid "Properties"
+msgstr "Propriétés"
+
+#: src/pyams_portal/zmi/layout.py:164
+msgid "Add row..."
+msgstr "Ajouter une ligne..."
+
+#: src/pyams_portal/zmi/layout.py:206
+msgid "Add slot..."
+msgstr "Ajouter un panneau..."
+
+#: src/pyams_portal/zmi/layout.py:222
+#: src/pyams_portal/zmi/templates/layout.pt:27
+msgid "Add slot"
+msgstr "Ajout d'un panneau"
+
+#: src/pyams_portal/zmi/layout.py:303
+msgid "Edit slot properties"
+msgstr "Propriétés d'un panneau"
+
+#: src/pyams_portal/zmi/layout.py:368
+msgid "Add portlet..."
+msgstr "Ajouter un composant..."
+
+#: src/pyams_portal/zmi/layout.py:384
+msgid "Add portlet"
+msgstr "Ajouter un composant"
+
+#: src/pyams_portal/zmi/layout.py:68
+msgid "Template management"
+msgstr "Ce modèle"
+
+#: src/pyams_portal/zmi/layout.py:101
+msgid "Template configuration"
+msgstr "Configuration d'un modèle"
+
+#: src/pyams_portal/zmi/layout.py:96
+msgid "Local template configuration"
+msgstr "Configuration d'un modèle local"
+
+#: src/pyams_portal/zmi/layout.py:300
 #, python-format
 msgid "« {0} »  portal template - {1} slot"
-msgstr "Modèle de présentation « {0} » - Slot {1}"
-
-#: src/pyams_portal/zmi/template/workflow.py:109
-msgid "Publish template"
-msgstr "Publier un modèle"
-
-#: src/pyams_portal/zmi/template/workflow.py:151
-msgid "Retire template"
-msgstr "Retirer un modèle"
-
-#: src/pyams_portal/zmi/template/workflow.py:180
-msgid "Archive template"
-msgstr "Archiver un modèle"
-
-#: src/pyams_portal/zmi/template/workflow.py:209
-#: src/pyams_portal/zmi/template/workflow.py:201
-msgid "Create new version"
-msgstr "Créer une nouvelle version"
+msgstr "Modèle de présentation « {0} » - Panneau {1}"
 
-#: src/pyams_portal/zmi/template/workflow.py:100
-#: src/pyams_portal/zmi/template/workflow.py:142
-#: src/pyams_portal/zmi/template/workflow.py:171
-#: src/pyams_portal/zmi/template/workflow.py:200
-msgid "Close"
-msgstr "Fermer"
-
-#: src/pyams_portal/zmi/template/workflow.py:101
-msgid "Publish"
-msgstr "Publier"
+#: src/pyams_portal/zmi/layout.py:99
+#, python-format
+msgid "Shared template configuration ({0})"
+msgstr "Configuration d'un modèle partagé ({0})"
 
-#: src/pyams_portal/zmi/template/workflow.py:143
-msgid "Retire"
-msgstr "Retirer"
-
-#: src/pyams_portal/zmi/template/workflow.py:172
-msgid "Archive"
-msgstr "Archiver"
-
-#: src/pyams_portal/zmi/template/__init__.py:88
-#: src/pyams_portal/zmi/template/__init__.py:196
-#: src/pyams_portal/zmi/template/__init__.py:240
-msgid "Portal templates"
-msgstr "Modèles de présentation"
-
-#: src/pyams_portal/zmi/template/__init__.py:97
+#: src/pyams_portal/zmi/container.py:87
 msgid "Shared portal templates"
 msgstr "Modèles de présentation partagés"
 
-#: src/pyams_portal/zmi/template/__init__.py:163
+#: src/pyams_portal/zmi/container.py:127
 msgid "Delete template"
 msgstr "Supprimer le modèle"
 
-#: src/pyams_portal/zmi/template/__init__.py:195
-msgid "Portal"
-msgstr "Portail"
+#: src/pyams_portal/zmi/container.py:170
+msgid "Selected portlets..."
+msgstr "Composants sélectionnés..."
 
-#: src/pyams_portal/zmi/template/__init__.py:229
-msgid "Add shared template..."
-msgstr "Ajouter un modèle partagé..."
+#: src/pyams_portal/zmi/container.py:186
+msgid "Portal templates container"
+msgstr "Gestionnaire des modèles"
 
-#: src/pyams_portal/zmi/template/__init__.py:241
-msgid "Add shared template"
-msgstr "Ajout d'un modèle de présentation"
+#: src/pyams_portal/zmi/container.py:187
+msgid "Edit selected portlets"
+msgstr "Sélection des composants"
 
-#: src/pyams_portal/zmi/template/__init__.py:153
-msgid "Older versions"
-msgstr "Versions précédentes"
+#: src/pyams_portal/zmi/templates/portlet.pt:129
+#: src/pyams_portal/zmi/templates/portlet.pt:144
+msgid "Title"
+msgstr "Titre"
+
+#: src/pyams_portal/zmi/templates/portlet.pt:159
+msgid "Tab label"
+msgstr "Libellé de l'onglet"
 
-#: src/pyams_portal/zmi/template/__init__.py:212
-#: src/pyams_portal/zmi/template/__init__.py:146
-#, python-format
-msgid "Version {version} ({state} - last update {date})"
-msgstr "Version {version} ({state} - dernière modification {date})"
+#: src/pyams_portal/zmi/templates/layout.pt:23
+msgid "Add row"
+msgstr "Ajouter une ligne..."
 
-#: src/pyams_portal/zmi/template/templates/config.pt:15
-#: src/pyams_portal/zmi/template/templates/config.pt:29
-msgid "Version ${version} - ${state}"
-msgstr "Version ${version} - ${state}"
+#: src/pyams_portal/zmi/templates/layout.pt:47
+msgid "Add another portlet..."
+msgstr "Ajouter un composant..."
 
-#: src/pyams_portal/zmi/template/templates/config.pt:42
+#: src/pyams_portal/zmi/templates/layout.pt:54
 msgid "Selected display:"
 msgstr "Type de périphérique sélectionné :"
 
-#: src/pyams_portal/zmi/template/templates/config.pt:47
+#: src/pyams_portal/zmi/templates/layout.pt:59
 msgid "Current device"
 msgstr "Périphérique actuel"
 
-#: src/pyams_portal/zmi/template/templates/config.pt:48
+#: src/pyams_portal/zmi/templates/layout.pt:60
 msgid "Extra small device (phone)"
 msgstr "Très petits périphériques (téléphone)"
 
-#: src/pyams_portal/zmi/template/templates/config.pt:49
+#: src/pyams_portal/zmi/templates/layout.pt:61
 msgid "Small device (tablet)"
 msgstr "Petits périphériques (tablette)"
 
-#: src/pyams_portal/zmi/template/templates/config.pt:50
+#: src/pyams_portal/zmi/templates/layout.pt:62
 msgid "Medium desktop device (> 970px)"
 msgstr "Écrans de taille moyenne (> 970 px)"
 
-#: src/pyams_portal/zmi/template/templates/config.pt:51
+#: src/pyams_portal/zmi/templates/layout.pt:63
 msgid "Large desktop device (> 1170px)"
 msgstr "Écrans de grande taille (> 1170 px)"
 
-#: src/pyams_portal/zmi/template/templates/config.pt:111
+#: src/pyams_portal/zmi/templates/layout.pt:123
 msgid "Delete row..."
 msgstr "Supprimer la ligne..."
 
-#: src/pyams_portal/zmi/template/templates/config.pt:119
+#: src/pyams_portal/zmi/templates/layout.pt:132
 msgid "Edit slot properties..."
 msgstr "Propriétés..."
 
-#: src/pyams_portal/zmi/template/templates/config.pt:126
+#: src/pyams_portal/zmi/templates/layout.pt:139
 msgid "Delete slot..."
-msgstr "Supprimer le slot..."
+msgstr "Supprimer le panneau..."
 
-#: src/pyams_portal/zmi/template/templates/config.pt:134
+#: src/pyams_portal/zmi/templates/layout.pt:147
 msgid "Edit portlet properties..."
 msgstr "Propriétés..."
 
-#: src/pyams_portal/zmi/template/templates/config.pt:141
+#: src/pyams_portal/zmi/templates/layout.pt:155
 msgid "Delete portlet..."
 msgstr "Supprimer le composant..."
 
-#: src/pyams_portal/portlets/context/__init__.py:43
-msgid "Context content"
-msgstr "Contenu du contexte"
-
-#: src/pyams_portal/portlets/image/__init__.py:44
+#: src/pyams_portal/portlets/image/__init__.py:49
 msgid "Image"
 msgstr "Image"
 
@@ -329,176 +219,168 @@
 msgid "Selected image"
 msgstr "Image sélectionnée"
 
-#: src/pyams_portal/interfaces/__init__.py:49
+#: src/pyams_portal/portlets/content/__init__.py:46
+msgid "Context content"
+msgstr "Contenu du contexte"
+
+#: src/pyams_portal/interfaces/__init__.py:65
 msgid "Portlet"
 msgstr "Composant"
 
-#: src/pyams_portal/interfaces/__init__.py:52
-#: src/pyams_portal/interfaces/__init__.py:63
-#: src/pyams_portal/interfaces/__init__.py:117
+#: src/pyams_portal/interfaces/__init__.py:68
+#: src/pyams_portal/interfaces/__init__.py:171
 msgid "Slot name"
-msgstr "Nom du slot"
-
-#: src/pyams_portal/interfaces/__init__.py:53
-#: src/pyams_portal/interfaces/__init__.py:64
-msgid "Slot name to which this configuration applies"
-msgstr "Nom du slot correspondant à la configuration"
+msgstr "Nom du panneau"
 
 #: src/pyams_portal/interfaces/__init__.py:69
-msgid "Position"
-msgstr "Position"
+msgid "Slot name to which this configuration applies"
+msgstr "Nom du panneau correspondant à la configuration"
 
-#: src/pyams_portal/interfaces/__init__.py:70
-msgid "Portlet position inside slot"
-msgstr "Position du composant au sein du slot"
-
-#: src/pyams_portal/interfaces/__init__.py:74
+#: src/pyams_portal/interfaces/__init__.py:82
 msgid "Visible portlet?"
 msgstr "Composant visible ?"
 
-#: src/pyams_portal/interfaces/__init__.py:75
-msgid ""
-"Select 'no' to hide this portlet. This will not break configuration "
-"inheritance..."
-msgstr ""
-"Sélectionnez 'non' pour masquer ce composant. Ce paramètre pourra être "
-"surchargé par héritage au sein des pages qui utilisent ce composant."
+#: src/pyams_portal/interfaces/__init__.py:83
+msgid "Select 'no' to hide this portlet..."
+msgstr "Sélectionnez 'non' pour masquer ce composant..."
 
-#: src/pyams_portal/interfaces/__init__.py:81
-#: src/pyams_portal/interfaces/__init__.py:139
+#: src/pyams_portal/interfaces/__init__.py:109
 msgid "Inherit parent configuration?"
 msgstr "Hériter de la configuration du parent ?"
 
-#: src/pyams_portal/interfaces/__init__.py:82
+#: src/pyams_portal/interfaces/__init__.py:110
 msgid ""
-"This option is only available if context's parent is using the same template "
-"and if this portlet is also present in the same slot..."
+"This option is only available if context's parent is using the same "
+"template..."
 msgstr ""
 "Cette option n'est disponible que si le parent utilise le même modèle de "
-"présentation et si ce composant est bien présent dans le même slot..."
+"présentation..."
 
-#: src/pyams_portal/interfaces/__init__.py:118
+#: src/pyams_portal/interfaces/__init__.py:172
 msgid "This name must be unique in a given template"
 msgstr "Ce nom doit être unique au sein d'un modèle de présentation"
 
-#: src/pyams_portal/interfaces/__init__.py:121
+#: src/pyams_portal/interfaces/__init__.py:175
 msgid "Row ID"
 msgstr "ID de la ligne"
 
-#: src/pyams_portal/interfaces/__init__.py:132
+#: src/pyams_portal/interfaces/__init__.py:189
 msgid "Visible slot?"
-msgstr "Slot visible ?"
+msgstr "Panneau visible ?"
 
-#: src/pyams_portal/interfaces/__init__.py:133
-msgid ""
-"Select 'no' to hide this slot. This will not break configuration "
-"inheritance..."
-msgstr ""
-"Sélectionnez 'non' pour marquer ce slot. Ce paramètre pourra être surchargé "
-"par héritage..."
+#: src/pyams_portal/interfaces/__init__.py:190
+msgid "Select 'no' to hide this slot..."
+msgstr "Choisir 'non' pour masquer ce panneau..."
 
-#: src/pyams_portal/interfaces/__init__.py:140
-msgid ""
-"This option is only available if context's parent template is using a "
-"template containing the same slot..."
-msgstr ""
-"Cette option n'est disponible que si le parent utilise un modèle contenant "
-"un slot de même nom..."
-
-#: src/pyams_portal/interfaces/__init__.py:145
+#: src/pyams_portal/interfaces/__init__.py:194
 msgid "Extra small device width"
 msgstr "Largeur sur très petits périphériques"
 
-#: src/pyams_portal/interfaces/__init__.py:146
+#: src/pyams_portal/interfaces/__init__.py:195
 msgid ""
 "Slot width, in columns count, on extra small devices (phones...); set to 0 "
 "to hide the portlet"
 msgstr ""
-"Largeur du slot, en nombre de colonnes, sur les très petits périphériques "
+"Largeur du panneau, en nombre de colonnes, sur les très petits périphériques "
 "(téléphones...) ; indiquez une valeur de 0 pour masquer ce composant"
 
-#: src/pyams_portal/interfaces/__init__.py:152
+#: src/pyams_portal/interfaces/__init__.py:201
 msgid "Small device width"
 msgstr "Largeur sur petits périphériques"
 
-#: src/pyams_portal/interfaces/__init__.py:153
+#: src/pyams_portal/interfaces/__init__.py:202
 msgid ""
 "Slot width, in columns count, on small devices (tablets...); set to 0 to "
 "hide the portlet"
 msgstr ""
-"Largeur du slot, en nombre de colonnes, sur les petits périphériques "
+"Largeur du panneau, en nombre de colonnes, sur les petits périphériques "
 "(tablettes...) ; indiquez une valeur de 0 pour masquer ce composant"
 
-#: src/pyams_portal/interfaces/__init__.py:159
+#: src/pyams_portal/interfaces/__init__.py:208
 msgid "Medium devices width"
 msgstr "Largeur sur périphériques moyens"
 
-#: src/pyams_portal/interfaces/__init__.py:160
+#: src/pyams_portal/interfaces/__init__.py:209
 msgid ""
 "Slot width, in columns count, on medium desktop devices (>= 992 pixels); set "
 "to 0 to hide the portlet"
 msgstr ""
-"Largeur du slot, en nombre de colonnes, sur les périphériques moyens (>= 992 "
-"pixels) ; indiquez une valeur de 0 pour masquer ce composant"
+"Largeur du panneau, en nombre de colonnes, sur les périphériques moyens (>= "
+"992 pixels) ; indiquez une valeur de 0 pour masquer ce composant"
 
-#: src/pyams_portal/interfaces/__init__.py:166
+#: src/pyams_portal/interfaces/__init__.py:215
 msgid "Large devices width"
 msgstr "Largeur sur grands périphériques"
 
-#: src/pyams_portal/interfaces/__init__.py:167
+#: src/pyams_portal/interfaces/__init__.py:216
 msgid ""
 "Slot width, in columns count, on large desktop devices (>= 1200 pixels); set "
 "to 0 to hide the portlet"
 msgstr ""
-"Largeur du slot, en nombre de colonnes, sur les grands périphériques (>= "
+"Largeur du panneau, en nombre de colonnes, sur les grands périphériques (>= "
 "1200 pixels) ; indiquez une valeur de 0 pour masquer ce composant"
 
-#: src/pyams_portal/interfaces/__init__.py:173
+#: src/pyams_portal/interfaces/__init__.py:222
 msgid "CSS class"
 msgstr "Class CSS"
 
-#: src/pyams_portal/interfaces/__init__.py:174
+#: src/pyams_portal/interfaces/__init__.py:223
 msgid "CSS class applied to this slot"
-msgstr "Classe CSS spécifique appliquée à ce slot"
+msgstr "Classe CSS spécifique appliquée à ce panneau"
 
-#: src/pyams_portal/interfaces/__init__.py:276
+#: src/pyams_portal/interfaces/__init__.py:334
 msgid "Template name"
 msgstr "Nom du modèle"
 
-#: src/pyams_portal/interfaces/__init__.py:277
+#: src/pyams_portal/interfaces/__init__.py:335
 msgid "Two registered templates can't share the same name..."
 msgstr "Deux modèles partagés ne peuvent pas utiliser le même nom..."
 
-#: src/pyams_portal/interfaces/__init__.py:309
+#: src/pyams_portal/interfaces/__init__.py:356
+msgid "Toolbar portlets"
+msgstr "Composants de la barre d'outils"
+
+#: src/pyams_portal/interfaces/__init__.py:357
+msgid ""
+"These portlets will be directly available in templates configuration page "
+"toolbar"
+msgstr ""
+"Ces composants seront directement accessibles dans la page de configuration "
+"des modèles de présentation sous la forme d'une barre d'icônes"
+
+#: src/pyams_portal/interfaces/__init__.py:383
 msgid "Inherit parent template?"
 msgstr "Hériter du modèle du parent ?"
 
-#: src/pyams_portal/interfaces/__init__.py:310
+#: src/pyams_portal/interfaces/__init__.py:384
 msgid "Should we reuse parent template?"
 msgstr "Doit-on ré-utiliser le modèle du parent ?"
 
-#: src/pyams_portal/interfaces/__init__.py:314
+#: src/pyams_portal/interfaces/__init__.py:388
 msgid "Use local template?"
 msgstr "Utiliser un modèle local ?"
 
-#: src/pyams_portal/interfaces/__init__.py:315
+#: src/pyams_portal/interfaces/__init__.py:389
 msgid ""
 "If 'yes', you can define a custom local template instead of a shared template"
 msgstr ""
 "Si 'oui', vous pouvez définir un modèle de présentation local au lieu d'un "
 "modèle partagé"
 
-#: src/pyams_portal/interfaces/__init__.py:320
+#: src/pyams_portal/interfaces/__init__.py:394
 msgid "Page template"
 msgstr "Modèle de page"
 
-#: src/pyams_portal/interfaces/__init__.py:321
+#: src/pyams_portal/interfaces/__init__.py:395
 msgid "Template used for this page"
 msgstr "Modèle de présentation utilisé pour cette page"
 
-#: src/pyams_portal/interfaces/__init__.py:325
+#: src/pyams_portal/interfaces/__init__.py:404
 msgid "Local template"
 msgstr "Modèle local"
 
-#~ msgid "Portlet templates"
-#~ msgstr "Modèles de présentation"
+#: src/pyams_portal/interfaces/__init__.py:402
+msgid "You must choose to use a local template or select a shared one!"
+msgstr ""
+"Vous devez choisir un modèle de présentation partagé lorsque vous "
+"n'appliquez pas de modèle local !"
--- a/src/pyams_portal/locales/pyams_portal.pot	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/locales/pyams_portal.pot	Mon Jan 18 18:09:46 2016 +0100
@@ -1,12 +1,12 @@
 # 
 # SOME DESCRIPTIVE TITLE
 # This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
 #, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2015-05-12 13:59+0200\n"
+"POT-Creation-Date: 2016-01-06 11:55+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -16,289 +16,202 @@
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: Lingua 3.10.dev0\n"
 
-#: ./src/pyams_portal/workflow.py:41
-msgid "Draft"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:42
-msgid "Published"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:43
-msgid "Retired"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:44
-msgid "Archived"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:45
-msgid "Deleted"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:103
-msgid "Initialize"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:108
-msgid "Publish..."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:116
-msgid ""
-"This content is currently in DRAFT mode.\n"
-"                                               Publishing it will make it publicly visible."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:120
-msgid "Retire..."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:128
-msgid ""
-"This content is actually published.\n"
-"                                                 You can retire it to make it invisible, but contents using this\n"
-"                                                 template won't be visible anymore!"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:133 ./src/pyams_portal/workflow.py:181
-msgid "Create new version..."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:143
-msgid "Re-publish..."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:151
-msgid ""
-"This content was published and retired.\n"
-"                                                 You can re-publish it to make it visible again."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:155 ./src/pyams_portal/workflow.py:168
-msgid "Archive..."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:163
-msgid ""
-"This content is currently published.\n"
-"                                                  If it is archived, it will not be possible to make it visible again\n"
-"                                                  except by creating a new version!"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:176
-msgid ""
-"This content has been published but is currently retired.\n"
-"                                                If it is archived, it will not be possible to make it visible again\n"
-"                                                except by creating a new version!"
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:191
-msgid "Delete..."
-msgstr ""
-
-#: ./src/pyams_portal/workflow.py:199
-msgid ""
-"This content has never been published.\n"
-"                                    It can be removed and definitely deleted."
-msgstr ""
-
-#: ./src/pyams_portal/__init__.py:31
+#: ./src/pyams_portal/__init__.py:34
 msgid "Manage portal templates"
 msgstr ""
 
-#: ./src/pyams_portal/__init__.py:35
+#: ./src/pyams_portal/__init__.py:38
 msgid "Portal templates manager"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/portlet.py:41
-msgid "Edit portlet configuration"
+#: ./src/pyams_portal/zmi/portlet.py:49
+msgid "Edit portlet settings"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/portlet.py:85
+msgid "Main properties"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/portlet.py:38
+#: ./src/pyams_portal/zmi/portlet.py:64
+msgid "Override parent settings"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/portlet.py:66
+msgid "Override template settings"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/portlet.py:46
 #, python-format
 msgid "« {0} »  portal template - {1}"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:61
-msgid "Properties"
+#: ./src/pyams_portal/zmi/page.py:60
+msgid "Presentation"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:72
-msgid "Portal template configuration"
+#: ./src/pyams_portal/zmi/page.py:75
+msgid "Edit template configuration"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:120
-msgid "Portlets configuration"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/config.py:133
-msgid "Add row..."
+#: ./src/pyams_portal/zmi/page.py:119
+msgid "Template properties"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:175
-msgid "Add slot..."
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/config.py:191
-msgid "Add slot"
+#: ./src/pyams_portal/zmi/template.py:83
+msgid "Add template"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:265
-msgid "Edit slot properties"
+#: ./src/pyams_portal/zmi/template.py:93 ./src/pyams_portal/zmi/container.py:78
+msgid "Portal templates"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:333
-msgid "Add portlet..."
+#: ./src/pyams_portal/zmi/template.py:94
+msgid "Add shared template"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:349
-msgid "Add portlet"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/config.py:209
-#: ./src/pyams_portal/zmi/template/__init__.py:269
+#: ./src/pyams_portal/zmi/template.py:118 ./src/pyams_portal/zmi/layout.py:246
 msgid "Specified name is already used!"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:118
-#: ./src/pyams_portal/zmi/template/config.py:189
-#: ./src/pyams_portal/zmi/template/config.py:347
+#: ./src/pyams_portal/zmi/template.py:62 ./src/pyams_portal/zmi/layout.py:220
+#: ./src/pyams_portal/zmi/layout.py:382
 #, python-format
 msgid "« {0} »  portal template"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/config.py:262
+#: ./src/pyams_portal/zmi/layout.py:78
+msgid "Properties"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:164
+msgid "Add row..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:206
+msgid "Add slot..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:222
+#: ./src/pyams_portal/zmi/templates/layout.pt:27
+msgid "Add slot"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:303
+msgid "Edit slot properties"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:368
+msgid "Add portlet..."
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:384
+msgid "Add portlet"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:68
+msgid "Template management"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:101
+msgid "Template configuration"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:96
+msgid "Local template configuration"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/layout.py:300
 #, python-format
 msgid "« {0} »  portal template - {1} slot"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/workflow.py:109
-msgid "Publish template"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/workflow.py:151
-msgid "Retire template"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/workflow.py:180
-msgid "Archive template"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/workflow.py:209
-#: ./src/pyams_portal/zmi/template/workflow.py:201
-msgid "Create new version"
+#: ./src/pyams_portal/zmi/layout.py:99
+#, python-format
+msgid "Shared template configuration ({0})"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/workflow.py:100
-#: ./src/pyams_portal/zmi/template/workflow.py:142
-#: ./src/pyams_portal/zmi/template/workflow.py:171
-#: ./src/pyams_portal/zmi/template/workflow.py:200
-msgid "Close"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/workflow.py:101
-msgid "Publish"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/workflow.py:143
-msgid "Retire"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/workflow.py:172
-msgid "Archive"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/__init__.py:88
-#: ./src/pyams_portal/zmi/template/__init__.py:196
-#: ./src/pyams_portal/zmi/template/__init__.py:240
-msgid "Portal templates"
-msgstr ""
-
-#: ./src/pyams_portal/zmi/template/__init__.py:97
+#: ./src/pyams_portal/zmi/container.py:87
 msgid "Shared portal templates"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/__init__.py:163
+#: ./src/pyams_portal/zmi/container.py:127
 msgid "Delete template"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/__init__.py:195
-msgid "Portal"
+#: ./src/pyams_portal/zmi/container.py:170
+msgid "Selected portlets..."
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/__init__.py:229
-msgid "Add shared template..."
+#: ./src/pyams_portal/zmi/container.py:186
+msgid "Portal templates container"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/__init__.py:241
-msgid "Add shared template"
+#: ./src/pyams_portal/zmi/container.py:187
+msgid "Edit selected portlets"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/__init__.py:153
-msgid "Older versions"
+#: ./src/pyams_portal/zmi/templates/portlet.pt:129
+#: ./src/pyams_portal/zmi/templates/portlet.pt:144
+msgid "Title"
+msgstr ""
+
+#: ./src/pyams_portal/zmi/templates/portlet.pt:159
+msgid "Tab label"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/__init__.py:212
-#: ./src/pyams_portal/zmi/template/__init__.py:146
-#, python-format
-msgid "Version {version} ({state} - last update {date})"
+#: ./src/pyams_portal/zmi/templates/layout.pt:23
+msgid "Add row"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:15
-#: ./src/pyams_portal/zmi/template/templates/config.pt:29
-msgid "Version ${version} - ${state}"
+#: ./src/pyams_portal/zmi/templates/layout.pt:47
+msgid "Add another portlet..."
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:42
+#: ./src/pyams_portal/zmi/templates/layout.pt:54
 msgid "Selected display:"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:47
+#: ./src/pyams_portal/zmi/templates/layout.pt:59
 msgid "Current device"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:48
+#: ./src/pyams_portal/zmi/templates/layout.pt:60
 msgid "Extra small device (phone)"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:49
+#: ./src/pyams_portal/zmi/templates/layout.pt:61
 msgid "Small device (tablet)"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:50
+#: ./src/pyams_portal/zmi/templates/layout.pt:62
 msgid "Medium desktop device (> 970px)"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:51
+#: ./src/pyams_portal/zmi/templates/layout.pt:63
 msgid "Large desktop device (> 1170px)"
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:111
+#: ./src/pyams_portal/zmi/templates/layout.pt:123
 msgid "Delete row..."
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:119
+#: ./src/pyams_portal/zmi/templates/layout.pt:132
 msgid "Edit slot properties..."
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:126
+#: ./src/pyams_portal/zmi/templates/layout.pt:139
 msgid "Delete slot..."
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:134
+#: ./src/pyams_portal/zmi/templates/layout.pt:147
 msgid "Edit portlet properties..."
 msgstr ""
 
-#: ./src/pyams_portal/zmi/template/templates/config.pt:141
+#: ./src/pyams_portal/zmi/templates/layout.pt:155
 msgid "Delete portlet..."
 msgstr ""
 
-#: ./src/pyams_portal/portlets/context/__init__.py:43
-msgid "Context content"
-msgstr ""
-
-#: ./src/pyams_portal/portlets/image/__init__.py:44
+#: ./src/pyams_portal/portlets/image/__init__.py:49
 msgid "Image"
 msgstr ""
 
@@ -306,155 +219,152 @@
 msgid "Selected image"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:49
+#: ./src/pyams_portal/portlets/content/__init__.py:46
+msgid "Context content"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:65
 msgid "Portlet"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:52
-#: ./src/pyams_portal/interfaces/__init__.py:63
-#: ./src/pyams_portal/interfaces/__init__.py:117
+#: ./src/pyams_portal/interfaces/__init__.py:68
+#: ./src/pyams_portal/interfaces/__init__.py:171
 msgid "Slot name"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:53
-#: ./src/pyams_portal/interfaces/__init__.py:64
+#: ./src/pyams_portal/interfaces/__init__.py:69
 msgid "Slot name to which this configuration applies"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:69
-msgid "Position"
-msgstr ""
-
-#: ./src/pyams_portal/interfaces/__init__.py:70
-msgid "Portlet position inside slot"
-msgstr ""
-
-#: ./src/pyams_portal/interfaces/__init__.py:74
+#: ./src/pyams_portal/interfaces/__init__.py:82
 msgid "Visible portlet?"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:75
-msgid ""
-"Select 'no' to hide this portlet. This will not break configuration "
-"inheritance..."
+#: ./src/pyams_portal/interfaces/__init__.py:83
+msgid "Select 'no' to hide this portlet..."
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:81
-#: ./src/pyams_portal/interfaces/__init__.py:139
+#: ./src/pyams_portal/interfaces/__init__.py:109
 msgid "Inherit parent configuration?"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:82
+#: ./src/pyams_portal/interfaces/__init__.py:110
 msgid ""
-"This option is only available if context's parent is using the same template "
-"and if this portlet is also present in the same slot..."
+"This option is only available if context's parent is using the same "
+"template..."
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:118
+#: ./src/pyams_portal/interfaces/__init__.py:172
 msgid "This name must be unique in a given template"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:121
+#: ./src/pyams_portal/interfaces/__init__.py:175
 msgid "Row ID"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:132
+#: ./src/pyams_portal/interfaces/__init__.py:189
 msgid "Visible slot?"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:133
-msgid ""
-"Select 'no' to hide this slot. This will not break configuration "
-"inheritance..."
+#: ./src/pyams_portal/interfaces/__init__.py:190
+msgid "Select 'no' to hide this slot..."
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:140
-msgid ""
-"This option is only available if context's parent template is using a "
-"template containing the same slot..."
-msgstr ""
-
-#: ./src/pyams_portal/interfaces/__init__.py:145
+#: ./src/pyams_portal/interfaces/__init__.py:194
 msgid "Extra small device width"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:146
+#: ./src/pyams_portal/interfaces/__init__.py:195
 msgid ""
 "Slot width, in columns count, on extra small devices (phones...); set to 0 to"
 " hide the portlet"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:152
+#: ./src/pyams_portal/interfaces/__init__.py:201
 msgid "Small device width"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:153
+#: ./src/pyams_portal/interfaces/__init__.py:202
 msgid ""
 "Slot width, in columns count, on small devices (tablets...); set to 0 to hide"
 " the portlet"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:159
+#: ./src/pyams_portal/interfaces/__init__.py:208
 msgid "Medium devices width"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:160
+#: ./src/pyams_portal/interfaces/__init__.py:209
 msgid ""
 "Slot width, in columns count, on medium desktop devices (>= 992 pixels); set "
 "to 0 to hide the portlet"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:166
+#: ./src/pyams_portal/interfaces/__init__.py:215
 msgid "Large devices width"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:167
+#: ./src/pyams_portal/interfaces/__init__.py:216
 msgid ""
 "Slot width, in columns count, on large desktop devices (>= 1200 pixels); set "
 "to 0 to hide the portlet"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:173
+#: ./src/pyams_portal/interfaces/__init__.py:222
 msgid "CSS class"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:174
+#: ./src/pyams_portal/interfaces/__init__.py:223
 msgid "CSS class applied to this slot"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:276
+#: ./src/pyams_portal/interfaces/__init__.py:334
 msgid "Template name"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:277
+#: ./src/pyams_portal/interfaces/__init__.py:335
 msgid "Two registered templates can't share the same name..."
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:309
+#: ./src/pyams_portal/interfaces/__init__.py:356
+msgid "Toolbar portlets"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:357
+msgid ""
+"These portlets will be directly available in templates configuration page "
+"toolbar"
+msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:383
 msgid "Inherit parent template?"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:310
+#: ./src/pyams_portal/interfaces/__init__.py:384
 msgid "Should we reuse parent template?"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:314
+#: ./src/pyams_portal/interfaces/__init__.py:388
 msgid "Use local template?"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:315
+#: ./src/pyams_portal/interfaces/__init__.py:389
 msgid ""
 "If 'yes', you can define a custom local template instead of a shared template"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:320
+#: ./src/pyams_portal/interfaces/__init__.py:394
 msgid "Page template"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:321
+#: ./src/pyams_portal/interfaces/__init__.py:395
 msgid "Template used for this page"
 msgstr ""
 
-#: ./src/pyams_portal/interfaces/__init__.py:325
+#: ./src/pyams_portal/interfaces/__init__.py:404
 msgid "Local template"
 msgstr ""
+
+#: ./src/pyams_portal/interfaces/__init__.py:402
+msgid "You must choose to use a local template or select a shared one!"
+msgstr ""
--- a/src/pyams_portal/page.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/page.py	Mon Jan 18 18:09:46 2016 +0100
@@ -9,12 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from pyramid.view import view_config
-from zope.traversing.interfaces import ITraversable
-from pyams_portal.template import PortalWfTemplate, PortalTemplate
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.registry import query_utility
-from pyams_workflow.interfaces import IWorkflowInfo, IWorkflowVersions
 
 __docformat__ = 'restructuredtext'
 
@@ -22,24 +16,29 @@
 # import standard library
 
 # import interfaces
-from pyams_portal.interfaces import IPortalPage, IPortalContext, IPortalTemplateRenderer, \
-    IPortalTemplateConfiguration, IPortalWfTemplate
+from pyams_portal.interfaces import IPortalPage, IPortalContext, IPortalTemplateConfiguration, \
+    IPortalTemplate, PORTAL_PAGE_KEY, IPortalPortletsConfiguration, PORTLETS_CONFIGURATION_KEY, \
+    ILocalTemplateHandler
 from zope.annotation.interfaces import IAnnotations
+from zope.traversing.interfaces import ITraversable
 
 # import packages
 from persistent import Persistent
+from pyams_portal.template import PortalTemplate
 from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
 from pyramid.threadlocal import get_current_registry
 from zope.container.contained import Contained
-from zope.interface import implementer
-from zope.lifecycleevent import ObjectCreatedEvent, ObjectAddedEvent
-from zope.location.location import locate
+from zope.copy import clone
+from zope.interface import implementer, alsoProvides, noLongerProvides
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
 from zope.schema.fieldproperty import FieldProperty
 
 
 @implementer(IPortalPage)
 class PortalPage(Persistent, Contained):
-    """Portal page"""
+    """Portal page persistent class"""
 
     _inherit_parent = FieldProperty(IPortalPage['inherit_parent'])
     _use_local_template = FieldProperty(IPortalPage['use_local_template'])
@@ -56,48 +55,51 @@
 
     @inherit_parent.setter
     def inherit_parent(self, value):
-        self._inherit_parent = value
+        if (not value) or self.can_inherit:
+            self._inherit_parent = value
+
+    @property
+    def parent(self):
+        parent = self.__parent__
+        page = IPortalPage(parent)
+        while page.inherit_parent:
+            parent = parent.__parent__
+            page = IPortalPage(parent)
+        return parent
 
     @property
     def use_local_template(self):
-        if self.inherit_parent:
-            return IPortalPage(self.__parent__.__parent__).use_local_template
-        else:
-            return self._use_local_template
+        return self._use_local_template if not self.inherit_parent else False
 
     @use_local_template.setter
     def use_local_template(self, value):
         self._use_local_template = value
-        if value and (self._local_template is None):
+        if value and (self._local_template is None) and not self.inherit_parent:
             registry = get_current_registry()
-            wf_template = self._local_template = PortalWfTemplate()
-            registry.notify(ObjectCreatedEvent(wf_template))
-            locate(wf_template, self, '++template++')
             template = PortalTemplate()
+            template.name = "local"
+            self._local_template = template
             registry.notify(ObjectCreatedEvent(template))
-            IWorkflowVersions(wf_template).add_version(template, None)
-            IWorkflowInfo(template).fire_transition('init')
+            locate(template, self, '++template++')
+        if self.use_local_template:
+            alsoProvides(self, ILocalTemplateHandler)
+        else:
+            noLongerProvides(self, ILocalTemplateHandler)
 
     @property
     def shared_template(self):
-        if self.inherit_parent:
-            return IPortalPage(self.__parent__.__parent__).shared_template
-        else:
-            return self._shared_template
+        return IPortalPage(self.parent).shared_template if self.inherit_parent else self._shared_template
 
     @shared_template.setter
     def shared_template(self, value):
         if not self.inherit_parent:
-            if isinstance(value, IPortalWfTemplate):
-                value = value.__name__
+            if IPortalTemplate.providedBy(value):
+                value = value.name
             self._shared_template = value
 
     @property
     def local_template(self):
-        if self.inherit_parent:
-            return IPortalPage(self.__parent__.__parent__).local_template
-        else:
-            return self._local_template
+        return IPortalPage(self.parent).local_template if self.inherit_parent else self._local_template
 
     @local_template.setter
     def local_template(self, value):
@@ -107,30 +109,17 @@
     @property
     def template(self):
         if self.use_local_template:
-            return self.local_template
+            template = self.local_template
         else:
             template = self.shared_template
-            if isinstance(template, str):
-                template = query_utility(IPortalWfTemplate, name=template)
-            return template
-
-
-PORTAL_PAGE_KEY = 'pyams_portal.page'
-
-
-@adapter_config(name='template', context=IPortalContext, provides=ITraversable)
-class PortalPageTemplateTraverser(ContextAdapter):
-    """++template++ portal context traverser"""
-
-    def traverse(self, name, furtherpath=None):
-        page = IPortalPage(self.context)
-        if page.use_local_template:
-            return page.template
+        if isinstance(template, str):
+            template = query_utility(IPortalTemplate, name=template)
+        return template
 
 
 @adapter_config(context=IPortalContext, provides=IPortalPage)
-def PortalPageFactory(context):
-    """Portal page factory"""
+def PortalContextPageAdapter(context):
+    """Portal context page factory"""
     annotations = IAnnotations(context)
     page = annotations.get(PORTAL_PAGE_KEY)
     if page is None:
@@ -140,12 +129,49 @@
     return page
 
 
-@view_config(context=IPortalContext, request_type=IPyAMSLayer)
-def PortalPageRenderer(request):
-    page = IPortalPage(request.context)
-    template = page.template
-    registry = request.registry
-    renderer = registry.queryMultiAdapter((request.context, request, template), IPortalTemplateRenderer)
-    if renderer is not None:
-        configuration = registry.queryMultiAdapter((request.context, template), IPortalTemplateConfiguration)
-        return renderer(configuration)
+@adapter_config(name='template', context=IPortalContext, provides=ITraversable)
+class PortalContextTemplateTraverser(ContextAdapter):
+    """++template++ portal context traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        page = IPortalPage(self.context)
+        if page.use_local_template:
+            return page.local_template
+
+
+@adapter_config(context=IPortalContext, provides=IPortalTemplateConfiguration)
+def PortalContextTemplateConfigurationAdapter(context):
+    """Portal context template configuration adapter"""
+    template = IPortalPage(context).template
+    return IPortalTemplateConfiguration(template)
+
+
+@adapter_config(context=IPortalContext, provides=IPortalPortletsConfiguration)
+def PortalContextPortletsConfigurationAdapter(context):
+    """Portal context portlets configuration adapter"""
+    page = IPortalPage(context)
+    if page.inherit_parent:
+        return IPortalPortletsConfiguration(page.parent)
+    else:
+        annotations = IAnnotations(context)
+        config = annotations.get(PORTLETS_CONFIGURATION_KEY)
+        if config is None:
+            config = annotations[PORTLETS_CONFIGURATION_KEY] = clone(IPortalPortletsConfiguration(page.template))
+            get_current_registry().notify(ObjectCreatedEvent(config))
+            locate(config, context)
+            for portlet_id, portlet_config in config.items():
+                portlet_cloned_config = clone(portlet_config)
+                config.set_portlet_configuration(portlet_id, portlet_cloned_config)
+        return config
+
+
+@adapter_config(name='portlet', context=IPortalContext, provides=ITraversable)
+class PortalContextPortletTraverser(ContextAdapter):
+    """++portlet++ portal context traverser"""
+
+    def traverse(self, name, thurtherpath=None):
+        config = IPortalPortletsConfiguration(self.context)
+        if name:
+            return config.get_portlet_configuration(int(name))
+        else:
+            return config
--- a/src/pyams_portal/portlet.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/portlet.py	Mon Jan 18 18:09:46 2016 +0100
@@ -20,97 +20,42 @@
 import venusian
 
 # import interfaces
-from pyams_portal.interfaces import IPortlet, IPortletRenderer, IPortletConfiguration, \
-    IPortalPage, IPortletPreviewer
+from pyams_portal.interfaces import IPortlet, IPortletSettings, IPortletConfiguration, IPortletPreviewer, \
+    IPortletRenderer, IPortalPortletsConfiguration, IPortalTemplate, IPortalContext, IPortalPage
 from zope.schema.interfaces import IVocabularyFactory
+from zope.traversing.interfaces import ITraversable
 
 # import packages
 from persistent import Persistent
+from persistent.mapping import PersistentMapping
+from pyams_utils.adapter import adapter_config, ContextAdapter
 from pyams_utils.request import check_request
 from pyams_viewlet.viewlet import ContentProvider
 from pyramid.exceptions import ConfigurationError
+from pyramid.threadlocal import get_current_registry
 from zope.container.contained import Contained
+from zope.copy import clone
 from zope.interface import implementer, provider
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
 from zope.schema.fieldproperty import FieldProperty
-from zope.schema.vocabulary import getVocabularyRegistry, SimpleVocabulary, SimpleTerm
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
 
 
-@implementer(IPortletConfiguration)
-class PortletConfiguration(Persistent, Contained):
-    """Portlet configuration"""
-
-    template = None
-    portlet_name = None
-    slot_name = FieldProperty(IPortletConfiguration['slot_name'])
-    position = FieldProperty(IPortletConfiguration['position'])
-    visible = FieldProperty(IPortletConfiguration['visible'])
-    _inherit_parent = FieldProperty(IPortletConfiguration['inherit_parent'])
-
-    def __init__(self, portlet):
-        self.portlet_name = portlet.name
-
-    @property
-    def can_inherit(self):
-        return IPortalPage.providedBy(self.__parent__)
-
-    @property
-    def inherit_parent(self):
-        return self._inherit_parent if self.can_inherit else False
-
-    @inherit_parent.setter
-    def inherit_parent(self, value):
-        self._inherit_parent = value
-
+#
+# Portlets utilities
+#
 
 @implementer(IPortlet)
 class Portlet(object):
-    """Base portlet content provider"""
+    """Base portlet utility"""
 
     permission = FieldProperty(IPortlet['permission'])
 
     toolbar_image = None
     toolbar_css_class = 'fa fa-fw fa-2x fa-edit'
 
-
-@provider(IVocabularyFactory)
-class PortletVocabulary(SimpleVocabulary):
-    """Portlet vocabulary"""
-
-    def __init__(self, context):
-        request = check_request()
-        translate = request.localizer.translate
-        utils = request.registry.getUtilitiesFor(IPortlet)
-        terms = [SimpleTerm(name, title=translate(util.label))
-                 for name, util in sorted(utils, key=lambda x: translate(x[1].label))]
-        super(PortletVocabulary, self).__init__(terms)
-
-getVocabularyRegistry().register('PyAMS portal portlets', PortletVocabulary)
-
-
-class PortletContentProvider(ContentProvider):
-    """Bae portlet content provider"""
-
-    def __init__(self, context, request, view, portlet_config):
-        super(PortletContentProvider, self).__init__(context, request, view)
-        self.__parent__ = view
-        self.configuration = portlet_config
-        self.portlet = self.request.registry.getUtility(IPortlet, name=portlet_config.portlet_name)
-
-    def __call__(self):
-        if self.portlet.permission and not self.request.has_permission(self.portlet.permission):
-            return ''
-        self.update()
-        return self.render()
-
-
-@implementer(IPortletPreviewer)
-class PortletPreviewer(PortletContentProvider):
-    """Portlet previewer adapter"""
-
-
-@implementer(IPortletRenderer)
-class PortletRenderer(PortletContentProvider):
-    """Portlet renderer adapter"""
+    settings_class = None
 
 
 class portlet_config(object):
@@ -158,3 +103,176 @@
 
         settings['_info'] = info.codeinfo  # fbo "action_method"
         return wrapped
+
+
+@provider(IVocabularyFactory)
+class PortletVocabulary(SimpleVocabulary):
+    """Portlet vocabulary"""
+
+    def __init__(self, context):
+        request = check_request()
+        translate = request.localizer.translate
+        utils = request.registry.getUtilitiesFor(IPortlet)
+        terms = [SimpleTerm(name, title=translate(util.label))
+                 for name, util in sorted(utils, key=lambda x: translate(x[1].label))]
+        super(PortletVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS portal portlets', PortletVocabulary)
+
+
+#
+# Portlets renderers
+#
+
+class PortletContentProvider(ContentProvider):
+    """Base portlet content provider"""
+
+    def __init__(self, context, request, view, settings):
+        super(PortletContentProvider, self).__init__(context, request, view)
+        self.__parent__ = view
+        self.settings = settings
+        self.portlet = self.request.registry.getUtility(IPortlet, name=settings.configuration.portlet_name)
+
+    def __call__(self):
+        if self.portlet.permission and not self.request.has_permission(self.portlet.permission):
+            return ''
+        self.update()
+        return self.render()
+
+
+@implementer(IPortletPreviewer)
+class PortletPreviewer(PortletContentProvider):
+    """Portlet previewer adapter"""
+
+
+@implementer(IPortletRenderer)
+class PortletRenderer(PortletContentProvider):
+    """Portlet renderer adapter"""
+
+
+#
+# Portlet configuration
+#
+
+@implementer(IPortletSettings)
+class PortletSettings(Persistent, Contained):
+    """Portlet settings persistent class"""
+
+    visible = FieldProperty(IPortletSettings['visible'])
+
+    __name__ = '++settings++'
+
+    def __init__(self, configuration):
+        self.__parent__ = configuration
+
+    @property
+    def configuration(self):
+        return self.__parent__
+
+
+@implementer(IPortletConfiguration)
+class PortletConfiguration(Persistent, Contained):
+    """Portlet configuration persistent class
+
+    PortletConfiguration.__parent__ points to context where configuration is applied
+    PortletConfiguration.parent points to context from where configuration is inherited
+    """
+
+    portlet_id = FieldProperty(IPortletConfiguration['portlet_id'])
+    portlet_name = None
+    _inherit_parent = FieldProperty(IPortletConfiguration['inherit_parent'])
+    _settings = FieldProperty(IPortletConfiguration['settings'])
+
+    def __init__(self, portlet):
+        self.portlet_name = portlet.name
+        self._settings = portlet.settings_class(self)
+
+    @property
+    def can_inherit(self):
+        return not IPortalTemplate.providedBy(self.__parent__)
+
+    @property
+    def inherit_parent(self):
+        return self._inherit_parent if self.can_inherit else False
+
+    @inherit_parent.setter
+    def inherit_parent(self, value):
+        if (not value) or self.can_inherit:
+            self._inherit_parent = value
+
+    @property
+    def parent(self):
+        parent = self.__parent__
+        if IPortalTemplate.providedBy(parent):
+            return parent
+        while IPortalContext.providedBy(parent):
+            configuration = IPortalPortletsConfiguration(parent).get_portlet_configuration(self.portlet_id)
+            if not configuration.inherit_parent:
+                return parent
+            if not IPortalContext.providedBy(parent.__parent__):
+                break
+            parent = parent.__parent__
+        page = IPortalPage(parent, None)
+        if page is not None:
+            return page.template
+
+    @property
+    def settings(self):
+        if self.inherit_parent:
+            return IPortalPortletsConfiguration(self.parent).get_portlet_configuration(self.portlet_id).settings
+        else:
+            return self._settings
+
+    @property
+    def editor_settings(self):
+        return self._settings
+
+
+@adapter_config(context=IPortlet, provides=IPortletConfiguration)
+def PortletConfigurationAdapter(portlet):
+    """Portlet configuration factory"""
+    return PortletConfiguration(portlet)
+
+
+@adapter_config(context=IPortletConfiguration, provides=IPortletSettings)
+def PortletConfigurationSettingsAdapter(configuration):
+    """Portlet configuration settings adapter"""
+    return configuration.settings
+
+
+@adapter_config(name='settings', context=IPortletConfiguration, provides=ITraversable)
+class PortletConfigurationSettingsTraverser(ContextAdapter):
+    """++settings++ portlet configuration traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        return self.context.settings
+
+
+#
+# Template portlets configuration
+#
+
+@implementer(IPortalPortletsConfiguration)
+class PortalPortletsConfiguration(PersistentMapping, Contained):
+    """Portal portlets configuration"""
+
+    def get_portlet_configuration(self, portlet_id):
+        configuration = self.get(portlet_id)
+        if (configuration is None) and not IPortalTemplate.providedBy(self.__parent__):
+            template = IPortalPage(self.__parent__).template
+            portlets = IPortalPortletsConfiguration(template)
+            configuration = clone(portlets.get_portlet_configuration(portlet_id))
+            get_current_registry().notify(ObjectCreatedEvent(configuration))
+            self.set_portlet_configuration(portlet_id, configuration)
+        return configuration
+
+    def set_portlet_configuration(self, portlet_id, config):
+        config.portlet_id = portlet_id
+        self[portlet_id] = config
+        locate(config, self.__parent__, '++portlet++{0}'.format(portlet_id))
+
+    def delete_portlet_configuration(self, portlet_id):
+        if isinstance(portlet_id, int):
+            portlet_id = (portlet_id,)
+        for p_id in portlet_id:
+            del self[p_id]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/content/__init__.py	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,54 @@
+#
+# 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 .interfaces import IContentPortletSettings
+from pyams_portal.interfaces import IPortletRenderer, IPortalContext
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_PERMISSION
+
+# import packages
+from pyams_portal.portlet import PortletSettings, Portlet, PortletRenderer, portlet_config
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from zope.interface import implementer, Interface
+
+from pyams_portal import _
+
+
+CONTENT_PORTLET_NAME = 'pyams_portal.portlet.content'
+
+
+@implementer(IContentPortletSettings)
+class ContentPortletSettings(PortletSettings):
+    """Content portlet persistent settings"""
+
+
+@portlet_config(permission=VIEW_PERMISSION)
+class ContentPortlet(Portlet):
+    """Content portlet"""
+
+    name = CONTENT_PORTLET_NAME
+    label = _("Context content")
+
+    settings_class = ContentPortletSettings
+
+
+@adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ContentPortlet), provides=IPortletRenderer)
+@template_config(template='content.pt', layer=IPyAMSLayer)
+class ContentPortletRenderer(PortletRenderer):
+    """Content portlet renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/content/content.pt	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,1 @@
+<h3>This is my context!!!</h3>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/portlets/content/interfaces.py	Mon Jan 18 18:09:46 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_portal.interfaces import IPortletSettings
+
+# import packages
+
+
+class IContentPortletSettings(IPortletSettings):
+    """Content portlet settings interface"""
--- a/src/pyams_portal/portlets/context/__init__.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-#
-# 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 .interfaces import IContextPortletConfiguration
-from pyams_portal.interfaces import IPortletRenderer, IPortalContext
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_PERMISSION
-
-# import packages
-from pyams_portal.portlet import Portlet, portlet_config, PortletRenderer, PortletConfiguration
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config
-from zope.interface import implementer, Interface
-
-from pyams_portal import _
-
-
-CONTEXT_PORTLET_NAME = 'pyams_portal.portlet.context'
-
-
-@portlet_config(permission=VIEW_PERMISSION)
-class ContextPortlet(Portlet):
-    """Context portlet
-
-    The goal of this portlet is to provide context content
-    """
-
-    name = CONTEXT_PORTLET_NAME
-    label = _("Context content")
-
-
-@adapter_config(context=ContextPortlet, provides=IContextPortletConfiguration)
-@implementer(IContextPortletConfiguration)
-class ContextPortletConfiguration(PortletConfiguration):
-    """Context portlet configuration"""
-
-
-@adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ContextPortlet), provides=IPortletRenderer)
-@template_config(template='context.pt', layer=IPyAMSLayer)
-class ContextPortletRenderer(PortletRenderer):
-    """Context portlet renderer"""
-
-
--- a/src/pyams_portal/portlets/context/context.pt	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-<h3>This is my context!!!</h3>
--- a/src/pyams_portal/portlets/context/interfaces.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-#
-# 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_portal.interfaces import IPortletConfiguration
-
-# import packages
-
-
-class IContextPortletConfiguration(IPortletConfiguration):
-    """Context portlet configuration interface"""
--- a/src/pyams_portal/portlets/image/__init__.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/portlets/image/__init__.py	Mon Jan 18 18:09:46 2016 +0100
@@ -16,14 +16,14 @@
 # import standard library
 
 # import interfaces
-from .interfaces import IImagePortletConfiguration
+from .interfaces import IImagePortletSettings
 from pyams_portal.interfaces import IPortalContext, IPortletRenderer
 from pyams_skin.layer import IPyAMSLayer
 from pyams_utils.interfaces import VIEW_PERMISSION
 
 # import packages
 from pyams_file.property import FileProperty
-from pyams_portal.portlet import portlet_config, Portlet, PortletConfiguration, PortletRenderer
+from pyams_portal.portlet import portlet_config, Portlet, PortletSettings, PortletRenderer
 from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config
 from zope.interface import implementer, Interface
@@ -34,12 +34,16 @@
 IMAGE_PORTLET_NAME = 'pyams_portal.portlet.image'
 
 
+@implementer(IImagePortletSettings)
+class ImagePortletSettings(PortletSettings):
+    """Image portlet settings"""
+
+    image = FileProperty(IImagePortletSettings['image'])
+
+
 @portlet_config(permission=VIEW_PERMISSION)
 class ImagePortlet(Portlet):
-    """Image portlet
-
-    The goal of this portlet is to display an image
-    """
+    """Image portlet"""
 
     name = IMAGE_PORTLET_NAME
     label = _("Image")
@@ -47,13 +51,7 @@
     toolbar_image = None
     toolbar_css_class = 'fa fa-fw fa-2x fa-picture-o'
 
-
-@adapter_config(context=ImagePortlet, provides=IImagePortletConfiguration)
-@implementer(IImagePortletConfiguration)
-class ImagePortletConfiguration(PortletConfiguration):
-    """Image portlet configuration"""
-
-    image = FileProperty(IImagePortletConfiguration['image'])
+    settings_class = ImagePortletSettings
 
 
 @adapter_config(context=(IPortalContext, IPyAMSLayer, Interface, ImagePortlet), provides=IPortletRenderer)
--- a/src/pyams_portal/portlets/image/interfaces.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/portlets/image/interfaces.py	Mon Jan 18 18:09:46 2016 +0100
@@ -9,7 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from pyams_file.schema import ImageField
 
 __docformat__ = 'restructuredtext'
 
@@ -17,15 +16,16 @@
 # import standard library
 
 # import interfaces
-from pyams_portal.interfaces import IPortletConfiguration
+from pyams_file.schema import ImageField
+from pyams_portal.interfaces import IPortletSettings
 
 # import packages
 
 from pyams_portal import _
 
 
-class IImagePortletConfiguration(IPortletConfiguration):
-    """Image portlet configuration interface"""
+class IImagePortletSettings(IPortletSettings):
+    """Image portlet settings interface"""
 
     image = ImageField(title=_("Selected image"),
                        required=False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/resources/css/portal.min.css	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,1 @@
+#portal_config .rows{min-height:15px}#portal_config .row{position:relative;margin:5px 0;padding:2px 4px;border:1px solid rgba(199,81,0,0.4);border-top-width:1.5em;min-height:20px;cursor:move}#portal_config .row>.row_id{position:absolute;right:2px;top:-1.6em}#portal_config .row .slot{margin:3px 0;padding:3px;border:1px solid rgba(98,120,128,0.65);border-bottom-width:6px;min-height:20px!important}#portal_config .row .slot>.header{background-color:rgba(98,120,128,0.6);color:white}#portal_config .row .portlet{margin:3px 0;padding:3px;border:1px solid rgba(98,120,128,0.6);min-height:20px!important}#portal_config .row .portlet>.header{background-color:rgba(92,109,115,0.8);color:white}#portal_config .row-highlight{margin:5px 0;border:1px solid #c75100;min-height:40px}#portal_config .slots{min-height:15px}#portal_config .slot-highlight{margin:3px 0;border:1px solid #7b939c;min-height:40px}#portal_config .portlets{min-height:15px}#portal_config .portlets-hover{background-color:silver}#portal_config .portlets-active{background-color:silver}#portal_config .portlet-highlight{margin:0;border:1px solid #7b939c;min-height:40px}#portal_config.container .col-12{float:left;width:100%!important}#portal_config.container .col-11{float:left;width:91.66666667%!important}#portal_config.container .col-10{float:left;width:83.33333333%!important}#portal_config.container .col-9{float:left;width:75%!important}#portal_config.container .col-8{float:left;width:66.66666667%!important}#portal_config.container .col-7{float:left;width:58.33333333%!important}#portal_config.container .col-6{float:left;width:50%!important}#portal_config.container .col-5{float:left;width:41.66666667%!important}#portal_config.container .col-4{float:left;width:33.33333333%!important}#portal_config.container .col-3{float:left;width:25%!important}#portal_config.container .col-2{float:left;width:16.66666667%!important}#portal_config.container .col-1{float:left;width:8.33333333%!important}#portal_config.container .col-0{float:left;width:100%!important;opacity:.5}#portal_config.container .col-0>.portlets{display:none}#portal_config.container-xs{max-width:750px!important}#portal_config.container-sm{width:750px!important}#portal_config.container-md{width:970px!important}#portal_config.container-lg{width:1170px!important}
\ No newline at end of file
--- a/src/pyams_portal/resources/js/portal.js	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/resources/js/portal.js	Mon Jan 18 18:09:46 2016 +0100
@@ -1,6 +1,10 @@
-(function($) {
+(function($, globals) {
+
+	'use strict';
 
-	window.PyAMS_portal = {
+	var MyAMS = globals.MyAMS;
+
+	var PyAMS_portal = {
 
 		/**
 		 * Templates management
@@ -26,37 +30,7 @@
 					});
 					$('.rows', config).droppable({
 						accept: '.btn-row',
-						drop: function(event, ui) {
-							if (ui.draggable.hasClass('already-dropped'))
-								return;
-							ui.draggable.addClass('already-dropped');
-							MyAMS.ajax.post('add-template-row.json', {}, function(result) {
-								var row_id = result.row_id;
-								var rows = $('.rows', '#portal_config');
-								ui.draggable.removeClassPrefix('btn')
-											.removeClassPrefix('ui-')
-											.removeClass('already-dropped')
-											.removeAttr('style')
-											.addClass('row context-menu')
-											.attr('data-ams-row-id', row_id)
-											.empty()
-											.append($('<span></span>').addClass('row_id label label-success pull-right')
-																	  .text(row_id))
-											.append($('<div></div>').addClass('slots')
-																	.sortable({
-																		placeholder: 'slot-highlight',
-																		connectWith: '.slots',
-																		over: PyAMS_portal.template.overSlots,
-																		stop: PyAMS_portal.template.sortSlots
-																	}))
-											.contextMenu({
-												menuSelector: '#rowMenu',
-												menuSelected: MyAMS.helpers.contextMenuHandler
-											});
-								PyAMS_portal.template.sortRows();
-								rows.sortable('refresh');
-							});
-						}
+						drop: PyAMS_portal.template.dropRowButton
 					});
 					// Init slot toolbar drag and drop
 					$('.btn-slot', '.btn-toolbar').draggable({
@@ -67,13 +41,7 @@
 					});
 					$('.slots', config).droppable({
 						accept: '.btn-slot',
-						drop: function(event, ui) {
-							if (ui.draggable.hasClass('already-dropped'))
-								return;
-							ui.draggable.addClass('already-dropped');
-							var row_id = ui.helper.parents('.row:first').data('ams-row-id');
-							MyAMS.dialog.open('add-template-slot.html?form.widgets.row_id=' + row_id);
-						}
+						drop: PyAMS_portal.template.dropSlotButton
 					});
 					// Init portlets toolbar drag and drop
 					$('.btn-portlet', '.btn-toolbar').draggable({
@@ -86,20 +54,7 @@
 						accept: '.btn-portlet',
 						hoverClass: 'portlets-hover',
 						activeClass: 'portlets-active',
-						drop: function(event, ui) {
-							if (ui.draggable.hasClass('already-dropped'))
-								return;
-							ui.draggable.addClass('already-dropped');
-							var source = ui.draggable;
-							var target = $(this);
-							var slot = target.parents('.slot:first');
-							MyAMS.ajax.post('drag-template-portlet.json', {
-								portlet_name: source.data('ams-portlet-name'),
-								slot_name: slot.data('ams-slot-name')
-							}, function(result) {
-								MyAMS.ajax.handleJSON(result);
-							});
-						}
+						drop: PyAMS_portal.template.dropPortletButton
 					});
 				}
 			},
@@ -121,12 +76,18 @@
 									}
 									$('.slot', config).removeClassPrefix('col-');
 									for (var slot_name in result) {
+										if (!result.hasOwnProperty(slot_name)) {
+											continue;
+										}
 										var widths = result[slot_name];
 										var slot = $('.slot[data-ams-slot-name="' + slot_name + '"]', config);
 										if (device) {
 											slot.addClass('col-' + widths[device]);
 										} else {
 											for (var display in widths) {
+												if (!widths.hasOwnProperty(display)) {
+													continue;
+												}
 												slot.addClass('col-' + display + '-' + widths[display]);
 											}
 										}
@@ -154,6 +115,10 @@
 																	connectWith: '.slots',
 																	over: PyAMS_portal.template.overSlots,
 																	stop: PyAMS_portal.template.sortSlots
+																})
+																.droppable({
+																	accept: '.btn-slot',
+																	drop: PyAMS_portal.template.dropSlotButton
 																}))
 										.contextMenu({
 											menuSelector: '#rowMenu',
@@ -165,6 +130,43 @@
 				};
 			},
 
+			dropRowButton: function(event, ui) {
+				if (ui.draggable.hasClass('already-dropped')) {
+					return;
+				}
+				ui.draggable.addClass('already-dropped');
+				MyAMS.ajax.post('add-template-row.json', {}, function(result) {
+					var row_id = result.row_id;
+					var rows = $('.rows', '#portal_config');
+					ui.draggable.removeClassPrefix('btn')
+								.removeClassPrefix('ui-')
+								.removeClass('already-dropped')
+								.removeAttr('style')
+								.addClass('row context-menu')
+								.attr('data-ams-row-id', row_id)
+								.empty()
+								.append($('<span></span>').addClass('row_id label label-success pull-right')
+														  .text(row_id))
+								.append($('<div></div>').addClass('slots')
+														.sortable({
+															placeholder: 'slot-highlight',
+															connectWith: '.slots',
+															over: PyAMS_portal.template.overSlots,
+															stop: PyAMS_portal.template.sortSlots
+														})
+														.droppable({
+															accept: '.btn-slot',
+															drop: PyAMS_portal.template.dropSlotButton
+														}))
+								.contextMenu({
+									menuSelector: '#rowMenu',
+									menuSelected: MyAMS.helpers.contextMenuHandler
+								});
+					PyAMS_portal.template.sortRows();
+					rows.sortable('refresh');
+				});
+			},
+
 			overRows: function(event, ui) {
 				$(ui.placeholder).attr('class', $(ui.item).attr('class'))
 								 .removeClassPrefix('ui-')
@@ -173,18 +175,19 @@
 			},
 
 			sortRows: function(event, ui) {
-				if (ui && ui.item.hasClass('already-dropped'))
+				if (ui && ui.item.hasClass('already-dropped')) {
 					return;
+				}
 				var config = $('#portal_config');
 				var ids = $('.row', config).listattr('data-ams-row-id');
 				MyAMS.ajax.post('set-template-row-order.json',
 								{rows: JSON.stringify(ids)},
 								function(result) {
-									if (result.status == 'success') {
+									if (result.status === 'success') {
 										$('.row', config).each(function (index) {
 											$(this).attr('data-ams-row-id', index);
 											$('span.row_id', $(this)).text(index);
-										})
+										});
 									}
 								});
 			},
@@ -196,19 +199,20 @@
 						content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; ' + MyAMS.i18n.DELETE_WARNING,
 						buttons: MyAMS.i18n.BTN_OK_CANCEL
 					}, function(button) {
-						if (button == MyAMS.i18n.BTN_OK) {
-							if (!row.hasClass('row'))
+						if (button === MyAMS.i18n.BTN_OK) {
+							if (!row.hasClass('row')) {
 								row = row.parents('.row');
+							}
 							MyAMS.ajax.post('delete-template-row.json',
 											{row_id: row.data('ams-row-id')},
 											function(result) {
-												if (result.status == 'success') {
+												if (result.status === 'success') {
 													row.remove();
 													$('.row', '#portal_config').each(function (index) {
 														$(this).removeData()
 															   .attr('data-ams-row-id', index);
 														$('span.row_id', $(this)).text(index);
-													})
+													});
 												}
 											});
 						}
@@ -224,7 +228,7 @@
 			addSlotCallback: function(result) {
 				var slots = $('.slots', '.row[data-ams-row-id="' + result.row_id + '"]');
 				var slot_name = result.slot_name;
-				var new_slot = $('<div></div>').addClass('slot context-menu col col-md-12')
+				var new_slot = $('<div></div>').addClass('slot context-menu col col-xs-12 col-sm-12 col-md-12 col-lg-12 resizable')
 											   .attr('data-ams-slot-name', slot_name)
 											   .append($('<div></div>').addClass('header padding-x-5')
 																	   .text(slot_name))
@@ -234,6 +238,12 @@
 																			connectWith: '.portlets',
 																			over: PyAMS_portal.template.overPortlets,
 																			stop: PyAMS_portal.template.sortPortlets
+																	   })
+																	   .droppable({
+																			accept: '.btn-portlet',
+																			hoverClass: 'portlets-hover',
+																			activeClass: 'portlets-active',
+																			drop: PyAMS_portal.template.dropPortletButton
 																	   }))
 											   .append($('<div></div>').addClass('clearfix'))
 											   .contextMenu({
@@ -250,9 +260,23 @@
 				} else {
 					new_slot.appendTo(slots);
 				}
+				new_slot.resizable({
+					start: PyAMS_portal.template.startSlotResize,
+					stop: PyAMS_portal.template.stopSlotResize,
+					handles: 'e'
+				});
 				slots.sortable('refresh');
 			},
 
+			dropSlotButton: function(event, ui) {
+				if (ui.draggable.hasClass('already-dropped')) {
+					return;
+				}
+				ui.draggable.addClass('already-dropped');
+				var row_id = ui.helper.parents('.row:first').data('ams-row-id');
+				MyAMS.dialog.open('add-template-slot.html?form.widgets.row_id=' + row_id);
+			},
+
 			startSlotResize: function(event, ui) {
 				var slot = ui.element;
 				var row = slot.parents('.slots:first');
@@ -273,14 +297,15 @@
 				var device = $('#device_selector').val();
 				if (!device) {
 					var deviceWidth = $('body').width();
-					if (deviceWidth > 1170)
+					if (deviceWidth > 1170) {
 						device = 'lg';
-					else if (deviceWidth > 970)
+					} else if (deviceWidth > 970) {
 						device = 'md';
-					else if (deviceWidth > 750)
+					} else if (deviceWidth > 750) {
 						device = 'sm';
-					else
+					} else {
 						device = 'xs';
+					}
 				}
 				MyAMS.ajax.post('set-slot-width.json',
 								{slot_name: slot.data('ams-slot-name'),
@@ -301,8 +326,9 @@
 
 			editSlot: function() {
 				return function(slot) {
-					if (!slot.hasClass('slot'))
+					if (!slot.hasClass('slot')) {
 						slot = slot.parents('.slot');
+					}
 					MyAMS.dialog.open('slot-properties.html?form.widgets.slot_name=' + slot.data('ams-slot-name'));
 				};
 			},
@@ -311,10 +337,13 @@
 				var slot = $('.slot[data-ams-slot-name="' + result.slot_name + '"]');
 				slot.attr('class', 'slot context-menu col');
 				var device = $('#device_selector').val();
-				if (device)
+				if (device) {
 					slot.addClass('col-' + result.width[device]);
-				else {
+				} else {
 					for (device in result.width) {
+						if (!result.width.hasOwnProperty(device)) {
+							continue;
+						}
 						slot.addClass('col-' + device + '-' + result.width[device]);
 					}
 				}
@@ -328,8 +357,9 @@
 			},
 
 			sortSlots: function(event, ui) {
-				if (ui && ui.item.hasClass('already-dropped'))
+				if (ui && ui.item.hasClass('already-dropped')) {
 					return;
+				}
 				var config = $('#portal_config');
 				var order = {};
 				$('.row', config).each(function() {
@@ -341,10 +371,7 @@
 					order[parseInt(row.attr('data-ams-row-id'))] = row_config;
 				});
 				MyAMS.ajax.post('set-template-slot-order.json',
-								{order: JSON.stringify(order)},
-								function(result) {
-									if (result.status == 'success') {}
-								});
+								{order: JSON.stringify(order)});
 			},
 
 			deleteSlot: function() {
@@ -354,13 +381,14 @@
 						content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; ' + MyAMS.i18n.DELETE_WARNING,
 						buttons: MyAMS.i18n.BTN_OK_CANCEL
 					}, function(button) {
-						if (button == MyAMS.i18n.BTN_OK) {
-							if (!slot.hasClass('slot'))
+						if (button === MyAMS.i18n.BTN_OK) {
+							if (!slot.hasClass('slot')) {
 								slot = slot.parents('.slot');
+							}
 							MyAMS.ajax.post('delete-template-slot.json',
 											{slot_name: slot.data('ams-slot-name')},
 											function(result) {
-												if (result.status == 'success') {
+												if (result.status === 'success') {
 													slot.remove();
 													$('.slot', '#portal_config').each(function() {
 														$(this).removeData();
@@ -380,9 +408,7 @@
 			addPortletCallback: function(result) {
 				var portlets = $('.portlets', '.slot[data-ams-slot-name="' + result.slot_name + '"]');
 				var portlet = $('<div></div>').addClass('portlet context-menu')
-											  .attr('data-ams-portlet-name', result.portlet_name)
-											  .attr('data-ams-portlet-slot', result.slot_name)
-											  .attr('data-ams-portlet-position', result.position)
+											  .attr('data-ams-portlet-id', result.portlet_id)
 											  .append($('<div></div>').addClass('header padding-x-5')
 																	  .text(result.label))
 											.  append($('<div></div>').addClass('preview')
@@ -405,22 +431,37 @@
 				portlets.sortable('refresh');
 			},
 
+			dropPortletButton: function(event, ui) {
+				if (ui.draggable.hasClass('already-dropped')) {
+					return;
+				}
+				ui.draggable.addClass('already-dropped');
+				var source = ui.draggable;
+				var target = $(this);
+				var slot = target.parents('.slot:first');
+				MyAMS.ajax.post('drag-template-portlet.json', {
+					portlet_name: source.data('ams-portlet-name'),
+					slot_name: slot.data('ams-slot-name')
+				}, function(result) {
+					MyAMS.ajax.handleJSON(result);
+				});
+			},
+
 			editPortlet: function() {
 				return function(portlet) {
-					if (!portlet.hasClass('portlet'))
+					if (!portlet.hasClass('portlet')) {
 						portlet = portlet.parents('.portlet:first');
+					}
 					var slot = portlet.parents('.slot:first');
 					var row = slot.parents('.row:first');
-					MyAMS.dialog.open('portlet-properties.html?form.widgets.slot_name=' + slot.data('ams-slot-name') +
-															 '&form.widgets.position=' + portlet.data('ams-portlet-position'));
+					MyAMS.dialog.open('portlet-properties.html?form.widgets.portlet_id=' + portlet.data('ams-portlet-id'));
 				};
 			},
 
 			editPortletCallback: function(result) {
 				if (result.preview) {
 					var config = $('#portal_config');
-					var portlet = $('.portlet[data-ams-portlet-slot="' + result.slot_name + '"]' +
-											'[data-ams-portlet-position="' + result.position + '"]', config);
+					var portlet = $('.portlet[data-ams-portlet-id="' + result.portlet_id + '"]', config);
 					$('.preview', portlet).html(result.preview);
 					MyAMS.initContent($('.preview', portlet));
 				}
@@ -434,34 +475,17 @@
 			},
 
 			sortPortlets: function(event, ui) {
-				if (ui.item.hasClass('already-dropped'))
+				if (ui.item.hasClass('already-dropped')) {
 					return;
+				}
 				var portlet = ui.item;
 				var to_slot = portlet.parents('.slot');
 				var to_portlets = $('.portlet', to_slot);
-				var order = {from: {name: portlet.data('ams-portlet-name'),
-									slot: portlet.data('ams-portlet-slot'),
-									position: portlet.data('ams-portlet-position')},
+				var order = {from: portlet.data('ams-portlet-id'),
 							 to: {slot: to_slot.data('ams-slot-name'),
-								  names: to_portlets.listattr('data-ams-portlet-name'),
-								  slots: to_portlets.listattr('data-ams-portlet-slot'),
-								  positions: to_portlets.listattr('data-ams-portlet-position')}};
+								  portlet_ids: to_portlets.listattr('data-ams-portlet-id')}};
 				MyAMS.ajax.post('set-template-portlet-order.json',
-								{order: JSON.stringify(order)},
-								function(result) {
-									if (result.status == 'success') {
-										var from_slot = $('.slot[data-ams-slot-name="' + portlet.attr('data-ams-portlet-slot') + '"]', '#portal_config');
-										$('.portlet', from_slot).each(function(index) {
-											$(this).removeData()
-												   .attr('data-ams-portlet-position', index);
-										});
-										$('.portlet', to_slot).each(function(index) {
-											$(this).removeData()
-												   .attr('data-ams-portlet-slot', to_slot.attr('data-ams-slot-name'))
-												   .attr('data-ams-portlet-position', index);
-										});
-									}
-								});
+								{order: JSON.stringify(order)});
 			},
 
 			deletePortlet: function() {
@@ -471,14 +495,14 @@
 						content: '<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; ' + MyAMS.i18n.DELETE_WARNING,
 						buttons: MyAMS.i18n.BTN_OK_CANCEL
 					}, function(button) {
-						if (button == MyAMS.i18n.BTN_OK) {
-							if (!portlet.hasClass('portlet'))
+						if (button === MyAMS.i18n.BTN_OK) {
+							if (!portlet.hasClass('portlet')) {
 								portlet = portlet.parents('.portlet');
+							}
 							MyAMS.ajax.post('delete-template-portlet.json',
-											{slot_name: portlet.data('ams-portlet-slot'),
-											 position: portlet.data('ams-portlet-position')},
+											{portlet_id: portlet.data('ams-portlet-id')},
 											function(result) {
-												if (result.status == 'success') {
+												if (result.status === 'success') {
 													portlet.remove();
 													$('.portlet', '#portal_config').each(function() {
 														$(this).removeData();
@@ -491,5 +515,6 @@
 			}
 		}
 	};
+	globals.PyAMS_portal = PyAMS_portal;
 
-})(jQuery);
+})(jQuery, this);
--- a/src/pyams_portal/resources/js/portal.min.js	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/resources/js/portal.min.js	Mon Jan 18 18:09:46 2016 +0100
@@ -1,1 +1,1 @@
-(function(a){window.PyAMS_portal={template:{initConfig:function(){var b=a("#portal_config");if(b.data("ams-allowed-change")){a(".rows",b).addClass("sortable");a(".slots",b).addClass("sortable");a(".slot",b).addClass("resizable");a(".portlets",b).addClass("sortable");MyAMS.plugins.enabled.sortable(b);MyAMS.plugins.enabled.resizable(b);a(".btn-row",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".rows"});a(".rows",b).droppable({accept:".btn-row",drop:function(c,d){if(d.draggable.hasClass("already-dropped")){return}d.draggable.addClass("already-dropped");MyAMS.ajax.post("add-template-row.json",{},function(e){var f=e.row_id;var g=a(".rows","#portal_config");d.draggable.removeClassPrefix("btn").removeClassPrefix("ui-").removeClass("already-dropped").removeAttr("style").addClass("row context-menu").attr("data-ams-row-id",f).empty().append(a("<span></span>").addClass("row_id label label-success pull-right").text(f)).append(a("<div></div>").addClass("slots").sortable({placeholder:"slot-highlight",connectWith:".slots",over:PyAMS_portal.template.overSlots,stop:PyAMS_portal.template.sortSlots})).contextMenu({menuSelector:"#rowMenu",menuSelected:MyAMS.helpers.contextMenuHandler});PyAMS_portal.template.sortRows();g.sortable("refresh")})}});a(".btn-slot",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".slots"});a(".slots",b).droppable({accept:".btn-slot",drop:function(d,e){if(e.draggable.hasClass("already-dropped")){return}e.draggable.addClass("already-dropped");var c=e.helper.parents(".row:first").data("ams-row-id");MyAMS.dialog.open("add-template-slot.html?form.widgets.row_id="+c)}});a(".btn-portlet",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".portlets"});a(".portlets",b).droppable({accept:".btn-portlet",hoverClass:"portlets-hover",activeClass:"portlets-active",drop:function(c,e){if(e.draggable.hasClass("already-dropped")){return}e.draggable.addClass("already-dropped");var d=e.draggable;var f=a(this);var g=f.parents(".slot:first");MyAMS.ajax.post("drag-template-portlet.json",{portlet_name:d.data("ams-portlet-name"),slot_name:g.data("ams-slot-name")},function(h){MyAMS.ajax.handleJSON(h)})}})}},selectDisplay:function(){var b=a(this).val();MyAMS.ajax.post("get-slots-width.json",{device:b},function(c){var d=a("#portal_config");d.removeClassPrefix("container-");if(b){d.addClass("container-"+b)}a(".slot",d).removeClassPrefix("col-");for(var e in c){var f=c[e];var h=a('.slot[data-ams-slot-name="'+e+'"]',d);if(b){h.addClass("col-"+f[b])}else{for(var g in f){h.addClass("col-"+g+"-"+f[g])}}}})},addRow:function(){return function(){a(this).parents(".btn-group").removeClass("open");MyAMS.ajax.post("add-template-row.json",{},function(b){var c=b.row_id;var d=a(".rows","#portal_config");a("<div></div>").addClass("row context-menu").attr("data-ams-row-id",c).append(a("<span></span>").addClass("row_id label label-success pull-right").text(c)).append(a("<div></div>").addClass("slots").sortable({placeholder:"slot-highlight",connectWith:".slots",over:PyAMS_portal.template.overSlots,stop:PyAMS_portal.template.sortSlots})).contextMenu({menuSelector:"#rowMenu",menuSelected:MyAMS.helpers.contextMenuHandler}).appendTo(d);d.sortable("refresh")})}},overRows:function(b,c){a(c.placeholder).attr("class",a(c.item).attr("class")).removeClassPrefix("ui-").addClass("row-highlight").css("height",a(c.item).outerHeight())},sortRows:function(d,e){if(e&&e.item.hasClass("already-dropped")){return}var b=a("#portal_config");var c=a(".row",b).listattr("data-ams-row-id");MyAMS.ajax.post("set-template-row-order.json",{rows:JSON.stringify(c)},function(f){if(f.status=="success"){a(".row",b).each(function(g){a(this).attr("data-ams-row-id",g);a("span.row_id",a(this)).text(g)})}})},deleteRow:function(){return function(b){MyAMS.skin.bigBox({title:MyAMS.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+MyAMS.i18n.DELETE_WARNING,buttons:MyAMS.i18n.BTN_OK_CANCEL},function(c){if(c==MyAMS.i18n.BTN_OK){if(!b.hasClass("row")){b=b.parents(".row")}MyAMS.ajax.post("delete-template-row.json",{row_id:b.data("ams-row-id")},function(d){if(d.status=="success"){b.remove();a(".row","#portal_config").each(function(e){a(this).removeData().attr("data-ams-row-id",e);a("span.row_id",a(this)).text(e)})}})}})}},addSlotCallback:function(b){var e=a(".slots",'.row[data-ams-row-id="'+b.row_id+'"]');var d=b.slot_name;var c=a("<div></div>").addClass("slot context-menu col col-md-12").attr("data-ams-slot-name",d).append(a("<div></div>").addClass("header padding-x-5").text(d)).append(a("<div></div>").addClass("portlets").sortable({placeholder:"portlet-highlight",connectWith:".portlets",over:PyAMS_portal.template.overPortlets,stop:PyAMS_portal.template.sortPortlets})).append(a("<div></div>").addClass("clearfix")).contextMenu({menuSelector:"#slotMenu",menuSelected:MyAMS.helpers.contextMenuHandler});var f=a(".btn-slot",e);if(f.exists()){f.replaceWith(c);a(".slot",e).each(function(){a(this).removeData()});PyAMS_portal.template.sortSlots()}else{c.appendTo(e)}e.sortable("refresh")},startSlotResize:function(c,e){var g=e.element;var f=g.parents(".slots:first");var b=(f.innerWidth()-110)/12;var d=g.height();e.element.resizable("option","grid",[b,d]);e.element.resizable("option","minWidth",b);e.element.resizable("option","minHeight",d);e.element.resizable("option","maxWidth",f.innerWidth());e.element.resizable("option","maxHeight",d)},stopSlotResize:function(e,g){var i=g.element;var h=i.parents(".slots:first");var c=(h.innerWidth()-10)/12;var f=Math.round(a(i).width()/c);var d=a("#device_selector").val();if(!d){var b=a("body").width();if(b>1170){d="lg"}else{if(b>970){d="md"}else{if(b>750){d="sm"}else{d="xs"}}}}MyAMS.ajax.post("set-slot-width.json",{slot_name:i.data("ams-slot-name"),device:d,width:f},function(j){i.removeClassPrefix("col-");i.removeAttr("style");var k=i.data("ams-slot-name");var l=j[k];if(d){i.addClass("col-"+d+"-"+l[d])}else{i.addClass("col-"+l[d])}})},editSlot:function(){return function(b){if(!b.hasClass("slot")){b=b.parents(".slot")}MyAMS.dialog.open("slot-properties.html?form.widgets.slot_name="+b.data("ams-slot-name"))}},editSlotCallback:function(b){var d=a('.slot[data-ams-slot-name="'+b.slot_name+'"]');d.attr("class","slot context-menu col");var c=a("#device_selector").val();if(c){d.addClass("col-"+b.width[c])}else{for(c in b.width){d.addClass("col-"+c+"-"+b.width[c])}}},overSlots:function(b,c){a(c.placeholder).attr("class",a(c.item).attr("class")).removeClassPrefix("ui-").addClass("slot-highlight").css("height",a(c.item).outerHeight())},sortSlots:function(d,e){if(e&&e.item.hasClass("already-dropped")){return}var c=a("#portal_config");var b={};a(".row",c).each(function(){var g=a(this);var f=[];a(".slot",g).each(function(){f.push(a(this).data("ams-slot-name"))});b[parseInt(g.attr("data-ams-row-id"))]=f});MyAMS.ajax.post("set-template-slot-order.json",{order:JSON.stringify(b)},function(f){if(f.status=="success"){}})},deleteSlot:function(){return function(b){MyAMS.skin.bigBox({title:MyAMS.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+MyAMS.i18n.DELETE_WARNING,buttons:MyAMS.i18n.BTN_OK_CANCEL},function(c){if(c==MyAMS.i18n.BTN_OK){if(!b.hasClass("slot")){b=b.parents(".slot")}MyAMS.ajax.post("delete-template-slot.json",{slot_name:b.data("ams-slot-name")},function(d){if(d.status=="success"){b.remove();a(".slot","#portal_config").each(function(){a(this).removeData()})}})}})}},addPortletCallback:function(b){var c=a(".portlets",'.slot[data-ams-slot-name="'+b.slot_name+'"]');var e=a("<div></div>").addClass("portlet context-menu").attr("data-ams-portlet-name",b.portlet_name).attr("data-ams-portlet-slot",b.slot_name).attr("data-ams-portlet-position",b.position).append(a("<div></div>").addClass("header padding-x-5").text(b.label)).append(a("<div></div>").addClass("preview").html(b.preview||"")).contextMenu({menuSelector:"#portletMenu",menuSelected:MyAMS.helpers.contextMenuHandler});MyAMS.initContent(a(".preview",e));var d=a(".btn-portlet",c);if(d.exists()){d.replaceWith(e);a(".portlet",c).each(function(){a(this).removeData()});PyAMS_portal.template.sortPortlets(null,{item:e})}else{e.appendTo(c)}c.sortable("refresh")},editPortlet:function(){return function(c){if(!c.hasClass("portlet")){c=c.parents(".portlet:first")}var d=c.parents(".slot:first");var b=d.parents(".row:first");MyAMS.dialog.open("portlet-properties.html?form.widgets.slot_name="+d.data("ams-slot-name")+"&form.widgets.position="+c.data("ams-portlet-position"))}},editPortletCallback:function(b){if(b.preview){var c=a("#portal_config");var d=a('.portlet[data-ams-portlet-slot="'+b.slot_name+'"][data-ams-portlet-position="'+b.position+'"]',c);a(".preview",d).html(b.preview);MyAMS.initContent(a(".preview",d))}},overPortlets:function(b,c){a(c.placeholder).attr("class",a(c.item).attr("class")).removeClassPrefix("ui-").addClass("portlet-highlight").css("height",a(c.item).outerHeight())},sortPortlets:function(c,f){if(f.item.hasClass("already-dropped")){return}var g=f.item;var e=g.parents(".slot");var d=a(".portlet",e);var b={from:{name:g.data("ams-portlet-name"),slot:g.data("ams-portlet-slot"),position:g.data("ams-portlet-position")},to:{slot:e.data("ams-slot-name"),names:d.listattr("data-ams-portlet-name"),slots:d.listattr("data-ams-portlet-slot"),positions:d.listattr("data-ams-portlet-position")}};MyAMS.ajax.post("set-template-portlet-order.json",{order:JSON.stringify(b)},function(h){if(h.status=="success"){var i=a('.slot[data-ams-slot-name="'+g.attr("data-ams-portlet-slot")+'"]',"#portal_config");a(".portlet",i).each(function(j){a(this).removeData().attr("data-ams-portlet-position",j)});a(".portlet",e).each(function(j){a(this).removeData().attr("data-ams-portlet-slot",e.attr("data-ams-slot-name")).attr("data-ams-portlet-position",j)})}})},deletePortlet:function(){return function(b){MyAMS.skin.bigBox({title:MyAMS.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+MyAMS.i18n.DELETE_WARNING,buttons:MyAMS.i18n.BTN_OK_CANCEL},function(c){if(c==MyAMS.i18n.BTN_OK){if(!b.hasClass("portlet")){b=b.parents(".portlet")}MyAMS.ajax.post("delete-template-portlet.json",{slot_name:b.data("ams-portlet-slot"),position:b.data("ams-portlet-position")},function(d){if(d.status=="success"){b.remove();a(".portlet","#portal_config").each(function(){a(this).removeData()})}})}})}}}}})(jQuery);
\ No newline at end of file
+(function(c,b){var d=b.MyAMS;var a={template:{initConfig:function(){var e=c("#portal_config");if(e.data("ams-allowed-change")){c(".rows",e).addClass("sortable");c(".slots",e).addClass("sortable");c(".slot",e).addClass("resizable");c(".portlets",e).addClass("sortable");d.plugins.enabled.sortable(e);d.plugins.enabled.resizable(e);c(".btn-row",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".rows"});c(".rows",e).droppable({accept:".btn-row",drop:a.template.dropRowButton});c(".btn-slot",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".slots"});c(".slots",e).droppable({accept:".btn-slot",drop:a.template.dropSlotButton});c(".btn-portlet",".btn-toolbar").draggable({cursor:"move",helper:"clone",revert:"invalid",connectToSortable:".portlets"});c(".portlets",e).droppable({accept:".btn-portlet",hoverClass:"portlets-hover",activeClass:"portlets-active",drop:a.template.dropPortletButton})}},selectDisplay:function(){var e=c(this).val();d.ajax.post("get-slots-width.json",{device:e},function(f){var g=c("#portal_config");g.removeClassPrefix("container-");if(e){g.addClass("container-"+e)}c(".slot",g).removeClassPrefix("col-");for(var h in f){if(!f.hasOwnProperty(h)){continue}var i=f[h];var k=c('.slot[data-ams-slot-name="'+h+'"]',g);if(e){k.addClass("col-"+i[e])}else{for(var j in i){if(!i.hasOwnProperty(j)){continue}k.addClass("col-"+j+"-"+i[j])}}}})},addRow:function(){return function(){c(this).parents(".btn-group").removeClass("open");d.ajax.post("add-template-row.json",{},function(e){var f=e.row_id;var g=c(".rows","#portal_config");c("<div></div>").addClass("row context-menu").attr("data-ams-row-id",f).append(c("<span></span>").addClass("row_id label label-success pull-right").text(f)).append(c("<div></div>").addClass("slots").sortable({placeholder:"slot-highlight",connectWith:".slots",over:a.template.overSlots,stop:a.template.sortSlots}).droppable({accept:".btn-slot",drop:a.template.dropSlotButton})).contextMenu({menuSelector:"#rowMenu",menuSelected:d.helpers.contextMenuHandler}).appendTo(g);g.sortable("refresh")})}},dropRowButton:function(e,f){if(f.draggable.hasClass("already-dropped")){return}f.draggable.addClass("already-dropped");d.ajax.post("add-template-row.json",{},function(g){var h=g.row_id;var i=c(".rows","#portal_config");f.draggable.removeClassPrefix("btn").removeClassPrefix("ui-").removeClass("already-dropped").removeAttr("style").addClass("row context-menu").attr("data-ams-row-id",h).empty().append(c("<span></span>").addClass("row_id label label-success pull-right").text(h)).append(c("<div></div>").addClass("slots").sortable({placeholder:"slot-highlight",connectWith:".slots",over:a.template.overSlots,stop:a.template.sortSlots}).droppable({accept:".btn-slot",drop:a.template.dropSlotButton})).contextMenu({menuSelector:"#rowMenu",menuSelected:d.helpers.contextMenuHandler});a.template.sortRows();i.sortable("refresh")})},overRows:function(e,f){c(f.placeholder).attr("class",c(f.item).attr("class")).removeClassPrefix("ui-").addClass("row-highlight").css("height",c(f.item).outerHeight())},sortRows:function(g,h){if(h&&h.item.hasClass("already-dropped")){return}var e=c("#portal_config");var f=c(".row",e).listattr("data-ams-row-id");d.ajax.post("set-template-row-order.json",{rows:JSON.stringify(f)},function(i){if(i.status==="success"){c(".row",e).each(function(j){c(this).attr("data-ams-row-id",j);c("span.row_id",c(this)).text(j)})}})},deleteRow:function(){return function(e){d.skin.bigBox({title:d.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+d.i18n.DELETE_WARNING,buttons:d.i18n.BTN_OK_CANCEL},function(f){if(f===d.i18n.BTN_OK){if(!e.hasClass("row")){e=e.parents(".row")}d.ajax.post("delete-template-row.json",{row_id:e.data("ams-row-id")},function(g){if(g.status==="success"){e.remove();c(".row","#portal_config").each(function(h){c(this).removeData().attr("data-ams-row-id",h);c("span.row_id",c(this)).text(h)})}})}})}},addSlotCallback:function(e){var h=c(".slots",'.row[data-ams-row-id="'+e.row_id+'"]');var g=e.slot_name;var f=c("<div></div>").addClass("slot context-menu col col-xs-12 col-sm-12 col-md-12 col-lg-12 resizable").attr("data-ams-slot-name",g).append(c("<div></div>").addClass("header padding-x-5").text(g)).append(c("<div></div>").addClass("portlets").sortable({placeholder:"portlet-highlight",connectWith:".portlets",over:a.template.overPortlets,stop:a.template.sortPortlets}).droppable({accept:".btn-portlet",hoverClass:"portlets-hover",activeClass:"portlets-active",drop:a.template.dropPortletButton})).append(c("<div></div>").addClass("clearfix")).contextMenu({menuSelector:"#slotMenu",menuSelected:d.helpers.contextMenuHandler});var i=c(".btn-slot",h);if(i.exists()){i.replaceWith(f);c(".slot",h).each(function(){c(this).removeData()});a.template.sortSlots()}else{f.appendTo(h)}f.resizable({start:a.template.startSlotResize,stop:a.template.stopSlotResize,handles:"e"});h.sortable("refresh")},dropSlotButton:function(f,g){if(g.draggable.hasClass("already-dropped")){return}g.draggable.addClass("already-dropped");var e=g.helper.parents(".row:first").data("ams-row-id");d.dialog.open("add-template-slot.html?form.widgets.row_id="+e)},startSlotResize:function(f,h){var j=h.element;var i=j.parents(".slots:first");var e=(i.innerWidth()-110)/12;var g=j.height();h.element.resizable("option","grid",[e,g]);h.element.resizable("option","minWidth",e);h.element.resizable("option","minHeight",g);h.element.resizable("option","maxWidth",i.innerWidth());h.element.resizable("option","maxHeight",g)},stopSlotResize:function(h,j){var l=j.element;var k=l.parents(".slots:first");var f=(k.innerWidth()-10)/12;var i=Math.round(c(l).width()/f);var g=c("#device_selector").val();if(!g){var e=c("body").width();if(e>1170){g="lg"}else{if(e>970){g="md"}else{if(e>750){g="sm"}else{g="xs"}}}}d.ajax.post("set-slot-width.json",{slot_name:l.data("ams-slot-name"),device:g,width:i},function(m){l.removeClassPrefix("col-");l.removeAttr("style");var n=l.data("ams-slot-name");var o=m[n];if(g){l.addClass("col-"+g+"-"+o[g])}else{l.addClass("col-"+o[g])}})},editSlot:function(){return function(e){if(!e.hasClass("slot")){e=e.parents(".slot")}d.dialog.open("slot-properties.html?form.widgets.slot_name="+e.data("ams-slot-name"))}},editSlotCallback:function(e){var g=c('.slot[data-ams-slot-name="'+e.slot_name+'"]');g.attr("class","slot context-menu col");var f=c("#device_selector").val();if(f){g.addClass("col-"+e.width[f])}else{for(f in e.width){if(!e.width.hasOwnProperty(f)){continue}g.addClass("col-"+f+"-"+e.width[f])}}},overSlots:function(e,f){c(f.placeholder).attr("class",c(f.item).attr("class")).removeClassPrefix("ui-").addClass("slot-highlight").css("height",c(f.item).outerHeight())},sortSlots:function(g,h){if(h&&h.item.hasClass("already-dropped")){return}var f=c("#portal_config");var e={};c(".row",f).each(function(){var j=c(this);var i=[];c(".slot",j).each(function(){i.push(c(this).data("ams-slot-name"))});e[parseInt(j.attr("data-ams-row-id"))]=i});d.ajax.post("set-template-slot-order.json",{order:JSON.stringify(e)})},deleteSlot:function(){return function(e){d.skin.bigBox({title:d.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+d.i18n.DELETE_WARNING,buttons:d.i18n.BTN_OK_CANCEL},function(f){if(f===d.i18n.BTN_OK){if(!e.hasClass("slot")){e=e.parents(".slot")}d.ajax.post("delete-template-slot.json",{slot_name:e.data("ams-slot-name")},function(g){if(g.status==="success"){e.remove();c(".slot","#portal_config").each(function(){c(this).removeData()})}})}})}},addPortletCallback:function(e){var f=c(".portlets",'.slot[data-ams-slot-name="'+e.slot_name+'"]');var h=c("<div></div>").addClass("portlet context-menu").attr("data-ams-portlet-id",e.portlet_id).append(c("<div></div>").addClass("header padding-x-5").text(e.label)).append(c("<div></div>").addClass("preview").html(e.preview||"")).contextMenu({menuSelector:"#portletMenu",menuSelected:d.helpers.contextMenuHandler});d.initContent(c(".preview",h));var g=c(".btn-portlet",f);if(g.exists()){g.replaceWith(h);c(".portlet",f).each(function(){c(this).removeData()});a.template.sortPortlets(null,{item:h})}else{h.appendTo(f)}f.sortable("refresh")},dropPortletButton:function(e,g){if(g.draggable.hasClass("already-dropped")){return}g.draggable.addClass("already-dropped");var f=g.draggable;var h=c(this);var i=h.parents(".slot:first");d.ajax.post("drag-template-portlet.json",{portlet_name:f.data("ams-portlet-name"),slot_name:i.data("ams-slot-name")},function(j){d.ajax.handleJSON(j)})},editPortlet:function(){return function(f){if(!f.hasClass("portlet")){f=f.parents(".portlet:first")}var g=f.parents(".slot:first");var e=g.parents(".row:first");d.dialog.open("portlet-properties.html?form.widgets.portlet_id="+f.data("ams-portlet-id"))}},editPortletCallback:function(e){if(e.preview){var f=c("#portal_config");var g=c('.portlet[data-ams-portlet-id="'+e.portlet_id+'"]',f);c(".preview",g).html(e.preview);d.initContent(c(".preview",g))}},overPortlets:function(e,f){c(f.placeholder).attr("class",c(f.item).attr("class")).removeClassPrefix("ui-").addClass("portlet-highlight").css("height",c(f.item).outerHeight())},sortPortlets:function(f,i){if(i.item.hasClass("already-dropped")){return}var j=i.item;var h=j.parents(".slot");var g=c(".portlet",h);var e={from:j.data("ams-portlet-id"),to:{slot:h.data("ams-slot-name"),portlet_ids:g.listattr("data-ams-portlet-id")}};d.ajax.post("set-template-portlet-order.json",{order:JSON.stringify(e)})},deletePortlet:function(){return function(e){d.skin.bigBox({title:d.i18n.WARNING,content:'<i class="text-danger fa fa-2x fa-bell shake animated"></i>&nbsp; '+d.i18n.DELETE_WARNING,buttons:d.i18n.BTN_OK_CANCEL},function(f){if(f===d.i18n.BTN_OK){if(!e.hasClass("portlet")){e=e.parents(".portlet")}d.ajax.post("delete-template-portlet.json",{portlet_id:e.data("ams-portlet-id")},function(g){if(g.status==="success"){e.remove();c(".portlet","#portal_config").each(function(){c(this).removeData()})}})}})}}}};b.PyAMS_portal=a})(jQuery,this);
\ No newline at end of file
--- a/src/pyams_portal/slot.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/slot.py	Mon Jan 18 18:09:46 2016 +0100
@@ -20,29 +20,28 @@
 
 # import packages
 from persistent import Persistent
+from persistent.list import PersistentList
 from zope.container.contained import Contained
 from zope.interface import implementer
 from zope.schema.fieldproperty import FieldProperty
 
 
-PORTAL_SLOTS_KEY = 'pyams_portal.slots'
-
-
 @implementer(ISlotConfiguration)
 class SlotConfiguration(Persistent, Contained):
     """Portal slot class"""
 
     slot_name = FieldProperty(ISlotConfiguration['slot_name'])
+    _portlet_ids = FieldProperty(ISlotConfiguration['portlet_ids'])
     visible = FieldProperty(ISlotConfiguration['visible'])
-    _inherit_parent = FieldProperty(ISlotConfiguration['inherit_parent'])
-    _xs_width = FieldProperty(ISlotConfiguration['xs_width'])
-    _sm_width = FieldProperty(ISlotConfiguration['sm_width'])
-    _md_width = FieldProperty(ISlotConfiguration['md_width'])
-    _lg_width = FieldProperty(ISlotConfiguration['lg_width'])
-    _css_class = FieldProperty(ISlotConfiguration['css_class'])
+    xs_width = FieldProperty(ISlotConfiguration['xs_width'])
+    sm_width = FieldProperty(ISlotConfiguration['sm_width'])
+    md_width = FieldProperty(ISlotConfiguration['md_width'])
+    lg_width = FieldProperty(ISlotConfiguration['lg_width'])
+    css_class = FieldProperty(ISlotConfiguration['css_class'])
 
     def __init__(self, slot_name, **kwargs):
         self.slot_name = slot_name
+        self._portlet_ids = PersistentList()
         self.xs_width = 12
         self.sm_width = 12
         self.md_width = 12
@@ -58,76 +57,17 @@
             return IPortalPage(self.__parent__).template
 
     @property
-    def can_inherit(self):
-        return IPortalPage.providedBy(self.__parent__)
-
-    @property
-    def inherit_parent(self):
-        return self._inherit_parent if self.can_inherit else False
-
-    @inherit_parent.setter
-    def inherit_parent(self, value):
-        self._inherit_parent = value
-
-    @property
-    def xs_width(self):
-        if self.inherit_parent:
-            config = IPortalTemplateConfiguration(self.template)
-            return config.get_slot_configuration(self.slot_name).xs_width
+    def portlet_ids(self):
+        if IPortalTemplate.providedBy(self.__parent__):
+            return self._portlet_ids
         else:
-            return self._xs_width
-        
-    @xs_width.setter
-    def xs_width(self, value):
-        self._xs_width = value
-
-    @property
-    def sm_width(self):
-        if self.inherit_parent:
             config = IPortalTemplateConfiguration(self.template)
-            return config.get_slot_configuration(self.slot_name).sm_width
-        else:
-            return self._sm_width
-        
-    @sm_width.setter
-    def sm_width(self, value):
-        self._sm_width = value
+            return config.get_slot_configuration(self.slot_name).portlet_ids
 
-    @property
-    def md_width(self):
-        if self.inherit_parent:
-            config = IPortalTemplateConfiguration(self.template)
-            return config.get_slot_configuration(self.slot_name).md_width
-        else:
-            return self._md_width
-        
-    @md_width.setter
-    def md_width(self, value):
-        self._md_width = value
-
-    @property
-    def lg_width(self):
-        if self.inherit_parent:
-            config = IPortalTemplateConfiguration(self.template)
-            return config.get_slot_configuration(self.slot_name).lg_width
-        else:
-            return self._lg_width
-        
-    @lg_width.setter
-    def lg_width(self, value):
-        self._lg_width = value
-
-    @property
-    def css_class(self):
-        if self.inherit_parent:
-            config = IPortalTemplateConfiguration(self.template)
-            return config.get_slot_configuration(self.slot_name).css_class
-        else:
-            return self._css_class
-
-    @css_class.setter
-    def css_class(self, value):
-        self._css_class = value
+    @portlet_ids.setter
+    def portlet_ids(self, value):
+        if IPortalTemplate.providedBy(self.__parent__):
+            self._portlet_ids = value
 
     def get_css_class(self, device=None):
         if not device:
--- a/src/pyams_portal/template.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/template.py	Mon Jan 18 18:09:46 2016 +0100
@@ -16,9 +16,9 @@
 # import standard library
 
 # import interfaces
-from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, IPortalContext, IPortalPage, \
-    IPortletConfiguration, IPortlet, IPortalTemplateContainer, IPortalWfTemplate, IPortalTemplateContainerConfiguration
-from pyams_workflow.interfaces import IWorkflowVersions
+from pyams_portal.interfaces import IPortalTemplateContainer, IPortalTemplateContainerConfiguration, \
+    IPortalTemplate, IPortalTemplateConfiguration, IPortalPortletsConfiguration, IPortlet, IPortletConfiguration, \
+    PORTLETS_CONFIGURATION_KEY, TEMPLATE_CONTAINER_CONFIGURATION_KEY, TEMPLATE_CONFIGURATION_KEY
 from zope.annotation.interfaces import IAnnotations
 from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
 from zope.traversing.interfaces import ITraversable
@@ -27,9 +27,10 @@
 from persistent import Persistent
 from persistent.list import PersistentList
 from persistent.mapping import PersistentMapping
+from pyams_portal.portlet import PortalPortletsConfiguration
 from pyams_portal.slot import SlotConfiguration
 from pyams_utils.adapter import adapter_config, ContextAdapter
-from pyams_utils.registry import get_local_registry
+from pyams_utils.registry import get_local_registry, get_utility
 from pyams_utils.request import check_request
 from pyramid.events import subscriber
 from pyramid.threadlocal import get_current_registry
@@ -39,79 +40,85 @@
 from zope.copy import clone
 from zope.interface import implementer
 from zope.lifecycleevent import ObjectCreatedEvent
-from zope.location.location import locate
+from zope.location import locate
 from zope.schema.fieldproperty import FieldProperty
 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
 
 
+#
+# Portal templates container
+#
+
 @implementer(IPortalTemplateContainer)
 class PortalTemplateContainer(Folder):
-    """Portal template container"""
+    """Portal templates container"""
+
+    last_portlet_id = FieldProperty(IPortalTemplateContainer['last_portlet_id'])
+
+    def get_portlet_id(self):
+        self.last_portlet_id += 1
+        return self.last_portlet_id
 
 
 @implementer(IPortalTemplateContainerConfiguration)
 class PortalTemplateContainerConfiguration(Persistent, Contained):
     """Portal template container configuration"""
 
-    selected_portlets = FieldProperty(IPortalTemplateContainerConfiguration['selected_portlets'])
-
-
-PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY = 'pyams_portal.container.configuration'
+    toolbar_portlets = FieldProperty(IPortalTemplateContainerConfiguration['toolbar_portlets'])
 
 
 @adapter_config(context=IPortalTemplateContainer, provides=IPortalTemplateContainerConfiguration)
-def PortalTemplateContainerConfigurationFactory(context):
+def PortalTemplateContainerConfigurationAdapter(context):
     """Portal template container configuration factory"""
     annotations = IAnnotations(context)
-    config = annotations.get(PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY)
+    config = annotations.get(TEMPLATE_CONTAINER_CONFIGURATION_KEY)
     if config is None:
-        config = annotations[PORTAL_TEMPLATE_CONTAINER_CONFIGURATION_KEY] = PortalTemplateContainerConfiguration()
+        config = annotations[TEMPLATE_CONTAINER_CONFIGURATION_KEY] = PortalTemplateContainerConfiguration()
         get_current_registry().notify(ObjectCreatedEvent(config))
         locate(config, context)
     return config
 
 
+#
+# Portal template base class
+#
+
 @implementer(IPortalTemplate)
 class PortalTemplate(Persistent, Contained):
-    """Portal template persistent class"""
+    """Portal template class"""
 
     name = FieldProperty(IPortalTemplate['name'])
 
 
-@implementer(IPortalWfTemplate)
-class PortalWfTemplate(Persistent, Contained):
-    """Portal template workflow manager class"""
-
-    content_class = PortalTemplate
-    workflow_name = 'PyAMS portal template workflow'
-    view_permission = None
-
-
-@subscriber(IObjectAddedEvent, context_selector=IPortalWfTemplate)
+@subscriber(IObjectAddedEvent, context_selector=IPortalTemplate)
 def handle_added_template(event):
     """Register shared template"""
     registry = get_local_registry()
     if (registry is not None) and IPortalTemplateContainer.providedBy(event.newParent):
-        registry.registerUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
+        registry.registerUtility(event.object, IPortalTemplate, name=event.object.name)
 
 
-@subscriber(IObjectRemovedEvent, context_selector=IPortalWfTemplate)
+@subscriber(IObjectRemovedEvent, context_selector=IPortalTemplate)
 def handle_removed_template(event):
     """Unregister removed template"""
     registry = get_local_registry()
     if (registry is not None) and IPortalTemplateContainer.providedBy(event.oldParent):
-        registry.unregisterUtility(event.object, IPortalWfTemplate, name=event.object.__name__)
+        registry.unregisterUtility(event.object, IPortalTemplate, name=event.object.name)
 
 
 class PortalTemplatesVocabulary(UtilityVocabulary):
     """Portal templates vocabulary"""
 
-    interface = IPortalWfTemplate
+    interface = IPortalTemplate
     nameOnly = True
 
 getVocabularyRegistry().register('PyAMS portal templates', PortalTemplatesVocabulary)
 
 
+#
+# Portal template configuration
+#
+
 @implementer(IPortalTemplateConfiguration)
 class PortalTemplateConfiguration(Persistent, Contained):
     """Portal template configuration"""
@@ -119,25 +126,21 @@
     rows = FieldProperty(IPortalTemplateConfiguration['rows'])
     _slot_names = FieldProperty(IPortalTemplateConfiguration['slot_names'])
     _slot_order = FieldProperty(IPortalTemplateConfiguration['slot_order'])
-    _slots = FieldProperty(IPortalTemplateConfiguration['slots'])
-    slot_config = FieldProperty(IPortalTemplateConfiguration['slot_config'])
-    portlet_config = FieldProperty(IPortalTemplateConfiguration['portlet_config'])
+    _slot_config = FieldProperty(IPortalTemplateConfiguration['slot_config'])
 
     def __init__(self):
         self._slot_names = PersistentList()
         self._slot_order = PersistentMapping()
         self._slot_order[0] = PersistentList()
-        self._slots = PersistentMapping()
-        self._slots[0] = PersistentMapping()
         self.slot_config = PersistentMapping()
-        self.portlet_config = PersistentMapping()
+
+    # rows management
 
     def add_row(self):
         """Add new row and return last row index (0 based)"""
         self.rows += 1
         last_index = self.rows - 1
         self.slot_order[last_index] = PersistentList()
-        self.slots[last_index] = PersistentMapping()
         return last_index
 
     def set_row_order(self, order):
@@ -145,35 +148,31 @@
         if not isinstance(order, (list, tuple)):
             order = list(order)
         old_slot_order = self.slot_order
-        old_slots = self.slots
         assert len(order) == self.rows
         new_slot_order = PersistentMapping()
-        new_slots = PersistentMapping()
         for index, row_id in enumerate(order):
             new_slot_order[index] = old_slot_order.get(row_id) or PersistentList()
-            new_slots[index] = old_slots.get(row_id) or PersistentMapping()
         if self.slot_order != new_slot_order:
             self.slot_order = new_slot_order
-            self.slots = new_slots
 
     def delete_row(self, row_id):
         """Delete template row"""
-        assert row_id in self.slots
-        for slot_name in self.slots.get(row_id, {}).keys():
+        assert row_id in self.slot_order
+        for slot_name in self.slot_order.get(row_id, ()):
+            config = IPortalPortletsConfiguration(self.__parent__)
+            config.delete_portlet_configuration(self.slot_config[slot_name].portlet_ids)
             if slot_name in self.slot_names:
                 self.slot_names.remove(slot_name)
             if slot_name in self.slot_config:
                 del self.slot_config[slot_name]
-            if slot_name in self.portlet_config:
-                del self.portlet_config[slot_name]
         for index in range(row_id, self.rows-1):
             self.slot_order[index] = self.slot_order[index+1]
-            self.slots[index] = self.slots[index+1]
         if self.rows > 0:
             del self.slot_order[self.rows-1]
-            del self.slots[self.rows-1]
         self.rows -= 1
 
+    # slots management
+
     @property
     def slot_names(self):
         if IPortalTemplate.providedBy(self.__parent__):
@@ -197,15 +196,15 @@
         self._slot_order = value
 
     @property
-    def slots(self):
+    def slot_config(self):
         if IPortalTemplate.providedBy(self.__parent__):
-            return self._slots
+            return self._slot_config
         else:
-            return IPortalTemplateConfiguration(self.__parent__).slots
+            return IPortalTemplateConfiguration(self.__parent__).slot_config
 
-    @slots.setter
-    def slots(self, value):
-        self._slots = value
+    @slot_config.setter
+    def slot_config(self, value):
+        self._slot_config = value
 
     def add_slot(self, slot_name, row_id=None):
         assert slot_name not in self.slot_names
@@ -216,10 +215,6 @@
         if row_id not in self.slot_order:
             self.slot_order[row_id] = PersistentList()
         self.slot_order[row_id].append(slot_name)
-        # init slots portlets
-        if row_id not in self.slots:
-            self.slots[row_id] = PersistentMapping()
-        self.slots[row_id][slot_name] = PersistentList()
         # init slots configuration
         slot = self.slot_config[slot_name] = SlotConfiguration(slot_name)
         locate(slot, self.__parent__)
@@ -228,18 +223,11 @@
     def set_slot_order(self, order):
         """Set slots order"""
         old_slot_order = self.slot_order
-        old_slots = self.slots
         new_slot_order = PersistentMapping()
-        new_slots = PersistentMapping()
         for row_id in sorted(map(int, order.keys())):
             new_slot_order[row_id] = PersistentList(order[row_id])
-            new_slots[row_id] = PersistentMapping()
-            for slot_name in order[row_id]:
-                old_row_id = self.get_slot_row(slot_name)
-                new_slots[row_id][slot_name] = old_slots[old_row_id][slot_name]
         if new_slot_order != old_slot_order:
             self.slot_order = new_slot_order
-            self.slots = new_slots
 
     def get_slot_row(self, slot_name):
         for row_id in self.slot_order:
@@ -280,102 +268,58 @@
         """Delete slot and associated portlets"""
         assert slot_name in self.slot_names
         row_id = self.get_slot_row(slot_name)
-        del self.portlet_config[slot_name]
+        # delete portlet configuration
+        config = IPortalPortletsConfiguration(self.__parent__)
+        config.delete_portlet_configuration(self.slot_config[slot_name].portlet_ids)
+        # delete slot configuration
         del self.slot_config[slot_name]
-        del self.slots[row_id][slot_name]
         self.slot_order[row_id].remove(slot_name)
         self.slot_names.remove(slot_name)
 
+    # portlets management
+
     def add_portlet(self, portlet_name, slot_name):
         """Add portlet to given slot"""
         assert slot_name in self.slot_names
-        row_id = self.get_slot_row(slot_name)
-        if slot_name not in self.slots.get(row_id):
-            self.slots[row_id][slot_name] = PersistentList()
-        self.slots[row_id][slot_name].append(portlet_name)
-        if slot_name not in self.portlet_config:
-            self.portlet_config[slot_name] = PersistentMapping()
-        position = len(self.slots[row_id][slot_name]) - 1
+        # get new portlet configuration
         portlet = get_current_registry().getUtility(IPortlet, name=portlet_name)
         config = IPortletConfiguration(portlet)
-        config.slot_name = slot_name
-        config.position = position
-        locate(config, self.__parent__, '++portlet++{0}::{1}'.format(slot_name, position))
-        self.portlet_config[slot_name][position] = config
+        # store portlet configuration
+        manager = get_utility(IPortalTemplateContainer)
+        IPortalPortletsConfiguration(self.__parent__).set_portlet_configuration(manager.get_portlet_id(), config)
+        # update slots configuration
+        self.slot_config[slot_name].portlet_ids.append(config.portlet_id)
         return {'portlet_name': portlet_name,
+                'portlet_id': config.portlet_id,
                 'slot_name': slot_name,
-                'position': position,
+                'position': len(self.slot_config[slot_name].portlet_ids) - 1,
                 'label': check_request().localizer.translate(portlet.label)}
 
+    def get_portlet_slot(self, portlet_id):
+        """Get portlet slot"""
+        for slot_name, config in self.slot_config.items():
+            if portlet_id in config.portlet_ids:
+                return self.get_slot_row(slot_name), slot_name
+        return None, None
+
     def set_portlet_order(self, order):
         """Set portlet order"""
-        source = order['from']
-        source_slot = source['slot']
-        source_row = self.get_slot_row(source_slot)
-        target = order['to']
-        target_slot = target['slot']
+        from_row, from_slot = self.get_portlet_slot(order['from'])
+        if from_slot is None:
+            return
+        target_slot = order['to']['slot']
         target_row = self.get_slot_row(target_slot)
-        portlet_config = self.portlet_config
-        old_config = portlet_config[source_slot].pop(source['position'])
-        target_config = PersistentMapping()
-        for index, (slot_name, portlet_name, position) in enumerate(zip(target['slots'], target['names'],
-                                                                        target['positions'])):
-            if (slot_name == source_slot) and (position == source['position']):
-                target_config[index] = old_config
-            else:
-                target_config[index] = portlet_config[slot_name][position]
-            target_config[index].slot_name = target_slot
-            target_config[index].position = index
-            locate(target_config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
-        portlet_config[target_slot] = target_config
-        # re-order source portlets
-        config = portlet_config[source_slot]
-        for index, key in enumerate(sorted(config)):
-            if index != key:
-                config[index] = config.pop(key)
-                config[index].position = index
-                locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(source_slot, index))
-        # re-order target portlets
-        if target_slot != source_slot:
-            config = portlet_config[target_slot]
-            for index, key in enumerate(sorted(config)):
-                config[index] = config.pop(key)
-                config[index].position = index
-                locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(target_slot, index))
-        self.portlet_config = portlet_config
-        del self.slots[source_row][source_slot][source['position']]
-        self.slots[target_row][target_slot] = PersistentList(target['names'])
+        if target_row is None:
+            return
+        self.slot_config[from_slot].portlet_ids.remove(order['from'])
+        self.slot_config[target_slot].portlet_ids = PersistentList(order['to']['portlet_ids'])
 
-    def get_portlet_configuration(self, slot_name, position):
-        """Get portlet configuration"""
-        if slot_name not in self.slot_names:
-            return None
-        config = self.portlet_config.get(slot_name, {}).get(position)
-        if config is None:
-            if IPortalTemplate.providedBy(self.__parent__):
-                portlet_name = self.slots[slot_name][position]
-                portlet = get_current_registry().queryUtility(IPortlet, name=portlet_name)
-                config = IPortletConfiguration(portlet)
-            else:
-                config = clone(IPortalTemplateConfiguration(self.__parent__).get_portlet_configuration(slot_name,
-                                                                                                       position))
-                config.inherit_parent = True
-            self.portlet_config[slot_name][position] = config
-            locate(config, self.__parent__)
-        return config
-
-    def delete_portlet(self, slot_name, position):
+    def delete_portlet(self, portlet_id):
         """Delete portlet"""
-        assert slot_name in self.slot_names
-        row_id = self.get_slot_row(slot_name)
-        config = self.portlet_config[slot_name]
-        del config[position]
-        if len(config) and (position < max(tuple(config.keys()))):
-            for index, key in enumerate(sorted(config)):
-                config[index] = config.pop(key)
-                config[index].position = index
-                locate(config[index], self.__parent__, '++portlet++{0}::{1}'.format(slot_name, index))
-        del self.slots[row_id][slot_name][position]
+        row_id, slot_name = self.get_portlet_slot(portlet_id)
+        if slot_name is not None:
+            self.slot_config[slot_name].portlet_ids.remove(portlet_id)
+            IPortalPortletsConfiguration(self.__parent__).delete_portlet_configuration(portlet_id)
 
 
 class PortalTemplateSlotsVocabulary(SimpleVocabulary):
@@ -389,25 +333,9 @@
 getVocabularyRegistry().register('PyAMS template slots', PortalTemplateSlotsVocabulary)
 
 
-@adapter_config(name='portlet', context=IPortalTemplate, provides=ITraversable)
-class PortalTemplatePortletTraverser(ContextAdapter):
-    """++portlet++ namespace traverser"""
-
-    def traverse(self, name, furtherpath=None):
-        config = IPortalTemplateConfiguration(self.context)
-        if name:
-            slot_name, position = name.split('::')
-            return config.get_portlet_configuration(slot_name, int(position))
-        else:
-            return config
-
-
-TEMPLATE_CONFIGURATION_KEY = 'pyams_portal.template'
-
-
 @adapter_config(context=IPortalTemplate, provides=IPortalTemplateConfiguration)
 def PortalTemplateConfigurationFactory(context):
-    """Portal template configuration factory"""
+    """Portal template configuration adapter"""
     annotations = IAnnotations(context)
     config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
     if config is None:
@@ -417,25 +345,29 @@
     return config
 
 
-@adapter_config(context=IPortalContext, provides=IPortalTemplateConfiguration)
-def PortalContextConfigurationFactory(context):
-    """Portal context configuration factory"""
-    page = IPortalPage(context)
-    if page.use_local_template:
-        template = IWorkflowVersions(page.template).get_last_versions()[0]
-        config = IPortalTemplateConfiguration(template)
-    else:
-        annotations = IAnnotations(context)
-        config = annotations.get(TEMPLATE_CONFIGURATION_KEY)
-        if config is None:
-            # we clone template configuration
-            config = annotations[TEMPLATE_CONFIGURATION_KEY] = clone(IPortalTemplateConfiguration(page.template))
-            get_current_registry().notify(ObjectCreatedEvent(config))
-            locate(config, context)
-    return config
+@adapter_config(name='portlet', context=IPortalTemplate, provides=ITraversable)
+class PortalTemplatePortletTraverser(ContextAdapter):
+    """++portlet++ template traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        config = IPortalPortletsConfiguration(self.context)
+        if name:
+            return config.get_portlet_configuration(int(name))
+        else:
+            return config
 
 
-@adapter_config(context=IPortletConfiguration, provides=IPortalTemplateConfiguration)
-def PortalPortletConfigurationFactory(context):
-    """Portal portlet configuration factory"""
-    return IPortalTemplateConfiguration(context.__parent__)
+#
+# Template portlets configuration
+#
+
+@adapter_config(context=IPortalTemplate, provides=IPortalPortletsConfiguration)
+def PortalTemplatePortletsConfigurationAdapter(template):
+    """Portal template portlets configuration adapter"""
+    annotations = IAnnotations(template)
+    config = annotations.get(PORTLETS_CONFIGURATION_KEY)
+    if config is None:
+        config = annotations[PORTLETS_CONFIGURATION_KEY] = PortalPortletsConfiguration()
+        get_current_registry().notify(ObjectCreatedEvent(config))
+        locate(config, template)
+    return config
--- a/src/pyams_portal/workflow.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,223 +0,0 @@
-#
-# 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 datetime import datetime
-
-# import interfaces
-from pyams_workflow.interfaces import IWorkflowPublicationInfo, IWorkflowState, IWorkflowVersions, IWorkflowInfo, \
-    ObjectClonedEvent, IWorkflow
-
-# import packages
-from pyams_utils.registry import utility_config
-from pyams_workflow.workflow import Transition, Workflow
-from pyramid.threadlocal import get_current_registry
-from zope.copy import copy
-from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
-
-from pyams_portal import _
-
-
-DRAFT = 'draft'  # new template
-PUBLISHED = 'published'  # published template
-RETIRED = 'retired'  # retired template
-ARCHIVED = 'archived'  # archive done
-DELETED = 'deleted'  # deleted template
-
-
-STATUS_IDS = ('draft', 'published', 'retired', 'archived', 'deleted')
-STATUS_LABELS = (_("Draft"),
-                 _("Published"),
-                 _("Retired"),
-                 _("Archived"),
-                 _("Deleted"))
-
-STATUS_VOCABULARY = SimpleVocabulary([SimpleTerm(STATUS_IDS[i], STATUS_IDS[i], t)
-                                      for i, t in enumerate(STATUS_LABELS)])
-
-
-def publish_action(wf, context):
-    """Publish template"""
-    now = datetime.utcnow()
-    wf_content = IWorkflowPublicationInfo(context)
-    if wf_content.first_publication_date is None:
-        wf_content.first_publication_date = now
-    IWorkflowPublicationInfo(context).publication_date = now
-    version_id = IWorkflowState(context).version_id
-    for version in IWorkflowVersions(context).get_versions(('published', 'retired')):
-        if version is not context:
-            IWorkflowInfo(version).fire_transition_toward('archived',
-                                                          comment="Published version {0}".format(version_id))
-
-
-def clone_action(wf, context):
-    """Duplicate template"""
-    result = copy(context)
-    registry = get_current_registry()
-    registry.notify(ObjectClonedEvent(result, context))
-    return result
-
-
-def retire_action(wf, context):
-    """Archive template"""
-    now = datetime.utcnow()
-    IWorkflowPublicationInfo(context).publication_expiration_date = now
-
-
-def archive_action(wf, context):
-    """Archive template"""
-    now = datetime.utcnow()
-    content = IWorkflowPublicationInfo(context)
-    content.publication_expiration_date = min(content.publication_expiration_date or now, now)
-
-
-def can_delete(wf, context):
-    content = IWorkflowPublicationInfo(context)
-    return content.publication_effective_date is None
-
-
-def delete_action(wf, context):
-    """Delete draft version"""
-    parent = context.__parent__
-    name = context.__name__
-    del parent[name]
-
-
-#
-# Workflow transitions
-#
-
-init = Transition(transition_id='init',
-                  title=_("Initialize"),
-                  source=None,
-                  destination=DRAFT)
-
-draft_to_published = Transition('draft_to_published',
-                                title=_("Publish..."),
-                                source=DRAFT,
-                                destination=PUBLISHED,
-                                permission='portal.templates.manage',
-                                action=publish_action,
-                                order=1,
-                                menu_css_class='fa fa-fw fa-play',
-                                view_name='wf-publish.html',
-                                html_help=_('''This content is currently in DRAFT mode.
-                                               Publishing it will make it publicly visible.'''))
-
-published_to_retired = Transition('published_to_retired',
-                                  title=_("Retire..."),
-                                  source=PUBLISHED,
-                                  destination=RETIRED,
-                                  permission='portal.templates.manage',
-                                  action=retire_action,
-                                  order=2,
-                                  menu_css_class='fa fa-fw fa-stop',
-                                  view_name='wf-retire.html',
-                                  html_help=_('''This content is actually published.
-                                                 You can retire it to make it invisible, but contents using this
-                                                 template won't be visible anymore!'''))
-
-published_to_draft = Transition('published_to_draft',
-                                title=_("Create new version..."),
-                                source=PUBLISHED,
-                                destination=DRAFT,
-                                permission='portal.templates.manage',
-                                action=clone_action,
-                                order=99,
-                                menu_css_class='fa fa-fw fa-copy',
-                                view_name='wf-clone.html')
-
-retired_to_published = Transition('retired_to_published',
-                                  title=_("Re-publish..."),
-                                  source=RETIRED,
-                                  destination=PUBLISHED,
-                                  permission='portal.templates.manage',
-                                  action=publish_action,
-                                  order=1,
-                                  menu_css_class='fa fa-fw fa-play',
-                                  view_name='wf-publish.html',
-                                  html_help=_('''This content was published and retired.
-                                                 You can re-publish it to make it visible again.'''))
-
-published_to_archived = Transition('published_to_archived',
-                                   title=_("Archive..."),
-                                   source=PUBLISHED,
-                                   destination=ARCHIVED,
-                                   permission='portal.templates.manage',
-                                   action=archive_action,
-                                   order=3,
-                                   menu_css_class='fa fa-fw fa-archive',
-                                   view_name='wf-archive.html',
-                                   html_help=_('''This content is currently published.
-                                                  If it is archived, it will not be possible to make it visible again
-                                                  except by creating a new version!'''))
-
-retired_to_archived = Transition('retired_to_archived',
-                                 title=_("Archive..."),
-                                 source=RETIRED,
-                                 destination=ARCHIVED,
-                                 permission='portal.templates.manage',
-                                 action=archive_action,
-                                 order=3,
-                                 menu_css_class='fa fa-fw fa-archive',
-                                 view_name='wf-archive.html',
-                                 html_help=_('''This content has been published but is currently retired.
-                                                If it is archived, it will not be possible to make it visible again
-                                                except by creating a new version!'''))
-
-archived_to_draft = Transition('archived_to_draft',
-                               title=_("Create new version..."),
-                               source=ARCHIVED,
-                               destination=DRAFT,
-                               permission='portal.templates.manage',
-                               action=clone_action,
-                               order=99,
-                               menu_css_class='fa fa-fw fa-copy',
-                               view_name='wf-clone.html')
-
-deleted = Transition('delete',
-                     title=_("Delete..."),
-                     source=DRAFT,
-                     destination=DELETED,
-                     condition=can_delete,
-                     action=delete_action,
-                     order=6,
-                     menu_css_class='fa fa-fw fa-trash',
-                     view_name='wf-delete.html',
-                     html_help=_('''This content has never been published.
-                                    It can be removed and definitely deleted.'''))
-
-wf_transitions = [init,
-                  draft_to_published,
-                  published_to_retired,
-                  published_to_draft,
-                  retired_to_published,
-                  published_to_archived,
-                  retired_to_archived,
-                  archived_to_draft,
-                  deleted]
-
-
-wf = Workflow(wf_transitions,
-              states=STATUS_VOCABULARY,
-              published_states=(PUBLISHED,))
-
-
-@utility_config(name='PyAMS portal template workflow', provides=IWorkflow)
-class WorkflowUtility(object):
-    """Workflow utility registration"""
-
-    def __new__(cls):
-        return wf
--- a/src/pyams_portal/zmi/container.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/zmi/container.py	Mon Jan 18 18:09:46 2016 +0100
@@ -16,13 +16,13 @@
 # import standard library
 
 # import interfaces
-from pyams_portal.interfaces import IPortalTemplateContainer, IPortalWfTemplate, IPortalTemplateContainerConfiguration
+from pyams_portal.interfaces import IPortalTemplateContainer, IPortalTemplate, IPortalTemplateContainerConfiguration, \
+    MANAGE_TEMPLATE_PERMISSION
 from pyams_portal.zmi.interfaces import IPortalTemplateContainerMenu
 from pyams_skin.interfaces import IInnerPage, IPageHeader
 from pyams_skin.interfaces.container import ITable, ITableElementEditor
 from pyams_skin.layer import IPyAMSLayer
 from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION, MANAGE_SYSTEM_PERMISSION
-from pyams_workflow.interfaces import IWorkflowVersions
 from pyams_zmi.interfaces.menu import IControlPanelMenu
 from pyams_zmi.layer import IAdminLayer
 from z3c.table.interfaces import IColumn, IValues
@@ -33,7 +33,7 @@
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_skin.container import ContainerView
 from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.table import DefaultElementEditorAdapter, BaseTable, TrashColumn
+from pyams_skin.table import DefaultElementEditorAdapter, BaseTable, NameColumn, TrashColumn
 from pyams_skin.viewlet.menu import MenuItem
 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
 from pyams_utils.registry import query_utility
@@ -41,7 +41,6 @@
 from pyams_utils.url import absolute_url
 from pyams_viewlet.manager import viewletmanager_config
 from pyams_viewlet.viewlet import viewlet_config
-from pyams_workflow.zmi.workflow import WorkflowContentNameColumn
 from pyams_zmi.form import AdminDialogEditForm
 from pyams_zmi.view import AdminView
 from pyramid.url import resource_url
@@ -103,7 +102,7 @@
         return attributes
 
 
-@adapter_config(context=(IPortalWfTemplate, IAdminLayer, PortalTemplateContainerTable), provides=ITableElementEditor)
+@adapter_config(context=(IPortalTemplate, IAdminLayer, PortalTemplateContainerTable), provides=ITableElementEditor)
 class PortalTemplateTableElementEditor(DefaultElementEditorAdapter):
     """Portal template table element editor"""
 
@@ -111,18 +110,14 @@
 
     @property
     def url(self):
-        wf_versions = IWorkflowVersions(self.context).get_last_versions(count=1)
-        if wf_versions:
-            return resource_url(wf_versions[0], self.request, 'admin.html#{0}'.format(self.view_name))
-        else:
-            return None
+        return resource_url(self.context, self.request, 'admin.html#{0}'.format(self.view_name))
 
 
 @adapter_config(name='name', context=(Interface, IAdminLayer, PortalTemplateContainerTable), provides=IColumn)
-class PortalTemplateContainerNameColumn(WorkflowContentNameColumn):
+class PortalTemplateContainerNameColumn(NameColumn):
     """Portal template container name column"""
 
-    name_field = 'name'
+    attrName = 'name'
 
 
 @adapter_config(name='trash', context=(Interface, IAdminLayer, PortalTemplateContainerTable), provides=IColumn)
@@ -130,7 +125,7 @@
     """Portal template container trash column"""
 
     icon_hint = _("Delete template")
-    permission = 'portal.templates.manage'
+    permission = MANAGE_TEMPLATE_PERMISSION
 
 
 @adapter_config(context=(ISite, IAdminLayer, PortalTemplateContainerTable), provides=IValues)
@@ -161,8 +156,6 @@
     """Portal template container header adapter"""
 
     icon_class = 'fa fa-fw fa-columns'
-    title = _("Portal")
-    subtitle = _("Portal templates")
 
 
 #
@@ -198,6 +191,9 @@
     ajax_handler = 'properties.json'
     edit_permission = MANAGE_SYSTEM_PERMISSION
 
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
 
 @view_config(name='properties.json', context=IPortalTemplateContainer, request_type=IPyAMSLayer,
              permission=MANAGE_SYSTEM_PERMISSION, renderer='json', xhr=True)
--- a/src/pyams_portal/zmi/interfaces.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/zmi/interfaces.py	Mon Jan 18 18:09:46 2016 +0100
@@ -26,5 +26,9 @@
     """Portal template container menu interface"""
 
 
+class IPortalContextTemplatePropertiesMenu(IMenuItem):
+    """Portal template properties menu interface"""
+
+
 class IPortletConfigurationEditor(IForm):
     """Portlet configuration editor interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/layout.py	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,519 @@
+#
+# 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 json
+
+# import interfaces
+from pyams_pagelet.interfaces import IPagelet, PageletCreatedEvent
+from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, ISlot, \
+    IPortletAddingInfo, IPortlet, ISlotConfiguration, IPortletPreviewer, IPortalTemplateContainer, \
+    IPortalTemplateContainerConfiguration, IPortalPortletsConfiguration, IPortalContext, IPortalPage, \
+    MANAGE_TEMPLATE_PERMISSION
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IMenuHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu, IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+from transaction.interfaces import ITransactionManager
+from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE
+
+# import packages
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.zmi.template import PortalTemplateHeaderAdapter
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import JsToolbarMenuItem, ToolbarMenuDivider, ToolbarMenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.decorator import reify
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, Invalid, Interface
+
+from pyams_portal import _
+
+
+@adapter_config(context=(IPortalTemplate, IContentManagementMenu), provides=IMenuHeader)
+class PortalTemplateMenuHeader(object):
+    """Portal template menu header"""
+
+    def __init__(self, context, menu):
+        self.context = context
+        self.menu = menu
+
+    @property
+    def header(self):
+        return _("Template management")
+
+
+@viewlet_config(name='template-properties.menu', context=IPortalTemplate, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='template-properties.menu', layer=IAdminLayer, provides=IPropertiesMenu)
+@implementer(IPropertiesMenu)
+class PortalTemplatePropertiesMenu(MenuItem):
+    """Portal template properties menu"""
+
+    label = _("Properties")
+    icon_class = 'fa-twitch'
+    url = '#properties.html'
+
+
+@pagelet_config(name='properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/layout.pt', layer=IAdminLayer)
+@implementer(IInnerPage)
+class PortalTemplateLayoutView(AdminView):
+    """Portal template main layout configuration view"""
+
+    @property
+    def title(self):
+        container = get_parent(self.context, IPortalTemplateContainer)
+        if container is None:
+            context = get_parent(self.context, IPortalContext)
+            page = IPortalPage(context)
+            if page.use_local_template:
+                return _("Local template configuration")
+            else:
+                translate = self.request.localizer.translate
+                return translate(_("Shared template configuration ({0})")).format(page.template.name)
+        else:
+            return _("Template configuration")
+
+    def get_template(self):
+        return self.context
+
+    def get_context(self):
+        return self.context
+
+    @property
+    def can_change(self):
+        return self.request.has_permission(MANAGE_TEMPLATE_PERMISSION)
+
+    @reify
+    def template_configuration(self):
+        return IPortalTemplateConfiguration(self.get_template())
+
+    @reify
+    def portlet_configuration(self):
+        return IPortalPortletsConfiguration(self.get_context())
+
+    @property
+    def selected_portlets(self):
+        container = query_utility(IPortalTemplateContainer)
+        configuration = IPortalTemplateContainerConfiguration(container)
+        return filter(lambda x: x is not None,
+                      [query_utility(IPortlet, name=portlet_name)
+                       for portlet_name in configuration.toolbar_portlets or ()])
+
+    def get_portlet(self, name):
+        return self.request.registry.getUtility(IPortlet, name=name)
+
+    def get_portlet_label(self, name):
+        return self.request.localizer.translate(self.get_portlet(name).label)
+
+    def get_portlet_preview(self, portlet_id):
+        settings = self.portlet_configuration.get_portlet_configuration(portlet_id).settings
+        previewer = self.request.registry.queryMultiAdapter((self.get_context(), self.request, self, settings),
+                                                            IPortletPreviewer)
+        if previewer is not None:
+            previewer.update()
+            return previewer.render()
+        else:
+            return ''
+
+
+@adapter_config(context=(IPortalTemplate, IAdminLayer, Interface), provides=IPageHeader)
+class PortalTemplateLayoutHeaderAdapter(PortalTemplateHeaderAdapter):
+    """Portal template configuration header adapter"""
+
+    back_url = '/admin.html#portal-templates.html'
+    back_target = None
+
+
+#
+# Rows views
+#
+
+@viewlet_config(name='add-template-row.menu', context=IPortalTemplate, layer=IAdminLayer,
+                view=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, weight=1)
+class PortalTemplateRowAddMenu(JsToolbarMenuItem):
+    """Portal template row add menu"""
+
+    label = _("Add row...")
+    label_css_class = 'fa fa-fw fa-indent'
+    url = 'PyAMS_portal.template.addRow'
+
+
+@view_config(name='add-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def add_template_row(request):
+    """Add template raw"""
+    config = IPortalTemplateConfiguration(request.context)
+    return {'row_id': config.add_row()}
+
+
+@view_config(name='set-template-row-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_template_row_order(request):
+    """Set template rows order"""
+    config = IPortalTemplateConfiguration(request.context)
+    row_ids = map(int, json.loads(request.params.get('rows')))
+    config.set_row_order(row_ids)
+    return {'status': 'success'}
+
+
+@view_config(name='delete-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def delete_template_row(request):
+    """Delete template row"""
+    config = IPortalTemplateConfiguration(request.context)
+    config.delete_row(int(request.params.get('row_id')))
+    return {'status': 'success'}
+
+
+#
+# Slots views
+#
+
+@viewlet_config(name='add-template-slot.menu', context=IPortalTemplate, layer=IAdminLayer,
+                view=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, weight=2)
+class PortalTemplateSlotAddMenu(ToolbarMenuItem):
+    """Portal template slot add menu"""
+
+    label = _("Add slot...")
+    label_css_class = 'fa fa-fw fa-columns'
+    url = 'add-template-slot.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-template-slot.html', context=IPortalTemplate, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+class PortalTemplateSlotAddForm(AdminDialogAddForm):
+    """Portal template slot add form"""
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        return translate(_("« {0} »  portal template")).format(self.context.name)
+
+    legend = _("Add slot")
+    icon_css_class = 'fa fa-fw fa-columns'
+
+    fields = field.Fields(ISlot)
+    ajax_handler = 'add-template-slot.json'
+    edit_permission = None
+
+    def updateWidgets(self, prefix=None):
+        super(PortalTemplateSlotAddForm, self).updateWidgets()
+        self.widgets['row_id'].value = self.request.params.get('form.widgets.row_id')
+        if self.widgets['row_id'].value:
+            self.widgets['row_id'].mode = HIDDEN_MODE
+
+    def createAndAdd(self, data):
+        config = IPortalTemplateConfiguration(self.context)
+        return config.add_slot(data.get('name'), data.get('row_id'))
+
+
+@subscriber(IDataExtractedEvent, form_selector=PortalTemplateSlotAddForm)
+def handle_new_slot_data_extraction(event):
+    """Handle new slot form data extraction"""
+    config = IPortalTemplateConfiguration(event.form.context)
+    name = event.data.get('name')
+    if name in config.slot_names:
+        event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
+
+
+@view_config(name='add-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class PortalTemplateSlotAJAXAddForm(AJAXAddForm, PortalTemplateSlotAddForm):
+    """Portal template slot add form, AJAX handler"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'callback',
+                'callback': 'PyAMS_portal.template.addSlotCallback',
+                'options': {'row_id': changes[0],
+                            'slot_name': changes[1]}}
+
+
+@view_config(name='set-template-slot-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_template_slot_order(request):
+    """Set template slots order"""
+    config = IPortalTemplateConfiguration(request.context)
+    order = json.loads(request.params.get('order'))
+    for key in order.copy().keys():
+        order[int(key)] = order.pop(key)
+    config.set_slot_order(order)
+    return {'status': 'success'}
+
+
+@view_config(name='get-slots-width.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def get_template_slots_width(request):
+    """Get template slots width"""
+    config = IPortalTemplateConfiguration(request.context)
+    return config.get_slots_width(request.params.get('device'))
+
+
+@view_config(name='set-slot-width.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_template_slot_width(request):
+    """Set template slot width"""
+    config = IPortalTemplateConfiguration(request.context)
+    config.set_slot_width(request.params.get('slot_name'),
+                          request.params.get('device'),
+                          int(request.params.get('width')))
+    return config.get_slots_width(request.params.get('device'))
+
+
+@pagelet_config(name='slot-properties.html', context=IPortalTemplate, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class PortalTemplateSlotPropertiesEditForm(AdminDialogEditForm):
+    """Slot properties edit form"""
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        return translate(_("« {0} »  portal template - {1} slot")).format(self.context.name,
+                                                                          self.getContent().slot_name)
+
+    legend = _("Edit slot properties")
+    fields = field.Fields(ISlotConfiguration).omit('portlet_ids')
+
+    label_css_class = 'control-label col-md-5'
+    input_css_class = 'col-md-7'
+
+    ajax_handler = 'slot-properties.json'
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    def __init__(self, context, request):
+        super(PortalTemplateSlotPropertiesEditForm, self).__init__(context, request)
+        self.config = IPortalTemplateConfiguration(context)
+
+    def getContent(self):
+        slot_name = self.request.params.get('form.widgets.slot_name')
+        return self.config.slot_config[slot_name]
+
+    def updateWidgets(self, prefix=None):
+        super(PortalTemplateSlotPropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['slot_name'].mode = HIDDEN_MODE
+
+
+@view_config(name='slot-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class PortalTemplateSlotPropertiesAJAXEditForm(AJAXEditForm, PortalTemplateSlotPropertiesEditForm):
+    """Slot properties edit form, AJAX renderer"""
+
+    def get_ajax_output(self, changes):
+        if changes:
+            slot_name = self.widgets['slot_name'].value
+            slot_config = self.config.slot_config[slot_name]
+            return {'status': 'success',
+                    'callback': 'PyAMS_portal.template.editSlotCallback',
+                    'options': {'slot_name': slot_name,
+                                'width': slot_config.get_width()}}
+        else:
+            return super(PortalTemplateSlotPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+@view_config(name='delete-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def delete_template_slot(request):
+    """Delete template slot"""
+    config = IPortalTemplateConfiguration(request.context)
+    config.delete_slot(request.params.get('slot_name'))
+    return {'status': 'success'}
+
+
+#
+# Portlet views
+#
+
+@viewlet_config(name='add-template-portlet.divider', context=IPortalTemplate, layer=IAdminLayer,
+                view=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, weight=10)
+class PortalTemplateAddMenuDivider(ToolbarMenuDivider):
+    """Portal template menu divider"""
+
+
+@viewlet_config(name='add-template-portlet.menu', context=IPortalTemplate, layer=IAdminLayer,
+                view=PortalTemplateLayoutView, manager=IToolbarAddingMenu,
+                permission=MANAGE_TEMPLATE_PERMISSION, weight=20)
+class PortalTemplatePortletAddMenu(ToolbarMenuItem):
+    """Portal template portlet add menu"""
+
+    label = _("Add portlet...")
+    label_css_class = 'fa fa-fw fa-columns'
+    url = 'add-template-portlet.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-template-portlet.html', context=IPortalTemplate, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+class PortalTemplatePortletAddForm(AdminDialogAddForm):
+    """Portal template portlet add form"""
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        return translate(_("« {0} »  portal template")).format(self.context.name)
+
+    legend = _("Add portlet")
+    icon_css_class = 'fa fa-fw fa-columns'
+
+    fields = field.Fields(IPortletAddingInfo)
+    ajax_handler = 'add-template-portlet.json'
+    edit_permission = None
+
+    def createAndAdd(self, data):
+        config = IPortalTemplateConfiguration(self.context)
+        return config.add_portlet(data.get('portlet_name'), data.get('slot_name'))
+
+
+@view_config(name='add-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class PortalTemplatePortletAJAXAddForm(AJAXAddForm, PortalTemplatePortletAddForm):
+    """Portal template portlet add form, AJAX handler"""
+
+    def get_ajax_output(self, changes):
+        configuration = IPortalPortletsConfiguration(self.context)
+        settings = configuration.get_portlet_configuration(changes['portlet_id']).settings
+        previewer = self.request.registry.queryMultiAdapter((self.context, self.request, self, settings),
+                                                            IPortletPreviewer)
+        if previewer is not None:
+            previewer.update()
+            changes['preview'] = previewer.render()
+        return {'status': 'callback',
+                'callback': 'PyAMS_portal.template.addPortletCallback',
+                'options': changes}
+
+
+@view_config(name='drag-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def drag_template_portlet(request):
+    """Drag portlet icon to slot"""
+    tmpl_config = IPortalTemplateConfiguration(request.context)
+    portlets_config = IPortalPortletsConfiguration(request.context)
+    portlet_name = request.params.get('portlet_name')
+    slot_name = request.params.get('slot_name')
+    changes = tmpl_config.add_portlet(portlet_name, slot_name)
+    settings = portlets_config.get_portlet_configuration(changes['portlet_id']).settings
+    previewer = request.registry.queryMultiAdapter((request.context, request, request, settings),
+                                                   IPortletPreviewer)
+    if previewer is not None:
+        previewer.update()
+        changes['preview'] = previewer.render()
+    return {'status': 'callback',
+            'close_form': False,
+            'callback': 'PyAMS_portal.template.addPortletCallback',
+            'options': changes}
+
+
+@view_config(name='set-template-portlet-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def set_template_portlet_order(request):
+    """Set template portlet order"""
+    order = json.loads(request.params.get('order'))
+    order['from'] = int(order['from'])
+    order['to']['portlet_ids'] = list(map(int, order['to']['portlet_ids']))
+    IPortalTemplateConfiguration(request.context).set_portlet_order(order)
+    return {'status': 'success'}
+
+
+@view_config(name='portlet-properties.html', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION)
+class PortalTemplatePortletEditForm(AdminDialogEditForm):
+    """Portal template portlet edit form"""
+
+    dialog_class = 'modal-large'
+
+    def __call__(self):
+        request = self.request
+        request.registry.notify(PageletCreatedEvent(self))
+        portlet_id = int(request.params.get('form.widgets.portlet_id'))
+        portlet_config = IPortalPortletsConfiguration(self.context).get_portlet_configuration(portlet_id)
+        if portlet_config is None:
+            raise NotFound()
+        editor = self.request.registry.queryMultiAdapter((portlet_config.editor_settings, request),
+                                                         IPagelet, name='properties.html')
+        if editor is None:
+            raise NotFound()
+        request.registry.notify(PageletCreatedEvent(editor))
+        editor.ajax_handler = 'portlet-properties.json'
+        editor.update()
+        return editor()
+
+
+@view_config(name='portlet-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class PortalTemplatePortletAJAXEditForm(AJAXEditForm, PortalTemplatePortletEditForm):
+    """Portal template portlet edit form, AJAX renderer"""
+
+    def __call__(self):
+        request = self.request
+        request.registry.notify(PageletCreatedEvent(self))
+        # load portlet config
+        portlet_id = int(request.params.get('form.widgets.portlet_id'))
+        portlet_config = IPortalPortletsConfiguration(self.context).get_portlet_configuration(portlet_id)
+        if portlet_config is None:
+            raise NotFound()
+        # check inheritance
+        old_override = portlet_config.inherit_parent
+        new_override = request.params.get('form.widgets.override_parent')
+        if new_override:
+            portlet_config.inherit_parent = False
+        else:
+            portlet_config.inherit_parent = True
+        changed_override = portlet_config.inherit_parent != old_override
+        # update settings
+        editor = request.registry.queryMultiAdapter((portlet_config.editor_settings, request),
+                                                    IPagelet, name='properties.json')
+        if editor is None:
+            raise NotFound()
+        changes = editor()
+        translate = self.request.localizer.translate
+        if changed_override or changes:
+            # we commit before loading previewer to avoid BLOBs "uncommited changes" error
+            ITransactionManager(self.context).commit()
+            previewer = request.registry.queryMultiAdapter((self.context, request, self, portlet_config.settings),
+                                                           IPortletPreviewer)
+            if previewer is not None:
+                previewer.update()
+                changes.update({'status': 'success',
+                                'message': translate(self.successMessage),
+                                'callback': 'PyAMS_portal.template.editPortletCallback',
+                                'options': {'portlet_id': portlet_id,
+                                            'preview': previewer.render()}})
+        return changes
+
+
+@view_config(name='delete-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def delete_template_portlet(request):
+    """Delete template portlet"""
+    config = IPortalTemplateConfiguration(request.context)
+    config.delete_portlet(int(request.params.get('portlet_id')))
+    return {'status': 'success'}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/page.py	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,178 @@
+#
+# 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.
+#
+from pyams_zmi.site import PropertiesEditFormHeaderAdapter
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_form.interfaces.form import IWidgetForm
+from pyams_portal.interfaces import IPortalContext, IPortalPage, IPortalTemplateConfiguration
+from pyams_portal.zmi.interfaces import IPortalContextTemplatePropertiesMenu
+from pyams_skin.interfaces import IPageHeader, IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import MANAGE_PERMISSION, VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import ISiteManagementMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyramid.view import view_config
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.zmi.layout import PortalTemplateLayoutView, PortalTemplatePortletEditForm, \
+    PortalTemplatePortletAJAXEditForm
+from pyams_portal.zmi.template import PortalTemplateHeaderAdapter
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config
+from pyams_utils.url import absolute_url
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminEditForm
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_portal import _
+
+
+#
+# Portal context template configuration
+#
+
+@viewlet_config(name='template-properties.menu', context=IPortalContext, layer=IAdminLayer,
+                manager=ISiteManagementMenu, permission=MANAGE_PERMISSION, weight=10)
+@viewletmanager_config(name='template-properties.menu', layer=IAdminLayer, context=IPortalContext,
+                       provides=IPortalContextTemplatePropertiesMenu)
+@implementer(IPortalContextTemplatePropertiesMenu)
+class PortalContextTemplatePropertiesMenu(MenuItem):
+    """Portal context template properties menu"""
+
+    label = _("Presentation")
+    icon_class = 'fa-columns'
+    url = '#template-properties.html'
+
+
+@pagelet_config(name='template-properties.html', context=IPortalContext, layer=IPyAMSLayer,
+                permission=MANAGE_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class PortalContextTemplatePropertiesEditForm(AdminEditForm):
+    """Portal context template properties edit form"""
+
+    @property
+    def title(self):
+        return self.context.title
+
+    legend = _("Edit template configuration")
+
+    @property
+    def fields(self):
+        fields = field.Fields(IPortalPage).select('inherit_parent', 'use_local_template', 'shared_template')
+        if not self.getContent().can_inherit:
+            fields = fields.omit('inherit_parent')
+        return fields
+
+    ajax_handler = 'template-properties.json'
+    edit_permission = MANAGE_PERMISSION
+
+    def getContent(self):
+        return IPortalPage(self.context)
+
+
+@view_config(name='template-properties.json', context=IPortalContext, request_type=IPyAMSLayer,
+             permission=MANAGE_PERMISSION, renderer='json', xhr=True)
+class PortalContextTemplatePropertiesAJAXEditForm(AJAXEditForm, PortalContextTemplatePropertiesEditForm):
+    """Portal context template properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'use_local_template' in changes.get(IPortalPage, ()):
+            return {'status': 'redirect'}
+        else:
+            return super(PortalContextTemplatePropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+@adapter_config(context=(Interface, IPyAMSLayer, PortalContextTemplatePropertiesEditForm), provides=IPageHeader)
+class PortalContextPropertiesEditFormHeaderAdapter(PropertiesEditFormHeaderAdapter):
+    """Portal context template properties edit form header adapter"""
+
+    icon_class = 'fa fa-fw fa-columns'
+
+
+#
+#
+#
+
+@viewlet_config(name='template-config.menu', context=IPortalContext, layer=IAdminLayer,
+                manager=IPortalContextTemplatePropertiesMenu, permission=MANAGE_PERMISSION, weight=50)
+class PortalContextTemplateConfigMenu(MenuItem):
+    """Portal context template configuration menu"""
+
+    label = _("Template properties")
+
+    url = '#template-config.html'
+
+    def __new__(cls, context, request, view, manager=None):
+        page = IPortalPage(context)
+        if page.template is None:
+            return None
+        return MenuItem.__new__(cls)
+
+    def get_url(self):
+        page = IPortalPage(self.context)
+        if page.use_local_template:
+            return absolute_url(page.template, self.request, 'admin.html#properties.html')
+        else:
+            return super(PortalContextTemplateConfigMenu, self).get_url()
+
+
+@pagelet_config(name='template-config.html', context=IPortalContext, layer=IPyAMSLayer,
+                permission=MANAGE_PERMISSION)
+class PortalContextTemplateLayoutView(PortalTemplateLayoutView):
+    """Portal context template configuration view"""
+
+    def get_template(self):
+        return IPortalPage(self.context).template
+
+    @property
+    def can_change(self):
+        if not IPortalPage(self.context).use_local_template:
+            return False
+        return self.request.has_permission(MANAGE_PERMISSION)
+
+
+@adapter_config(context=(IPortalContext, IAdminLayer, PortalContextTemplateLayoutView), provides=IPageHeader)
+class PortalContextTemplateLayoutHeaderAdapter(PortalTemplateHeaderAdapter):
+    """Portal context template configuration header adapter"""
+
+
+#
+# Template management views
+#
+
+@view_config(name='get-slots-width.json', context=IPortalContext, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
+def get_template_slots_width(request):
+    """Get template slots width"""
+    config = IPortalTemplateConfiguration(request.context)
+    return config.get_slots_width(request.params.get('device'))
+
+
+@view_config(name='portlet-properties.html', context=IPortalContext, request_type=IPyAMSLayer,
+             permission=MANAGE_PERMISSION)
+class PortalContextTemplatePortletEditForm(PortalTemplatePortletEditForm):
+    """Portal context template portlet edit form"""
+
+
+@view_config(name='portlet-properties.json', context=IPortalContext, request_type=IPyAMSLayer,
+             permission=MANAGE_PERMISSION, renderer='json', xhr=True)
+class PortalContextTemplatePortletAJAXEditForm(PortalTemplatePortletAJAXEditForm):
+    """Portal context template portlet edit form, JSON renderer"""
--- a/src/pyams_portal/zmi/portlet.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/zmi/portlet.py	Mon Jan 18 18:09:46 2016 +0100
@@ -16,48 +16,76 @@
 # import standard library
 
 # import interfaces
-from pyams_portal.interfaces import IPortlet
-from z3c.form.interfaces import HIDDEN_MODE
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_portal.interfaces import IPortlet, IPortalTemplate, IPortalPage, IPortalContext, MANAGE_TEMPLATE_PERMISSION
+from pyams_skin.layer import IPyAMSLayer
 
 # import packages
-from pyams_zmi.form import AdminDialogEditForm
-from pyramid.url import resource_url
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.url import absolute_url
+from pyams_zmi.form import AdminDialogEditForm, InnerAdminEditForm
+from pyramid.decorator import reify
 from z3c.form import field
+from zope.interface import Interface
 
 from pyams_portal import _
 
 
-class PortletConfigurationEditor(AdminDialogEditForm):
-    """Base portlet configuration editor"""
+@template_config(template='templates/portlet.pt', layer=IPyAMSLayer)
+class PortletSettingsEditor(AdminDialogEditForm):
+    """Portlet settings edit form"""
 
     @property
     def title(self):
         translate = self.request.localizer.translate
-        registry = self.request.registry
-        portlet = registry.queryUtility(IPortlet, name=self.context.portlet_name)
-        return translate(_("« {0} »  portal template - {1}")).format(self.context.__parent__.name,
-                                                                     translate(portlet.label))
+        parent = self.configuration.__parent__
+        if not IPortalTemplate.providedBy(parent):
+            parent = IPortalPage(parent).template
+        return translate(_("« {0} »  portal template - {1}")).format(parent.name,
+                                                                     translate(self.portlet.label))
 
-    legend = _("Edit portlet configuration")
+    legend = _("Edit portlet settings")
     dialog_class = 'modal-large'
 
-    interface = None
-    edit_permission = 'portal.templates.manage'
+    settings = None
+    fields = field.Fields(Interface)
+    edit_permission = MANAGE_TEMPLATE_PERMISSION
+
+    @reify
+    def configuration(self):
+        return self.context.configuration
+
+    @property
+    def override_label(self):
+        translate = self.request.localizer.translate
+        if IPortalContext.providedBy(self.configuration.__parent__.__parent__):
+            return translate(_("Override parent settings"))
+        else:
+            return translate(_("Override template settings"))
 
     def get_form_action(self):
-        return resource_url(self.context.__parent__, self.request, self.request.view_name)
+        return absolute_url(self.configuration.__parent__, self.request, self.request.view_name)
 
     def get_ajax_handler(self):
-        return resource_url(self.context.__parent__, self.request, self.ajax_handler)
+        return absolute_url(self.configuration.__parent__, self.request, self.ajax_handler)
+
+    @reify
+    def portlet(self):
+        registry = self.request.registry
+        return registry.queryUtility(IPortlet, name=self.configuration.portlet_name)
+
+
+@adapter_config(name='properties', context=(Interface, IPyAMSLayer, PortletSettingsEditor), provides=IInnerTabForm)
+class PortletSettingsPropertiesEditor(InnerAdminEditForm):
+    """Portlet settings properties editor"""
+
+    id = 'properties_form'
+    tab_label = _("Main properties")
+    legend = None
 
     @property
     def fields(self):
-        fields = field.Fields(self.interface)
-        if not self.getContent().can_inherit:
-            fields = fields.omit('inherit_parent')
-        return fields
+        return field.Fields(self.parent_form.settings).omit('__name__')
 
-    def updateWidgets(self, prefix=None):
-        super(PortletConfigurationEditor, self).updateWidgets(prefix)
-        self.widgets['slot_name'].mode = HIDDEN_MODE
-        self.widgets['position'].mode = HIDDEN_MODE
+    weight = 10
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/portlets/content.py	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,52 @@
+#
+# 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_pagelet.interfaces import IPagelet
+from pyams_portal.interfaces import IPortletPreviewer
+from pyams_portal.portlets.content.interfaces import IContentPortletSettings
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.portlet import PortletPreviewer
+from pyams_portal.zmi.portlet import PortletSettingsEditor
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from zope.interface import Interface
+
+
+@pagelet_config(name='properties.html', context=IContentPortletSettings, request_type=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class ContentPortletSettingsEditor(PortletSettingsEditor):
+    """Content portlet settings editor"""
+
+    settings = IContentPortletSettings
+
+
+@adapter_config(name='properties.json', context=(IContentPortletSettings, IPyAMSLayer), provides=IPagelet)
+class ContentPortletConfigurationAJAXEditor(AJAXEditForm, ContentPortletSettingsEditor):
+    """Content portlet settings editor, AJAX renderer"""
+
+
+@adapter_config(context=(Interface, IPyAMSLayer, Interface, IContentPortletSettings),
+                provides=IPortletPreviewer)
+@template_config(template='templates/context-preview.pt', layer=IPyAMSLayer)
+class ContentPortletPreviewer(PortletPreviewer):
+    """Content portlet previewer"""
--- a/src/pyams_portal/zmi/portlets/context.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-#
-# 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_pagelet.interfaces import IPagelet
-from pyams_portal.interfaces import IPortletPreviewer
-from pyams_portal.portlets.context.interfaces import IContextPortletConfiguration
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-
-# import packages
-from pyams_form.form import AJAXEditForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_portal.portlet import PortletPreviewer
-from pyams_portal.zmi.portlet import PortletConfigurationEditor
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config
-from zope.interface import Interface
-
-
-@pagelet_config(name='properties.html', context=IContextPortletConfiguration, request_type=IPyAMSLayer,
-                permission=VIEW_SYSTEM_PERMISSION)
-class ContextPortletConfigurationEditor(PortletConfigurationEditor):
-    """Context portlet configuration editor"""
-
-    interface = IContextPortletConfiguration
-
-
-@adapter_config(name='properties.json', context=(IContextPortletConfiguration, IPyAMSLayer), provides=IPagelet)
-class ContextPortletConfigurationAJAXEditor(AJAXEditForm, ContextPortletConfigurationEditor):
-    """Context portlet configuration editor, AJAX renderer"""
-
-
-@adapter_config(context=(Interface, IPyAMSLayer, Interface, IContextPortletConfiguration),
-                provides=IPortletPreviewer)
-@template_config(template='templates/context-preview.pt', layer=IPyAMSLayer)
-class ContextPortletPreviewer(PortletPreviewer):
-    """Context portlet previewer"""
--- a/src/pyams_portal/zmi/portlets/image.py	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/zmi/portlets/image.py	Mon Jan 18 18:09:46 2016 +0100
@@ -18,7 +18,7 @@
 # import interfaces
 from pyams_pagelet.interfaces import IPagelet
 from pyams_portal.interfaces import IPortletPreviewer
-from pyams_portal.portlets.image.interfaces import IImagePortletConfiguration
+from pyams_portal.portlets.image.interfaces import IImagePortletSettings
 from pyams_skin.layer import IPyAMSLayer
 from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
 
@@ -26,26 +26,26 @@
 from pyams_form.form import AJAXEditForm
 from pyams_pagelet.pagelet import pagelet_config
 from pyams_portal.portlet import PortletPreviewer
-from pyams_portal.zmi.portlet import PortletConfigurationEditor
+from pyams_portal.zmi.portlet import PortletSettingsEditor
 from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config
 from zope.interface import Interface
 
 
-@pagelet_config(name='properties.html', context=IImagePortletConfiguration, request_type=IPyAMSLayer,
+@pagelet_config(name='properties.html', context=IImagePortletSettings, request_type=IPyAMSLayer,
                 permission=VIEW_SYSTEM_PERMISSION)
-class ImagePortletConfigurationEditor(PortletConfigurationEditor):
-    """Image portlet configuration editor"""
+class ImagePortletSettingsEditor(PortletSettingsEditor):
+    """Image portlet settings editor"""
 
-    interface = IImagePortletConfiguration
+    settings = IImagePortletSettings
 
 
-@adapter_config(name='properties.json', context=(IImagePortletConfiguration, IPyAMSLayer), provides=IPagelet)
-class ImagePortletConfigurationAJAXEditor(AJAXEditForm, ImagePortletConfigurationEditor):
-    """Image portlet configuration editor, AJAX renderer"""
+@adapter_config(name='properties.json', context=(IImagePortletSettings, IPyAMSLayer), provides=IPagelet)
+class ImagePortletConfigurationAJAXEditor(AJAXEditForm, ImagePortletSettingsEditor):
+    """Image portlet settings editor, AJAX renderer"""
 
 
-@adapter_config(context=(Interface, IPyAMSLayer, Interface, IImagePortletConfiguration),
+@adapter_config(context=(Interface, IPyAMSLayer, Interface, IImagePortletSettings),
                 provides=IPortletPreviewer)
 @template_config(template='templates/image-preview.pt', layer=IPyAMSLayer)
 class ImagePortletPreviewer(PortletPreviewer):
--- a/src/pyams_portal/zmi/portlets/templates/image-preview.pt	Thu Oct 08 12:26:42 2015 +0200
+++ b/src/pyams_portal/zmi/portlets/templates/image-preview.pt	Mon Jan 18 18:09:46 2016 +0100
@@ -1,18 +1,18 @@
-<tal:var define="config view.configuration">
-	<tal:if condition="config.visible">
-		<tal:if condition="config.image">
+<tal:var define="settings view.settings">
+	<tal:if condition="settings.visible">
+		<tal:if condition="settings.image">
 			<a class="fancybox margin-left-5" data-toggle
 			   data-ams-fancybox-type="image"
-			   tal:define="image config.image;
+			   tal:define="image settings.image;
 						   thumbnails extension:thumbnails(image);
 						   target python:thumbnails.get_thumbnail('800x600', 'jpeg');"
-			   tal:attributes="href extension:absolute_url(target);">
+			   tal:attributes="href extension:absolute_url(target)">
 				<img class="thumbnail padding-5 margin-5"
-					 tal:define="thumbnail python:thumbnails.get_thumbnail('128x128', 'jpeg');"
+					 tal:define="thumbnail python:thumbnails.get_thumbnail('128x128', 'jpeg')"
 					 tal:attributes="src extension:absolute_url(thumbnail)" src="" alt="" />
 			</a>
 		</tal:if>
-		<tal:if condition="not:config.image">
+		<tal:if condition="not settings.image">
 			<div class="text-center padding-y-5">
 				<span class="fa-stack fa-lg">
 					<i class="fa fa-picture-o fa-stack-1x"></i>
@@ -21,7 +21,7 @@
 			</div>
 		</tal:if>
 	</tal:if>
-	<tal:if condition="not:config.visible">
+	<tal:if condition="not settings.visible">
 		<div class="text-center padding-y-5">
 			<span class="fa-stack fa-lg">
 				<i class="fa fa-eye fa-stack-1x"></i>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/template.py	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,132 @@
+#
+# 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.
+#
+from pyams_utils.unicode import translate_string
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_portal.interfaces import IPortalTemplateContainer, IPortalTemplate, IPortalContext, \
+    MANAGE_TEMPLATE_PERMISSION
+from pyams_skin.interfaces import IPageHeader, IContentTitle
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import IDataExtractedEvent
+from zope.component.interfaces import ISite
+
+# import packages
+from pyams_form.form import AJAXAddForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.template import PortalTemplate
+from pyams_portal.zmi.container import PortalTemplateContainerTable
+from pyams_skin.container import delete_container_element
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem, ToolbarAction
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm
+from pyramid.events import subscriber
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import Interface, Invalid
+
+from pyams_portal import _
+
+
+@adapter_config(context=(IPortalTemplate, IPyAMSLayer, Interface), provides=IPageHeader)
+class PortalTemplateHeaderAdapter(DefaultPageHeaderAdapter):
+    """Portal template header adapter"""
+
+    icon_class = 'fa fa-fw fa-columns'
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        # check for templates container
+        container = get_parent(self.context, IPortalTemplateContainer)
+        if container is not None:
+            return translate(_("« {0} »  portal template")).format(self.context.name)
+        # check for portal context
+        context = get_parent(self.context, IPortalContext)
+        if context is not None:
+            adapter = self.request.registry.queryMultiAdapter((context, self.request, self.view), IContentTitle)
+            if adapter is None:
+                adapter = IContentTitle(context, None)
+            if adapter is not None:
+                return adapter.title
+
+
+#
+# Template views
+#
+
+@viewlet_config(name='add-portal-template.action', context=ISite, layer=IAdminLayer,
+                view=PortalTemplateContainerTable, manager=IWidgetTitleViewletManager,
+                permission=MANAGE_TEMPLATE_PERMISSION, weight=1)
+class PortalTemplateAddAction(ToolbarAction):
+    """Portal template add action"""
+
+    label = _("Add template")
+    url = 'add-portal-template.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-portal-template.html', context=ISite, layer=IPyAMSLayer,
+                permission=MANAGE_TEMPLATE_PERMISSION)
+class PortalTemplateAddForm(AdminDialogAddForm):
+    """Portal template add form"""
+
+    title = _("Portal templates")
+    legend = _("Add shared template")
+    icon_css_class = 'fa fa-fw fa-columns'
+
+    fields = field.Fields(IPortalTemplate)
+    ajax_handler = 'add-portal-template.json'
+    edit_permission = None
+
+    def create(self, data):
+        return PortalTemplate()
+
+    def add(self, template):
+        context = query_utility(IPortalTemplateContainer)
+        context[translate_string(template.name, spaces='-')] = template
+
+    def nextURL(self):
+        return absolute_url(self.context, self.request, 'portal-templates.html')
+
+
+@subscriber(IDataExtractedEvent, form_selector=PortalTemplateAddForm)
+def handle_new_template_data_extraction(event):
+    """Handle new template form data extraction"""
+    container = query_utility(IPortalTemplateContainer)
+    name = event.data.get('name')
+    if name in container:
+        event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
+
+
+@view_config(name='add-portal-template.json', context=ISite, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+class PortalTemplateAJAXAddForm(AJAXAddForm, PortalTemplateAddForm):
+    """Portal template add form, AJAX handler"""
+
+
+@view_config(name='delete-element.json', context=IPortalTemplateContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_TEMPLATE_PERMISSION, renderer='json', xhr=True)
+def delete_portal_template(request):
+    """Delete template from portal"""
+    return delete_container_element(request)
--- a/src/pyams_portal/zmi/template/__init__.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-#
-# 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.
-#
-from pyams_skin.interfaces import IContentTitle
-from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
-
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
-
-# import interfaces
-from pyams_portal.interfaces import IPortalTemplateContainer, IPortalTemplate
-from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
-from pyams_skin.layer import IPyAMSLayer
-from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowInfo
-from pyams_zmi.layer import IAdminLayer
-from z3c.form.interfaces import IDataExtractedEvent
-from zope.component.interfaces import ISite
-
-# import packages
-from pyams_form.form import AJAXAddForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_portal.template import PortalTemplate, PortalWfTemplate
-from pyams_portal.zmi.container import PortalTemplateContainerTable
-from pyams_skin.container import delete_container_element
-from pyams_skin.viewlet.toolbar import ToolbarMenuItem
-from pyams_utils.registry import query_utility
-from pyams_utils.url import absolute_url
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm
-from pyramid.events import subscriber
-from pyramid.view import view_config
-from z3c.form import field
-from zope.interface import Interface, Invalid
-from zope.lifecycleevent import ObjectCreatedEvent
-
-from pyams_portal import _
-
-
-@adapter_config(context=(IPortalTemplate, IPyAMSLayer, Interface), provides=IContentTitle)
-class PortalTemplateTitleAdapter(ContextRequestViewAdapter):
-    """Portal template title adapter"""
-
-    @property
-    def title(self):
-        translate = self.request.localizer.translate
-        return translate(_("« {0} »  portal template")).format(self.context.name)
-
-
-#
-# Template views
-#
-
-@viewlet_config(name='add-portal-template.menu', context=ISite, layer=IAdminLayer,
-                view=PortalTemplateContainerTable, manager=IToolbarAddingMenu,
-                permission='portal.templates.manage', weight=20)
-class PortalTemplateAddMenu(ToolbarMenuItem):
-    """Portal template add menu"""
-
-    label = _("Add shared template...")
-    label_css_class = 'fa fa-fw fa-columns'
-    url = 'add-portal-template.html'
-    modal_target = True
-
-
-@pagelet_config(name='add-portal-template.html', context=ISite, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplateAddForm(AdminDialogAddForm):
-    """Portal template add form"""
-
-    title = _("Portal templates")
-    legend = _("Add shared template")
-    icon_css_class = 'fa fa-fw fa-columns'
-
-    fields = field.Fields(IPortalTemplate)
-    ajax_handler = 'add-portal-template.json'
-    edit_permission = None
-
-    def create(self, data):
-        return PortalTemplate()
-
-    def add(self, template):
-        wf_template = PortalWfTemplate()
-        self.request.registry.notify(ObjectCreatedEvent(wf_template))
-        context = query_utility(IPortalTemplateContainer)
-        context[template.name] = wf_template
-        IWorkflowVersions(wf_template).add_version(template, None)
-        IWorkflowInfo(template).fire_transition('init')
-
-    def nextURL(self):
-        return absolute_url(self.context, self.request, 'portal-templates.html')
-
-
-@subscriber(IDataExtractedEvent, form_selector=PortalTemplateAddForm)
-def handle_new_template_data_extraction(event):
-    """Handle new template form data extraction"""
-    container = query_utility(IPortalTemplateContainer)
-    name = event.data.get('name')
-    if name in container:
-        event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
-
-
-@view_config(name='add-portal-template.json', context=ISite, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateAJAXAddForm(AJAXAddForm, PortalTemplateAddForm):
-    """Portal template add form, AJAX handler"""
-
-
-@view_config(name='delete-element.json', context=IPortalTemplateContainer, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def delete_portal_template(request):
-    """Delete template from portal"""
-    return delete_container_element(request)
--- a/src/pyams_portal/zmi/template/config.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,485 +0,0 @@
-#
-# 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 json
-
-# import interfaces
-from pyams_pagelet.interfaces import IPagelet, PageletCreatedEvent
-from pyams_portal.interfaces import IPortalTemplate, IPortalTemplateConfiguration, ISlot, \
-    IPortletAddingInfo, IPortlet, ISlotConfiguration, IPortletPreviewer, IPortalTemplateContainer, \
-    IPortalTemplateContainerConfiguration
-from pyams_skin.interfaces import IInnerPage, IPageHeader
-from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
-from pyams_workflow.interfaces import IWorkflowState
-from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-from transaction.interfaces import ITransactionManager
-from z3c.form.interfaces import IDataExtractedEvent, HIDDEN_MODE
-
-# import packages
-from pyams_form.form import AJAXAddForm, AJAXEditForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_portal.workflow import PUBLISHED, ARCHIVED
-from pyams_skin.page import DefaultPageHeaderAdapter
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_skin.viewlet.toolbar import JsToolbarMenuItem, ToolbarMenuDivider, ToolbarMenuItem
-from pyams_template.template import template_config
-from pyams_utils.adapter import adapter_config
-from pyams_utils.registry import query_utility
-from pyams_viewlet.manager import viewletmanager_config
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
-from pyams_zmi.view import AdminView
-from pyramid.decorator import reify
-from pyramid.events import subscriber
-from pyramid.exceptions import NotFound
-from pyramid.view import view_config
-from z3c.form import field
-from zope.interface import implementer, Invalid
-
-from pyams_portal import _
-
-
-@viewlet_config(name='template-properties.menu', context=IPortalTemplate, layer=IAdminLayer,
-                manager=ISiteManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=1)
-@viewletmanager_config(name='template-properties.menu', layer=IAdminLayer, provides=IPropertiesMenu)
-@implementer(IPropertiesMenu)
-class PortalTemplatePropertiesMenu(MenuItem):
-    """Portal template properties menu"""
-
-    label = _("Properties")
-    icon_class = 'fa-twitch'
-    url = '#properties.html'
-
-
-@pagelet_config(name='properties.html', context=IPortalTemplate, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
-@template_config(template='templates/config.pt', layer=IAdminLayer)
-@implementer(IInnerPage)
-class PortalTemplateConfigView(AdminView):
-    """Portal template configuration view"""
-
-    title = _("Shared portal template configuration")
-
-    def get_context(self):
-        return self.context
-
-    @property
-    def can_change(self):
-        return self.request.has_permission('portal.templates.manage') and \
-               IWorkflowState(self.get_context()).state not in (PUBLISHED, ARCHIVED)
-
-    @reify
-    def configuration(self):
-        return IPortalTemplateConfiguration(self.get_context())
-
-    @property
-    def selected_portlets(self):
-        container = query_utility(IPortalTemplateContainer)
-        configuration = IPortalTemplateContainerConfiguration(container)
-        return [query_utility(IPortlet, name=portlet_name) for portlet_name in configuration.selected_portlets or ()]
-
-    def get_portlet(self, name):
-        return self.request.registry.getUtility(IPortlet, name=name)
-
-    def get_portlet_label(self, name):
-        return self.request.localizer.translate(self.get_portlet(name).label)
-
-    def get_portlet_preview(self, slot_name, position):
-        portlet_config = self.configuration.get_portlet_configuration(slot_name, position)
-        previewer = self.request.registry.queryMultiAdapter((self.get_context(), self.request, self, portlet_config),
-                                                            IPortletPreviewer)
-        if previewer is not None:
-            previewer.update()
-            return previewer.render()
-        else:
-            return ''
-
-
-@adapter_config(context=(IPortalTemplate, IAdminLayer, PortalTemplateConfigView), provides=IPageHeader)
-class PortalTemplateConfigHeaderAdapter(DefaultPageHeaderAdapter):
-    """Portal template configuration header adapter"""
-
-    back_url = '/admin.html#portal-templates.html'
-    back_target = None
-
-    icon_class = 'fa fa-fw fa-columns'
-    subtitle = _("Portlets configuration")
-
-
-#
-# Rows views
-#
-
-@viewlet_config(name='add-template-row.menu', context=IPortalTemplate, layer=IAdminLayer,
-                view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
-                permission='portal.templates.manage', weight=1)
-class PortalTemplateRowAddMenu(JsToolbarMenuItem):
-    """Portal template row add menu"""
-
-    label = _("Add row...")
-    label_css_class = 'fa fa-fw fa-indent'
-    url = 'PyAMS_portal.template.addRow'
-
-
-@view_config(name='add-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def add_template_row(request):
-    """Add template raw"""
-    config = IPortalTemplateConfiguration(request.context)
-    return {'row_id': config.add_row()}
-
-
-@view_config(name='set-template-row-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def set_template_row_order(request):
-    """Set template rows order"""
-    config = IPortalTemplateConfiguration(request.context)
-    row_ids = map(int, json.loads(request.params.get('rows')))
-    config.set_row_order(row_ids)
-    return {'status': 'success'}
-
-
-@view_config(name='delete-template-row.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def delete_template_row(request):
-    """Delete template row"""
-    config = IPortalTemplateConfiguration(request.context)
-    config.delete_row(int(request.params.get('row_id')))
-    return {'status': 'success'}
-
-
-#
-# Slots views
-#
-
-@viewlet_config(name='add-template-slot.menu', context=IPortalTemplate, layer=IAdminLayer,
-                view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
-                permission='portal.templates.manage', weight=2)
-class PortalTemplateSlotAddMenu(ToolbarMenuItem):
-    """Portal template slot add menu"""
-
-    label = _("Add slot...")
-    label_css_class = 'fa fa-fw fa-columns'
-    url = 'add-template-slot.html'
-    modal_target = True
-
-
-@pagelet_config(name='add-template-slot.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplateSlotAddForm(AdminDialogAddForm):
-    """Portal template slot add form"""
-
-    @property
-    def title(self):
-        translate = self.request.localizer.translate
-        return translate(_("« {0} »  portal template")).format(self.context.name)
-
-    legend = _("Add slot")
-    icon_css_class = 'fa fa-fw fa-columns'
-
-    fields = field.Fields(ISlot)
-    ajax_handler = 'add-template-slot.json'
-    edit_permission = None
-
-    def updateWidgets(self, prefix=None):
-        super(PortalTemplateSlotAddForm, self).updateWidgets()
-        self.widgets['row_id'].value = self.request.params.get('form.widgets.row_id')
-
-    def createAndAdd(self, data):
-        config = IPortalTemplateConfiguration(self.context)
-        return config.add_slot(data.get('name'), data.get('row_id'))
-
-
-@subscriber(IDataExtractedEvent, form_selector=PortalTemplateSlotAddForm)
-def handle_new_slot_data_extraction(event):
-    """Handle new slot form data extraction"""
-    config = IPortalTemplateConfiguration(event.form.context)
-    name = event.data.get('name')
-    if name in config.slot_names:
-        event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
-
-
-@view_config(name='add-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateSlotAJAXAddForm(AJAXAddForm, PortalTemplateSlotAddForm):
-    """Portal template slot add form, AJAX handler"""
-
-    def get_ajax_output(self, changes):
-        return {'status': 'callback',
-                'callback': 'PyAMS_portal.template.addSlotCallback',
-                'options': {'row_id': changes[0],
-                            'slot_name': changes[1]}}
-
-
-@view_config(name='set-template-slot-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def set_template_slot_order(request):
-    """Set template slots order"""
-    config = IPortalTemplateConfiguration(request.context)
-    order = json.loads(request.params.get('order'))
-    for key in order.copy().keys():
-        order[int(key)] = order.pop(key)
-    config.set_slot_order(order)
-    return {'status': 'success'}
-
-
-@view_config(name='get-slots-width.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission=VIEW_SYSTEM_PERMISSION, renderer='json', xhr=True)
-def get_template_slots_width(request):
-    """Get template slots width"""
-    config = IPortalTemplateConfiguration(request.context)
-    return config.get_slots_width(request.params.get('device'))
-
-
-@view_config(name='set-slot-width.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def set_template_slot_width(request):
-    """Set template slot width"""
-    config = IPortalTemplateConfiguration(request.context)
-    config.set_slot_width(request.params.get('slot_name'),
-                          request.params.get('device'),
-                          int(request.params.get('width')))
-    return config.get_slots_width(request.params.get('device'))
-
-
-@pagelet_config(name='slot-properties.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission=VIEW_SYSTEM_PERMISSION)
-class PortalTemplateSlotPropertiesEditForm(AdminDialogEditForm):
-    """Slot properties edit form"""
-
-    @property
-    def title(self):
-        translate = self.request.localizer.translate
-        return translate(_("« {0} »  portal template - {1} slot")).format(self.context.name,
-                                                                          self.getContent().slot_name)
-
-    legend = _("Edit slot properties")
-
-    label_css_class = 'control-label col-md-5'
-    input_css_class = 'col-md-7'
-
-    @property
-    def fields(self):
-        fields = field.Fields(ISlotConfiguration)
-        if not self.getContent().can_inherit:
-            fields = fields.omit('inherit_parent')
-        return fields
-
-    ajax_handler = 'slot-properties.json'
-    edit_permission = 'portal.templates.manage'
-
-    def __init__(self, context, request):
-        super(PortalTemplateSlotPropertiesEditForm, self).__init__(context, request)
-        self.config = IPortalTemplateConfiguration(context)
-
-    def getContent(self):
-        slot_name = self.request.params.get('form.widgets.slot_name')
-        return self.config.slot_config[slot_name]
-
-    def updateWidgets(self, prefix=None):
-        super(PortalTemplateSlotPropertiesEditForm, self).updateWidgets(prefix)
-        self.widgets['slot_name'].mode = HIDDEN_MODE
-
-
-@view_config(name='slot-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateSlotPropertiesAJAXEditForm(AJAXEditForm, PortalTemplateSlotPropertiesEditForm):
-    """Slot properties edit form, AJAX renderer"""
-
-    def get_ajax_output(self, changes):
-        if changes:
-            slot_name = self.widgets['slot_name'].value
-            slot_config = self.config.slot_config[slot_name]
-            return {'status': 'success',
-                    'callback': 'PyAMS_portal.template.editSlotCallback',
-                    'options': {'slot_name': slot_name,
-                                'width': slot_config.get_width()}}
-        else:
-            return super(PortalTemplateSlotPropertiesAJAXEditForm, self).get_ajax_output(changes)
-
-
-@view_config(name='delete-template-slot.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def delete_template_slot(request):
-    """Delete template slot"""
-    config = IPortalTemplateConfiguration(request.context)
-    config.delete_slot(request.params.get('slot_name'))
-    return {'status': 'success'}
-
-
-#
-# Portlet views
-#
-
-@viewlet_config(name='add-template-portlet.divider', context=IPortalTemplate, layer=IAdminLayer,
-                view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
-                permission='portal.templates.manage', weight=10)
-class PortalTemplateAddMenuDivider(ToolbarMenuDivider):
-    """Portal template menu divider"""
-
-
-@viewlet_config(name='add-template-portlet.menu', context=IPortalTemplate, layer=IAdminLayer,
-                view=PortalTemplateConfigView, manager=IToolbarAddingMenu,
-                permission='portal.templates.manage', weight=20)
-class PortalTemplatePortletAddMenu(ToolbarMenuItem):
-    """Portal template portlet add menu"""
-
-    label = _("Add portlet...")
-    label_css_class = 'fa fa-fw fa-columns'
-    url = 'add-template-portlet.html'
-    modal_target = True
-
-
-@pagelet_config(name='add-template-portlet.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplatePortletAddForm(AdminDialogAddForm):
-    """Portal template portlet add form"""
-
-    @property
-    def title(self):
-        translate = self.request.localizer.translate
-        return translate(_("« {0} »  portal template")).format(self.context.name)
-
-    legend = _("Add portlet")
-    icon_css_class = 'fa fa-fw fa-columns'
-
-    fields = field.Fields(IPortletAddingInfo)
-    ajax_handler = 'add-template-portlet.json'
-    edit_permission = None
-
-    def createAndAdd(self, data):
-        config = IPortalTemplateConfiguration(self.context)
-        return config.add_portlet(data.get('portlet_name'), data.get('slot_name'))
-
-
-@view_config(name='add-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplatePortletAJAXAddForm(AJAXAddForm, PortalTemplatePortletAddForm):
-    """Portal template portlet add form, AJAX handler"""
-
-    def get_ajax_output(self, changes):
-        config = IPortalTemplateConfiguration(self.context)
-        portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position'])
-        previewer = self.request.registry.queryMultiAdapter((self.context, self.request, self, portlet_config),
-                                                            IPortletPreviewer)
-        if previewer is not None:
-            previewer.update()
-            changes['preview'] = previewer.render()
-        return {'status': 'callback',
-                'callback': 'PyAMS_portal.template.addPortletCallback',
-                'options': changes}
-
-
-@view_config(name='drag-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def drag_template_portlet(request):
-    """Drag portlet icon to slot"""
-    config = IPortalTemplateConfiguration(request.context)
-    portlet_name = request.params.get('portlet_name')
-    slot_name = request.params.get('slot_name')
-    changes = config.add_portlet(portlet_name, slot_name)
-    portlet_config = config.get_portlet_configuration(changes['slot_name'], changes['position'])
-    previewer = request.registry.queryMultiAdapter((request.context, request, request, portlet_config),
-                                                   IPortletPreviewer)
-    if previewer is not None:
-        previewer.update()
-        changes['preview'] = previewer.render()
-    return {'status': 'callback',
-            'close_form': False,
-            'callback': 'PyAMS_portal.template.addPortletCallback',
-            'options': changes}
-
-
-@view_config(name='set-template-portlet-order.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def set_template_portlet_order(request):
-    """Set template portlet order"""
-    config = IPortalTemplateConfiguration(request.context)
-    order = json.loads(request.params.get('order'))
-    order['from']['position'] = int(order['from']['position'])
-    order['to']['positions'] = list(map(int, order['to']['positions']))
-    config.set_portlet_order(order)
-    return {'status': 'success'}
-
-
-@view_config(name='portlet-properties.html', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission=VIEW_SYSTEM_PERMISSION)
-class PortalTemplatePortletEditForm(AdminDialogEditForm):
-    """Portal template portlet edit form"""
-
-    dialog_class = 'modal-large'
-
-    def __call__(self):
-        request = self.request
-        request.registry.notify(PageletCreatedEvent(self))
-        slot_name = request.params.get('form.widgets.slot_name')
-        position = int(request.params.get('form.widgets.position'))
-        config = IPortalTemplateConfiguration(self.context)
-        portlet_config = config.get_portlet_configuration(slot_name, position)
-        if portlet_config is None:
-            raise NotFound()
-        editor = self.request.registry.queryMultiAdapter((portlet_config, request),
-                                                         IPagelet, name='properties.html')
-        if editor is None:
-            raise NotFound()
-        editor.ajax_handler = 'portlet-properties.json'
-        editor.update()
-        return editor()
-
-
-@view_config(name='portlet-properties.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplatePortletAJAXEditForm(AJAXEditForm, PortalTemplatePortletEditForm):
-    """Portal template portlet edit form, AJAX renderer"""
-
-    def __call__(self):
-        request = self.request
-        request.registry.notify(PageletCreatedEvent(self))
-        slot_name = request.params.get('form.widgets.slot_name')
-        position = int(request.params.get('form.widgets.position'))
-        config = IPortalTemplateConfiguration(self.context)
-        portlet_config = config.get_portlet_configuration(slot_name, position)
-        if portlet_config is None:
-            raise NotFound()
-        editor = request.registry.queryMultiAdapter((portlet_config, request),
-                                                    IPagelet, name='properties.json')
-        if editor is None:
-            raise NotFound()
-        changes = editor()
-        if changes:
-            # we commit before loading previewer to avoid BLOBs "uncommited changes" error
-            ITransactionManager(self.context).commit()
-            previewer = request.registry.queryMultiAdapter((self.context, request, self, portlet_config),
-                                                           IPortletPreviewer)
-            if previewer is not None:
-                previewer.update()
-                changes.update({'status': 'callback',
-                                'callback': 'PyAMS_portal.template.editPortletCallback',
-                                'options': {'slot_name': slot_name,
-                                            'position': position,
-                                            'preview': previewer.render()}})
-        return changes
-
-
-@view_config(name='delete-template-portlet.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-def delete_template_portlet(request):
-    """Delete template portlet"""
-    config = IPortalTemplateConfiguration(request.context)
-    config.delete_portlet(request.params.get('slot_name'), int(request.params.get('position')))
-    return {'status': 'success'}
--- a/src/pyams_portal/zmi/template/page.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-#
-# 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_pagelet.interfaces import PageletCreatedEvent, IPagelet
-from pyams_portal.interfaces import IPortalContext, IPortalPage, IPortalTemplateConfiguration
-from pyams_skin.layer import IPyAMSLayer
-from pyams_utils.interfaces import MANAGE_PERMISSION
-from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowState
-from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
-from pyams_zmi.layer import IAdminLayer
-
-# import packages
-from pyramid.exceptions import NotFound
-from pyramid.view import view_config
-from pyams_form.form import AJAXEditForm
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_portal.workflow import PUBLISHED, ARCHIVED
-from pyams_portal.zmi.template.config import PortalTemplateConfigView
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_utils.url import absolute_url
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogEditForm
-from z3c.form import field
-
-from pyams_portal import _
-
-
-
-@viewlet_config(name='template-properties.menu', context=IPortalContext, layer=IAdminLayer,
-                manager=IPropertiesMenu, permission=MANAGE_PERMISSION, weight=5)
-class PortalContextTemplatePropertiesMenu(MenuItem):
-    """Portal context template properties menu"""
-
-    label = _("Presentation template...")
-    icon_class = 'fa-columns'
-
-    url = 'template-properties.html'
-    modal_target = True
-
-
-@pagelet_config(name='template-properties.html', context=IPortalContext, layer=IPyAMSLayer,
-                permission=MANAGE_PERMISSION)
-class PortalContextTemplatePropertiesEditForm(AdminDialogEditForm):
-    """Portal context template properties edit form"""
-
-    @property
-    def title(self):
-        return self.context.title
-
-    legend = _("Edit template configuration")
-
-    @property
-    def fields(self):
-        fields = field.Fields(IPortalPage).select('inherit_parent', 'use_local_template', 'shared_template')
-        if not self.getContent().can_inherit:
-            fields = fields.omit('inherit_parent')
-        return fields
-
-    ajax_handler = 'template-properties.json'
-    edit_permission = MANAGE_PERMISSION
-
-    def getContent(self):
-        return IPortalPage(self.context)
-
-
-@view_config(name='template-properties.json', context=IPortalContext, request_type=IPyAMSLayer,
-             permission=MANAGE_PERMISSION, renderer='json', xhr=True)
-class PortalContextTemplatePropertiesAJAXEditForm(AJAXEditForm, PortalContextTemplatePropertiesEditForm):
-    """Portal context template properties edit form, JSON renderer"""
-
-
-@viewlet_config(name='template-config.menu', context=IPortalContext, layer=IAdminLayer,
-                manager=ISiteManagementMenu, permission=MANAGE_PERMISSION, weight=20)
-class PortalContextTemplateConfigMenu(MenuItem):
-    """Portal context template configuration menu"""
-
-    label = _("Template properties")
-    icon_class = 'fa-columns'
-
-    url = '#template-config.html'
-
-    def __new__(cls, context, request, view, manager=None):
-        page = IPortalPage(context)
-        if page.template is None:
-            return None
-        return MenuItem.__new__(cls)
-
-    def get_url(self):
-        page = IPortalPage(self.context)
-        if page.use_local_template:
-            template = IWorkflowVersions(page.template).get_last_versions()[0]
-            return absolute_url(template, self.request, 'admin.html#properties.html')
-        else:
-            return super(PortalContextTemplateConfigMenu, self).get_url()
-
-
-@pagelet_config(name='template-config.html', context=IPortalContext, layer=IPyAMSLayer,
-                permission=MANAGE_PERMISSION)
-class PortalContextTemplateConfigView(PortalTemplateConfigView):
-    """Portal context template configuration view"""
-
-    title = _("Local portal template configuration")
-
-    def get_context(self):
-        template = IPortalPage(self.context).template
-        return IWorkflowVersions(template).get_last_versions()[0]
-
-    @property
-    def can_change(self):
-        if not IPortalPage(self.context).use_local_template:
-            return False
-        return self.request.has_permission(MANAGE_PERMISSION) and \
-               IWorkflowState(self.get_context()).state not in (PUBLISHED, ARCHIVED)
-
-
-@view_config(name='portlet-properties.html', context=IPortalContext, request_type=IPyAMSLayer,
-             permission=MANAGE_PERMISSION)
-class PortalContextTemplatePortletEditForm(AdminDialogEditForm):
-    """Portal context template portlet edit form"""
-
-    dialog_class = 'modal-large'
-
-    def __call__(self):
-        request = self.request
-        request.registry.notify(PageletCreatedEvent(self))
-        slot_name = request.params.get('form.widgets.slot_name')
-        position = int(request.params.get('form.widgets.position'))
-        config = IPortalTemplateConfiguration(self.context)
-        portlet_config = config.get_portlet_configuration(slot_name, position)
-        if portlet_config is None:
-            raise NotFound()
-        editor = self.request.registry.queryMultiAdapter((portlet_config, request),
-                                                         IPagelet, name='properties.html')
-        if editor is None:
-            raise NotFound()
-        editor.ajax_handler = 'portlet-properties.json'
-        editor.update()
-        return editor()
--- a/src/pyams_portal/zmi/template/templates/config.pt	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-<tal:var define="config view.configuration" i18n:domain="pyams_portal">
-	<div class="ams-widget"
-		 data-ams-plugins="pyams_portal"
-		 data-ams-plugin-pyams_portal-src="/--static--/pyams_portal/js/portal{MyAMS.devext}.js"
-		 data-ams-plugin-pyams_portal-css="/--static--/pyams_portal/css/portal{MyAMS.devext}.css"
-		 data-ams-plugin-pyams_portal-callback="PyAMS_portal.template.initConfig">
-		<header>
-			<span tal:condition="view.widget_icon_class | nothing"
-				  class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
-			</span>
-			<h2 tal:content="view.title">Title</h2>
-			<tal:var content="structure provider:pyams.widget_title" />
-			<tal:var content="structure provider:pyams.toolbar" />
-		</header>
-		<div class="widget-body" tal:define="can_change view.can_change">
-			<div class="btn-toolbar" role="toolbar"
-				 tal:condition="can_change">
-				<div class="btn-group" role="group">
-					<div class="btn btn-default btn-row hint" title="Add row" i18n:attributes="title"
-						 data-ams-hint-gravity="n">
-						<i class="fa fa-fw fa-2x fa-indent"></i>
-					</div>
-					<div class="btn btn-default btn-slot hint" title="Add slot" i18n:attributes="title"
-						 data-ams-hint-gravity="n">
-						<i class="fa fa-fw fa-2x fa-columns"></i>
-					</div>
-				</div>
-				<div class="btn-group" role="group">
-					<div tal:repeat="portlet view.selected_portlets"
-						 class="btn btn-default btn-portlet hint"
-						 data-ams-hint-gravity="n"
-						 tal:attributes="data-ams-portlet-name portlet.name;
-										 title portlet.label;">
-						<img tal:condition="portlet.toolbar_image"
-							 tal:attributes="src portlet.toolbar_image" />
-						<i tal:condition="portlet.toolbar_css_class"
-						   tal:attributes="class portlet.toolbar_css_class"></i>
-					</div>
-				</div>
-				<div class="btn-group" role="group">
-					<div class="btn btn-default hint" data-ams-url="add-template-portlet.html" data-toggle="modal"
-						 data-ams-hint-gravity="n"
-						 title="Add another portlet..." i18n:attributes="title">
-						<i class="fa fa-fw fa-2x fa-plus"></i>
-					</div>
-				</div>
-			</div>
-			<div class="clearfix">
-				<div class="ams-form form-horizontal margin-bottom-10">
-					<label class="control-label col-md-6 padding-right-5" i18n:translate="">Selected display:</label>
-					<div class="col-md-5">
-						<select id="device_selector" class="select2"
-								data-ams-select2-width="300px"
-								data-ams-change-handler="PyAMS_portal.template.selectDisplay">
-							<option value="" selected i18n:translate="">Current device</option>
-							<option value="xs" i18n:translate="">Extra small device (phone)</option>
-							<option value="sm" i18n:translate="">Small device (tablet)</option>
-							<option value="md" i18n:translate="">Medium desktop device (> 970px)</option>
-							<option value="lg" i18n:translate="">Large desktop device (> 1170px)</option>
-						</select>
-					</div>
-				</div>
-			</div>
-			<div id="portal_config" class="container"
-				 tal:attributes="data-ams-allowed-change can_change">
-				<div class="rows"
-					 data-ams-sortable-placeholder="row-highlight"
-					 data-ams-sortable-items="> .row"
-					 data-ams-sortable-over="PyAMS_portal.template.overRows"
-					 data-ams-sortable-stop="PyAMS_portal.template.sortRows">
-					<div class="row context-menu"
-						 data-ams-contextmenu-selector="#rowMenu"
-						 tal:repeat="row range(config.rows)"
-						 tal:attributes="data-ams-row-id row;">
-						<span class="row_id label label-success pull-right"
-							  tal:content="row"></span>
-						<div class="slots"
-							 data-ams-sortable-placeholder="slot-highlight"
-							 data-ams-sortable-connectwith=".slots"
-							 data-ams-sortable-over="PyAMS_portal.template.overSlots"
-							 data-ams-sortable-stop="PyAMS_portal.template.sortSlots">
-							<div class="slot context-menu col col-md-12 no-padding"
-								 data-ams-contextmenu-selector="#slotMenu"
-								 data-ams-resizable-start="PyAMS_portal.template.startSlotResize"
-								 data-ams-resizable-stop="PyAMS_portal.template.stopSlotResize"
-								 data-ams-resizable-handles="e"
-								 tal:repeat="slot_name config.get_slots(row)"
-								 tal:attributes="class string:slot context-menu col ${config.get_slot_configuration(slot_name).get_css_class()};
-												 data-ams-slot-name slot_name;">
-								<div class="header padding-x-5"
-									 tal:content="slot_name"></div>
-								<div class="portlets"
-									 data-ams-sortable-placeholder="portlet-highlight"
-									 data-ams-sortable-connectwith=".portlets"
-									 data-ams-sortable-over="PyAMS_portal.template.overPortlets"
-									 data-ams-sortable-stop="PyAMS_portal.template.sortPortlets">
-									<div class="portlet context-menu"
-										 data-ams-contextmenu-selector="#portletMenu"
-										 tal:repeat="portlet_name config.slots.get(row,{}).get(slot_name, ())"
-										 tal:attributes="data-ams-portlet-name portlet_name;
-														 data-ams-portlet-slot slot_name;
-														 data-ams-portlet-position repeat['portlet_name'].index();">
-										<div class="header padding-x-5"
-											 tal:content="string:${view.get_portlet_label(portlet_name)}"></div>
-										<div class="preview"
-											 tal:content="structure view.get_portlet_preview(slot_name, repeat['portlet_name'].index())"></div>
-									</div>
-								</div>
-								<div class="clearfix"></div>
-							</div>
-						</div>
-					</div>
-				</div>
-			</div>
-			<ul id="rowMenu" class="dropdown-menu" role="menu" style="display:none;"
-				tal:condition="can_change">
-				<li class="small">
-					<a tabindex="-1" data-ams-url="PyAMS_portal.template.deleteRow">
-						<i class="fa fa-fw fa-trash"></i>
-						<i18n:var translate="">Delete row...</i18n:var>
-					</a>
-				</li>
-			</ul>
-			<ul id="slotMenu" class="dropdown-menu" role="menu" style="display:none;" >
-				<li class="small">
-					<a tabindex="-1" data-ams-url="PyAMS_portal.template.editSlot">
-						<i class="fa fa-fw fa-edit"></i>
-						<i18n:var translate="">Edit slot properties...</i18n:var>
-					</a>
-				</li>
-				<tal:if condition="can_change">
-					<li class="divider"></li>
-					<li class="small" tal:condition="can_change">
-						<a tabindex="-1" data-ams-url="PyAMS_portal.template.deleteSlot">
-							<i class="fa fa-fw fa-trash"></i>
-							<i18n:var translate="">Delete slot...</i18n:var>
-						</a>
-					</li>
-				</tal:if>
-			</ul>
-			<ul id="portletMenu" class="dropdown-menu" role="menu" style="display:none;" >
-				<li class="small">
-					<a tabindex="-1" data-ams-url="PyAMS_portal.template.editPortlet">
-						<i class="fa fa-fw fa-edit"></i>
-						<i18n:var translate="">Edit portlet properties...</i18n:var>
-					</a>
-				</li>
-				<tal:if condition="can_change">
-					<li class="divider"></li>
-					<li class="small">
-						<a tabindex="-1" data-ams-url="PyAMS_portal.template.deletePortlet">
-							<i class="fa fa-fw fa-trash"></i>
-							<i18n:var translate="">Delete portlet...</i18n:var>
-						</a>
-					</li>
-				</tal:if>
-			</ul>
-		</div>
-	</div>
-</tal:var>
--- a/src/pyams_portal/zmi/template/workflow.py	Thu Oct 08 12:26:42 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,176 +0,0 @@
-#
-# 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_portal.interfaces import IPortalTemplate
-from pyams_skin.layer import IPyAMSLayer
-from pyams_workflow.interfaces import IWorkflowPublicationInfo, IWorkflowCommentInfo, IWorkflowInfo, \
-    IWorkflowTransitionInfo
-
-# import packages
-from pyams_form.form import AJAXAddForm
-from pyams_form.schema import CloseButton
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_utils.url import absolute_url
-from pyams_workflow.zmi.transition import WorkflowContentTransitionForm
-from pyramid.view import view_config
-from z3c.form import field, button
-from zope.interface import Interface
-from zope.lifecycleevent import ObjectModifiedEvent
-
-from pyams_portal import _
-
-
-#
-# Base workflow form
-#
-
-class PortalTemplateWorkflowForm(WorkflowContentTransitionForm):
-    """Base portal template workflow form"""
-
-
-#
-# Publish forms
-#
-
-class IPortalTemplatePublishButtons(Interface):
-    """Portal template publish buttons"""
-
-    close = CloseButton(name='close', title=_("Close"))
-    action = button.Button(name='action', title=_("Publish"))
-
-
-@pagelet_config(name='wf-publish.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplatePublishForm(PortalTemplateWorkflowForm):
-    """Portal template publish form"""
-
-    legend = _("Publish template")
-
-    fields = field.Fields(IWorkflowTransitionInfo) + \
-             field.Fields(IWorkflowPublicationInfo).select('publication_effective_date',
-                                                           'publication_expiration_date') + \
-             field.Fields(IWorkflowCommentInfo)
-    buttons = button.Buttons(IPortalTemplatePublishButtons)
-    ajax_handler = 'wf-publish.json'
-
-    def createAndAdd(self, data):
-        pub_info = IWorkflowPublicationInfo(self.context)
-        pub_info.publication_effective_date = data.get('publication_effective_date')
-        pub_info.publication_expiration_date = data.get('publication_expiration_date')
-        info = IWorkflowInfo(self.context)
-        info.fire_transition_toward('published', comment=data.get('comment'))
-        self.request.registry.notify(ObjectModifiedEvent(self.context))
-        return info
-
-
-@view_config(name='wf-publish.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateAJAXPublishForm(AJAXAddForm, PortalTemplatePublishForm):
-    """Portal template publish form, AJAX renderer"""
-
-
-#
-# Retire form
-#
-
-class IPortalTemplateRetireButtons(Interface):
-    """Portal template retire buttons"""
-
-    close = CloseButton(name='close', title=_("Close"))
-    action = button.Button(name='action', title=_("Retire"))
-
-
-@pagelet_config(name='wf-retire.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplateRetireForm(PortalTemplateWorkflowForm):
-    """Portal template retire form"""
-
-    legend = _("Retire template")
-
-    buttons = button.Buttons(IPortalTemplateRetireButtons)
-    ajax_handler = 'wf-retire.json'
-
-
-@view_config(name='wf-retire.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateAJAXRetireForm(AJAXAddForm, PortalTemplateRetireForm):
-    """Portal template retire form, AJAX renderer"""
-
-
-#
-# Archive form
-#
-
-class IPortalTemplateArchiveButtons(Interface):
-    """Portal template archive buttons"""
-
-    close = CloseButton(name='close', title=_("Close"))
-    action = button.Button(name='action', title=_("Archive"))
-
-
-@pagelet_config(name='wf-archive.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplateArchiveForm(PortalTemplateWorkflowForm):
-    """Portal template archive form"""
-
-    legend = _("Archive template")
-
-    buttons = button.Buttons(IPortalTemplateArchiveButtons)
-    ajax_handler = 'wf-archive.json'
-
-
-@view_config(name='wf-archive.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateAJAXArchiveForm(AJAXAddForm, PortalTemplateArchiveForm):
-    """Portal template archive form, AJAX renderer"""
-
-
-#
-# Clone forms
-#
-
-class IPortalTemplateCloneButtons(Interface):
-    """Portal template clone buttons"""
-
-    close = CloseButton(name='close', title=_("Close"))
-    action = button.Button(name='action', title=_("Create new version"))
-
-
-@pagelet_config(name='wf-clone.html', context=IPortalTemplate, layer=IPyAMSLayer,
-                permission='portal.templates.manage')
-class PortalTemplateCloneForm(PortalTemplateWorkflowForm):
-    """Portal template clone form"""
-
-    legend = _("Create new version")
-
-    buttons = button.Buttons(IPortalTemplateCloneButtons)
-    ajax_handler = 'wf-clone.json'
-
-    def createAndAdd(self, data):
-        info = IWorkflowInfo(self.context)
-        return info.fire_transition_toward('draft', comment=data.get('comment'))
-
-
-@view_config(name='wf-clone.json', context=IPortalTemplate, request_type=IPyAMSLayer,
-             permission='portal.templates.manage', renderer='json', xhr=True)
-class PortalTemplateAJAXCloneForm(AJAXAddForm, PortalTemplateCloneForm):
-    """Portal template clone form, AJAX renderer"""
-
-    def get_ajax_output(self, changes):
-        return {'status': 'redirect',
-                'location': absolute_url(changes, self.request, 'admin.html#properties.html')}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/templates/layout.pt	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,162 @@
+<tal:var define="template_config view.template_configuration;
+				 portlet_config view.portlet_configuration;
+				 can_change view.can_change;"
+		 i18n:domain="pyams_portal">
+	<div class="ams-widget"
+		 data-ams-focus-target
+		 data-ams-plugins="pyams_portal"
+		 data-ams-plugin-pyams_portal-src="/--static--/pyams_portal/js/portal{MyAMS.devext}.js"
+		 data-ams-plugin-pyams_portal-css="/--static--/pyams_portal/css/portal{MyAMS.devext}.css"
+		 data-ams-plugin-pyams_portal-callback="PyAMS_portal.template.initConfig">
+		<header>
+			<span tal:condition="view.widget_icon_class | nothing"
+				  class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
+			</span>
+			<h2 tal:content="view.title">Title</h2>
+			<tal:var content="structure provider:pyams.widget_title" />
+			<tal:var content="structure provider:pyams.toolbar" />
+		</header>
+		<div class="widget-body">
+			<div class="btn-toolbar" role="toolbar"
+				 tal:condition="can_change">
+				<div class="btn-group" role="group">
+					<div class="btn btn-default btn-row hint" title="Add row" i18n:attributes="title"
+						 data-ams-hint-gravity="n">
+						<i class="fa fa-fw fa-2x fa-indent"></i>
+					</div>
+					<div class="btn btn-default btn-slot hint" title="Add slot" i18n:attributes="title"
+						 data-ams-hint-gravity="n">
+						<i class="fa fa-fw fa-2x fa-columns"></i>
+					</div>
+				</div>
+				<div class="btn-group" role="group">
+					<div tal:repeat="portlet view.selected_portlets"
+						 class="btn btn-default btn-portlet hint"
+						 data-ams-hint-gravity="n"
+						 tal:attributes="data-ams-portlet-name portlet.name;
+										 title portlet.label;">
+						<img tal:condition="portlet.toolbar_image"
+							 tal:attributes="src portlet.toolbar_image" />
+						<i tal:condition="portlet.toolbar_css_class"
+						   tal:attributes="class portlet.toolbar_css_class"></i>
+					</div>
+				</div>
+				<div class="btn-group" role="group">
+					<div class="btn btn-default hint" data-ams-url="add-template-portlet.html" data-toggle="modal"
+						 data-ams-hint-gravity="n"
+						 title="Add another portlet..." i18n:attributes="title">
+						<i class="fa fa-fw fa-2x fa-plus"></i>
+					</div>
+				</div>
+			</div>
+			<div class="clearfix">
+				<div class="ams-form form-horizontal margin-bottom-10">
+					<label class="control-label col-md-6 padding-right-5" i18n:translate="">Selected display:</label>
+					<div class="col-md-5">
+						<select id="device_selector" class="select2"
+								data-ams-select2-width="300px"
+								data-ams-change-handler="PyAMS_portal.template.selectDisplay">
+							<option value="" selected i18n:translate="">Current device</option>
+							<option value="xs" i18n:translate="">Extra small device (phone)</option>
+							<option value="sm" i18n:translate="">Small device (tablet)</option>
+							<option value="md" i18n:translate="">Medium desktop device (> 970px)</option>
+							<option value="lg" i18n:translate="">Large desktop device (> 1170px)</option>
+						</select>
+					</div>
+				</div>
+			</div>
+			<div id="portal_config" class="container"
+				 tal:attributes="data-ams-allowed-change can_change">
+				<div class="rows"
+					 data-ams-sortable-placeholder="row-highlight"
+					 data-ams-sortable-items="> .row"
+					 data-ams-sortable-over="PyAMS_portal.template.overRows"
+					 data-ams-sortable-stop="PyAMS_portal.template.sortRows">
+					<div class="row context-menu"
+						 data-ams-contextmenu-selector="#rowMenu"
+						 tal:repeat="row range(template_config.rows)"
+						 tal:attributes="data-ams-row-id row;">
+						<span class="row_id label label-success pull-right"
+							  tal:content="row"></span>
+						<div class="slots"
+							 data-ams-sortable-placeholder="slot-highlight"
+							 data-ams-sortable-connectwith=".slots"
+							 data-ams-sortable-over="PyAMS_portal.template.overSlots"
+							 data-ams-sortable-stop="PyAMS_portal.template.sortSlots">
+							<div class="slot context-menu col col-md-12 no-padding"
+								 data-ams-contextmenu-selector="#slotMenu"
+								 data-ams-resizable-start="PyAMS_portal.template.startSlotResize"
+								 data-ams-resizable-stop="PyAMS_portal.template.stopSlotResize"
+								 data-ams-resizable-handles="e"
+								 tal:repeat="slot_name template_config.get_slots(row)"
+								 tal:attributes="class string:slot context-menu col ${template_config.get_slot_configuration(slot_name).get_css_class()};
+												 data-ams-slot-name slot_name;">
+								<div class="header padding-x-5"
+									 tal:content="slot_name"></div>
+								<div class="portlets"
+									 data-ams-sortable-placeholder="portlet-highlight"
+									 data-ams-sortable-connectwith=".portlets"
+									 data-ams-sortable-over="PyAMS_portal.template.overPortlets"
+									 data-ams-sortable-stop="PyAMS_portal.template.sortPortlets">
+									<div class="portlet context-menu"
+										 data-ams-contextmenu-selector="#portletMenu"
+										 tal:repeat="portlet_id template_config.slot_config[slot_name].portlet_ids"
+										 tal:attributes="data-ams-portlet-id portlet_id;">
+										<div class="header padding-x-5"
+											 tal:define="portlet_name portlet_config.get_portlet_configuration(portlet_id).portlet_name"
+											 tal:content="view.get_portlet_label(portlet_name)"></div>
+										<div class="preview"
+											 tal:content="structure view.get_portlet_preview(portlet_id)"></div>
+									</div>
+								</div>
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<ul id="rowMenu" class="dropdown-menu" role="menu" style="display:none;"
+				tal:condition="can_change">
+				<li class="small">
+					<a tabindex="-1" data-ams-url="PyAMS_portal.template.deleteRow">
+						<i class="fa fa-fw fa-trash"></i>
+						<i18n:var translate="">Delete row...</i18n:var>
+					</a>
+				</li>
+			</ul>
+			<ul id="slotMenu" class="dropdown-menu" role="menu" style="display:none;"
+				tal:condition="can_change">
+				<li class="small">
+					<a tabindex="-1" data-ams-url="PyAMS_portal.template.editSlot">
+						<i class="fa fa-fw fa-edit"></i>
+						<i18n:var translate="">Edit slot properties...</i18n:var>
+					</a>
+				</li>
+				<li class="divider"></li>
+				<li class="small">
+					<a tabindex="-1" data-ams-url="PyAMS_portal.template.deleteSlot">
+						<i class="fa fa-fw fa-trash"></i>
+						<i18n:var translate="">Delete slot...</i18n:var>
+					</a>
+				</li>
+			</ul>
+			<ul id="portletMenu" class="dropdown-menu" role="menu" style="display:none;" >
+				<li class="small">
+					<a tabindex="-1" data-ams-url="PyAMS_portal.template.editPortlet">
+						<i class="fa fa-fw fa-edit"></i>
+						<i18n:var translate="">Edit portlet properties...</i18n:var>
+					</a>
+				</li>
+				<tal:if condition="can_change">
+					<li class="divider"></li>
+					<li class="small">
+						<a tabindex="-1" data-ams-url="PyAMS_portal.template.deletePortlet">
+							<i class="fa fa-fw fa-trash"></i>
+							<i18n:var translate="">Delete portlet...</i18n:var>
+						</a>
+					</li>
+				</tal:if>
+			</ul>
+		</div>
+	</div>
+</tal:var>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_portal/zmi/templates/portlet.pt	Mon Jan 18 18:09:46 2016 +0100
@@ -0,0 +1,184 @@
+<div class="modal-content" i18n:domain="pyams_portal">
+	<div class="modal-header"
+		 tal:define="header provider:form_header">
+		<tal:if condition="header">
+			<tal:var replace="structure header" />
+		</tal:if>
+		<tal:if condition="not:header">
+			<button type="button" class="close" data-dismiss="modal" aria-hidden="true"
+					tal:condition="view.is_dialog">
+				<i class="fa fa-fw fa-times-circle"></i>
+			</button>
+			<h3 class="modal-title"
+				tal:define="config extension:configuration;">
+				<span class="title" tal:content="view.title">Title</span>
+			</h3>
+			<tal:var replace="structure provider:form_toolbar" />
+		</tal:if>
+	</div>
+	<div class="modal-body no-padding">
+		<div class="form-prefix"
+			 tal:define="prefix provider:form_prefix"
+			 tal:condition="prefix"
+			 tal:content="structure prefix">Form prefix</div>
+		<form method="post"
+			  data-async
+			  tal:attributes="id view.id;
+							  name view.name;
+							  action view.get_form_action();
+							  method view.method;
+							  enctype view.enctype;
+							  acceptCharset view.acceptCharset;
+							  accept view.accept;
+							  autocomplete view.autocomplete;
+							  class view.css_class;
+							  data-ams-data extension:view_data;
+							  data-ams-form-handler view.get_ajax_handler() | nothing;
+							  data-ams-form-options view.get_form_options() | nothing;
+							  data-ams-form-submit-target view.form_target | nothing;
+							  data-ams-form-download-target view.download_target | nothing;
+							  data-ams-warn-on-change view.warn_on_change;">
+			<div class="modal-viewport">
+				<fieldset tal:attributes="class view.fieldset_class | default">
+					<legend tal:define="legend view.legend"
+							tal:condition="legend">
+						<i tal:attributes="class view.icon_css_class | nothing"></i>
+						<tal:var content="legend">Legend</tal:var>
+						<tal:var condition="python:getattr(view, 'show_widget_title', False)"
+								 content="structure provider:pyams.widget_title" />
+					</legend>
+					<tal:var content="structure provider:form_help" />
+					<fieldset class="bordered"
+							  tal:define="configuration view.getContent().configuration;
+										  can_inherit configuration.can_inherit;"
+							  tal:omit-tag="not can_inherit">
+						<input type="hidden" name="form.widgets.portlet_id"
+							   tal:attributes="value configuration.portlet_id" />
+						<legend tal:condition="can_inherit"
+								class="inner checker"
+								data-ams-checker-value="selected"
+								data-ams-checker-mode="disable"
+								data-ams-checker-fieldname="form.widgets.override_parent"
+								tal:attributes="data-ams-checker-state 'off' if configuration.inherit_parent else 'on';">
+							<label tal:content="view.override_label">Override parent settings</label>
+						</legend>
+						<div class="widgets-prefix"
+							 tal:define="prefix provider:widgets_prefix"
+							 tal:condition="prefix"
+							 tal:content="structure prefix">Widgets prefix</div>
+						<tal:loop repeat="group view.groups">
+							<fieldset tal:define="legend group.legend"
+									  tal:omit-tag="not:legend"
+									  tal:attributes="class 'bordered' if group.bordered else None">
+								<tal:if condition="group.checkbox_switch">
+									<legend data-ams-checker-value="selected"
+											tal:condition="legend"
+											tal:attributes="class group.css_class;
+															data-ams-checker-fieldname '{0}:list'.format(group.checkbox_widget.name);
+															data-ams-checker-readonly 'readonly' if group.checkbox_widget.mode == 'display' else None;
+															data-ams-checker-mode 'disable' if group.checkbox_mode == 'disable' else None;
+															data-ams-checker-marker '{0}-empty-marker'.format(group.checkbox_widget.name);
+															data-ams-checker-state group.checker_state;">
+										<label tal:content="legend">Legend</label>
+									</legend>
+								</tal:if>
+								<tal:if condition="not:group.checkbox_switch">
+									<legend tal:condition="legend"
+											tal:content="legend"
+											tal:attributes="class group.css_class;
+															data-ams-switcher-state group.switcher_state;">Legend</legend>
+								</tal:if>
+								<tal:var define="help group.help" condition="help">
+									<div class="alert alert-info padding-5"
+										 tal:define="html import:pyams_utils.text.text_to_html;
+													 i18n_help html(request.localizer.translate(help));"
+										 tal:content="structure i18n_help"></div>
+								</tal:var>
+								<tal:loop repeat="widget group.visible_widgets">
+									<input type="hidden"
+										   tal:condition="widget.mode == 'hidden'"
+										   tal:replace="structure widget.render()" />
+									<tal:if condition="widget.mode != 'hidden'">
+										<div tal:define="required 'required-field' if widget.required and (widget.mode != 'display') else ''"
+											 tal:attributes="class string:form-group ${required}">
+											<label tal:attributes="class group.label_css_class | view.label_css_class">
+												<span>
+													<tal:var content="widget.label" />
+													<i class="fa fa-question-circle hint" title="Input hint"
+													   tal:define="description widget.field.description"
+													   tal:condition="description"
+													   tal:attributes="title description;
+																	   data-ams-hint-html '<' in description;"></i>
+												</span>
+											</label>
+											<div tal:attributes="class widget.input_css_class | group.input_css_class | view.input_css_class">
+												<label class="input"
+													   tal:attributes="class widget.label_css_class | default;
+																	   data-ams-data extension:object_data(widget);
+																	   data-ams-form-validator view.get_widget_callback(widget.field.getName())">
+													<input tal:replace="structure widget.render()" />
+												</label>
+											</div>
+										</div>
+									</tal:if>
+								</tal:loop>
+							</fieldset>
+							<div class="subforms"
+								 tal:condition="group.subforms">
+								<fieldset tal:define="title group.subforms_legend"
+										  tal:omit-tag="not:title">
+									<legend tal:condition="title" tal:content="title" i18n:translate="">Title</legend>
+									<tal:loop repeat="subform group.subforms">
+										<tal:var replace="structure subform.render()" />
+									</tal:loop>
+								</fieldset>
+							</div>
+						</tal:loop>
+						<div class="widgets-suffix"
+							 tal:define="suffix provider:widgets_suffix"
+							 tal:condition="suffix"
+							 tal:content="structure suffix">Widgets suffix</div>
+						<div class="subforms"
+							 tal:condition="view.subforms">
+							<fieldset tal:define="title view.subforms_legend"
+									  tal:omit-tag="not:title">
+								<legend tal:condition="title" tal:content="title" i18n:translate="">Title</legend>
+								<tal:loop repeat="subform view.subforms">
+									<tal:var replace="structure subform.render()" />
+								</tal:loop>
+							</fieldset>
+						</div>
+						<div class="tabforms"
+							 tal:condition="view.tabforms">
+							<ul class="nav nav-tabs">
+								<li tal:repeat="tabform view.tabforms"
+									tal:attributes="class 'small {active} {errors}'.format(active='active' if repeat['tabform'].start() else '',
+																						   errors='state-error' if tabform.widgets.errors else '')">
+									<a data-toggle="tab"
+									   tal:attributes="href string:#${tabform.id};
+													   data-ams-url python:getattr(tabform, 'tab_target', None);"
+									   tal:content="tabform.tab_label" i18n:translate="">Tab label</a>
+								</li>
+							</ul>
+							<div class="tab-content bordered padding-x-10">
+								<div class="tab-pane fade in"
+									 tal:repeat="tabform view.tabforms"
+									 tal:attributes="id tabform.id;
+													 class 'tab-pane {active} fade in'.format(active='active' if repeat['tabform'].start() else '');"
+									 tal:content="structure tabform.render()"></div>
+							</div>
+						</div>
+					</fieldset>
+				</fieldset>
+			</div>
+			<footer tal:condition="view.actions and (view.is_dialog or (view.mode == 'input'))">
+				<button tal:repeat="action view.actions.values()"
+						tal:replace="structure action.render()">Action</button>
+			</footer>
+		</form>
+		<div class="form-suffix"
+			 tal:define="suffix provider:form_suffix"
+			 tal:condition="suffix"
+			 tal:content="structure suffix">Form suffix</div>
+	</div>
+</div>