Version 0.1.0 0.1.0
authorThierry Florac <thierry.florac@onf.fr>
Thu, 08 Oct 2015 13:37:29 +0200
changeset 0 7c0001cacf8e
child 1 f673dd3f94c6
Version 0.1.0
.hgignore
LICENSE
MANIFEST.in
bootstrap.py
buildout.cfg
docs/HISTORY.txt
docs/README.txt
setup.py
src/pyams_content.egg-info/PKG-INFO
src/pyams_content.egg-info/SOURCES.txt
src/pyams_content.egg-info/dependency_links.txt
src/pyams_content.egg-info/entry_points.txt
src/pyams_content.egg-info/namespace_packages.txt
src/pyams_content.egg-info/not-zip-safe
src/pyams_content.egg-info/requires.txt
src/pyams_content.egg-info/top_level.txt
src/pyams_content/__init__.py
src/pyams_content/component/__init__.py
src/pyams_content/component/extfile/__init__.py
src/pyams_content/component/extfile/container.py
src/pyams_content/component/extfile/interfaces/__init__.py
src/pyams_content/component/extfile/zmi/__init__.py
src/pyams_content/component/extfile/zmi/container.py
src/pyams_content/component/extfile/zmi/templates/container.pt
src/pyams_content/component/extfile/zmi/templates/widget-display.pt
src/pyams_content/component/extfile/zmi/templates/widget-input.pt
src/pyams_content/component/extfile/zmi/widget.py
src/pyams_content/component/gallery/__init__.py
src/pyams_content/component/gallery/container.py
src/pyams_content/component/gallery/interfaces/__init__.py
src/pyams_content/component/gallery/zmi/__init__.py
src/pyams_content/component/gallery/zmi/container.py
src/pyams_content/component/gallery/zmi/gallery.py
src/pyams_content/component/gallery/zmi/interfaces.py
src/pyams_content/component/gallery/zmi/templates/container.pt
src/pyams_content/component/gallery/zmi/templates/gallery-images.pt
src/pyams_content/component/gallery/zmi/templates/widget-display.pt
src/pyams_content/component/gallery/zmi/templates/widget-input.pt
src/pyams_content/component/gallery/zmi/widget.py
src/pyams_content/component/illustration/__init__.py
src/pyams_content/component/links/__init__.py
src/pyams_content/component/links/container.py
src/pyams_content/component/links/interfaces/__init__.py
src/pyams_content/component/links/zmi/__init__.py
src/pyams_content/component/links/zmi/container.py
src/pyams_content/component/links/zmi/reverse.py
src/pyams_content/component/links/zmi/templates/container.pt
src/pyams_content/component/links/zmi/templates/widget-display.pt
src/pyams_content/component/links/zmi/templates/widget-input.pt
src/pyams_content/component/links/zmi/widget.py
src/pyams_content/component/paragraph/__init__.py
src/pyams_content/component/paragraph/container.py
src/pyams_content/component/paragraph/html.py
src/pyams_content/component/paragraph/illustration.py
src/pyams_content/component/paragraph/interfaces/__init__.py
src/pyams_content/component/paragraph/zmi/__init__.py
src/pyams_content/component/paragraph/zmi/container.py
src/pyams_content/component/paragraph/zmi/html.py
src/pyams_content/component/paragraph/zmi/illustration.py
src/pyams_content/component/paragraph/zmi/summary.py
src/pyams_content/component/paragraph/zmi/templates/container.pt
src/pyams_content/component/paragraph/zmi/templates/html-summary.pt
src/pyams_content/component/paragraph/zmi/templates/illustration-left.pt
src/pyams_content/component/paragraph/zmi/templates/illustration-right.pt
src/pyams_content/component/paragraph/zmi/templates/illustration.pt
src/pyams_content/component/paragraph/zmi/templates/summary.pt
src/pyams_content/component/theme/__init__.py
src/pyams_content/component/theme/interfaces/__init__.py
src/pyams_content/component/theme/zmi/__init__.py
src/pyams_content/component/theme/zmi/manager.py
src/pyams_content/component/theme/zmi/templates/themes-info.pt
src/pyams_content/configure.zcml
src/pyams_content/doctests/README.txt
src/pyams_content/generations/__init__.py
src/pyams_content/include.py
src/pyams_content/interfaces/__init__.py
src/pyams_content/interfaces/container.py
src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo
src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po
src/pyams_content/locales/pyams_content.pot
src/pyams_content/profile/__init__.py
src/pyams_content/profile/admin.py
src/pyams_content/profile/interfaces/__init__.py
src/pyams_content/profile/zmi/__init__.py
src/pyams_content/root/__init__.py
src/pyams_content/root/interfaces/__init__.py
src/pyams_content/root/zmi/__init__.py
src/pyams_content/root/zmi/search.py
src/pyams_content/root/zmi/templates/dashboard.pt
src/pyams_content/shared/__init__.py
src/pyams_content/shared/common/__init__.py
src/pyams_content/shared/common/interfaces/__init__.py
src/pyams_content/shared/common/interfaces/templates/summary-layout.pt
src/pyams_content/shared/common/interfaces/zmi.py
src/pyams_content/shared/common/manager.py
src/pyams_content/shared/common/security.py
src/pyams_content/shared/common/zmi/__init__.py
src/pyams_content/shared/common/zmi/dashboard.py
src/pyams_content/shared/common/zmi/header.py
src/pyams_content/shared/common/zmi/manager.py
src/pyams_content/shared/common/zmi/owner.py
src/pyams_content/shared/common/zmi/properties.py
src/pyams_content/shared/common/zmi/search.py
src/pyams_content/shared/common/zmi/security.py
src/pyams_content/shared/common/zmi/summary.py
src/pyams_content/shared/common/zmi/templates/advanced-search.pt
src/pyams_content/shared/common/zmi/templates/dashboard.pt
src/pyams_content/shared/common/zmi/templates/header.pt
src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt
src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt
src/pyams_content/shared/common/zmi/templates/wf-cancel-archiving-message.pt
src/pyams_content/shared/common/zmi/templates/wf-cancel-propose-message.pt
src/pyams_content/shared/common/zmi/templates/wf-cancel-retiring-message.pt
src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt
src/pyams_content/shared/common/zmi/templates/wf-create-message.pt
src/pyams_content/shared/common/zmi/templates/wf-delete-message.pt
src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt
src/pyams_content/shared/common/zmi/templates/wf-operator-warning.pt
src/pyams_content/shared/common/zmi/templates/wf-owner-warning.pt
src/pyams_content/shared/common/zmi/templates/wf-propose-message.pt
src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt
src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt
src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt
src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt
src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt
src/pyams_content/shared/common/zmi/workflow.py
src/pyams_content/shared/news/__init__.py
src/pyams_content/shared/news/interfaces/__init__.py
src/pyams_content/shared/news/manager.py
src/pyams_content/shared/news/zmi/__init__.py
src/pyams_content/shared/news/zmi/properties.py
src/pyams_content/site.py
src/pyams_content/site/__init__.py
src/pyams_content/site/interfaces/__init__.py
src/pyams_content/skin/__init__.py
src/pyams_content/skin/resources/js/pyams_content.js
src/pyams_content/skin/resources/js/pyams_content.min.js
src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.js
src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.min.js
src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js
src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js
src/pyams_content/skin/routes.py
src/pyams_content/tests/__init__.py
src/pyams_content/tests/test_utilsdocs.py
src/pyams_content/tests/test_utilsdocstrings.py
src/pyams_content/workflow/__init__.py
src/pyams_content/workflow/interfaces.py
src/pyams_content/zmi/__init__.py
src/pyams_content/zmi/interfaces/__init__.py
src/pyams_content/zmi/tinymce.py
src/pyams_content/zmi/viewlet/__init__.py
src/pyams_content/zmi/viewlet/toplinks/__init__.py
src/pyams_content/zmi/viewlet/toplinks/templates/user-addings.pt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,19 @@
+
+syntax: regexp
+^develop-eggs$
+syntax: regexp
+^parts$
+syntax: regexp
+^bin$
+syntax: regexp
+^\.installed\.cfg$
+syntax: regexp
+^\.settings$
+syntax: regexp
+^build$
+syntax: regexp
+^dist$
+syntax: regexp
+^\.idea$
+syntax: regexp
+.*\.pyc$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,42 @@
+Zope Public License (ZPL) Version 2.1
+=====================================
+
+A copyright notice accompanies this license document that identifies
+the copyright holders.
+
+This license has been certified as open source. It has also been designated
+as GPL compatible by the Free Software Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+   1. Redistributions in source code must retain the accompanying copyright
+      notice, this list of conditions, and the following disclaimer.
+   2. Redistributions in binary form must reproduce the accompanying copyright
+      notice, this list of conditions, and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+   3. Names of the copyright holders must not be used to endorse or promote
+      products derived from this software without prior written permission
+      from the copyright holders.
+   4. The right to distribute this software or to use it for any purpose does
+      not give you the right to use Servicemarks (sm) or Trademarks (tm) of the
+      copyright holders. Use of them is covered by separate agreement with the
+      copyright holders.
+   5. If any files are modified, you must cause the modified files to carry
+      prominent notices stating that you changed the files and the date of any
+      change.
+
+
+Disclaimer
+==========
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,5 @@
+include *.txt
+recursive-include docs *
+recursive-include src *
+global-exclude *.pyc
+global-exclude *.*~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bootstrap.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,178 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# 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.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep 
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", help="use a specific zc.buildout version")
+
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+                  help=("Specify the path to the buildout configuration "
+                        "file to be used."))
+parser.add_option("-f", "--find-links",
+                  help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+                  action="store_true", default=False,
+                  help=("Let bootstrap.py use existing site packages"))
+
+
+options, args = parser.parse_args()
+
+######################################################################
+# load/install setuptools
+
+try:
+    if options.allow_site_packages:
+        import setuptools
+        import pkg_resources
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+ez = {}
+exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+    # ez_setup imports site, which adds site packages
+    # this will remove them from the path to ensure that incompatible versions 
+    # of setuptools are not in the path
+    import site
+    # inside a virtualenv, there is no 'getsitepackages'. 
+    # We can't remove these reliably
+    if hasattr(site, 'getsitepackages'):
+        for sitepackage_path in site.getsitepackages():
+            sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set.  We will
+# do it.
+for path in sys.path:
+    if path not in pkg_resources.working_set.entries:
+        pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+cmd = [sys.executable, '-c',
+       'from setuptools.command.easy_install import main; main()',
+       '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+    'bootstrap-testing-find-links',
+    options.find_links or
+    ('http://downloads.buildout.org/'
+     if options.accept_buildout_test_releases else None)
+    )
+if find_links:
+    cmd.extend(['-f', find_links])
+
+setuptools_path = ws.find(
+    pkg_resources.Requirement.parse('setuptools')).location
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+
+    def _final_version(parsed_version):
+        for part in parsed_version:
+            if (part[:1] == '*') and (part not in _final_parts):
+                return False
+        return True
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setuptools_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
+    raise Exception(
+        "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+    args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+    args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildout.cfg	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,90 @@
+[buildout]
+eggs-directory = /var/local/env/pyams/eggs
+
+socket-timeout = 3
+show-picked-versions = true
+newest = false
+
+allow-hosts =
+    bitbucket.org
+    *.python.org
+    *.sourceforge.net
+    github.com
+
+#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg
+versions = versions
+newest = false
+#allow-picked-versions = false
+
+src = src
+develop =
+    .
+    /var/local/src/pyams/pyams_catalog
+    /var/local/src/pyams/pyams_file
+    /var/local/src/pyams/pyams_form
+    /var/local/src/pyams/pyams_i18n
+    /var/local/src/pyams/pyams_media
+    /var/local/src/pyams/pyams_pagelet
+    /var/local/src/pyams/pyams_security
+    /var/local/src/pyams/pyams_sequence
+    /var/local/src/pyams/pyams_skin
+    /var/local/src/pyams/pyams_template
+    /var/local/src/pyams/pyams_thesaurus
+    /var/local/src/pyams/pyams_utils
+    /var/local/src/pyams/pyams_viewlet
+    /var/local/src/pyams/pyams_workflow
+    /var/local/src/pyams/pyams_zmi
+    /var/local/src/pyams/pyams_zmq
+    /var/local/src/pyams/ext/lingua
+
+parts =
+    package
+    i18n
+    pyflakes
+    test
+
+[package]
+recipe = zc.recipe.egg
+eggs =
+    pyams_catalog
+    pyams_content
+    pyams_file
+    pyams_form
+    pyams_i18n
+    pyams_media
+    pyams_pagelet
+    pyams_security
+    pyams_sequence
+    pyams_skin
+    pyams_template
+    pyams_thesaurus
+    pyams_utils
+    pyams_viewlet
+    pyams_workflow
+    zope.component
+    zope.interface
+
+[i18n]
+recipe = zc.recipe.egg
+eggs =
+    babel
+    lingua
+
+[pyflakes]
+recipe = zc.recipe.egg
+eggs = pyflakes
+scripts = pyflakes
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"]
+
+[pyflakesrun]
+recipe = collective.recipe.cmd
+on_install = true
+cmds = ${buildout:develop}/bin/${pyflakes:scripts}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = pyams_content [test]
+
+[versions]
+pyams_content = 0.1.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/HISTORY.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,6 @@
+History
+=======
+
+0.1.0
+-----
+ - first preview release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,82 @@
+#
+# 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.
+#
+
+"""
+This module contains pyams_content package
+"""
+import os
+from setuptools import setup, find_packages
+
+DOCS = os.path.join(os.path.dirname(__file__),
+                    'docs')
+
+README = os.path.join(DOCS, 'README.txt')
+HISTORY = os.path.join(DOCS, 'HISTORY.txt')
+
+version = '0.1.0'
+long_description = open(README).read() + '\n\n' + open(HISTORY).read()
+
+tests_require = []
+
+setup(name='pyams_content',
+      version=version,
+      description="PyAMS base content interfaces and classes",
+      long_description=long_description,
+      classifiers=[
+          "License :: OSI Approved :: Zope Public License",
+          "Development Status :: 4 - Beta",
+          "Programming Language :: Python",
+          "Framework :: Pyramid",
+          "Topic :: Software Development :: Libraries :: Python Modules",
+      ],
+      keywords='Pyramid PyAMS',
+      author='Thierry Florac',
+      author_email='tflorac@ulthar.net',
+      url='http://hg.ztfy.org/pyams/pyams_content',
+      license='ZPL',
+      packages=find_packages('src'),
+      package_dir={'': 'src'},
+      namespace_packages=[],
+      include_package_data=True,
+      package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg',
+                         '*.css', '*.js']},
+      zip_safe=False,
+      # uncomment this to be able to run tests with setup.py
+      test_suite="pyams_content.tests.test_utilsdocs.test_suite",
+      tests_require=tests_require,
+      extras_require=dict(test=tests_require),
+      install_requires=[
+          'setuptools',
+          # -*- Extra requirements: -*-
+          'pyams_catalog',
+          'pyams_file',
+          'pyams_form',
+          'pyams_i18n',
+          'pyams_media',
+          'pyams_pagelet',
+          'pyams_security',
+          'pyams_sequence',
+          'pyams_skin',
+          'pyams_template',
+          'pyams_thesaurus',
+          'pyams_utils',
+          'pyams_viewlet',
+          'pyams_workflow',
+          'pyams_zmi',
+          'zope.component',
+          'zope.interface',
+      ],
+      entry_points={
+          'fanstatic.libraries': [
+              'pyams_content = pyams_content.skin:library'
+          ]
+      })
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/PKG-INFO	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,18 @@
+Metadata-Version: 1.1
+Name: pyams-content
+Version: 0.1.0
+Summary: PyAMS base content interfaces and classes
+Home-page: http://hg.ztfy.org/pyams/pyams_content
+Author: Thierry Florac
+Author-email: tflorac@ulthar.net
+License: ZPL
+Description: 
+        
+        
+Keywords: Pyramid PyAMS
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Zope Public License
+Classifier: Development Status :: 4 - Beta
+Classifier: Programming Language :: Python
+Classifier: Framework :: Pyramid
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/SOURCES.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,149 @@
+MANIFEST.in
+setup.py
+docs/HISTORY.txt
+docs/README.txt
+src/pyams_content/__init__.py
+src/pyams_content/configure.zcml
+src/pyams_content/include.py
+src/pyams_content/site.py
+src/pyams_content.egg-info/PKG-INFO
+src/pyams_content.egg-info/SOURCES.txt
+src/pyams_content.egg-info/dependency_links.txt
+src/pyams_content.egg-info/entry_points.txt
+src/pyams_content.egg-info/namespace_packages.txt
+src/pyams_content.egg-info/not-zip-safe
+src/pyams_content.egg-info/requires.txt
+src/pyams_content.egg-info/top_level.txt
+src/pyams_content/component/__init__.py
+src/pyams_content/component/extfile/__init__.py
+src/pyams_content/component/extfile/container.py
+src/pyams_content/component/extfile/interfaces/__init__.py
+src/pyams_content/component/extfile/zmi/__init__.py
+src/pyams_content/component/extfile/zmi/container.py
+src/pyams_content/component/extfile/zmi/widget.py
+src/pyams_content/component/extfile/zmi/templates/container.pt
+src/pyams_content/component/extfile/zmi/templates/widget-display.pt
+src/pyams_content/component/extfile/zmi/templates/widget-input.pt
+src/pyams_content/component/gallery/__init__.py
+src/pyams_content/component/gallery/container.py
+src/pyams_content/component/gallery/interfaces/__init__.py
+src/pyams_content/component/gallery/zmi/__init__.py
+src/pyams_content/component/gallery/zmi/container.py
+src/pyams_content/component/gallery/zmi/gallery.py
+src/pyams_content/component/gallery/zmi/interfaces.py
+src/pyams_content/component/gallery/zmi/widget.py
+src/pyams_content/component/gallery/zmi/templates/container.pt
+src/pyams_content/component/gallery/zmi/templates/gallery-images.pt
+src/pyams_content/component/gallery/zmi/templates/widget-display.pt
+src/pyams_content/component/gallery/zmi/templates/widget-input.pt
+src/pyams_content/component/illustration/__init__.py
+src/pyams_content/component/links/__init__.py
+src/pyams_content/component/links/container.py
+src/pyams_content/component/links/interfaces/__init__.py
+src/pyams_content/component/links/zmi/__init__.py
+src/pyams_content/component/links/zmi/container.py
+src/pyams_content/component/links/zmi/reverse.py
+src/pyams_content/component/links/zmi/widget.py
+src/pyams_content/component/links/zmi/templates/container.pt
+src/pyams_content/component/links/zmi/templates/widget-display.pt
+src/pyams_content/component/links/zmi/templates/widget-input.pt
+src/pyams_content/component/paragraph/__init__.py
+src/pyams_content/component/paragraph/container.py
+src/pyams_content/component/paragraph/html.py
+src/pyams_content/component/paragraph/illustration.py
+src/pyams_content/component/paragraph/interfaces/__init__.py
+src/pyams_content/component/paragraph/zmi/__init__.py
+src/pyams_content/component/paragraph/zmi/container.py
+src/pyams_content/component/paragraph/zmi/html.py
+src/pyams_content/component/paragraph/zmi/illustration.py
+src/pyams_content/component/paragraph/zmi/summary.py
+src/pyams_content/component/paragraph/zmi/templates/container.pt
+src/pyams_content/component/paragraph/zmi/templates/html-summary.pt
+src/pyams_content/component/paragraph/zmi/templates/illustration-left.pt
+src/pyams_content/component/paragraph/zmi/templates/illustration-right.pt
+src/pyams_content/component/paragraph/zmi/templates/illustration.pt
+src/pyams_content/component/paragraph/zmi/templates/summary.pt
+src/pyams_content/component/theme/__init__.py
+src/pyams_content/component/theme/interfaces/__init__.py
+src/pyams_content/component/theme/zmi/__init__.py
+src/pyams_content/component/theme/zmi/manager.py
+src/pyams_content/component/theme/zmi/templates/themes-info.pt
+src/pyams_content/doctests/README.txt
+src/pyams_content/generations/__init__.py
+src/pyams_content/interfaces/__init__.py
+src/pyams_content/interfaces/container.py
+src/pyams_content/locales/pyams_content.pot
+src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo
+src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po
+src/pyams_content/profile/__init__.py
+src/pyams_content/profile/admin.py
+src/pyams_content/profile/interfaces/__init__.py
+src/pyams_content/profile/zmi/__init__.py
+src/pyams_content/root/__init__.py
+src/pyams_content/root/interfaces/__init__.py
+src/pyams_content/root/zmi/__init__.py
+src/pyams_content/root/zmi/search.py
+src/pyams_content/root/zmi/templates/dashboard.pt
+src/pyams_content/shared/__init__.py
+src/pyams_content/shared/common/__init__.py
+src/pyams_content/shared/common/manager.py
+src/pyams_content/shared/common/security.py
+src/pyams_content/shared/common/interfaces/__init__.py
+src/pyams_content/shared/common/interfaces/zmi.py
+src/pyams_content/shared/common/interfaces/templates/summary-layout.pt
+src/pyams_content/shared/common/zmi/__init__.py
+src/pyams_content/shared/common/zmi/dashboard.py
+src/pyams_content/shared/common/zmi/header.py
+src/pyams_content/shared/common/zmi/manager.py
+src/pyams_content/shared/common/zmi/owner.py
+src/pyams_content/shared/common/zmi/properties.py
+src/pyams_content/shared/common/zmi/search.py
+src/pyams_content/shared/common/zmi/security.py
+src/pyams_content/shared/common/zmi/summary.py
+src/pyams_content/shared/common/zmi/workflow.py
+src/pyams_content/shared/common/zmi/templates/advanced-search.pt
+src/pyams_content/shared/common/zmi/templates/dashboard.pt
+src/pyams_content/shared/common/zmi/templates/header.pt
+src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-cancel-archiving-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-cancel-propose-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-cancel-retiring-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-create-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-delete-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-operator-warning.pt
+src/pyams_content/shared/common/zmi/templates/wf-owner-warning.pt
+src/pyams_content/shared/common/zmi/templates/wf-propose-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt
+src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt
+src/pyams_content/shared/news/__init__.py
+src/pyams_content/shared/news/manager.py
+src/pyams_content/shared/news/interfaces/__init__.py
+src/pyams_content/shared/news/zmi/__init__.py
+src/pyams_content/shared/news/zmi/properties.py
+src/pyams_content/site/__init__.py
+src/pyams_content/site/interfaces/__init__.py
+src/pyams_content/skin/__init__.py
+src/pyams_content/skin/routes.py
+src/pyams_content/skin/resources/js/pyams_content.js
+src/pyams_content/skin/resources/js/pyams_content.min.js
+src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js
+src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js
+src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.js
+src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.min.js
+src/pyams_content/tests/__init__.py
+src/pyams_content/tests/test_utilsdocs.py
+src/pyams_content/tests/test_utilsdocstrings.py
+src/pyams_content/workflow/__init__.py
+src/pyams_content/workflow/interfaces.py
+src/pyams_content/zmi/__init__.py
+src/pyams_content/zmi/tinymce.py
+src/pyams_content/zmi/interfaces/__init__.py
+src/pyams_content/zmi/viewlet/__init__.py
+src/pyams_content/zmi/viewlet/toplinks/__init__.py
+src/pyams_content/zmi/viewlet/toplinks/templates/user-addings.pt
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/dependency_links.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/entry_points.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+[fanstatic.libraries]
+pyams_content = pyams_content.skin:library
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/namespace_packages.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/not-zip-safe	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/requires.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+setuptools
+pyams_catalog
+pyams_file
+pyams_form
+pyams_i18n
+pyams_media
+pyams_pagelet
+pyams_security
+pyams_sequence
+pyams_skin
+pyams_template
+pyams_thesaurus
+pyams_utils
+pyams_viewlet
+pyams_workflow
+pyams_zmi
+zope.component
+zope.interface
+
+[test]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content.egg-info/top_level.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+pyams_content
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,98 @@
+#
+# 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'
+
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_content')
+
+
+def includeme(config):
+    """Pyramid include"""
+
+    from .include import include_package
+    include_package(config)
+
+    from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, MANAGE_SITE_PERMISSION, MANAGE_TOOL_PERMISSION, \
+        CREATE_CONTENT_PERMISSION, MANAGE_CONTENT_PERMISSION, COMMENT_CONTENT_PERMISSION, PUBLISH_CONTENT_PERMISSION
+    from pyams_utils.interfaces import PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION, \
+        VIEW_SYSTEM_PERMISSION, MANAGE_SECURITY_PERMISSION, MANAGE_ROLES_PERMISSION
+
+    # register custom permissions
+    config.register_permission({'id': MANAGE_SITE_ROOT_PERMISSION,
+                                'title': _("Manage site root")})
+    config.register_permission({'id': MANAGE_SITE_PERMISSION,
+                                'title': _("Manage site")})
+    config.register_permission({'id': MANAGE_TOOL_PERMISSION,
+                                'title': _("Manage tool")})
+    config.register_permission({'id': CREATE_CONTENT_PERMISSION,
+                                'title': _("Create content")})
+    config.register_permission({'id': MANAGE_CONTENT_PERMISSION,
+                                'title': _("Manage content")})
+    config.register_permission({'id': COMMENT_CONTENT_PERMISSION,
+                                'title': _("Comment content")})
+    config.register_permission({'id': PUBLISH_CONTENT_PERMISSION,
+                                'title': _("Publish content")})
+
+    # register custom roles
+    config.register_role({'id': 'pyams.Webmaster',
+                          'title': _("Webmaster (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION,
+                                          VIEW_SYSTEM_PERMISSION, MANAGE_SECURITY_PERMISSION, MANAGE_ROLES_PERMISSION,
+                                          MANAGE_SITE_ROOT_PERMISSION, MANAGE_SITE_PERMISSION, MANAGE_TOOL_PERMISSION,
+                                          CREATE_CONTENT_PERMISSION, MANAGE_CONTENT_PERMISSION,
+                                          COMMENT_CONTENT_PERMISSION, PUBLISH_CONTENT_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager', 'role:pyams.Webmaster'}})
+    config.register_role({'id': 'pyams.Pilot',
+                          'title': _("Pilot (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION,
+                                          VIEW_SYSTEM_PERMISSION, MANAGE_ROLES_PERMISSION,
+                                          MANAGE_SITE_PERMISSION, MANAGE_TOOL_PERMISSION,
+                                          MANAGE_CONTENT_PERMISSION, COMMENT_CONTENT_PERMISSION,
+                                          PUBLISH_CONTENT_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager', 'role:pyams.Webmaster'}})
+    config.register_role({'id': 'pyams.Manager',
+                          'title': _("Manager (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION,
+                                          VIEW_SYSTEM_PERMISSION, MANAGE_CONTENT_PERMISSION,
+                                          COMMENT_CONTENT_PERMISSION, PUBLISH_CONTENT_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager', 'role:pyams.Webmaster',
+                                       'role:pyams.Pilot'}})
+    config.register_role({'id': 'pyams.Owner',
+                          'title': _("Creator (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION,
+                                          VIEW_SYSTEM_PERMISSION, MANAGE_ROLES_PERMISSION,
+                                          MANAGE_CONTENT_PERMISSION, COMMENT_CONTENT_PERMISSION}})
+    config.register_role({'id': 'pyams.Contributor',
+                          'title': _("Contributor (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION,
+                                          VIEW_SYSTEM_PERMISSION,
+                                          CREATE_CONTENT_PERMISSION, MANAGE_CONTENT_PERMISSION,
+                                          COMMENT_CONTENT_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager', 'role:pyams.Webmaster',
+                                       'role:pyams.Pilot', 'role:pyams.Owner'}})
+    config.register_role({'id': 'pyams.Reader',
+                          'title': _("Reader (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, MANAGE_PERMISSION,
+                                          VIEW_SYSTEM_PERMISSION, COMMENT_CONTENT_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager', 'role:pyams.Webmaster',
+                                       'role:pyams.Pilot', 'role:pyams.Manager', 'role:pyams.Contributor'}})
+    config.register_role({'id': 'pyams.Operator',
+                          'title': _("Operator (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION, VIEW_SYSTEM_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager'}})
+    config.register_role({'id': 'pyams.Guest',
+                          'title': _("Guest user (role)"),
+                          'permissions': {PUBLIC_PERMISSION, VIEW_PERMISSION},
+                          'managers': {'system:admin', 'role:system.Manager', 'role:pyams.Webmaster',
+                                       'role:pyams.Pilot', 'role:pyams.Manager', 'role:pyams.Contributor'}})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,138 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.extfile.interfaces import IBaseExtFile, IExtFile, IExtImage, IExtVideo, IExtAudio
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from persistent import Persistent
+from pyams_i18n.property import I18nFileProperty
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer, provider
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+from pyams_content import _
+
+
+EXTERNAL_FILES_FACTORIES = {}
+
+
+def register_file_factory(key, factory, name=None):
+    """Register new file factory"""
+    if key not in EXTERNAL_FILES_FACTORIES:
+        EXTERNAL_FILES_FACTORIES[key] = (factory, name or key)
+
+
+@provider(IVocabularyFactory)
+class ExternalFilesFactoriesVocabulary(SimpleVocabulary):
+    """External files factories vocabulary"""
+
+    def __init__(self, context):
+        terms = sorted([SimpleTerm(k, title=v[1]) for k, v in EXTERNAL_FILES_FACTORIES.items()],
+                       key=lambda x: x.title)
+        super(ExternalFilesFactoriesVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS files factories', ExternalFilesFactoriesVocabulary)
+
+
+@implementer(IBaseExtFile)
+class BaseExtFile(Persistent, Contained):
+    """External file persistent class"""
+
+    title = FieldProperty(IExtFile['title'])
+    description = FieldProperty(IExtFile['description'])
+    author = FieldProperty(IExtFile['author'])
+
+
+@adapter_config(context=IBaseExtFile, provides=IFormContextPermissionChecker)
+class BaseExtFilePermissionChecker(ContextAdapter):
+    """External file permission checker"""
+
+    @property
+    def edit_permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IBaseExtFile)
+def handle_added_extfile(event):
+    """Handle added external file"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IBaseExtFile)
+def handle_modified_extfile(event):
+    """Handle modified external file"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IBaseExtFile)
+def handle_removed_extfile(event):
+    """Handle removed external file"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@implementer(IExtFile)
+class ExtFile(BaseExtFile):
+    """Generic external file persistent class"""
+
+    data = I18nFileProperty(IExtFile['data'])
+
+register_file_factory('file', ExtFile, _("Standard file"))
+
+
+@implementer(IExtImage)
+class ExtImage(BaseExtFile):
+    """External image persistent class"""
+
+    data = I18nFileProperty(IExtImage['data'])
+
+register_file_factory('image', ExtImage, _("Image"))
+
+
+@implementer(IExtVideo)
+class ExtVideo(BaseExtFile):
+    """External video file persistent class"""
+
+    data = I18nFileProperty(IExtVideo['data'])
+
+register_file_factory('video', ExtVideo, _("Video"))
+
+
+@implementer(IExtAudio)
+class ExtAudio(BaseExtFile):
+    """External audio file persistent class"""
+
+    data = I18nFileProperty(IExtAudio['data'])
+
+register_file_factory('audio', ExtAudio, _("Audio file"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,148 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.extfile.interfaces import IExtFileContainer, IExtFileContainerTarget, \
+    EXTFILE_CONTAINER_KEY, IExtFileLinksContainer, IExtFileLinksContainerTarget, EXTFILE_LINKS_CONTAINER_KEY
+from pyams_file.interfaces import IMediaFile, IImage, IVideo, IAudio
+from pyams_i18n.interfaces import II18n
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.schema.interfaces import IVocabularyFactory
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from persistent.list import PersistentList
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.interface import implementer, provider
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.vocabulary import SimpleVocabulary, getVocabularyRegistry, SimpleTerm
+
+
+#
+# External files container
+#
+
+@implementer(IExtFileContainer)
+class ExtFileContainer(Folder):
+    """External files container"""
+
+    last_id = 1
+
+    def __setitem__(self, key, value):
+        key = str(self.last_id)
+        super(ExtFileContainer, self).__setitem__(key, value)
+        self.last_id += 1
+
+    @property
+    def files(self):
+        return (file for file in self.values() if not IMediaFile.providedBy(II18n(file).query_attribute('data')))
+
+    @property
+    def medias(self):
+        return (file for file in self.values() if IMediaFile.providedBy(II18n(file).query_attribute('data')))
+
+    @property
+    def images(self):
+        return (file for file in self.values() if IImage.providedBy(II18n(file).query_attribute('data')))
+
+    @property
+    def videos(self):
+        return (file for file in self.values() if IVideo.providedBy(II18n(file).query_attribute('data')))
+
+    @property
+    def audios(self):
+        return (file for file in self.values() if IAudio.providedBy(II18n(file).query_attribute('data')))
+
+
+@adapter_config(context=IExtFileContainerTarget, provides=IExtFileContainer)
+def extfile_container_factory(target):
+    """External files container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(EXTFILE_CONTAINER_KEY)
+    if container is None:
+        container = annotations[EXTFILE_CONTAINER_KEY] = ExtFileContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++files++')
+    return container
+
+
+@adapter_config(name='files', context=IExtFileContainerTarget, provides=ITraversable)
+class ExtFileContainerNamespace(ContextAdapter):
+    """++files++ namespace adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return IExtFileContainer(self.context)
+
+
+@adapter_config(name='extfile', context=IExtFileContainerTarget, provides=ISublocations)
+class ExtFileContainerSublocations(ContextAdapter):
+    """External files container sublocations"""
+
+    def sublocations(self):
+        return IExtFileContainer(self.context).values()
+
+
+@provider(IVocabularyFactory)
+class ExtFileContainerFilesVocabulary(SimpleVocabulary):
+    """External files container files vocabulary"""
+
+    def __init__(self, context):
+        target = get_parent(context, IExtFileContainerTarget)
+        terms = [SimpleTerm(file.__name__, title=II18n(file).query_attribute('title'))
+                 for file in IExtFileContainer(target).values()]
+        super(ExtFileContainerFilesVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS content external files', ExtFileContainerFilesVocabulary)
+
+
+#
+# External file links container
+#
+
+@implementer(IExtFileLinksContainer)
+class ExtFileLinksContainer(Persistent, Contained):
+    """External files links container"""
+
+    def __init__(self):
+        self.files = PersistentList()
+
+
+@adapter_config(context=IExtFileLinksContainerTarget, provides=IExtFileLinksContainer)
+def extfile_links_container_factory(target):
+    """External files links container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(EXTFILE_LINKS_CONTAINER_KEY)
+    if container is None:
+        container = annotations[EXTFILE_LINKS_CONTAINER_KEY] = ExtFileLinksContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++files-links++')
+    return container
+
+
+@adapter_config(name='files-links', context=IExtFileLinksContainerTarget, provides=ITraversable)
+class ExtFileLinksContainerNamespace(ContextAdapter):
+    """++files-links++ namespace adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return IExtFileLinksContainer(self.context)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,108 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer
+
+# import packages
+from pyams_i18n.schema import I18nTextLineField, I18nTextField, I18nFileField, I18nThumbnailImageField
+from pyams_utils.schema import PersistentList
+from zope.container.constraints import containers, contains
+from zope.interface import Interface, Attribute
+from zope.schema import TextLine, Choice
+
+from pyams_content import _
+
+
+EXTFILE_CONTAINER_KEY = 'pyams_content.extfile'
+EXTFILE_LINKS_CONTAINER_KEY = 'pyams_content.extfile.links'
+
+
+class IBaseExtFile(IAttributeAnnotatable):
+    """Base external file interface"""
+
+    containers('.IExtFileContainer')
+
+    title = I18nTextLineField(title=_("Title"),
+                              description=_("File title, as shown in front-office"),
+                              required=True)
+
+    description = I18nTextField(title=_("Description"),
+                                description=_("File description displayed by front-office template"),
+                                required=False)
+
+    author = TextLine(title=_("Author"),
+                      description=_("Name of document's author"),
+                      required=False)
+
+
+class IExtFile(IBaseExtFile):
+    """Generic external file interface"""
+
+    data = I18nFileField(title=_("File data"),
+                         description=_("File content"),
+                         required=True)
+
+
+class IExtMedia(IExtFile):
+    """External media file interface"""
+
+
+class IExtImage(IExtMedia):
+    """External image file interface"""
+
+    data = I18nThumbnailImageField(title=_("Image data"),
+                                   description=_("Image content"),
+                                   required=True)
+
+
+class IExtVideo(IExtMedia):
+    """External video file interface"""
+
+
+class IExtAudio(IExtMedia):
+    """External audio file interface"""
+
+
+class IExtFileContainer(IContainer):
+    """External files container"""
+
+    contains(IBaseExtFile)
+
+    files = Attribute("Files list iterator")
+    medias = Attribute("Medias list iterator")
+    images = Attribute("Images list iterator")
+    videos = Attribute("Videos list iterator")
+    audios = Attribute("Audios list iterator")
+
+
+class IExtFileContainerTarget(Interface):
+    """External files container marker interface"""
+
+
+class IExtFileLinksContainer(Interface):
+    """External files links container interface"""
+
+    files = PersistentList(title=_("External files"),
+                           description=_("List of external files linked to this object"),
+                           value_type=Choice(vocabulary="PyAMS content external files"),
+                           required=False)
+
+
+class IExtFileLinksContainerTarget(Interface):
+    """External files links container marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,203 @@
+#
+# 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_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.extfile.interfaces import IExtFileContainer, IExtFileContainerTarget, IBaseExtFile, IExtFile, \
+    IExtImage
+from pyams_file.interfaces import IFileInfo
+from pyams_i18n.interfaces import INegotiator, II18n
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from z3c.form.interfaces import NOT_CHANGED
+
+# import packages
+from pyams_content.component.extfile import EXTERNAL_FILES_FACTORIES
+from pyams_content.component.extfile.zmi.container import ExtFileContainerView
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import Interface
+from zope.schema import Choice
+
+from pyams_content import _
+
+
+#
+# External file view
+#
+
+class IExtFileFactoryChooser(Interface):
+    """External file factory chooser interface"""
+
+    factory = Choice(title=_("External file type"),
+                     vocabulary='PyAMS files factories',
+                     default='file',
+                     required=True)
+
+
+@viewlet_config(name='add-extfile.menu', context=IExtFileContainerTarget, view=ExtFileContainerView,
+                layer=IPyAMSLayer, manager=IWidgetTitleViewletManager, weight=50)
+class ExtFileAddMenu(ProtectedFormObjectMixin, ToolbarAction):
+    """External file add menu"""
+
+    label = _("Add external file")
+
+    url = 'add-extfile.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-extfile.html', context=IExtFileContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class ExtFileAddForm(AdminDialogAddForm):
+    """External file add form"""
+
+    legend = _("Add new external file")
+    icon_css_class = 'fa fa-fw fa-file-text-o'
+
+    fields = field.Fields(IExtFileFactoryChooser) + \
+             field.Fields(IExtFile).omit('__parent__', '__name__')
+
+    @property
+    def ajax_handler(self):
+        origin = self.request.params.get('origin')
+        if origin == 'link':
+            return 'add-extfile-link.json'
+        else:
+            return 'add-extfile.json'
+
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(ExtFileAddForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+    def create(self, data):
+        factory = EXTERNAL_FILES_FACTORIES.get(data.get('factory'))
+        if factory is not None:
+            return factory[0]()
+
+    def update_content(self, content, data):
+        data['factory'] = NOT_CHANGED
+        return super(ExtFileAddForm, self).update_content(content, data)
+
+    def add(self, object):
+        IExtFileContainer(self.context)['none'] = object
+        i18n = query_utility(INegotiator)
+        if i18n is not None:
+            lang = i18n.server_language
+            data = II18n(object).get_attribute('data', lang, self.request)
+            if data:
+                info = IFileInfo(data)
+                info.title = II18n(object).get_attribute('title', lang, self.request)
+                info.description = II18n(object).get_attribute('description', lang, self.request)
+            for lang, data in object.data.items():
+                if data is not None:
+                    IFileInfo(data).language = lang
+
+
+@view_config(name='add-extfile.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtFileAJAXAddForm(AJAXAddForm, ExtFileAddForm):
+    """External file add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': '#external-files.html'}
+
+
+@view_config(name='add-extfile-link.json', context=IExtFileContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtFileLinkAJAXAddForm(AJAXAddForm, ExtFileAddForm):
+    """External file link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        target = get_parent(self.context, IExtFileContainerTarget)
+        container = IExtFileContainer(target)
+        files = [{'id': file.__name__,
+                  'text': II18n(file).query_attribute('title', request=self.request)}
+                 for file in container.values()]
+        return {'status': 'callback',
+                'callback': 'PyAMS_content.extfiles.refresh',
+                'options': {'files': files,
+                            'new_file': {'id': changes.__name__,
+                                         'text': II18n(changes).query_attribute('title', request=self.request)}}}
+
+
+@pagelet_config(name='properties.html', context=IExtFile, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class ExtFilePropertiesEditForm(AdminDialogEditForm):
+    """External file properties edit form"""
+
+    legend = _("Update file properties")
+    icon_css_class = 'fa fa-fw fa-file-text-o'
+    dialog_class = 'modal-large'
+
+    fields = field.Fields(IExtFile).omit('__parent__', '__file__')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(ExtFilePropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+    def update_content(self, content, data):
+        changes = super(ExtFilePropertiesEditForm, self).update_content(content, data)
+        if changes:
+            i18n = query_utility(INegotiator)
+            if i18n is not None:
+                lang = i18n.server_language
+                data = II18n(content).get_attribute('data', lang, self.request)
+                if data:
+                    info = IFileInfo(data)
+                    info.title = II18n(content).get_attribute('title', lang, self.request)
+                    info.description = II18n(content).get_attribute('description', lang, self.request)
+            for lang, data in content.data.items():
+                if data and not IFileInfo(data).language:
+                    IFileInfo(data).language = lang
+        return changes
+
+
+@pagelet_config(name='properties.html', context=IExtImage, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class ExtImagePropertiesEditForm(ExtFilePropertiesEditForm):
+    """External image properties edit form"""
+
+    legend = _("Update image properties")
+    icon_css_class = 'fa fa-fw fa-picture-o'
+
+    fields = field.Fields(IExtImage).omit('__parent__', '__name__')
+
+
+@view_config(name='properties.json', context=IExtFile, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtFilePropertiesAJAXEditForm(AJAXEditForm, ExtFilePropertiesEditForm):
+    """External file properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if ('title' in changes.get(IBaseExtFile, ())) or \
+           ('data' in changes.get(IExtFile, ())):
+            return {'status': 'reload',
+                    'location': '#external-files.html'}
+        else:
+            return super(ExtFilePropertiesAJAXEditForm, self).get_ajax_output(changes)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/zmi/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,253 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget, IExtFileContainer, \
+    IExtFileLinksContainerTarget, IExtFileLinksContainer
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_file.interfaces import IFileInfo, IFile, IImage
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_utils.interfaces.data import IObjectData
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues, IColumn
+
+# import packages
+from pyams_content.component.extfile.zmi.widget import ExtFileLinkSelectFieldWidget
+from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
+from pyams_form.form import AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, I18nColumn, TrashColumn
+from pyams_skin.viewlet.menu import MenuItem, MenuDivider
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.size import get_human_size
+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 AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.decorator import reify
+from pyramid.view import view_config
+from z3c.form import field
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, alsoProvides, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='external-files.menu', context=IExtFileContainerTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=200)
+class ExtFileContainerMenu(MenuItem):
+    """External files container menu"""
+
+    label = _("External files...")
+    icon_class = 'fa-file-text-o'
+    url = '#external-files.html'
+
+
+#
+# External files container views
+#
+
+@view_config(name='get-files-list.json', context=Interface, request_type=IPyAMSLayer,
+             renderer='json', xhr=True)
+def get_files_list(request):
+    """Get container files in JSON format for TinyMCE editor"""
+    target = get_parent(request.context, IExtFileContainerTarget)
+    if target is None:
+        return []
+    container = IExtFileContainer(target)
+    return sorted([{'title': II18n(file).query_attribute('title', request=request),
+                    'value': absolute_url(II18n(file).query_attribute('data', request=request), request)}
+                   for file in container.values()],
+                  key=lambda x: x['title'])
+
+
+@view_config(name='get-images-list.json', context=Interface, request_type=IPyAMSLayer,
+             renderer='json', xhr=True)
+def get_images_list(request):
+    """Get container images in JSON format for TinyMCE editor"""
+    target = get_parent(request.context, IExtFileContainerTarget)
+    if target is None:
+        return []
+    container = IExtFileContainer(target)
+    return sorted([{'title': II18n(img).query_attribute('title', request=request),
+                    'value': absolute_url(II18n(img).query_attribute('data', request=request), request)}
+                   for img in container.images],
+                  key=lambda x: x['title'])
+
+
+@pagelet_config(name='external-files.html', context=IExtFileContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/container.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ExtFileContainerView(AdminView):
+    """External files container view"""
+
+    title = _("External files list")
+
+    def __init__(self, context, request):
+        super(ExtFileContainerView, self).__init__(context, request)
+        self.files_table = ExtFileContainerTable(context, request, self, _("External files"), 'files')
+        self.images_table = ExtFileContainerTable(context, request, self, _("Images"), 'images')
+        self.videos_table = ExtFileContainerTable(context, request, self, _("Videos"), 'videos')
+        self.audios_table = ExtFileContainerTable(context, request, self, _("Sounds"), 'audios')
+
+    def update(self):
+        super(ExtFileContainerView, self).update()
+        self.files_table.update()
+        self.images_table.update()
+        self.videos_table.update()
+        self.audios_table.update()
+
+
+class ExtFileContainerTable(BaseTable):
+    """External files container table"""
+
+    hide_toolbar = True
+    cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
+
+    def __init__(self, context, request, view, title, property):
+        super(ExtFileContainerTable, self).__init__(context, request)
+        self.view = view
+        self.title = title
+        self.files_property = property
+        self.object_data = {'ams-widget-toggle-button': 'false'}
+        alsoProvides(self, IObjectData)
+
+    @property
+    def data_attributes(self):
+        attributes = super(ExtFileContainerTable, self).data_attributes
+        attributes['table'] = {'data-ams-location': absolute_url(IExtFileContainer(self.context), self.request),
+                               'data-ams-datatable-sort': 'false',
+                               'data-ams-datatable-pagination': 'false'}
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(ExtFileContainerTable, self).values)
+
+    def render(self):
+        if not self.values:
+            if self.files_property == 'files':
+                for table in (self.view.images_table, self.view.videos_table, self.view.audios_table):
+                    if table.values:
+                        return ''
+                translate = self.request.localizer.translate
+                return translate(_("No currently stored external file."))
+            else:
+                return ''
+        return super(ExtFileContainerTable, self).render()
+
+
+@adapter_config(name='name', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
+class ExtFileContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
+    """External files container name column"""
+
+    _header = _("Title")
+
+    weight = 10
+
+    def getValue(self, obj):
+        return II18n(obj).query_attribute('title', request=self.request)
+
+
+@adapter_config(name='filename', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
+class ExtFileContainerFilenameColumn(I18nColumn, GetAttrColumn):
+    """External file container filename column"""
+
+    _header = _("Filename")
+
+    weight = 15
+
+    def getValue(self, obj):
+        data = II18n(obj).query_attribute('data', request=self.request)
+        if data is not None:
+            return IFileInfo(data).filename
+        else:
+            return '--'
+
+
+@adapter_config(name='filesize', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
+class ExtFileContainerFileSizeColumn(I18nColumn, GetAttrColumn):
+    """External file container file size column"""
+
+    _header = _("Size")
+
+    weight = 20
+
+    def getValue(self, obj):
+        data = II18n(obj).query_attribute('data', request=self.request)
+        if data is not None:
+            result = get_human_size(IFile(data).get_size())
+            if IImage.providedBy(data):
+                result = '{0} ({1[0]}x{1[1]})'.format(result, IImage(data).get_image_size())
+            return result
+        else:
+            return 'N/A'
+
+
+@adapter_config(name='trash', context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IColumn)
+class ExtFileContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
+    """External files container trash column"""
+
+
+@adapter_config(context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerTable), provides=IValues)
+class ExtFileContainerValues(ContextRequestViewAdapter):
+    """External files container values"""
+
+    @property
+    def values(self):
+        return getattr(IExtFileContainer(self.context), self.view.files_property)
+
+
+@adapter_config(context=(IExtFileContainerTarget, IPyAMSLayer, ExtFileContainerView), provides=IPageHeader)
+class ExtFileHeaderAdapter(DefaultPageHeaderAdapter):
+    """External files container header adapter"""
+
+    back_url = '#properties.html'
+    icon_class = 'fa fa-fw fa-file-text-o'
+
+
+#
+# External files links edit form
+#
+
+@pagelet_config(name='extfile-links.html', context=IExtFileLinksContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class ExtFileLinksContainerLinksEditForm(AdminDialogEditForm):
+    """External file links container edit form"""
+
+    legend = _("Edit external files links")
+
+    fields = field.Fields(IExtFileLinksContainer)
+    fields['files'].widgetFactory = ExtFileLinkSelectFieldWidget
+
+    ajax_handler = 'extfile-links.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+
+@view_config(name='extfile-links.json', context=IExtFileLinksContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExtFileLinksContainerAJAXEditForm(AJAXEditForm, ExtFileLinksContainerLinksEditForm):
+    """External file links container edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/zmi/templates/container.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,16 @@
+<div class="ams-widget">
+	<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"></h2>
+		<tal:var content="structure provider:pyams.widget_title" />
+		<tal:var content="structure provider:pyams.toolbar" />
+	</header>
+	<div class="widget-body no-widget-toolbar">
+		<tal:var content="structure view.files_table.render()" />
+		<tal:var content="structure view.images_table.render()" />
+		<tal:var content="structure view.videos_table.render()" />
+		<tal:var content="structure view.audios_table.render()" />
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/zmi/templates/widget-display.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,22 @@
+<input type="hidden" autocomplete="off" readonly
+	data-ams-select2-multiple="true"
+	tal:attributes="id view/id;
+					name view/name;
+					class string:select2 ${view/klass} ordered;
+					style view/style;
+					title view/title;
+					value python:','.join(view.value);
+					lang view/lang;
+					onclick view/onclick;
+					ondblclick view/ondblclick;
+					onmousedown view/onmousedown;
+					onmouseup view/onmouseup;
+					onmouseover view/onmouseover;
+					onmousemove view/onmousemove;
+					onmouseout view/onmouseout;
+					onkeypress view/onkeypress;
+					onkeydown view/onkeydown;
+					onkeyup view/onkeyup;
+					disabled view/disabled;
+					tabindex view/tabindex;
+					data-ams-select2-values view/values_map;" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/zmi/templates/widget-input.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,44 @@
+<label class="input bordered with-icon" i18n:domain="pyams_content"
+	   data-ams-plugins="pyams_content"
+	   data-ams-plugin-pyams_content-src="/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js">
+	<i class="icon-append fa fa-plus-square txt-color-green hint opaque"
+		title="Add external file" i18n:attributes="title"
+		data-ams-url="add-extfile.html?origin=link" data-toggle="modal"
+		tal:attributes="data-ams-select2-target string:${view/name}:list"></i>
+	<div class="select2-parent">
+		<select class="select2 ordered"
+				data-ams-select2-allow-clear="true"
+				tal:attributes="id view/id;
+								name string:${view/name}:list;
+								class string:${view/klass} select2 ordered;
+								style view/style;
+								title view/title;
+								lang view/lang;
+								onclick view/onclick;
+								ondblclick view/ondblclick;
+								onmousedown view/onmousedown;
+								onmouseup view/onmouseup;
+								onmouseover view/onmouseover;
+								onmousemove view/onmousemove;
+								onmouseout view/onmouseout;
+								onkeypress view/onkeypress;
+								onkeydown view/onkeydown;
+								onkeyup view/onkeyup;
+								disabled view/disabled;
+								tabindex view/tabindex;
+								onfocus view/onfocus;
+								onblur view/onblur;
+								onchange view/onchange;
+								multiple view/multiple;
+								size view/size">
+			<option tal:repeat="entry view/selectedItems"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="entry/content"></option>
+			<option tal:repeat="entry view/notselectedItems"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="entry/content"></option>
+		</select>
+	</div>
+</label>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/extfile/zmi/widget.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+# import interfaces
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.widget import widgettemplate_config
+from z3c.form.browser.orderedselect import OrderedSelectWidget
+from z3c.form.widget import FieldWidget
+
+
+@widgettemplate_config(mode='input', template='templates/widget-input.pt', layer=IPyAMSLayer)
+@widgettemplate_config(mode='display', template='templates/widget-display.pt', layer=IPyAMSLayer)
+class ExtFileLinksSelectWidget(OrderedSelectWidget):
+    """External files links select widget"""
+
+    @property
+    def values_map(self):
+        result = {}
+        [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
+        return json.dumps(result)
+
+
+def ExtFileLinkSelectFieldWidget(field, request):
+    """External files links select widget factory"""
+    return FieldWidget(field, ExtFileLinksSelectWidget(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,183 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGalleryFileInfo, GALLERY_FILE_INFO_KEY, IGallery, IGalleryFile
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_file.interfaces import IMediaFile
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from pyams_i18n.interfaces import II18n
+from zope.annotation.interfaces import IAnnotations
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_file.property import FileProperty
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.container import BTreeOrderedContainer
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+
+#
+# Gallery file
+#
+
+@implementer(IGalleryFileInfo)
+class GalleryFileInfo(Persistent, Contained):
+    """Gallery file info"""
+
+    title = FieldProperty(IGalleryFileInfo['title'])
+    description = FieldProperty(IGalleryFileInfo['description'])
+    author = FieldProperty(IGalleryFileInfo['author'])
+    author_comments = FieldProperty(IGalleryFileInfo['author_comments'])
+    sound = FileProperty(IGalleryFileInfo['sound'])
+    sound_title = FieldProperty(IGalleryFileInfo['sound_title'])
+    sound_description = FieldProperty(IGalleryFileInfo['sound_description'])
+    pif_number = FieldProperty(IGalleryFileInfo['pif_number'])
+    visible = FieldProperty(IGalleryFileInfo['visible'])
+
+    def get_title(self, request=None):
+        return II18n(self).query_attribute('title', request=request)
+
+
+@adapter_config(context=IGalleryFile, provides=IGalleryFileInfo)
+def media_gallery_info_factory(file):
+    """Gallery file gallery info factory"""
+    annotations = IAnnotations(file)
+    info = annotations.get(GALLERY_FILE_INFO_KEY)
+    if info is None:
+        info = annotations[GALLERY_FILE_INFO_KEY] = GalleryFileInfo()
+        get_current_registry().notify(ObjectCreatedEvent(info))
+        locate(info, file, '++gallery-info++')
+    return info
+
+
+@adapter_config(name='gallery-info', context=IGalleryFile, provides=ITraversable)
+class MediaGalleryInfoTraverser(ContextAdapter):
+    """Gallery file gallery info adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return IGalleryFileInfo(self.context)
+
+
+@adapter_config(context=IGalleryFile, provides=IFormContextPermissionChecker)
+class GalleryFilePermissionChecker(ContextAdapter):
+    """Gallery file permission checker"""
+
+    @property
+    def edit_permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return IFormContextPermissionChecker(content).edit_permission
+
+
+@adapter_config(context=IGalleryFileInfo, provides=IFormContextPermissionChecker)
+class GalleryFileInfoPermissionChecker(ContextAdapter):
+    """Gallery file info permission checker"""
+
+    @property
+    def edit_permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IMediaFile)
+def handle_added_media(event):
+    """Handle added media file"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IMediaFile)
+def handle_modified_media(event):
+    """Handle modified media file"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IMediaFile)
+def handle_removed_media(event):
+    """Handle removed media file"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+#
+# Gallery
+#
+
+@implementer(IGallery)
+class Gallery(BTreeOrderedContainer):
+    """Gallery persistent class"""
+
+    title = FieldProperty(IGallery['title'])
+    description = FieldProperty(IGallery['description'])
+    visible = FieldProperty(IGallery['visible'])
+
+    last_id = 1
+
+    def __setitem__(self, key, value):
+        key = str(self.last_id)
+        super(Gallery, self).__setitem__(key, value)
+        self.last_id += 1
+
+    def get_visible_images(self):
+        return [image for image in self.values() if image.visible]
+
+
+@adapter_config(context=IGallery, provides=IFormContextPermissionChecker)
+class GalleryPermissionChecker(ContextAdapter):
+    """Gallery permission checker"""
+
+    @property
+    def edit_permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IGallery)
+def handle_added_gallery(event):
+    """Handle added gallery"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IGallery)
+def handle_modified_gallery(event):
+    """Handle modified gallery"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IGallery)
+def handle_removed_gallery(event):
+    """Handle removed gallery"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,127 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGalleryContainer, IGalleryContainerTarget, \
+    GALLERY_CONTAINER_KEY, IGalleryLinksContainer, IGalleryLinksContainerTarget, GALLERY_LINKS_CONTAINER_KEY
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.schema.interfaces import IVocabularyFactory
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from persistent.list import PersistentList
+from pyams_i18n.interfaces import II18n
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.interface import implementer, provider
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+
+#
+# Galleries container
+#
+
+@implementer(IGalleryContainer)
+class GalleryContainer(Folder):
+    """Galleries container"""
+
+    last_id = 1
+
+    def __setitem__(self, key, value):
+        key = str(self.last_id)
+        super(GalleryContainer, self).__setitem__(key, value)
+        self.last_id += 1
+
+
+@adapter_config(context=IGalleryContainerTarget, provides=IGalleryContainer)
+def gallery_container_factory(target):
+    """Galleries container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(GALLERY_CONTAINER_KEY)
+    if container is None:
+        container = annotations[GALLERY_CONTAINER_KEY] = GalleryContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++gallery++')
+    return container
+
+
+@adapter_config(name='gallery', context=IGalleryContainerTarget, provides=ITraversable)
+class GalleryContainerNamespace(ContextAdapter):
+    """++gallery++ namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        return IGalleryContainer(self.context)
+
+
+@adapter_config(name='gallery', context=IGalleryContainerTarget, provides=ISublocations)
+class GalleryContainerSublocations(ContextAdapter):
+    """Galleries container sublocations"""
+
+    def sublocations(self):
+        return IGalleryContainer(self.context).values()
+
+
+@provider(IVocabularyFactory)
+class GalleryContainerGalleriesVocabulary(SimpleVocabulary):
+    """Galleries container galleries vocabulary"""
+
+    def __init__(self, context):
+        target = get_parent(context, IGalleryContainerTarget)
+        terms = [SimpleTerm(gallery.__name__, title=II18n(gallery).query_attribute('title'))
+                 for gallery in IGalleryContainer(target).values()]
+        super(GalleryContainerGalleriesVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS content galleries', GalleryContainerGalleriesVocabulary)
+
+
+#
+# Galleries links container
+#
+
+@implementer(IGalleryLinksContainer)
+class GalleryLinksContainer(Persistent, Contained):
+    """Galleries links container"""
+
+    def __init__(self):
+        self.galleries = PersistentList()
+
+
+@adapter_config(context=IGalleryLinksContainerTarget, provides=IGalleryLinksContainer)
+def gallery_links_container_factory(target):
+    """Galleries links container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(GALLERY_LINKS_CONTAINER_KEY)
+    if container is None:
+        container = annotations[GALLERY_LINKS_CONTAINER_KEY] = GalleryLinksContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++gallery-links++')
+    return container
+
+
+@adapter_config(name='gallery-links', context=IGalleryLinksContainerTarget, provides=ITraversable)
+class GalleryLinksContainerNamespace(ContextAdapter):
+    """++gallery-links++ namespace adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return IGalleryLinksContainer(self.context)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,129 @@
+#
+# 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_file.interfaces import IMediaFile
+from zope.container.interfaces import IContainer, IOrderedContainer
+
+# import packages
+from pyams_file.schema import FileField
+from pyams_i18n.schema import I18nTextLineField, I18nTextField
+from pyams_utils.schema import PersistentList
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.constraints import containers, contains
+from zope.interface import Interface
+from zope.schema import Choice, Bool, TextLine
+
+from pyams_content import _
+
+
+GALLERY_CONTAINER_KEY = 'pyams_content.gallery'
+GALLERY_FILE_INFO_KEY = 'pyams_content.gallery.info'
+GALLERY_LINKS_CONTAINER_KEY = 'pyams_content.gallery.links'
+
+
+class IGalleryFile(Interface):
+    """Gallery file marker interface"""
+
+
+class IGalleryFileInfo(Interface):
+    """Gallery file info"""
+
+    title = I18nTextLineField(title=_("Title"),
+                              required=False)
+
+    description = I18nTextField(title=_("Description"),
+                                required=False)
+
+    author = TextLine(title=_("Author"),
+                      required=False)
+
+    author_comments = I18nTextField(title=_("Author's comments"),
+                                    description=_("Comments relatives to author's rights management"),
+                                    required=False)
+
+    sound = FileField(title=_("Audio data"),
+                      description=_("Sound file associated with the current media"),
+                      required=False)
+
+    sound_title = I18nTextLineField(title=_("Sound title"),
+                                    description=_("Title of associated sound file"),
+                                    required=False)
+
+    sound_description = I18nTextField(title=_("Sound description"),
+                                      description=_("Short description of associated sound file"),
+                                      required=False)
+
+    pif_number = TextLine(title=_("PIF number"),
+                          description=_("Number used to identify media into national library database"),
+                          required=False)
+
+    visible = Bool(title=_("Visible image?"),
+                   description=_("If 'no', this image won't be displayed in front office"),
+                   required=True,
+                   default=True)
+
+
+class IBaseGallery(IOrderedContainer, IAttributeAnnotatable):
+    """Base gallery interface"""
+
+    containers('.IGalleryContainer')
+
+    title = I18nTextLineField(title=_("Title"),
+                              description=_("Gallery title, as shown in front-office"),
+                              required=True)
+
+    description = I18nTextField(title=_("Description"),
+                                description=_("Gallery description displayed by front-office template"),
+                                required=False)
+
+    visible = Bool(title=_("Visible gallery?"),
+                   description=_("If 'no', this gallery won't be displayed in front office"),
+                   required=True,
+                   default=True)
+
+    def get_visible_images(self):
+        """Get iterator over visible images"""
+
+
+class IGallery(IBaseGallery):
+    """Gallery interface"""
+
+    contains(IMediaFile)
+
+
+class IGalleryContainer(IContainer):
+    """Galleries container"""
+
+    contains(IBaseGallery)
+
+
+class IGalleryContainerTarget(Interface):
+    """Galleries container marker interface"""
+
+
+class IGalleryLinksContainer(Interface):
+    """Galleries links container interface"""
+
+    galleries = PersistentList(title=_("Contained galleries"),
+                               description=_("List of images galleries linked to this object"),
+                               value_type=Choice(vocabulary="PyAMS content galleries"),
+                               required=False)
+
+
+class IGalleryLinksContainerTarget(Interface):
+    """Galleries links container marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,181 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGalleryContainerTarget, IGallery, IGalleryContainer, \
+    IGalleryFile, IGalleryFileInfo
+from pyams_content.component.gallery.zmi.interfaces import IGalleryImageAddFields
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_file.interfaces.archive import IArchiveExtractor
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from z3c.form.interfaces import NOT_CHANGED
+
+# import packages
+from pyams_content.component.gallery import Gallery
+from pyams_content.component.gallery.zmi.container import GalleryContainerView
+from pyams_file.file import get_magic_content_type, FileFactory
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import alsoProvides
+from zope.lifecycleevent import ObjectCreatedEvent
+
+from pyams_content import _
+
+
+@viewlet_config(name='add-gallery.menu', context=IGalleryContainerTarget, view=GalleryContainerView,
+                layer=IPyAMSLayer, manager=IWidgetTitleViewletManager, weight=50)
+class GalleryAddMenu(ProtectedFormObjectMixin, ToolbarAction):
+    """Gallery add menu"""
+
+    label = _("Add gallery")
+
+    url = 'add-gallery.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-gallery.html', context=IGalleryContainerTarget, layer=IPyAMSLayer,
+                permission='pyams.ManageContent')
+class GalleryAddForm(AdminDialogAddForm):
+    """Gallery add form"""
+
+    legend = _("Add new images gallery")
+    icon_css_class = 'fa fa-fw fa-picture-o'
+
+    fields = field.Fields(IGallery).omit('__parent__', '__name__') + \
+             field.Fields(IGalleryImageAddFields)
+
+    @property
+    def ajax_handler(self):
+        origin = self.request.params.get('origin')
+        if origin == 'link':
+            return 'add-gallery-link.json'
+        else:
+            return 'add-gallery.json'
+
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(GalleryAddForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+        self.widgets['author_comments'].label_css_class = 'textarea'
+
+    def create(self, data):
+        gallery = Gallery()
+        images = data['images_data']
+        if images and (images is not NOT_CHANGED):
+            medias = []
+            if isinstance(images, (list, tuple)):
+                images = images[1]
+            if hasattr(images, 'seek'):
+                images.seek(0)
+            registry = self.request.registry
+            content_type = get_magic_content_type(images)
+            if hasattr(images, 'seek'):
+                images.seek(0)
+            extractor = query_utility(IArchiveExtractor, name=content_type.decode())
+            if extractor is not None:
+                extractor.initialize(images)
+                for content, filename in extractor.get_contents():
+                    media = FileFactory(content)
+                    registry.notify(ObjectCreatedEvent(media))
+                    medias.append(media)
+            else:
+                media = FileFactory(images)
+                registry.notify(ObjectCreatedEvent(media))
+                medias.append(media)
+            for media in medias:
+                alsoProvides(media, IGalleryFile)
+                IGalleryFileInfo(media).author = data.get('author')
+                IGalleryFileInfo(media).author_comments = data.get('author_comments')
+                gallery['none'] = media
+        return gallery
+
+    def update_content(self, content, data):
+        content.title = data.get('title')
+        content.description = data.get('description')
+        content.visible = data.get('visible')
+
+    def add(self, object):
+        IGalleryContainer(self.context)['none'] = object
+
+
+@view_config(name='add-gallery.json', context=IGalleryContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryAJAXAddForm(AJAXAddForm, GalleryAddForm):
+    """Gallery add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': '#galleries.html'}
+
+
+@view_config(name='add-gallery-link.json', context=IGalleryContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryLinkAJAXAddForm(AJAXAddForm, GalleryAddForm):
+    """Gallery link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        target = get_parent(self.context, IGalleryContainerTarget)
+        container = IGalleryContainer(target)
+        galleries = [{'id': gallery.__name__,
+                      'text': II18n(gallery).query_attribute('title', request=self.request)}
+                     for gallery in container.values()]
+        return {'status': 'callback',
+                'callback': 'PyAMS_content.galleries.refresh',
+                'options': {'galleries': galleries,
+                            'new_gallery': {'id': changes.__name__,
+                                            'text': II18n(changes).query_attribute('title', request=self.request)}}}
+
+
+@pagelet_config(name='properties.html', context=IGallery, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class GalleryPropertiesEditForm(AdminDialogEditForm):
+    """Gallery properties edit form"""
+
+    legend = _("Update gallery properties")
+    icon_css_class = 'fa fa-fw fa-picture-o'
+
+    fields = field.Fields(IGallery).omit('__parent__', '__file__')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(GalleryPropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IGallery, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryPropertiesAJAXEditForm(AJAXEditForm, GalleryPropertiesEditForm):
+    """Gallery properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'title' in changes.get(IGallery, ()):
+            return {'status': 'reload',
+                    'location': '#external-files.html'}
+        else:
+            return super(GalleryPropertiesAJAXEditForm, self).get_ajax_output(changes)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,198 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGalleryContainerTarget, IGalleryContainer, \
+    IGalleryLinksContainerTarget, IGalleryLinksContainer
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_utils.interfaces.data import IObjectData
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IColumn, IValues
+
+# import packages
+from pyams_content.component.gallery.zmi.widget import GalleryLinkSelectFieldWidget
+from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
+from pyams_form.form import AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.layer import IPyAMSLayer
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, ActionColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.decorator import reify
+from pyramid.view import view_config
+from z3c.table.column import GetAttrColumn
+from z3c.form import field
+from zope.interface import implementer, alsoProvides
+
+from pyams_content import _
+
+
+@viewlet_config(name='galleries.menu', context=IGalleryContainerTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=220)
+class GalleryContainerMenu(MenuItem):
+    """Galleries container menu"""
+
+    label = _("Images galleries...")
+    icon_class = 'fa-picture-o'
+    url = '#galleries.html'
+
+
+#
+# Galleries container views
+#
+
+@pagelet_config(name='galleries.html', context=IGalleryContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/container.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class GalleryContainerView(AdminView):
+    """Galleries container view"""
+
+    title = _("Galleries list")
+
+    def __init__(self, context, request):
+        super(GalleryContainerView, self).__init__(context, request)
+        self.galleries_table = GalleryContainerTable(context, request)
+
+    def update(self):
+        super(GalleryContainerView, self).update()
+        self.galleries_table.update()
+
+
+class GalleryContainerTable(BaseTable):
+    """Galleries container table"""
+
+    hide_header = True
+    cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
+
+    def __init__(self, context, request):
+        super(GalleryContainerTable, self).__init__(context, request)
+        self.object_data = {'ams-widget-toggle-button': 'false'}
+        alsoProvides(self, IObjectData)
+
+    @property
+    def data_attributes(self):
+        attributes = super(GalleryContainerTable, self).data_attributes
+        attributes['table'] = {'data-ams-location': absolute_url(IGalleryContainer(self.context), self.request),
+                               'data-ams-datatable-sort': 'false',
+                               'data-ams-datatable-pagination': 'false'}
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(GalleryContainerTable, self).values)
+
+    def render(self):
+        if not self.values:
+            translate = self.request.localizer.translate
+            return translate(_("No currently defined gallery."))
+        return super(GalleryContainerTable, self).render()
+
+
+@adapter_config(name='name', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
+class GalleryContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
+    """Galleries container name column"""
+
+    _header = _("Title")
+
+    weight = 10
+
+    def getValue(self, obj):
+        return II18n(obj).query_attribute('title', request=self.request)
+
+
+@adapter_config(name='count', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
+class GalleryContainerCountColumn(I18nColumn, GetAttrColumn):
+    """Gallery container images counter column"""
+
+    _header = _("Images")
+
+    weight = 20
+
+    def getValue(self, obj):
+        return len(obj)
+
+
+@adapter_config(name='manage', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
+class GalleryContainerManageColumn(ActionColumn):
+    """Gallery container manage column"""
+
+    icon_class = 'fa fa-fw fa-camera'
+    icon_hint = _("Display gallery contents")
+
+    url = 'contents.html'
+    target = None
+    modal_target = True
+
+    weight = 30
+
+
+@adapter_config(name='trash', context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IColumn)
+class GalleryContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
+    """Galleries container trash column"""
+
+
+@adapter_config(context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerTable), provides=IValues)
+class GalleryContainerValues(ContextRequestViewAdapter):
+    """Galleries container values"""
+
+    @property
+    def values(self):
+        return IGalleryContainer(self.context).values()
+
+
+@adapter_config(context=(IGalleryContainerTarget, IPyAMSLayer, GalleryContainerView), provides=IPageHeader)
+class GalleryHeaderAdapter(DefaultPageHeaderAdapter):
+    """Galleries container header adapter"""
+
+    back_url = '#properties.html'
+    icon_class = 'fa fa-fw fa-picture-o'
+
+
+#
+# Galleries links edit form
+#
+
+@pagelet_config(name='gallery-links.html', context=IGalleryLinksContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class GalleryLinksContainerLinksEditForm(AdminDialogEditForm):
+    """Galleries links container edit form"""
+
+    legend = _("Edit galleries links")
+
+    fields = field.Fields(IGalleryLinksContainer)
+    fields['galleries'].widgetFactory = GalleryLinkSelectFieldWidget
+
+    ajax_handler = 'gallery-links.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+
+@view_config(name='gallery-links.json', context=IGalleryLinksContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryLinksContainerAJAXEditForm(AJAXEditForm, GalleryLinksContainerLinksEditForm):
+    """Galleries links container edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/gallery.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,241 @@
+#
+# 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_content.component.gallery.interfaces import IGallery, IGalleryFileInfo, IGalleryFile
+from pyams_content.component.gallery.zmi.interfaces import IGalleryImageAddFields
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_file.interfaces.archive import IArchiveExtractor
+from pyams_form.interfaces.form import IWidgetsPrefixViewletsManager
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager, IContextActions
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from z3c.form.interfaces import NOT_CHANGED
+
+# import packages
+from pyams_content.shared.common.zmi import WfSharedContentPermissionMixin
+from pyams_file.file import get_magic_content_type, FileFactory
+from pyams_file.zmi.file import FilePropertiesAction
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarAction, ToolbarMenuDivider, JsToolbarMenuItem
+from pyams_template.template import template_config
+from pyams_utils.registry import query_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_zmi.form import AdminDialogEditForm, AdminDialogAddForm, AdminDialogDisplayForm
+from pyramid.renderers import render_to_response
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import alsoProvides, Interface
+from zope.lifecycleevent import ObjectCreatedEvent
+
+from pyams_content import _
+
+
+@pagelet_config(name='contents.html', context=IGallery, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class GalleryContentForm(AdminDialogDisplayForm):
+    """Gallery contents form"""
+
+    legend = _("Update gallery contents")
+    dialog_class = 'modal-max'
+
+    fields = field.Fields(Interface)
+    show_widget_title = True
+
+
+@viewlet_config(name='add-image.menu', context=IGallery, view=GalleryContentForm, manager=IWidgetTitleViewletManager)
+class GalleryImageAddMenu(WfSharedContentPermissionMixin, ToolbarAction):
+    """Gallery image add menu"""
+
+    label = _("Add image(s)")
+
+    url = 'add-image.html'
+    modal_target = True
+    stop_propagation = True
+
+
+@pagelet_config(name='add-image.html', context=IGallery, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION)
+class GalleryImageAddForm(AdminDialogAddForm):
+    """Gallery image add form"""
+
+    legend = _("Add image(s)")
+    icon_css_class = 'fa -fa-fw fa-picture-o'
+
+    fields = field.Fields(IGalleryImageAddFields)
+    ajax_handler = 'add-image.json'
+
+    def updateWidgets(self, prefix=None):
+        super(GalleryImageAddForm, self).updateWidgets(prefix)
+        self.widgets['author_comments'].label_css_class = 'textarea'
+
+    def create(self, data):
+        medias = []
+        images = data['images_data']
+        if images and (images is not NOT_CHANGED):
+            if isinstance(images, (list, tuple)):
+                images = images[1]
+            if hasattr(images, 'seek'):
+                images.seek(0)
+            registry = self.request.registry
+            content_type = get_magic_content_type(images)
+            if hasattr(images, 'seek'):
+                images.seek(0)
+            extractor = query_utility(IArchiveExtractor, name=content_type.decode())
+            if extractor is not None:
+                extractor.initialize(images)
+                for content, filename in extractor.get_contents():
+                    media = FileFactory(content)
+                    registry.notify(ObjectCreatedEvent(media))
+                    medias.append(media)
+            else:
+                media = FileFactory(images)
+                registry.notify(ObjectCreatedEvent(media))
+                medias.append(media)
+            for media in medias:
+                alsoProvides(media, IGalleryFile)
+                IGalleryFileInfo(media).author = data.get('author')
+                IGalleryFileInfo(media).author_comments = data.get('author_comments')
+                self.context['none'] = media
+        return None
+
+
+@view_config(name='add-image.json', context=IGallery, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryImageAJAXAddForm(AJAXAddForm, GalleryImageAddForm):
+    """Gallery image add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': absolute_url(self.context, self.request, 'get-gallery-images.html'),
+                'target': '#gallery-images'}
+
+
+@viewlet_config(name='gallery-images', context=IGallery, view=GalleryContentForm, manager=IWidgetsPrefixViewletsManager)
+@template_config(template='templates/gallery-images.pt', layer=IPyAMSLayer)
+class GalleryImagesViewlet(Viewlet):
+    """Gallery images viewlet"""
+
+    def get_info(self, image):
+        return IGalleryFileInfo(image)
+
+    def get_title(self, image):
+        return II18n(IGalleryFileInfo(image)).query_attribute('title', request=self.request)
+
+
+@view_config(name='get-gallery-images.html', context=IGallery, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION)
+class GalleryImagesView(WfSharedContentPermissionMixin):
+    """Gallery images view"""
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self):
+        return render_to_response('templates/gallery-images.pt', {'view': self}, request=self.request)
+
+    def get_info(self, image):
+        return IGalleryFileInfo(image)
+
+    def get_title(self, image):
+        return II18n(IGalleryFileInfo(image)).query_attribute('title', request=self.request)
+
+
+@view_config(name='set-images-order.json', context=IGallery, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_images_order(request):
+    """Set gallery images order"""
+    images_names = json.loads(request.params.get('images'))
+    request.context.updateOrder(images_names)
+    return {'status': 'success'}
+
+
+@viewlet_config(name='file.properties.action', context=IGalleryFile, layer=IPyAMSLayer, view=Interface,
+                manager=IContextActions, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+class GalleryFilePropertiesAction(FilePropertiesAction):
+    """Media properties action"""
+
+    url = 'gallery-file-properties.html'
+
+
+@pagelet_config(name='gallery-file-properties.html', context=IGalleryFile, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class GalleryFilePropertiesEditForm(AdminDialogEditForm):
+    """Gallery file properties edit form"""
+
+    legend = _("Update image properties")
+    icon_css_class = 'fa fa-fw fa-edit'
+
+    fields = field.Fields(IGalleryFileInfo)
+    ajax_handler = 'gallery-file-properties.json'
+
+    def getContent(self):
+        return IGalleryFileInfo(self.context)
+
+    @property
+    def title(self):
+        return II18n(self.getContent()).query_attribute('title', request=self.request)
+
+    def updateWidgets(self, prefix=None):
+        super(GalleryFilePropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+        self.widgets['author_comments'].label_css_class = 'textarea'
+        self.widgets['sound_description'].label_css_class = 'textarea'
+
+
+@view_config(name='gallery-file-properties.json', context=IGalleryFile, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class GalleryFileInfoPropertiesAJAXEditForm(AJAXEditForm, GalleryFilePropertiesEditForm):
+    """Gallery file properties edit form, JSON renderer"""
+
+
+@viewlet_config(name='gallery-file-remover.divider', context=IGalleryFile, layer=IPyAMSLayer, view=Interface,
+                manager=IContextActions, weight=89)
+class GalleryFileRemoverDivider(WfSharedContentPermissionMixin, ToolbarMenuDivider):
+    """Gallery file remover divider"""
+
+
+@viewlet_config(name='gallery-file-remover.action', context=IGalleryFile, layer=IPyAMSLayer, view=Interface,
+                manager=IContextActions, weight=90)
+class GalleryFileRemoverAction(WfSharedContentPermissionMixin, JsToolbarMenuItem):
+    """Gallery file remover action"""
+
+    label = _("Remove image...")
+    label_css_class = 'fa fa-fw fa-trash'
+
+    url = 'PyAMS_content.galleries.removeFile'
+
+
+@view_config(name='delete-element.json', context=IGallery, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def delete_gallery_element(request):
+    """Delete gallery element"""
+    translate = request.localizer.translate
+    name = request.params.get('object_name')
+    if not name:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("No provided object_name argument!"))}}
+    if name not in request.context:
+        return {'status': 'message',
+                'messagebox': {'status': 'error',
+                               'content': translate(_("Given image name doesn't exist!"))}}
+    del request.context[name]
+    return {'status': 'success'}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/interfaces.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from pyams_file.schema import FileField
+from pyams_i18n.schema import I18nTextField
+from zope.interface import Interface
+from zope.schema import TextLine
+
+from pyams_content import _
+
+
+class IGalleryImageAddFields(Interface):
+    """Gallery image add fields"""
+
+    author = TextLine(title=_("Author"),
+                      required=False)
+
+    author_comments = I18nTextField(title=_("Author comments"),
+                                    description=_("Comments relatives to author's rights management"),
+                                    required=False)
+
+    images_data = FileField(title=_("Images data"),
+                            description=_("You can upload a single file or choose to upload a whole ZIP archive"),
+                            required=False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/templates/container.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,13 @@
+<div class="ams-widget">
+	<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"></h2>
+		<tal:var content="structure provider:pyams.widget_title" />
+		<tal:var content="structure provider:pyams.toolbar" />
+	</header>
+	<div class="widget-body no-widget-toolbar">
+		<tal:var content="structure view.galleries_table.render()" />
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/templates/gallery-images.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,51 @@
+<div id="gallery-images" class="sortable gallery" i18n:domain="pyams_content"
+	 data-ams-plugins="pyams_content"
+	 data-ams-plugin-pyams_content-src="/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js"
+	 data-ams-plugin-pyams_content-async="false"
+	 data-ams-sortable-stop="PyAMS_content.galleries.setOrder"
+	 tal:attributes="data-ams-location extension:absolute_url(context);
+					 class '{0} gallery'.format('sortable' if request.has_permission(view.permission) else '');">
+	<div tal:repeat="image context.values()"
+		 class="image margin-5 margin-bottom-10 radius-4 padding-5 pull-left text-center"
+		 style="position: relative;"
+		 tal:attributes="data-ams-element-name image.__name__">
+		<a class="fancybox" data-toggle
+		   data-ams-fancybox-type="image"
+		   tal:define="thumbnails extension:thumbnails(image);
+					   target thumbnails.get_thumbnail('800x600', 'jpeg');
+					   info view.get_info(image);"
+		   tal:attributes="href extension:absolute_url(target);">
+			<i class="fa fa-fw fa-eye-slash txt-color-red pull-right opaque hint"
+			   style="position: absolute; right: 8px; top: 8px;"
+			   title="Hidden image" i18n:attributes="title"
+			   tal:condition="not:info.visible"></i>
+			<img class="thumbnail hint"
+				 data-ams-hint-gravity="s"
+				 tal:define="thumbnail thumbnails.get_thumbnail('128x128', 'jpeg');
+							 image_size thumbnail.get_image_size();
+							 margin_left 64 - image_size[0] / 2;
+							 margin_top 64 - image_size[1] / 2;"
+				 tal:attributes="src extension:absolute_url(thumbnail);
+								 title info.get_title(request);
+								 style string:margin-left: ${margin_left}px;; margin-right: ${margin_left}px;; margin-top: ${margin_top}px;; margin-bottom: ${margin_top}px;;" />
+		</a>
+		<div class="btn-group dropup margin-top-10"
+			 tal:define="actions extension:context_actions(image);"
+			 tal:omit-tag="not:actions">
+			<a class="btn btn-xs btn-default" target="download_window"
+			   tal:attributes="href extension:absolute_url(image)" i18n:translate="">
+				Download
+			</a>
+			<tal:if condition="actions">
+				<button class="btn btn-xs btn-primary dropdown-toggle" data-toggle="dropdown">
+					<i class="fa fa-caret-up"></i>
+				</button>
+				<ul class="dropdown-menu">
+					<tal:loop repeat="viewlet actions.viewlets"
+							  content="structure viewlet.render()" />
+				</ul>
+			</tal:if>
+		</div>
+		<span class="clearfix"></span>
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/templates/widget-display.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,22 @@
+<input type="hidden" autocomplete="off" readonly
+	data-ams-select2-multiple="true"
+	tal:attributes="id view/id;
+					name view/name;
+					class string:select2 ${view/klass} ordered;
+					style view/style;
+					title view/title;
+					value python:','.join(view.value);
+					lang view/lang;
+					onclick view/onclick;
+					ondblclick view/ondblclick;
+					onmousedown view/onmousedown;
+					onmouseup view/onmouseup;
+					onmouseover view/onmouseover;
+					onmousemove view/onmousemove;
+					onmouseout view/onmouseout;
+					onkeypress view/onkeypress;
+					onkeydown view/onkeydown;
+					onkeyup view/onkeyup;
+					disabled view/disabled;
+					tabindex view/tabindex;
+					data-ams-select2-values view/values_map;" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/templates/widget-input.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,44 @@
+<label class="input bordered with-icon" i18n:domain="pyams_content"
+	   data-ams-plugins="pyams_content"
+	   data-ams-plugin-pyams_content-src="/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js">
+	<i class="icon-append fa fa-plus-square txt-color-green hint opaque"
+		title="Add gallery" i18n:attributes="title"
+		data-ams-url="add-gallery.html?origin=link" data-toggle="modal"
+		tal:attributes="data-ams-select2-target string:${view/name}:list"></i>
+	<div class="select2-parent">
+		<select class="select2 ordered"
+				data-ams-select2-allow-clear="true"
+				tal:attributes="id view/id;
+								name string:${view/name}:list;
+								class string:${view/klass} select2 ordered;
+								style view/style;
+								title view/title;
+								lang view/lang;
+								onclick view/onclick;
+								ondblclick view/ondblclick;
+								onmousedown view/onmousedown;
+								onmouseup view/onmouseup;
+								onmouseover view/onmouseover;
+								onmousemove view/onmousemove;
+								onmouseout view/onmouseout;
+								onkeypress view/onkeypress;
+								onkeydown view/onkeydown;
+								onkeyup view/onkeyup;
+								disabled view/disabled;
+								tabindex view/tabindex;
+								onfocus view/onfocus;
+								onblur view/onblur;
+								onchange view/onchange;
+								multiple view/multiple;
+								size view/size">
+			<option tal:repeat="entry view/selectedItems"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="entry/content"></option>
+			<option tal:repeat="entry view/notselectedItems"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="entry/content"></option>
+		</select>
+	</div>
+</label>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/gallery/zmi/widget.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+# import interfaces
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.widget import widgettemplate_config
+from z3c.form.browser.orderedselect import OrderedSelectWidget
+from z3c.form.widget import FieldWidget
+
+
+@widgettemplate_config(mode='input', template='templates/widget-input.pt', layer=IPyAMSLayer)
+@widgettemplate_config(mode='display', template='templates/widget-display.pt', layer=IPyAMSLayer)
+class GalleryLinksSelectWidget(OrderedSelectWidget):
+    """Galleries links select widget"""
+
+    @property
+    def values_map(self):
+        result = {}
+        [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
+        return json.dumps(result)
+
+
+def GalleryLinkSelectFieldWidget(field, request):
+    """Galleries links select widget factory"""
+    return FieldWidget(field, GalleryLinksSelectWidget(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/illustration/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,129 @@
+#
+# 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 hypatia.interfaces import ICatalog
+from pyams_content.component.links.interfaces import IBaseLink, IInternalLink, IExternalLink
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Eq, Any
+from persistent import Persistent
+from pyams_catalog.query import CatalogResultSet
+from pyams_content.workflow import VISIBLE_STATES
+from pyams_sequence.utility import get_last_version
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import get_utility
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IBaseLink)
+class BaseLink(Persistent, Contained):
+    """Base link persistent class"""
+
+    title = FieldProperty(IBaseLink['title'])
+    description = FieldProperty(IBaseLink['description'])
+
+
+@adapter_config(context=IBaseLink, provides=IFormContextPermissionChecker)
+class BaseLinkPermissionChecker(ContextAdapter):
+    """Base link permission checker"""
+
+    @property
+    def edit_permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IBaseLink)
+def handle_added_link(event):
+    """Handle added link"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IBaseLink)
+def handle_modified_link(event):
+    """Handle modified link"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IBaseLink)
+def handle_removed_link(event):
+    """Handle removed link"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@implementer(IInternalLink)
+class InternalLink(BaseLink):
+    """Internal link persistent class"""
+
+    reference = FieldProperty(IInternalLink['reference'])
+
+    def get_target(self, state=None):
+        catalog = get_utility(ICatalog)
+        params = Eq(catalog['oid'], self.reference)
+        if state:
+            if not isinstance(state, (list, tuple)):
+                state = (state, )
+            params &= Any(catalog['workflow_state'], state)
+            results = list(CatalogResultSet(CatalogQuery(catalog).query(params)))
+            if results:
+                return results[0]
+        else:
+            results = list(map(get_last_version, CatalogResultSet(CatalogQuery(catalog).query(params))))
+            if results:
+                return results[0]
+
+    def get_editor_url(self):
+        return 'oid://{0}'.format(self.reference)
+
+    def get_url(self, request, view_name=None):
+        target = self.get_target(state=VISIBLE_STATES)
+        if target is not None:
+            return absolute_url(target, request, view_name)
+        else:
+            return ''
+
+
+@implementer(IExternalLink)
+class ExternalLink(BaseLink):
+    """external link persistent class"""
+
+    url = FieldProperty(IExternalLink['url'])
+    language = FieldProperty(IExternalLink['language'])
+
+    def get_editor_url(self):
+        return self.url
+
+    def get_url(self, request, view_name=None):
+        return self.url
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,123 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.links.interfaces import ILinkContainer, ILinkContainerTarget, LINK_CONTAINER_KEY, \
+    ILinkLinksContainer, LINK_LINKS_CONTAINER_KEY, ILinkLinksContainerTarget
+from pyams_i18n.interfaces import II18n
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.schema.interfaces import IVocabularyFactory
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from persistent.list import PersistentList
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.interface import implementer, provider
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+
+@implementer(ILinkContainer)
+class LinkContainer(Folder):
+    """Links container"""
+
+    last_id = 1
+
+    def __setitem__(self, key, value):
+        key = str(self.last_id)
+        super(LinkContainer, self).__setitem__(key, value)
+        self.last_id += 1
+
+
+@adapter_config(context=ILinkContainerTarget, provides=ILinkContainer)
+def link_container_factory(target):
+    """Links container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(LINK_CONTAINER_KEY)
+    if container is None:
+        container = annotations[LINK_CONTAINER_KEY] = LinkContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++links++')
+    return container
+
+
+@adapter_config(name='links', context=ILinkContainerTarget, provides=ITraversable)
+class LinkContainerNamespace(ContextAdapter):
+    """++links++ namespace adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return ILinkContainer(self.context)
+
+
+@adapter_config(name='links', context=ILinkContainerTarget, provides=ISublocations)
+class LinkContainerSublocations(ContextAdapter):
+    """Links container sublocations"""
+
+    def sublocations(self):
+        return ILinkContainer(self.context).values()
+
+
+@provider(IVocabularyFactory)
+class LinkContainerLinksVocabulary(SimpleVocabulary):
+    """Links container links vocabulary"""
+
+    def __init__(self, context):
+        target = get_parent(context, ILinkContainerTarget)
+        terms = [SimpleTerm(link.__name__, title=II18n(link).query_attribute('title'))
+                 for link in ILinkContainer(target).values()]
+        super(LinkContainerLinksVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS content links', LinkContainerLinksVocabulary)
+
+
+#
+# Link links container
+#
+
+@implementer(ILinkLinksContainer)
+class LinkLinksContainer(Persistent, Contained):
+    """Links links container"""
+
+    def __init__(self):
+        self.links = PersistentList()
+
+
+@adapter_config(context=ILinkLinksContainerTarget, provides=ILinkLinksContainer)
+def link_links_container_factory(target):
+    """Links links container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(LINK_LINKS_CONTAINER_KEY)
+    if container is None:
+        container = annotations[LINK_LINKS_CONTAINER_KEY] = LinkLinksContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++links-links++')
+    return container
+
+
+@adapter_config(name='links-links', context=ILinkLinksContainerTarget, provides=ITraversable)
+class LinkLinksContainerNamespace(ContextAdapter):
+    """++links-links++ namespace adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return ILinkLinksContainer(self.context)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,103 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer
+
+# import packages
+from pyams_i18n.schema import I18nTextLineField, I18nTextField
+from pyams_sequence.schema import InternalReference
+from pyams_utils.schema import PersistentList
+from zope.container.constraints import containers, contains
+from zope.interface import Interface, Attribute
+from zope.schema import Choice, TextLine
+
+from pyams_content import _
+
+
+LINK_CONTAINER_KEY = 'pyams_content.link'
+LINK_LINKS_CONTAINER_KEY = 'pyams_content.link.links'
+
+
+class IBaseLink(IAttributeAnnotatable):
+    """Base link interface"""
+
+    containers('.ILinkContainer')
+
+    title = I18nTextLineField(title=_("Title"),
+                              description=_("Link title, as shown in front-office"),
+                              required=True)
+
+    description = I18nTextField(title=_("Description"),
+                                description=_("Link description displayed by front-office template"),
+                                required=False)
+
+    def get_editor_url(self):
+        """Get URL for use in HTML editor"""
+
+    def get_url(self, request, view_name=None):
+        """Get link URL"""
+
+
+class IInternalLink(IBaseLink):
+    """Internal link interface"""
+
+    reference = InternalReference(title=_("Internal reference"),
+                                  description=_("Internal link target reference. You can search a reference using "
+                                                "'+' followed by internal number, of by entering text matching "
+                                                "content title."),
+                                  required=True)
+
+    def get_target(self, state=None):
+        """Get reference target"""
+
+
+class IExternalLink(IBaseLink):
+    """External link interface"""
+
+    url = TextLine(title=_("Target URL"),
+                   description=_("URL used to access external resource"),
+                   required=True)
+
+    language = Choice(title=_("Language"),
+                      description=_("Language used in this remote resource"),
+                      vocabulary='PyAMS base languages',
+                      required=False)
+
+
+class ILinkContainer(IContainer):
+    """Links container"""
+
+    contains(IBaseLink)
+
+
+class ILinkContainerTarget(Interface):
+    """Links container marker interface"""
+
+
+class ILinkLinksContainer(Interface):
+    """Links links container interface"""
+
+    links = PersistentList(title=_("Contained links"),
+                           description=_("List of internal or external links linked to this object"),
+                           value_type=Choice(vocabulary="PyAMS content links"),
+                           required=False)
+
+
+class ILinkLinksContainerTarget(Interface):
+    """Links links container marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,250 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.links.interfaces import ILinkContainerTarget, IInternalLink, ILinkContainer, IBaseLink, \
+    IExternalLink
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
+# import packages
+from pyams_content.component.links import InternalLink, ExternalLink
+from pyams_content.component.links.zmi.container import LinkContainerView
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+
+from pyams_content import _
+
+
+#
+# Internal links views
+#
+
+@viewlet_config(name='add-internal-link.menu', context=ILinkContainerTarget, view=LinkContainerView,
+                layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=50)
+class InternalLinkAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+    """Internal link add menu"""
+
+    label = _("Add internal link")
+    label_css_class = 'fa fa-fw fa-link'
+
+    url = 'add-internal-link.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-internal-link.html', context=ILinkContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class InternalLinkAddForm(AdminDialogAddForm):
+    """Internal link add form"""
+
+    legend = _("Add new internal link")
+    icon_css_class = 'fa fa-fw fa-link'
+
+    fields = field.Fields(IInternalLink).omit('__parent__', '__name__')
+
+    @property
+    def ajax_handler(self):
+        origin = self.request.params.get('origin')
+        if origin == 'link':
+            return 'add-internal-link-link.json'
+        else:
+            return 'add-internal-link.json'
+
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(InternalLinkAddForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+    def create(self, data):
+        return InternalLink()
+
+    def add(self, object):
+        ILinkContainer(self.context)['none'] = object
+
+
+@view_config(name='add-internal-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class InternalLinkAJAXAddForm(AJAXAddForm, InternalLinkAddForm):
+    """Internal link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': '#links.html'}
+
+
+@view_config(name='add-internal-link-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class InternalLinkLinkAJAXAddForm(AJAXAddForm, InternalLinkAddForm):
+    """Internal link link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        target = get_parent(self.context, ILinkContainerTarget)
+        container = ILinkContainer(target)
+        links = [{'id': link.__name__,
+                  'text': II18n(link).query_attribute('title', request=self.request)}
+                 for link in container.values()]
+        return {'status': 'callback',
+                'callback': 'PyAMS_content.links.refresh',
+                'options': {'links': links,
+                            'new_link': {'id': changes.__name__,
+                                         'text': II18n(changes).query_attribute('title', request=self.request)}}}
+
+
+@pagelet_config(name='properties.html', context=IInternalLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class InternalLinkPropertiesEditForm(AdminDialogEditForm):
+    """Internal link properties edit form"""
+
+    legend = _("Edit link properties")
+    icon_css_class = 'fa fa-fw fa-link'
+
+    fields = field.Fields(IInternalLink).omit('__parent__', '__name__')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(InternalLinkPropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IInternalLink, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class InternalLinkPropertiesAJAXEditForm(AJAXEditForm, InternalLinkPropertiesEditForm):
+    """Internal link properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if ('title' in changes.get(IBaseLink, ())) or \
+           ('reference' in changes.get(IInternalLink, ())):
+            return {'status': 'reload',
+                    'location': '#links.html'}
+        else:
+            return super(InternalLinkPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# External links views
+#
+
+@viewlet_config(name='add-external-link.menu', context=ILinkContainerTarget, view=LinkContainerView,
+                layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=51)
+class ExternalLinkAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+    """External link add menu"""
+
+    label = _("Add external link")
+    label_css_class = 'fa fa-fw fa-external-link'
+
+    url = 'add-external-link.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-external-link.html', context=ILinkContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class ExternalLinkAddForm(AdminDialogAddForm):
+    """External link add form"""
+
+    legend = _("Add new External link")
+    icon_css_class = 'fa fa-fw fa-external-link'
+
+    fields = field.Fields(IExternalLink).omit('__parent__', '__name__')
+
+    @property
+    def ajax_handler(self):
+        origin = self.request.params.get('origin')
+        if origin == 'link':
+            return 'add-external-link-link.json'
+        else:
+            return 'add-external-link.json'
+
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(ExternalLinkAddForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+    def create(self, data):
+        return ExternalLink()
+
+    def add(self, object):
+        ILinkContainer(self.context)['none'] = object
+
+
+@view_config(name='add-external-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExternalLinkAJAXAddForm(AJAXAddForm, ExternalLinkAddForm):
+    """External link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': '#links.html'}
+
+
+@view_config(name='add-external-link-link.json', context=ILinkContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExternalLinkLinkAJAXAddForm(AJAXAddForm, ExternalLinkAddForm):
+    """External link link add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        target = get_parent(self.context, ILinkContainerTarget)
+        container = ILinkContainer(target)
+        links = [{'id': link.__name__,
+                  'text': II18n(link).query_attribute('title', request=self.request)}
+                 for link in container.values()]
+        return {'status': 'callback',
+                'callback': 'PyAMS_content.links.refresh',
+                'options': {'links': links,
+                            'new_link': {'id': changes.__name__,
+                                         'text': II18n(changes).query_attribute('title', request=self.request)}}}
+
+
+@pagelet_config(name='properties.html', context=IExternalLink, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class ExternalLinkPropertiesEditForm(AdminDialogEditForm):
+    """External link properties edit form"""
+
+    legend = _("Edit link properties")
+    icon_css_class = 'fa fa-fw fa-external-link'
+
+    fields = field.Fields(IExternalLink).omit('__parent__', '__name__')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(ExternalLinkPropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IExternalLink, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ExternalLinkPropertiesAJAXEditForm(AJAXEditForm, ExternalLinkPropertiesEditForm):
+    """External link properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if ('title' in changes.get(IBaseLink, ())) or \
+           ('reference' in changes.get(IExternalLink, ())):
+            return {'status': 'reload',
+                    'location': '#links.html'}
+        else:
+            return super(ExternalLinkPropertiesAJAXEditForm, self).get_ajax_output(changes)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,211 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.extfile.interfaces import IExtFileContainer, IExtFileContainerTarget
+from pyams_content.component.links.interfaces import ILinkContainerTarget, ILinkContainer, IInternalLink, \
+    ILinkLinksContainerTarget, ILinkLinksContainer
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_utils.interfaces.data import IObjectData
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IColumn, IValues
+
+# import packages
+from pyams_content.component.links.zmi.widget import LinkLinkSelectFieldWidget
+from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
+from pyams_form.form import AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_sequence.utility import get_sequence_dict
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, I18nColumn, TrashColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.view import view_config
+from pyramid.decorator import reify
+from z3c.form import field
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, alsoProvides, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='links.menu', context=ILinkContainerTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=210)
+class LinkContainerMenu(MenuItem):
+    """Links container menu"""
+
+    label = _("Useful links...")
+    icon_class = 'fa-link'
+    url = '#links.html'
+
+
+#
+# Links container views
+#
+
+@view_config(name='get-links-list.json', context=Interface, request_type=IPyAMSLayer,
+             renderer='json', xhr=True)
+def get_links_list(request):
+    """Get links list in JSON format for TinyMCE editor"""
+    result = []
+    target = get_parent(request.context, IExtFileContainerTarget)
+    if target is not None:
+        container = IExtFileContainer(target)
+        result.extend([{'title': II18n(file).query_attribute('title', request=request),
+                        'value': absolute_url(II18n(file).query_attribute('data', request=request), request)}
+                       for file in container.values()])
+    target = get_parent(request.context, ILinkContainerTarget)
+    if target is not None:
+        container = ILinkContainer(target)
+        result.extend([{'title': II18n(link).query_attribute('title', request=request),
+                        'value': link.get_editor_url()}
+                       for link in container.values()])
+    return sorted(result, key=lambda x: x['title'])
+
+
+@pagelet_config(name='links.html', context=ILinkContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/container.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class LinkContainerView(AdminView):
+    """Links container view"""
+
+    title = _("Useful links list")
+
+    def __init__(self, context, request):
+        super(LinkContainerView, self).__init__(context, request)
+        self.links_table = LinkContainerTable(context, request)
+
+    def update(self):
+        super(LinkContainerView, self).update()
+        self.links_table.update()
+
+
+class LinkContainerTable(BaseTable):
+    """Links container table"""
+
+    hide_header = True
+    cssClasses = {'table': 'table table-bordered table-striped table-hover table-tight'}
+
+    def __init__(self, context, request):
+        super(LinkContainerTable, self).__init__(context, request)
+        self.object_data = {'ams-widget-toggle-button': 'false'}
+        alsoProvides(self, IObjectData)
+
+    @property
+    def data_attributes(self):
+        attributes = super(LinkContainerTable, self).data_attributes
+        attributes['table'] = {'data-ams-location': absolute_url(ILinkContainer(self.context), self.request),
+                               'data-ams-datatable-sort': 'false',
+                               'data-ams-datatable-pagination': 'false'}
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(LinkContainerTable, self).values)
+
+    def render(self):
+        if not self.values:
+            translate = self.request.localizer.translate
+            return translate(_("No currently defined link."))
+        return super(LinkContainerTable, self).render()
+
+
+@adapter_config(name='name', context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IColumn)
+class LinkContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
+    """Links container name column"""
+
+    _header = _("Title")
+
+    weight = 10
+
+    def getValue(self, obj):
+        return II18n(obj).query_attribute('title', request=self.request)
+
+
+@adapter_config(name='target', context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IColumn)
+class LinkContainerTargetColumn(I18nColumn, GetAttrColumn):
+    """Links container target column"""
+
+    _header = _("Link target")
+
+    weight = 20
+
+    def getValue(self, obj):
+        if IInternalLink.providedBy(obj):
+            mapping = get_sequence_dict(obj.get_target())
+            return mapping['text']
+        else:
+            return obj.url
+
+
+@adapter_config(name='trash', context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IColumn)
+class LinkContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
+    """Links container trash column"""
+
+
+@adapter_config(context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerTable), provides=IValues)
+class LinkContainerValues(ContextRequestViewAdapter):
+    """Links container values"""
+
+    @property
+    def values(self):
+        return ILinkContainer(self.context).values()
+
+
+@adapter_config(context=(ILinkContainerTarget, IPyAMSLayer, LinkContainerView), provides=IPageHeader)
+class LinkHeaderAdapter(DefaultPageHeaderAdapter):
+    """Links container header adapter"""
+
+    back_url = '#properties.html'
+    icon_class = 'fa fa-fw fa-link'
+
+
+#
+# Links links edit form
+#
+
+@pagelet_config(name='link-links.html', context=ILinkLinksContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class LinkLinksContainerLinksEditForm(AdminDialogEditForm):
+    """Links links container edit form"""
+
+    legend = _("Edit useful links links")
+
+    fields = field.Fields(ILinkLinksContainer)
+    fields['links'].widgetFactory = LinkLinkSelectFieldWidget
+
+    ajax_handler = 'link-links.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+
+@view_config(name='link-links.json', context=ILinkLinksContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class LinkLinksContainerAJAXEditForm(AJAXEditForm, LinkLinksContainerLinksEditForm):
+    """Links links container edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/reverse.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,86 @@
+#
+# 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 hypatia.interfaces import ICatalog
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_content.shared.common.interfaces.zmi import ISiteRootDashboardTable
+from pyams_sequence.interfaces import ISequentialIdInfo
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_workflow.interfaces import IWorkflowVersions
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Eq
+from pyams_catalog.query import CatalogResultSet
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.table import BaseTable
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.list import unique
+from pyams_utils.registry import get_utility
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.view import AdminView
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@viewlet_config(name='reverse-links.menu', context=IWfSharedContent, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=40)
+class SequentialITargetReverseLinksMenu(MenuItem):
+    """Sequential ID target reverse links menu"""
+
+    label = _("Reverse links")
+    icon_class = 'fa-anchor'
+    url = '#reverse-links.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SequentialIdTargetReverseLinkTable(BaseTable):
+    """Sequential ID target reverse links table"""
+
+    title = _("Content's internal links")
+
+
+@adapter_config(context=(IWfSharedContent, IPyAMSLayer, SequentialIdTargetReverseLinkTable), provides=IValues)
+class SequentialIdTargetReverseLinkValues(ContextRequestViewAdapter):
+    """Sequential ID target reverse links values"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = Eq(catalog['link_reference'], ISequentialIdInfo(self.context).hex_oid)
+        return unique(map(lambda x: IWorkflowVersions(get_parent(x, IWfSharedContent)).get_last_versions(count=1)[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       sort_index='modified_date'))))
+
+
+@pagelet_config(name='reverse-links.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SequentialIdTargetReverseLinkView(AdminView, ContainerView):
+    """Sequential ID target reverse links view"""
+
+    table_class = SequentialIdTargetReverseLinkTable
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/templates/container.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,13 @@
+<div class="ams-widget">
+	<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"></h2>
+		<tal:var content="structure provider:pyams.widget_title" />
+		<tal:var content="structure provider:pyams.toolbar" />
+	</header>
+	<div class="widget-body no-widget-toolbar">
+		<tal:var content="structure view.links_table.render()" />
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/templates/widget-display.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,22 @@
+<input type="hidden" autocomplete="off" readonly
+	data-ams-select2-multiple="true"
+	tal:attributes="id view/id;
+					name view/name;
+					class string:select2 ${view/klass} ordered;
+					style view/style;
+					title view/title;
+					value python:','.join(view.value);
+					lang view/lang;
+					onclick view/onclick;
+					ondblclick view/ondblclick;
+					onmousedown view/onmousedown;
+					onmouseup view/onmouseup;
+					onmouseover view/onmouseover;
+					onmousemove view/onmousemove;
+					onmouseout view/onmouseout;
+					onkeypress view/onkeypress;
+					onkeydown view/onkeydown;
+					onkeyup view/onkeyup;
+					disabled view/disabled;
+					tabindex view/tabindex;
+					data-ams-select2-values view/values_map;" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/templates/widget-input.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,60 @@
+<label class="input bordered with-icon" i18n:domain="pyams_content"
+	   data-ams-plugins="pyams_content"
+	   data-ams-plugin-pyams_content-src="/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js">
+	<div class="btn-group icon-append">
+		<i class="fa fa-fw fa-bars txt-color-green opaque" data-toggle="dropdown"
+			tal:attributes="data-ams-select2-target string:${view/name}:list"></i>
+		<ul class="dropdown-menu pull-right">
+			<li class="small">
+				<a data-ams-url="add-internal-link.html?origin=link"
+				   data-ams-stop-propagation="true" data-toggle="modal">
+					<i class="fa fa-fw fa-link"></i>
+					<span i18n:translate="">Add internal link...</span>
+				</a>
+			</li>
+			<li class="small">
+				<a data-ams-url="add-external-link.html?origin=link"
+				   data-ams-stop-propagation="true" data-toggle="modal">
+					<i class="fa fa-fw fa-external-link"></i>
+					<span i18n:translate="">Add external link...</span>
+				</a>
+			</li>
+		</ul>
+	</div>
+	<div class="select2-parent">
+		<select class="select2 ordered"
+				data-ams-select2-allow-clear="true"
+				tal:attributes="id view/id;
+								name string:${view/name}:list;
+								class string:${view/klass} select2 ordered;
+								style view/style;
+								title view/title;
+								lang view/lang;
+								onclick view/onclick;
+								ondblclick view/ondblclick;
+								onmousedown view/onmousedown;
+								onmouseup view/onmouseup;
+								onmouseover view/onmouseover;
+								onmousemove view/onmousemove;
+								onmouseout view/onmouseout;
+								onkeypress view/onkeypress;
+								onkeydown view/onkeydown;
+								onkeyup view/onkeyup;
+								disabled view/disabled;
+								tabindex view/tabindex;
+								onfocus view/onfocus;
+								onblur view/onblur;
+								onchange view/onchange;
+								multiple view/multiple;
+								size view/size">
+			<option tal:repeat="entry view/selectedItems"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="entry/content"></option>
+			<option tal:repeat="entry view/notselectedItems"
+					tal:attributes="value entry/value;
+									selected python:entry['value'] in view.value;"
+					tal:content="entry/content"></option>
+		</select>
+	</div>
+</label>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/links/zmi/widget.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+# import interfaces
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.widget import widgettemplate_config
+from z3c.form.browser.orderedselect import OrderedSelectWidget
+from z3c.form.widget import FieldWidget
+
+
+@widgettemplate_config(mode='input', template='templates/widget-input.pt', layer=IPyAMSLayer)
+@widgettemplate_config(mode='display', template='templates/widget-display.pt', layer=IPyAMSLayer)
+class LinkLinksSelectWidget(OrderedSelectWidget):
+    """Links links select widget"""
+
+    @property
+    def values_map(self):
+        result = {}
+        [result.update({entry['value']: entry['content']}) for entry in self.selectedItems]
+        return json.dumps(result)
+
+
+def LinkLinkSelectFieldWidget(field, request):
+    """Links links select widget factory"""
+    return FieldWidget(field, LinkLinksSelectWidget(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,74 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IBaseParagraph, IHTMLParagraph
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IFormContextPermissionChecker
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectModifiedEvent
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IBaseParagraph)
+class BaseParagraph(Persistent, Contained):
+    """Base paragraph persistent class"""
+
+    title = FieldProperty(IBaseParagraph['title'])
+
+
+@adapter_config(context=IBaseParagraph, provides=IFormContextPermissionChecker)
+class BaseParagraphPermissionChecker(ContextAdapter):
+    """Paragraph permission checker"""
+
+    @property
+    def edit_permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return IFormContextPermissionChecker(content).edit_permission
+
+
+@subscriber(IObjectAddedEvent, context_selector=IBaseParagraph)
+def handle_added_paragraph(event):
+    """Handle added paragraph"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IBaseParagraph)
+def handle_modified_paragraph(event):
+    """Handle modified paragraph"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IBaseParagraph)
+def handle_removed_paragraph(event):
+    """Handle removed paragraph"""
+    content = get_parent(event.object, IWfSharedContent)
+    if content is not None:
+        get_current_registry().notify(ObjectModifiedEvent(content))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,71 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IParagraphContainer, IParagraphContainerTarget, \
+    PARAGRAPH_CONTAINER_KEY
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.container import BTreeOrderedContainer
+from pyramid.threadlocal import get_current_registry
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+
+
+@implementer(IParagraphContainer)
+class ParagraphContainer(BTreeOrderedContainer):
+    """Paragraphs container"""
+
+    last_id = 1
+
+    def __setitem__(self, key, value):
+        key = str(self.last_id)
+        super(ParagraphContainer, self).__setitem__(key, value)
+        self.last_id += 1
+
+
+@adapter_config(context=IParagraphContainerTarget, provides=IParagraphContainer)
+def paragraph_container_factory(target):
+    """Paragraphs container factory"""
+    annotations = IAnnotations(target)
+    container = annotations.get(PARAGRAPH_CONTAINER_KEY)
+    if container is None:
+        container = annotations[PARAGRAPH_CONTAINER_KEY] = ParagraphContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, target, '++paras++')
+    return container
+
+
+@adapter_config(name='paras', context=IParagraphContainerTarget, provides=ITraversable)
+class ParagraphContainerNamespace(ContextAdapter):
+    """++paras++ namespace adapter"""
+
+    def traverse(self, name, furtherpath=None):
+        return IParagraphContainer(self.context)
+
+
+@adapter_config(name='paras', context=IParagraphContainerTarget, provides=ISublocations)
+class ParagraphContainerSublocations(ContextAdapter):
+    """Paragraphs container sublocations"""
+
+    def sublocations(self):
+        return IParagraphContainer(self.context).values()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/html.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,38 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.extfile.interfaces import IExtFileLinksContainerTarget
+from pyams_content.component.gallery.interfaces import IGalleryLinksContainerTarget
+from pyams_content.component.links.interfaces import ILinkLinksContainerTarget
+from pyams_content.component.paragraph.interfaces import IHTMLParagraph
+
+# import packages
+from pyams_content.component.paragraph import BaseParagraph
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+#
+# HTML paragraph
+#
+
+@implementer(IHTMLParagraph, IExtFileLinksContainerTarget, ILinkLinksContainerTarget, IGalleryLinksContainerTarget)
+class HTMLParagraph(BaseParagraph):
+    """HTML paragraph"""
+
+    body = FieldProperty(IHTMLParagraph['body'])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/illustration.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,58 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IIllustrationParagraph, IIllustrationRenderer
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from pyams_content.component.paragraph import BaseParagraph
+from pyams_file.property import FileProperty
+from pyams_utils.request import check_request
+from zope.interface import implementer, provider
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import getVocabularyRegistry, SimpleVocabulary, SimpleTerm
+
+
+#
+# Illustration
+#
+
+@implementer(IIllustrationParagraph)
+class Illustration(BaseParagraph):
+    """Illustration class"""
+
+    data = FileProperty(IIllustrationParagraph['data'])
+    legend = FieldProperty(IIllustrationParagraph['legend'])
+    renderer = FieldProperty(IIllustrationParagraph['renderer'])
+
+
+@provider(IVocabularyFactory)
+class IllustrationRendererVocabulary(SimpleVocabulary):
+    """Illustration renderer utilities vocabulary"""
+
+    def __init__(self, context=None):
+        request = check_request()
+        translate = request.localizer.translate
+        registry = request.registry
+        context = Illustration()
+        terms = [SimpleTerm(name, title=translate(adapter.label))
+                 for name, adapter in sorted(registry.getAdapters((context, request), IIllustrationRenderer),
+                                             key=lambda x: x[1].weight)]
+        super(IllustrationRendererVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS illustration renderers', IllustrationRendererVocabulary)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,93 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IOrderedContainer
+from zope.contentprovider.interfaces import IContentProvider
+
+# import packages
+from pyams_file.schema import ImageField
+from pyams_i18n.schema import I18nTextLineField, I18nHTMLField
+from zope.container.constraints import containers, contains
+from zope.interface import Interface, Attribute
+from zope.schema import Choice
+
+from pyams_content import _
+
+
+PARAGRAPH_CONTAINER_KEY = 'pyams_content.paragraph'
+
+
+class IBaseParagraph(IAttributeAnnotatable):
+    """Base paragraph interface"""
+
+    containers('.IParagraphContainer')
+
+    title = I18nTextLineField(title=_("Title"),
+                              description=_("Paragraph title"),
+                              required=False)
+
+
+class IParagraphContainer(IOrderedContainer):
+    """Paragraphs container"""
+
+    contains(IBaseParagraph)
+
+
+class IParagraphContainerTarget(Interface):
+    """Paragraphs container marker interface"""
+
+
+class IParagraphSummary(IContentProvider):
+    """Paragraph summary renderer"""
+
+    language = Attribute("Summary language")
+
+
+#
+# HTML paragraph
+#
+
+class IHTMLParagraph(IBaseParagraph):
+    """HTML body paragraph"""
+
+    body = I18nHTMLField(title=_("Body"),
+                         required=True)
+
+
+#
+# Illustration
+#
+
+class IIllustrationRenderer(IContentProvider):
+    """Illustration renderer utility interface"""
+
+    label = Attribute("Renderer label")
+
+
+class IIllustrationParagraph(IBaseParagraph):
+    """Illustration paragraph"""
+
+    data = ImageField(title=_("Image data"),
+                      required=True)
+
+    legend = I18nTextLineField(title=_("Legend"),
+                               required=False)
+
+    renderer = Choice(title=_("Image style"),
+                      vocabulary='PyAMS illustration renderers')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,239 @@
+#
+# 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_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.component.extfile.interfaces import IExtFileLinksContainerTarget
+from pyams_content.component.gallery.interfaces import IGalleryLinksContainerTarget
+from pyams_content.component.links.interfaces import ILinkLinksContainerTarget
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IColumn, IValues
+
+# import packages
+from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, I18nColumn, TrashColumn, ActionColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.url import absolute_url
+from pyramid.view import view_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.view import AdminView
+from pyramid.decorator import reify
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@viewlet_config(name='paragraphs.menu', context=IParagraphContainerTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=100)
+class ParagraphsContainerMenu(MenuItem):
+    """Paragraphs container menu"""
+
+    label = _("Paragraphs...")
+    icon_class = 'fa-paragraph'
+    url = '#paragraphs.html'
+
+
+#
+# Paragraphs container view
+#
+
+@pagelet_config(name='paragraphs.html', context=IParagraphContainerTarget, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/container.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ParagraphContainerView(AdminView):
+    """Paragraphs container view"""
+
+    title = _("Paragraphs list")
+
+    def __init__(self, context, request):
+        super(ParagraphContainerView, self).__init__(context, request)
+        self.table = ParagraphContainerTable(context, request, self)
+
+    def update(self):
+        super(ParagraphContainerView, self).update()
+        self.table.update()
+
+
+class ParagraphContainerTable(ProtectedFormObjectMixin, BaseTable):
+    """Paragraphs container table"""
+
+    id = 'paragraphs_list'
+    hide_header = True
+    sortOn = None
+
+    def __init__(self, context, request, view):
+        super(ParagraphContainerTable, self).__init__(context, request)
+        self.view = view
+
+    @property
+    def cssClasses(self):
+        classes = ['table', 'table-bordered', 'table-striped', 'table-hover', 'table-tight']
+        permission = self.permission
+        if (not permission) or self.request.has_permission(permission, self.context):
+            classes.append('table-dnd')
+        return {'table': ' '.join(classes)}
+
+    @property
+    def data_attributes(self):
+        attributes = super(ParagraphContainerTable, self).data_attributes
+        del attributes['tr']['data-ams-url']
+        del attributes['tr']['data-toggle']
+        attributes['table'] = {'id': self.id,
+                               'data-ams-location': absolute_url(IParagraphContainer(self.context), self.request),
+                               'data-ams-tablednd-drop-target': 'set-paragraphs-order.json'}
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(ParagraphContainerTable, self).values)
+
+    def render(self):
+        if not self.values:
+            translate = self.request.localizer.translate
+            return translate(_("No currently defined paragraph."))
+        return super(ParagraphContainerTable, self).render()
+
+
+@adapter_config(name='properties', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerPropertiesColumn(ActionColumn):
+    """Paragraphs container properties column"""
+
+    icon_class = 'fa fa-fw fa-edit'
+    icon_hint = _("Paragraph properties")
+
+    url = 'properties.html'
+    modal_target = True
+
+    weight = 5
+
+
+@adapter_config(name='files', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerExtFileLinksColumn(ActionColumn):
+    """Paragraphs container external files links column"""
+
+    icon_class = 'fa fa-fw fa-file-text-o'
+    icon_hint = _("External files")
+
+    url = 'extfile-links.html'
+    modal_target = True
+
+    weight = 10
+
+    def renderCell(self, item):
+        if not IExtFileLinksContainerTarget.providedBy(item):
+            return ''
+        return super(ParagraphContainerExtFileLinksColumn, self).renderCell(item)
+
+
+@adapter_config(name='links', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerLinkLinksColumn(ActionColumn):
+    """Paragraphs container links links column"""
+
+    icon_class = 'fa fa-fw fa-link'
+    icon_hint = _("Useful links")
+
+    url = 'link-links.html'
+    modal_target = True
+
+    weight = 15
+
+    def renderCell(self, item):
+        if not ILinkLinksContainerTarget.providedBy(item):
+            return ''
+        return super(ParagraphContainerLinkLinksColumn, self).renderCell(item)
+
+
+@adapter_config(name='gallery', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerGalleryLinksColumn(ActionColumn):
+    """Paragraphs container gallery links column"""
+
+    icon_class = 'fa fa-fw fa-picture-o'
+    icon_hint = _("Images galleries")
+
+    url = 'gallery-links.html'
+    modal_target = True
+
+    weight = 20
+
+    def renderCell(self, item):
+        if not IGalleryLinksContainerTarget.providedBy(item):
+            return ''
+        return super(ParagraphContainerGalleryLinksColumn, self).renderCell(item)
+
+
+@adapter_config(name='name', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerTitleColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
+    """Paragraph container title column"""
+
+    _header = _("Title")
+
+    weight = 50
+
+    def getValue(self, obj):
+        return II18n(obj).query_attribute('title', request=self.request) or '--'
+
+
+@adapter_config(name='trash', context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable),
+                provides=IColumn)
+class ParagraphContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
+    """Paragraphs container trash column"""
+
+
+@adapter_config(context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerTable), provides=IValues)
+class ParagraphContainerValues(ContextRequestViewAdapter):
+    """Paragraphs container values"""
+
+    @property
+    def values(self):
+        return IParagraphContainer(self.context).values()
+
+
+@adapter_config(context=(IParagraphContainerTarget, IPyAMSLayer, ParagraphContainerView), provides=IPageHeader)
+class ParagraphHeaderAdapter(DefaultPageHeaderAdapter):
+    """Paragraphs container header adapter"""
+
+    back_url = '#properties.html'
+
+    icon_class = 'fa fa-fw fa-paragraph'
+
+
+@view_config(name='set-paragraphs-order.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_paragraphs_order(request):
+    """Update paragraphs order"""
+    container = IParagraphContainer(request.context)
+    order = list(map(str, json.loads(request.params.get('names'))))
+    container.updateOrder(order)
+    return {'status': 'success'}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/html.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,154 @@
+#
+# 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_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_template.template import template_config, ViewTemplate, get_view_template
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IHTMLParagraph, \
+    IParagraphContainer, IBaseParagraph, IParagraphSummary
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.component.paragraph.html import HTMLParagraph
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config, ContentProvider
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+
+from pyams_content import _
+
+
+#
+# HTML paragraph
+#
+
+@viewlet_config(name='add-html-paragraph.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
+                layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=50)
+class HTMLParagraphAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+    """HTML paragraph add menu"""
+
+    label = _("Add HTML paragraph...")
+    label_css_class = 'fa fa-fw fa-html5'
+    url = 'add-html-paragraph.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-html-paragraph.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class HTMLParagraphAddForm(AdminDialogAddForm):
+    """HTML paragraph add form"""
+
+    legend = _("Add new HTML paragraph")
+    dialog_class = 'modal-max'
+    icon_css_class = 'fa fa-fw fa-html5'
+    label_css_class = 'control-label col-md-2'
+    input_css_class = 'col-md-10'
+
+    fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__')
+    ajax_handler = 'add-html-paragraph.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(HTMLParagraphAddForm, self).updateWidgets(prefix)
+        self.widgets['body'].label_css_class = 'textarea'
+
+    def create(self, data):
+        return HTMLParagraph()
+
+    def add(self, object):
+        IParagraphContainer(self.context)['none'] = object
+
+
+@view_config(name='add-html-paragraph.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class HTMLParagraphAJAXAddForm(AJAXAddForm, HTMLParagraphAddForm):
+    """HTML paragraph add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': '#paragraphs.html'}
+
+
+@pagelet_config(name='properties.html', context=IHTMLParagraph, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class HTMLParagraphPropertiesEditForm(AdminDialogEditForm):
+    """HTML paragraph properties edit form"""
+
+    @property
+    def title(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return II18n(content).query_attribute('title', request=self.request)
+
+    legend = _("Edit paragraph properties")
+    dialog_class = 'modal-max'
+    icon_css_class = 'fa fa-fw fa-html5'
+    label_css_class = 'control-label col-md-2'
+    input_css_class = 'col-md-10'
+
+    fields = field.Fields(IHTMLParagraph).omit('__parent__', '__name__')
+    ajax_handler = 'properties.json'
+    edit_permission = 'pyams.ManageContent'
+
+    def updateWidgets(self, prefix=None):
+        super(HTMLParagraphPropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['body'].label_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IHTMLParagraph, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class HTMLParagraphPropertiesAJAXEditForm(AJAXEditForm, HTMLParagraphPropertiesEditForm):
+    """HTML paragraph properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'title' in changes.get(IBaseParagraph, ()):
+            return {'status': 'reload',
+                    'location': '#paragraphs.html'}
+        else:
+            return super(HTMLParagraphPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# HTML paragraph summary
+#
+
+@adapter_config(context=(IHTMLParagraph, IPyAMSLayer), provides=IParagraphSummary)
+@template_config(template='templates/html-summary.pt', layer=IPyAMSLayer)
+class HTMLParagraphSummary(ContextRequestAdapter):
+    """HTML paragraph renderer"""
+
+    language = None
+
+    def update(self):
+        i18n = II18n(self.context)
+        if self.language:
+            for attr in ('title', 'body'):
+                setattr(self, attr, i18n.get_attribute(attr, self.language, request=self.request))
+        else:
+            for attr in ('title', 'body'):
+                setattr(self, attr, i18n.query_attribute(attr, request=self.request))
+
+    render = get_view_template()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/illustration.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,192 @@
+#
+# 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 pyramid.view import view_config
+from pyams_content.component.paragraph.illustration import Illustration
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IIllustrationParagraph, \
+    IParagraphContainer, IBaseParagraph, IParagraphSummary, IIllustrationRenderer
+from pyams_content.component.paragraph.zmi.container import ParagraphContainerView
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_i18n.interfaces import II18n
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config, get_view_template
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from z3c.form import field
+
+from pyams_content import _
+
+
+#
+# Illustration
+#
+
+@viewlet_config(name='add-illustration.menu', context=IParagraphContainerTarget, view=ParagraphContainerView,
+                layer=IPyAMSLayer, manager=IToolbarAddingMenu, weight=60)
+class IllustrationAddMenu(ProtectedFormObjectMixin, ToolbarMenuItem):
+    """Illustration add menu"""
+
+    label = _("Add illustration...")
+    label_css_class = 'fa fa-fw fa-file-image-o'
+    url = 'add-illustration.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-illustration.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class IllustrationAddForm(AdminDialogAddForm):
+    """Illustration add form"""
+
+    legend = _("Add new illustration")
+    dialog_class = 'modal-large'
+    icon_css_class = 'fa fa-fw fa-file-image-o'
+
+    fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__')
+    ajax_handler = 'add-illustration.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def create(self, data):
+        return Illustration()
+
+    def add(self, object):
+        IParagraphContainer(self.context)['none'] = object
+
+
+@view_config(name='add-illustration.json', context=IParagraphContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class IllustrationAJAXAddForm(AJAXAddForm, IllustrationAddForm):
+    """HTML paragraph add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload',
+                'location': '#paragraphs.html'}
+
+
+@pagelet_config(name='properties.html', context=IIllustrationParagraph, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class IllustrationPropertiesEditForm(AdminDialogEditForm):
+    """Illustration properties edit form"""
+
+    @property
+    def title(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return II18n(content).query_attribute('title', request=self.request)
+
+    legend = _("Edit illustration properties")
+    dialog_class = 'modal-large'
+    icon_css_class = 'fa fa-fw fa-file-image-o'
+
+    fields = field.Fields(IIllustrationParagraph).omit('__parent__', '__name__')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+
+@view_config(name='properties.json', context=IIllustrationParagraph, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class IllustrationPropertiesAJAXEditForm(AJAXEditForm, IllustrationPropertiesEditForm):
+    """HTML paragraph properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        if 'title' in changes.get(IBaseParagraph, ()):
+            return {'status': 'reload',
+                    'location': '#paragraphs.html'}
+        else:
+            return super(IllustrationPropertiesAJAXEditForm, self).get_ajax_output(changes)
+
+
+#
+# Illustration summary
+#
+
+@adapter_config(context=(IIllustrationParagraph, IPyAMSLayer), provides=IParagraphSummary)
+class IllustrationSummary(ContextRequestAdapter):
+    """Illustration renderer"""
+
+    def __init__(self, context, request):
+        super(IllustrationSummary, self).__init__(context, request)
+        self.renderer = request.registry.queryMultiAdapter((context, request), IIllustrationRenderer,
+                                                           name=self.context.renderer)
+
+    language = None
+
+    def update(self):
+        if self.renderer is not None:
+            self.renderer.language = self.language
+            self.renderer.update()
+
+    def render(self):
+        if self.renderer is not None:
+            return self.renderer.render()
+        else:
+            return ''
+
+
+#
+# Illustration renderers
+#
+
+class BaseIllustrationRenderer(ContextRequestAdapter):
+    """Base illustration renderer"""
+
+    language = None
+
+    def update(self):
+        i18n = II18n(self.context)
+        if self.language:
+            self.legend = i18n.get_attribute('legend', self.language, request=self.request)
+        else:
+            self.legend = i18n.query_attribute('legend', request=self.request)
+
+    render = get_view_template()
+
+
+@adapter_config(name='default', context=(IIllustrationParagraph, IPyAMSLayer), provides=IIllustrationRenderer)
+@template_config(template='templates/illustration.pt', layer=IPyAMSLayer)
+class DefaultIllustrationRenderer(BaseIllustrationRenderer):
+    """Default illustration renderer"""
+
+    label = _("Centered illustration")
+    weight = 1
+
+
+@adapter_config(name='left+zoom', context=(IIllustrationParagraph, IPyAMSLayer), provides=IIllustrationRenderer)
+@template_config(template='templates/illustration-left.pt', layer=IPyAMSLayer)
+class LeftIllustrationWithZoomRenderer(BaseIllustrationRenderer):
+    """Illustrtaion renderer with small image and zoom"""
+
+    label = _("Small illustration on the left with zoom")
+    weight = 2
+
+
+@adapter_config(name='right+zoom', context=(IIllustrationParagraph, IPyAMSLayer), provides=IIllustrationRenderer)
+@template_config(template='templates/illustration-right.pt', layer=IPyAMSLayer)
+class RightIllustrationWithZoomRenderer(BaseIllustrationRenderer):
+    """Illustrtaion renderer with small image and zoom"""
+
+    label = _("Small illustration on the right with zoom")
+    weight = 3
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/summary.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,69 @@
+#
+# 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_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \
+    IParagraphSummary
+from pyams_content.shared.common.interfaces.zmi import IInnerSummaryView
+from pyams_content.shared.common.zmi.summary import SharedContentSummaryForm
+from pyams_form.form import InnerDisplayForm
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_i18n.interfaces import II18nManager
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.layer import IPyAMSLayer
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@adapter_config(name='paragraphs-summary',
+                context=(IParagraphContainerTarget, IPyAMSLayer, SharedContentSummaryForm),
+                provides=IInnerTabForm)
+class ParagraphsContainerSummary(InnerDisplayForm):
+    """Paragraphs container summary"""
+
+    weight = 20
+    tab_label = _("Paragraphs")
+    tab_target = 'paragraphs-summary.html'
+
+    fields = field.Fields(Interface)
+
+
+@pagelet_config(name='paragraphs-summary.html', context=IParagraphContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/summary.pt', layer=IPyAMSLayer)
+@implementer(IInnerSummaryView)
+class ParagraphsContainerSummaryView(object):
+    """Paragraphs container summary view"""
+
+    def __init__(self, context, request):
+        super(ParagraphsContainerSummaryView, self).__init__(context, request)
+        self.paragraphs = IParagraphContainer(self.context)
+        self.languages = II18nManager(self.context).get_languages()
+
+    def render_paragraph(self, paragraph, language=None):
+        renderer = self.request.registry.queryMultiAdapter((paragraph, self.request), IParagraphSummary)
+        if renderer is not None:
+            renderer.language = language
+            renderer.update()
+            return renderer.render()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/container.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,13 @@
+<div class="ams-widget">
+	<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"></h2>
+		<tal:var content="structure provider:pyams.widget_title" />
+		<tal:var content="structure provider:pyams.toolbar" />
+	</header>
+	<div class="widget-body no-widget-toolbar">
+		<tal:var content="structure view.table.render()" />
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/html-summary.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,2 @@
+<h3 tal:content="view.title">title</h3>
+<div tal:content="structure view.body">body</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/illustration-left.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,11 @@
+<div class="pull-left margin-10">
+	<a class="fancybox" data-toggle
+	   data-ams-fancybox-type="image"
+	   tal:define="thumbnails extension:thumbnails(context.data);
+				   target thumbnails.get_thumbnail('800x600', 'png');
+				   thumb thumbnails.get_thumbnail('300x200', 'png');"
+	   tal:attributes="href extension:absolute_url(target)">
+		<img tal:attributes="src extension:absolute_url(thumb)" /><br />
+		<span tal:content="view.legend">legend</span>
+	</a><br />
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/illustration-right.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,11 @@
+<div class="pull-right margin-10">
+	<a class="fancybox" data-toggle
+	   data-ams-fancybox-type="image"
+	   tal:define="thumbnails extension:thumbnails(context.data);
+				   target thumbnails.get_thumbnail('800x600', 'png');
+				   thumb thumbnails.get_thumbnail('300x200', 'png');"
+	   tal:attributes="href extension:absolute_url(target)">
+		<img tal:attributes="src extension:absolute_url(thumb)" /><br />
+		<span tal:content="view.legend">legend</span>
+	</a><br />
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/illustration.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,6 @@
+<div class="text-center margin-y-5">
+	<img tal:define="thumbnails extension:thumbnails(context.data);
+					 target thumbnails.get_thumbnail('800x600', 'jpeg');"
+		 tal:attributes="src extension:absolute_url(target)" /><br />
+	<span tal:content="view.legend">legend</span>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/paragraph/zmi/templates/summary.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,35 @@
+<tal:var define="langs view.languages" i18n:domain="pyams_content">
+	<tal:if condition="len(langs) == 1">
+		<div class="margin-top-10">
+			<tal:loop repeat="paragraph view.paragraphs.values()"
+					  replace="structure view.render_paragraph(paragraph)" />
+			<p tal:condition="not:view.paragraphs.values()"
+			   i18n:translate="">This content doesn't contain any paragraph.</p>
+		</div>
+	</tal:if>
+	<tal:if condition="len(langs) > 1">
+		<ul class="nav nav-tabs margin-top-10">
+			<tal:loop repeat="lang langs">
+				<li tal:define="active python:'active' if repeat['lang'].start() else ''"
+					tal:attributes="class string:small ${active}">
+					<a data-toggle="tab" class="xsmall"
+					   tal:attributes="href string:#paragraphs-${lang}">
+						<img tal:attributes="src string:/--static--/pyams_i18n/img/flags/${lang}.png" />
+					</a>
+				</li>
+			</tal:loop>
+		</ul>
+		<div class="tab-content i18n-content bordered nohover">
+			<tal:loop repeat="lang langs">
+				<div tal:define="active python:'active' if repeat['lang'].start() else ''"
+					 tal:attributes="class string:clearfix tab-pane ${active} fade in padding-5;
+									 id string:paragraphs-${lang};">
+					<tal:loop repeat="paragraph view.paragraphs.values()"
+							  replace="structure view.render_paragraph(paragraph, lang)" />
+					<p tal:condition="not:view.paragraphs.values()"
+					   i18n:translate="">This content doesn't contain any paragraph.</p>
+				</div>
+			</tal:loop>
+		</div>
+	</tal:if>
+</tal:var>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,70 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.theme.interfaces import IThemesManagerTarget, IThemesManager, THEMES_MANAGER_KEY, IThemesInfo, \
+    IThemesTarget, THEMES_INFO_KEY
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config
+from pyramid.threadlocal import get_current_registry
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IThemesManager)
+class ThemesManager(Persistent, Contained):
+    """Themes manager persistent class"""
+
+    thesaurus_name = FieldProperty(IThemesManager['thesaurus_name'])
+    extract_name = FieldProperty(IThemesManager['extract_name'])
+
+
+@adapter_config(context=IThemesManagerTarget, provides=IThemesManager)
+def ThemesManagerFactory(target):
+    """Themes manager factory"""
+    annotations = IAnnotations(target)
+    manager = annotations.get(THEMES_MANAGER_KEY)
+    if manager is None:
+        manager = annotations[THEMES_MANAGER_KEY] = ThemesManager()
+        get_current_registry().notify(ObjectCreatedEvent(manager))
+        locate(manager, target, '++themes-manager++')
+    return manager
+
+
+@implementer(IThemesInfo)
+class ThemesInfo(Persistent, Contained):
+    """Themes info persistent class"""
+
+    themes = FieldProperty(IThemesInfo['themes'])
+
+
+@adapter_config(context=IThemesTarget, provides=IThemesInfo)
+def ThemesInfoFactory(target):
+    """Themes info factory"""
+    annotations = IAnnotations(target)
+    info = annotations.get(THEMES_INFO_KEY)
+    if info is None:
+        info = annotations[THEMES_INFO_KEY] = ThemesInfo()
+        get_current_registry().notify(ObjectCreatedEvent(info))
+        locate(info, target, '++themes++')
+    return info
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,48 @@
+#
+# 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_thesaurus.interfaces.thesaurus import IThesaurusContextManager, IThesaurusContextManagerTarget
+
+# import packages
+from pyams_thesaurus.schema import ThesaurusTermsListField
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+THEMES_MANAGER_KEY = 'pyams_content.themes.manager'
+THEMES_INFO_KEY = 'pyams_content.themes.info'
+
+
+class IThemesManager(IThesaurusContextManager):
+    """Themes manager interface"""
+
+
+class IThemesManagerTarget(IThesaurusContextManagerTarget):
+    """Marker interface for tools managing themes"""
+
+
+class IThemesInfo(Interface):
+    """Themes information interface"""
+
+    themes = ThesaurusTermsListField(title=_("Terms"),
+                                     required=False)
+
+
+class IThemesTarget(Interface):
+    """Themes target interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,109 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.theme.interfaces import IThemesTarget, IThemesInfo, IThemesManagerTarget, IThemesManager
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_form.interfaces.form import IWidgetForm
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_thesaurus.interfaces.thesaurus import IThesaurus
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_content.shared.common.zmi import WfSharedContentHeaderAdapter
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+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.viewlet import viewlet_config
+from pyams_zmi.form import AdminEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@viewlet_config(name='themes.menu', context=IThemesTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=350)
+class ThemesMenu(MenuItem):
+    """Themes menu"""
+
+    label = _("Themes...")
+    icon_class = 'fa-tags'
+    url = '#themes.html'
+
+
+@pagelet_config(name='themes.html', context=IThemesTarget, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/themes-info.pt', layer=IPyAMSLayer)
+@implementer(IWidgetForm, IInnerPage)
+class ThemesEditForm(AdminEditForm):
+    """Themes edit form"""
+
+    legend = _("Content themes")
+
+    fields = field.Fields(IThemesInfo)
+
+    ajax_handler = 'themes.json'
+
+    def __init__(self, context, request):
+        super(ThemesEditForm, self).__init__(context, request)
+        target = get_parent(self.context, IThemesManagerTarget)
+        manager = IThemesManager(target)
+        self.thesaurus_name = manager.thesaurus_name
+        self.extract_name = manager.extract_name
+
+    def updateWidgets(self, prefix=None):
+        super(ThemesEditForm, self).updateWidgets(prefix)
+        widget = self.widgets['themes']
+        widget.thesaurus_name = self.thesaurus_name
+        widget.extract_name = self.extract_name
+
+    @property
+    def top_terms(self):
+        thesaurus = query_utility(IThesaurus, name=self.thesaurus_name)
+        if thesaurus is not None:
+            return sorted(thesaurus.get_top_terms(extract=self.extract_name),
+                          key=lambda x: x.label)
+        else:
+            return ()
+
+    def get_subterms(self, term):
+        for subterm in term.specifics:
+            if self.extract_name in subterm.extracts:
+                yield subterm
+                for another in self.get_subterms(subterm):
+                    yield another
+
+
+@view_config(name='themes.json', context=IThemesTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class ThemesAJAXEditForm(AJAXEditForm, ThemesEditForm):
+    """Themes edit form, JSON renderer"""
+
+
+@adapter_config(context=(IThemesTarget, IAdminLayer, ThemesEditForm), provides=IPageHeader)
+class ThemesHeaderAdapter(WfSharedContentHeaderAdapter):
+    """Shared content themes header adapter"""
+
+    icon_class = 'fa fa-fw fa-tags'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/zmi/manager.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,76 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.theme.interfaces import IThemesManagerTarget, IThemesManager
+from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_utils.interfaces.data import IObjectData
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import alsoProvides
+
+from pyams_content import _
+
+
+@viewlet_config(name='themes-manager.menu', context=IThemesManagerTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=200)
+class ThemesManagerMenu(MenuItem):
+    """Themes menu"""
+
+    label = _("Themes...")
+    icon_class = 'fa-tags'
+    url = 'themes.html'
+    modal_target = True
+
+
+@pagelet_config(name='themes.html', context=IThemesManagerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class ThemesManagerEditForm(AdminDialogEditForm):
+    """Themes manager edit form"""
+
+    legend = _("Selected themes")
+
+    fields = field.Fields(IThemesManager)
+    ajax_handler = 'themes.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(ThemesManagerEditForm, self).updateWidgets(prefix)
+        widget = self.widgets['extract_name']
+        widget.object_data = {'ams-plugins': 'pyams_content',
+                              'ams-plugin-pyams_content-src':
+                                  '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
+                              'ams-plugin-pyams_content-callback': 'PyAMS_content.themes.initExtracts',
+                              'ams-plugin-pyams_content-async': 'false'}
+        alsoProvides(widget, IObjectData)
+
+
+@view_config(name='themes.json', context=IThemesManagerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class ThemesManagerAJAXEditForm(AJAXEditForm, ThemesManagerEditForm):
+    """Themes manager edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/component/theme/zmi/templates/themes-info.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,102 @@
+<div class="ams-widget">
+	<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.legend"></h2>
+		<tal:var content="structure provider:pyams.widget_title" />
+		<tal:var content="structure provider:pyams.toolbar" />
+	</header>
+	<div class="widget-body no-padding">
+		<div tal:define="prefix provider:form_prefix"
+			 tal:replace="structure prefix">Form prefix</div>
+		<tal:var content="structure provider:content_help" />
+		<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>
+					<div class="widgets-prefix"
+						 tal:define="prefix provider:widgets_prefix"
+						 tal:condition="prefix"
+						 tal:content="structure prefix">Widgets prefix</div>
+					<tal:loop repeat="term view.top_terms">
+						<div class="col col-xs-6 col-sm-4 col-md-4 col-lg-3">
+							<div class="ams-widget" data-ams-widget-toggle-button="false"
+								 tal:attributes="id string:them_${repeat['term'].index()}">
+								<header class="no-margin"><h2 tal:content="term.label"></h2></header>
+								<div class="widget-body no-padding viewport-y viewport-200 viewport-x-none"
+									 style="height: 200px; width: calc(100% - 2px);">
+									<tal:loop repeat="subterm view.get_subterms(term)">
+										<div tal:define="padding (subterm.level - 1) * 20"
+											 tal:attributes="style string:padding-left: ${padding}px;; line-height: 1em;;">
+											<input type="checkbox" name="form.widgets.themes:list"
+												   tal:attributes="id string:term_${subterm.label};
+																   value subterm.label;
+																   checked subterm.label in view.widgets['themes'].value" />
+											<label tal:attributes="for string:term_${subterm.label}"
+												   tal:content="subterm.label"></label>
+										</div>
+									</tal:loop>
+								</div>
+							</div>
+						</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}"
+								   tal:content="tabform.tab_label" i18n:translate="">Tab label</a>
+							</li>
+						</ul>
+						<div class="tab-content">
+							<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>
+			</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 tal:define="prefix provider:form_suffix"
+			 tal:replace="structure prefix">Form suffix</div>
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/configure.zcml	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,17 @@
+<configure
+	xmlns="http://pylonshq.com/pyramid"
+	xmlns:i18n="http://namespaces.zope.org/i18n"
+	i18n_domain="pyams_content">
+
+	<include package="pyramid_zcml" />
+
+	<include package="zope.i18n" file="meta.zcml" />
+
+	<i18n:registerTranslations directory="locales" />
+
+	<!-- KeyReference adapters -->
+	<adapter
+		factory="zope.keyreference.persistent.KeyReferenceToPersistent"
+		for="persistent.interfaces.IPersistent" />
+
+</configure>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/doctests/README.txt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+=====================
+pyams_content package
+=====================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/generations/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,140 @@
+#
+# 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_content.component.links.interfaces import IInternalLink
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_catalog.interfaces import DATE_RESOLUTION
+from pyams_content.interfaces import IBaseContent
+from pyams_content.root.interfaces import ISiteRootToolsConfiguration
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_utils.interfaces.site import ISiteGenerations
+from pyams_utils.interfaces.traversing import IPathElements
+from pyams_workflow.interfaces import IWorkflowState
+from zope.dublincore.interfaces import IZopeDublinCore
+from zope.site.interfaces import INewLocalSite
+
+# import packages
+from hypatia.text.lexicon import Lexicon
+from pyams_catalog.index import FieldIndexWithInterface, KeywordIndexWithInterface, DatetimeIndexWithInterface
+from pyams_catalog.nltk import NltkFullTextProcessor
+from pyams_catalog.site import check_required_indexes
+from pyams_content.shared.common.manager import SharedToolContainer
+from pyams_content.shared.news.manager import NewsManager
+from pyams_i18n.index import I18nTextIndexWithInterface
+from pyams_security.index import PrincipalsRoleIndex
+from pyams_utils.registry import utility_config
+from pyams_utils.site import check_required_utilities
+from pyramid.events import subscriber
+from pyramid.path import DottedNameResolver
+from pyramid.threadlocal import get_current_registry
+from zope.lifecycleevent import ObjectCreatedEvent
+
+
+def get_fulltext_lexicon(language):
+    return Lexicon(NltkFullTextProcessor(language=language))
+
+
+REQUIRED_UTILITIES = ()
+
+
+REQUIRED_INDEXES = [('content_type', FieldIndexWithInterface, {'interface': IBaseContent,
+                                                               'discriminator': 'content_type'}),
+                    ('role:owner', PrincipalsRoleIndex, {'role_id': 'pyams.Owner'}),
+                    ('role:pilot', PrincipalsRoleIndex, {'role_id': 'pyams.Pilot'}),
+                    ('role:manager', PrincipalsRoleIndex, {'role_id': 'pyams.Manager'}),
+                    ('role:contributor', PrincipalsRoleIndex, {'role_id': 'pyams.Contributor'}),
+                    ('parents', KeywordIndexWithInterface, {'interface': IPathElements,
+                                                            'discriminator': 'parents'}),
+                    ('workflow_state', FieldIndexWithInterface, {'interface': IWorkflowState,
+                                                                 'discriminator': 'state'}),
+                    ('workflow_principal', FieldIndexWithInterface, {'interface': IWorkflowState,
+                                                                     'discriminator': 'state_principal'}),
+                    ('modifiers', KeywordIndexWithInterface, {'interface': IWfSharedContent,
+                                                              'discriminator': 'modifiers'}),
+                    ('created_date', DatetimeIndexWithInterface, {'interface': IZopeDublinCore,
+                                                                  'discriminator': 'created',
+                                                                  'resolution': DATE_RESOLUTION}),
+                    ('modified_date', DatetimeIndexWithInterface, {'interface': IZopeDublinCore,
+                                                                   'discriminator': 'modified',
+                                                                   'resolution': DATE_RESOLUTION}),
+                    ('link_reference', FieldIndexWithInterface, {'interface': IInternalLink,
+                                                                 'discriminator': 'reference'})]
+
+
+def get_required_indexes():
+    indexes = REQUIRED_INDEXES
+    registry = get_current_registry()
+    for code, language in map(lambda x: x.split(':'),
+                              registry.settings.get('pyams_content.lexicon.languages', 'en:english').split()):
+        indexes.append(('title:{0}'.format(code), I18nTextIndexWithInterface,
+                        {'language': code,
+                         'interface': IBaseContent,
+                         'discriminator': 'title',
+                         'lexicon': lambda: get_fulltext_lexicon(language)}))
+    return indexes
+
+
+@subscriber(INewLocalSite)
+def handle_new_local_site(event):
+    """Check for required utilities when a site is created"""
+    site = event.manager.__parent__
+    check_required_utilities(site, REQUIRED_UTILITIES)
+    check_required_indexes(site, get_required_indexes())
+
+
+@utility_config(name='PyAMS content', provides=ISiteGenerations)
+class WebsiteGenerationsChecker(object):
+    """PyAMS content package generations checker"""
+
+    generation = 1
+
+    def evolve(self, site, current=None):
+        """Check for required utilities"""
+        check_required_utilities(site, REQUIRED_UTILITIES)
+        check_required_indexes(site, get_required_indexes())
+        registry = get_current_registry()
+        tools_configuration = ISiteRootToolsConfiguration(site)
+        # check tools manager
+        tools_name = tools_configuration.tools_name or \
+                     registry.settings.get('pyams_content.config.tools_name', 'tools')
+        if tools_name not in site:
+            tools_manager = SharedToolContainer()
+            registry.notify(ObjectCreatedEvent(tools_manager))
+            tools_manager.title = {'en': "Shared tools",
+                                   'fr': "Outils partagés"}
+            tools_manager.short_name = {'en': "Shared tools",
+                                        'fr': "Outils partagés"}
+            tools_manager.navigation_name = {'en': "Shared tools",
+                                             'fr': "Outils partagés"}
+            site[tools_name] = tools_manager
+            tools_configuration.tools_name = tools_name
+        else:
+            tools_manager = site[tools_name]
+        # check news shared tool
+        factory = registry.settings.get('pyams_content.config.news_tool_factory')
+        if (factory is None) or (factory.upper() not in ('NONE', '--')):
+            news_tool_name = tools_configuration.news_tool_name or \
+                             registry.settings.get('pyams_content.config.news_tool_name', 'news')
+            if news_tool_name not in tools_manager:
+                if factory is not None:
+                    factory = DottedNameResolver().resolve(factory)
+                else:
+                    factory = NewsManager
+                tool = factory()
+                registry.notify(ObjectCreatedEvent(tool))
+                tools_manager[news_tool_name] = tool
+                tools_configuration.news_tool_name = news_tool_name
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/include.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,38 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def include_package(config):
+    """Pyramid include"""
+
+    # add translations
+    config.add_translation_dirs('pyams_content:locales')
+
+    # load registry components
+    try:
+        import pyams_zmi
+    except ImportError:
+        config.scan(ignore='pyams_content.zmi')
+    else:
+        config.scan()
+
+    if hasattr(config, 'load_zcml'):
+        config.load_zcml('configure.zcml')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,72 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.location.interfaces import IContained
+
+# import packages
+from pyams_i18n.schema import I18nTextLineField
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.interface import Interface
+from zope.schema import Datetime, TextLine
+
+from pyams_content import _
+
+
+#
+# Custom permissions
+#
+
+MANAGE_SITE_ROOT_PERMISSION = 'pyams.ManageSiteRoot'
+MANAGE_SITE_PERMISSION = 'pyams.ManageSite'
+MANAGE_TOOL_PERMISSION = 'pyams.ManageTool'
+CREATE_CONTENT_PERMISSION = 'pyams.CreateContent'
+MANAGE_CONTENT_PERMISSION = 'pyams.ManageContent'
+COMMENT_CONTENT_PERMISSION = 'pyams.CommentContent'
+PUBLISH_CONTENT_PERMISSION = 'pyams.PublishContent'
+
+
+#
+# Base content interfaces
+#
+
+class IBaseContent(IContained, IAttributeAnnotatable):
+    """Base content interface"""
+
+    __name__ = TextLine(title=_("Unique key"),
+                        description=_("WARNING: this key can't be modified after creation!!!"),
+                        required=True)
+
+    title = I18nTextLineField(title=_("Title"),
+                              description=_("Visible label used to display content"),
+                              required=True)
+
+    short_name = I18nTextLineField(title=_("Short name"),
+                                   description=_("Short name used in breadcrumbs"),
+                                   required=True)
+
+
+class IBaseContentInfo(Interface):
+    """Base content info interface"""
+
+    created_date = Datetime(title=_("Creation date"),
+                            required=False,
+                            readonly=True)
+
+    modified_date = Datetime(title=_("Modification date"),
+                             required=False,
+                             readonly=False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/interfaces/container.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+
+# import interfaces
+from zope.container.interfaces import IContainer
+
+# import packages
+from zope.interface import Interface
+
+
+#
+# Containers interfaces
+#
+
+class IOrderedContainerOrder(Interface):
+    """Ordered containers interface"""
+
+    def updateOrder(self, order):
+        """Reset items in given order
+
+        @order: new ordered list of container's items keys
+        """
+
+    def moveFirst(self, key):
+        """Move item with given key to first position"""
+
+    def moveUp(self, key):
+        """Move item with given key one position up"""
+
+    def moveDown(self, key):
+        """Move item with given key one position down"""
+
+    def moveLast(self, key):
+        """Move item with given key to last position"""
+
+
+class IOrderedContainer(IContainer, IOrderedContainerOrder):
+    """Marker interface for ordered containers"""
Binary file src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1852 @@
+#
+# French translations for PACKAGE package
+# This file is distributed under the same license as the PACKAGE package.
+# Thierry Florac <tflorac@ulthar.net>, 2015.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-10-07 15:42+0200\n"
+"PO-Revision-Date: 2015-09-10 10:42+0200\n"
+"Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_content/__init__.py:33
+msgid "Manage site root"
+msgstr "Gérer l'ensemble du site"
+
+#: src/pyams_content/__init__.py:35
+msgid "Manage site"
+msgstr "Gérer un site"
+
+#: src/pyams_content/__init__.py:37
+msgid "Manage tool"
+msgstr "Gérer un outil"
+
+#: src/pyams_content/__init__.py:39
+msgid "Create content"
+msgstr "Créer un contenu"
+
+#: src/pyams_content/__init__.py:41
+msgid "Manage content"
+msgstr "Gérer un contenu"
+
+#: src/pyams_content/__init__.py:43
+msgid "Comment content"
+msgstr "Commenter un contenu"
+
+#: src/pyams_content/__init__.py:45 src/pyams_content/workflow/__init__.py:305
+msgid "Publish content"
+msgstr "Publier le contenu"
+
+#: src/pyams_content/__init__.py:49
+msgid "Webmaster (role)"
+msgstr "Webmestre (rôle)"
+
+#: src/pyams_content/__init__.py:57
+msgid "Pilot (role)"
+msgstr "Pilote (rôle)"
+
+#: src/pyams_content/__init__.py:65
+msgid "Manager (role)"
+msgstr "Responsable (rôle)"
+
+#: src/pyams_content/__init__.py:72
+msgid "Creator (role)"
+msgstr "Créateur (rôle)"
+
+#: src/pyams_content/__init__.py:77
+msgid "Contributor (role)"
+msgstr "Contributeur (rôle)"
+
+#: src/pyams_content/__init__.py:85
+msgid "Reader (role)"
+msgstr "Relecteur (rôle)"
+
+#: src/pyams_content/__init__.py:91
+msgid "Operator (role)"
+msgstr "Opérateur (rôle)"
+
+#: src/pyams_content/__init__.py:95
+msgid "Guest user (role)"
+msgstr "Invité (rôle)"
+
+#: src/pyams_content/component/gallery/zmi/__init__.py:55
+#: src/pyams_content/component/gallery/zmi/templates/widget-input.pt:5
+msgid "Add gallery"
+msgstr "Ajouter une galerie"
+
+#: src/pyams_content/component/gallery/zmi/__init__.py:66
+msgid "Add new images gallery"
+msgstr "Ajout d'une galerie d'images"
+
+#: src/pyams_content/component/gallery/zmi/__init__.py:159
+msgid "Update gallery properties"
+msgstr "Propriétés de la galerie d'images"
+
+#: src/pyams_content/component/gallery/zmi/container.py:60
+msgid "Images galleries..."
+msgstr "Galeries d'images..."
+
+#: src/pyams_content/component/gallery/zmi/container.py:76
+msgid "Galleries list"
+msgstr "Liste des galeries d'images"
+
+#: src/pyams_content/component/gallery/zmi/container.py:121
+#: src/pyams_content/component/gallery/interfaces/__init__.py:46
+#: src/pyams_content/component/gallery/interfaces/__init__.py:86
+#: src/pyams_content/component/extfile/zmi/container.py:167
+#: src/pyams_content/component/extfile/interfaces/__init__.py:41
+#: src/pyams_content/component/paragraph/zmi/container.py:200
+#: src/pyams_content/component/paragraph/interfaces/__init__.py:41
+#: src/pyams_content/component/links/zmi/container.py:144
+#: src/pyams_content/component/links/interfaces/__init__.py:42
+#: src/pyams_content/shared/common/zmi/templates/advanced-search.pt:187
+#: src/pyams_content/interfaces/__init__.py:54
+msgid "Title"
+msgstr "Titre"
+
+#: src/pyams_content/component/gallery/zmi/container.py:133
+#: src/pyams_content/component/extfile/zmi/container.py:112
+msgid "Images"
+msgstr "Images"
+
+#: src/pyams_content/component/gallery/zmi/container.py:146
+msgid "Display gallery contents"
+msgstr "Contenu de la galerie"
+
+#: src/pyams_content/component/gallery/zmi/container.py:186
+msgid "Edit galleries links"
+msgstr "Galeries d'images associées"
+
+#: src/pyams_content/component/gallery/zmi/container.py:113
+msgid "No currently defined gallery."
+msgstr "Aucune galerie d'images associée à ce contenu."
+
+#: src/pyams_content/component/gallery/zmi/gallery.py:56
+msgid "Update gallery contents"
+msgstr "Contenu de la galerie d'images"
+
+#: src/pyams_content/component/gallery/zmi/gallery.py:67
+#: src/pyams_content/component/gallery/zmi/gallery.py:78
+msgid "Add image(s)"
+msgstr "Ajouter des images"
+
+#: src/pyams_content/component/gallery/zmi/gallery.py:183
+#: src/pyams_content/component/extfile/zmi/__init__.py:186
+msgid "Update image properties"
+msgstr "Modifier les propriétés d'une image"
+
+#: src/pyams_content/component/gallery/zmi/gallery.py:220
+msgid "Remove image..."
+msgstr "Supprimer l'image..."
+
+#: src/pyams_content/component/gallery/zmi/gallery.py:235
+msgid "No provided object_name argument!"
+msgstr "Argument 'object_name' non fourni !"
+
+#: src/pyams_content/component/gallery/zmi/gallery.py:239
+msgid "Given image name doesn't exist!"
+msgstr "L'image spécifiée n'existe pas !"
+
+#: src/pyams_content/component/gallery/zmi/interfaces.py:32
+#: src/pyams_content/component/gallery/interfaces/__init__.py:52
+#: src/pyams_content/component/extfile/interfaces/__init__.py:49
+msgid "Author"
+msgstr "Auteur"
+
+#: src/pyams_content/component/gallery/zmi/interfaces.py:35
+msgid "Author comments"
+msgstr "À propos de l'auteur"
+
+#: src/pyams_content/component/gallery/zmi/interfaces.py:36
+#: src/pyams_content/component/gallery/interfaces/__init__.py:56
+msgid "Comments relatives to author's rights management"
+msgstr "Commentaires relatifs à l'auteur et à la gestion de ses droits"
+
+#: src/pyams_content/component/gallery/zmi/interfaces.py:39
+msgid "Images data"
+msgstr "Image(s) à ajouter"
+
+#: src/pyams_content/component/gallery/zmi/interfaces.py:40
+msgid "You can upload a single file or choose to upload a whole ZIP archive"
+msgstr ""
+"Vous pouvez déposer une simple image ou choisir de télécharger une archive "
+"au format ZIP"
+
+#: src/pyams_content/component/gallery/zmi/templates/gallery-images.pt:20
+msgid "Hidden image"
+msgstr "Image masquée"
+
+#: src/pyams_content/component/gallery/zmi/templates/gallery-images.pt:36
+msgid "Download"
+msgstr "Télécharger"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:49
+#: src/pyams_content/component/gallery/interfaces/__init__.py:90
+#: src/pyams_content/component/extfile/interfaces/__init__.py:45
+#: src/pyams_content/component/links/interfaces/__init__.py:46
+#: src/pyams_content/shared/common/interfaces/__init__.py:103
+msgid "Description"
+msgstr "Description"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:55
+msgid "Author's comments"
+msgstr "À propos de l'auteur"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:59
+msgid "Audio data"
+msgstr "Contenu audio"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:60
+msgid "Sound file associated with the current media"
+msgstr "Vous pouvez associer un fichier audio à cette image"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:63
+msgid "Sound title"
+msgstr "Titre du fichier audio"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:64
+msgid "Title of associated sound file"
+msgstr "Titre du fichier audio associé à cette image"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:67
+msgid "Sound description"
+msgstr "Description"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:68
+msgid "Short description of associated sound file"
+msgstr "Courte description du fichier audio associé à cette image"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:71
+msgid "PIF number"
+msgstr "Numéro PIF"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:72
+msgid "Number used to identify media into national library database"
+msgstr ""
+"Numéro utilisé pour identifier cette image dans la médiathèque nationale"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:75
+msgid "Visible image?"
+msgstr "Image visible ?"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:76
+msgid "If 'no', this image won't be displayed in front office"
+msgstr "Si 'non', cette image ne sera pas visible en front-office"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:87
+msgid "Gallery title, as shown in front-office"
+msgstr "Titre de la galerie affiché en front-office"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:91
+msgid "Gallery description displayed by front-office template"
+msgstr "Description de la galerie d'images affichée en front-office"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:94
+msgid "Visible gallery?"
+msgstr "Galerie visible ?"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:95
+msgid "If 'no', this gallery won't be displayed in front office"
+msgstr "Si 'non', cette galerie ne sera pas affichée en front-office"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:122
+msgid "Contained galleries"
+msgstr "Galeries d'images"
+
+#: src/pyams_content/component/gallery/interfaces/__init__.py:123
+msgid "List of images galleries linked to this object"
+msgstr "Liste des galeries d'images associées à cet objet"
+
+#: src/pyams_content/component/extfile/__init__.py:111
+msgid "Standard file"
+msgstr "Fichier standard"
+
+#: src/pyams_content/component/extfile/__init__.py:120
+msgid "Image"
+msgstr "Image"
+
+#: src/pyams_content/component/extfile/__init__.py:129
+msgid "Video"
+msgstr "Vidéo"
+
+#: src/pyams_content/component/extfile/__init__.py:138
+msgid "Audio file"
+msgstr "Fichier audio"
+
+#: src/pyams_content/component/extfile/zmi/__init__.py:66
+#: src/pyams_content/component/extfile/zmi/templates/widget-input.pt:5
+msgid "Add external file"
+msgstr "Ajouter un fichier joint"
+
+#: src/pyams_content/component/extfile/zmi/__init__.py:77
+msgid "Add new external file"
+msgstr "Ajouter un fichier joint"
+
+#: src/pyams_content/component/extfile/zmi/__init__.py:153
+msgid "Update file properties"
+msgstr "Modifier les propriétés d'un fichier"
+
+#: src/pyams_content/component/extfile/zmi/__init__.py:55
+msgid "External file type"
+msgstr "Type de fichier joint"
+
+#: src/pyams_content/component/extfile/zmi/container.py:63
+msgid "External files..."
+msgstr "Fichiers joints..."
+
+#: src/pyams_content/component/extfile/zmi/container.py:107
+msgid "External files list"
+msgstr "Liste des fichiers joints"
+
+#: src/pyams_content/component/extfile/zmi/container.py:179
+msgid "Filename"
+msgstr "Nom de fichier"
+
+#: src/pyams_content/component/extfile/zmi/container.py:195
+msgid "Size"
+msgstr "Taille"
+
+#: src/pyams_content/component/extfile/zmi/container.py:241
+msgid "Edit external files links"
+msgstr "Fichiers joints associés"
+
+#: src/pyams_content/component/extfile/zmi/container.py:111
+#: src/pyams_content/component/extfile/interfaces/__init__.py:101
+#: src/pyams_content/component/paragraph/zmi/container.py:144
+msgid "External files"
+msgstr "Fichiers joints"
+
+#: src/pyams_content/component/extfile/zmi/container.py:113
+msgid "Videos"
+msgstr "Vidéos"
+
+#: src/pyams_content/component/extfile/zmi/container.py:114
+msgid "Sounds"
+msgstr "Sons"
+
+#: src/pyams_content/component/extfile/zmi/container.py:157
+msgid "No currently stored external file."
+msgstr "Aucun fichier joint associé à ce contenu."
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:42
+msgid "File title, as shown in front-office"
+msgstr "Titre du fichier, tel qu'affiché en front-office"
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:46
+msgid "File description displayed by front-office template"
+msgstr "Description du fichier affichée en front-office"
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:50
+msgid "Name of document's author"
+msgstr "Nom de l'auteur du document"
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:57
+msgid "File data"
+msgstr "Contenu du fichier"
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:58
+msgid "File content"
+msgstr ""
+"Cliquez sur le bouton 'Parcourir...' pour sélectionner un nouveau contenu..."
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:69
+#: src/pyams_content/component/paragraph/interfaces/__init__.py:86
+msgid "Image data"
+msgstr "Contenu de l'image"
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:70
+msgid "Image content"
+msgstr ""
+"Cliquez sur le bouton 'Parcourir...' pour sélectionner un nouveau contenu..."
+
+#: src/pyams_content/component/extfile/interfaces/__init__.py:102
+msgid "List of external files linked to this object"
+msgstr "Liste des fichiers joints associés à cet objet"
+
+#: src/pyams_content/component/paragraph/zmi/summary.py:46
+msgid "Paragraphs"
+msgstr "Paragraphes"
+
+#: src/pyams_content/component/paragraph/zmi/container.py:58
+msgid "Paragraphs..."
+msgstr "Paragraphes..."
+
+#: src/pyams_content/component/paragraph/zmi/container.py:73
+msgid "Paragraphs list"
+msgstr "Liste des paragraphes"
+
+#: src/pyams_content/component/paragraph/zmi/container.py:130
+msgid "Paragraph properties"
+msgstr "Propriétés"
+
+#: src/pyams_content/component/paragraph/zmi/container.py:163
+msgid "Useful links"
+msgstr "Liens utiles"
+
+#: src/pyams_content/component/paragraph/zmi/container.py:182
+msgid "Images galleries"
+msgstr "Galeries d'images"
+
+#: src/pyams_content/component/paragraph/zmi/container.py:120
+msgid "No currently defined paragraph."
+msgstr "Aucun paragraphe associé à ce contenu."
+
+#: src/pyams_content/component/paragraph/zmi/illustration.py:54
+msgid "Add illustration..."
+msgstr "Illustration..."
+
+#: src/pyams_content/component/paragraph/zmi/illustration.py:65
+msgid "Add new illustration"
+msgstr "Ajout d'une illustration"
+
+#: src/pyams_content/component/paragraph/zmi/illustration.py:100
+msgid "Edit illustration properties"
+msgstr "Modifier les propriétés d'une illustration"
+
+#: src/pyams_content/component/paragraph/zmi/illustration.py:173
+msgid "Centered illustration"
+msgstr "Illustration centrée"
+
+#: src/pyams_content/component/paragraph/zmi/illustration.py:181
+msgid "Small illustration on the left with zoom"
+msgstr "Petite illustration sur la gauche avec zoom"
+
+#: src/pyams_content/component/paragraph/zmi/illustration.py:189
+msgid "Small illustration on the right with zoom"
+msgstr "Petite illustration sur la droite avec zoom"
+
+#: src/pyams_content/component/paragraph/zmi/html.py:54
+msgid "Add HTML paragraph..."
+msgstr "Paragraphe HTML..."
+
+#: src/pyams_content/component/paragraph/zmi/html.py:65
+msgid "Add new HTML paragraph"
+msgstr "Ajout d'un paragraphe HTML"
+
+#: src/pyams_content/component/paragraph/zmi/html.py:106
+msgid "Edit paragraph properties"
+msgstr "Modifier les propriétés d'un paragraphe"
+
+#: src/pyams_content/component/paragraph/zmi/templates/summary.pt:7
+#: src/pyams_content/component/paragraph/zmi/templates/summary.pt:30
+msgid "This content doesn't contain any paragraph."
+msgstr "Aucun paragraphe n'est associé à ce contenu."
+
+#: src/pyams_content/component/paragraph/interfaces/__init__.py:42
+msgid "Paragraph title"
+msgstr "Titre du paragraphe"
+
+#: src/pyams_content/component/paragraph/interfaces/__init__.py:69
+msgid "Body"
+msgstr "Contenu HTML"
+
+#: src/pyams_content/component/paragraph/interfaces/__init__.py:89
+msgid "Legend"
+msgstr "Légende"
+
+#: src/pyams_content/component/paragraph/interfaces/__init__.py:92
+msgid "Image style"
+msgstr "Style de l'image"
+
+#: src/pyams_content/component/theme/zmi/__init__.py:52
+#: src/pyams_content/component/theme/zmi/manager.py:45
+msgid "Themes..."
+msgstr "Thèmes..."
+
+#: src/pyams_content/component/theme/zmi/__init__.py:63
+msgid "Content themes"
+msgstr "Thèmes du contenu"
+
+#: src/pyams_content/component/theme/zmi/manager.py:56
+msgid "Selected themes"
+msgstr "Thèmes sélectionnés"
+
+#: src/pyams_content/component/theme/interfaces/__init__.py:43
+msgid "Terms"
+msgstr "Termes"
+
+#: src/pyams_content/component/links/zmi/__init__.py:52
+msgid "Add internal link"
+msgstr "Ajouter un lien interne"
+
+#: src/pyams_content/component/links/zmi/__init__.py:64
+msgid "Add new internal link"
+msgstr "Ajout d'un lien interne"
+
+#: src/pyams_content/component/links/zmi/__init__.py:122
+#: src/pyams_content/component/links/zmi/__init__.py:227
+msgid "Edit link properties"
+msgstr "Modifier les propriétés d'un lien utile"
+
+#: src/pyams_content/component/links/zmi/__init__.py:157
+msgid "Add external link"
+msgstr "Ajouter un lien externe"
+
+#: src/pyams_content/component/links/zmi/__init__.py:169
+msgid "Add new External link"
+msgstr "Ajuout d'un lien externe"
+
+#: src/pyams_content/component/links/zmi/container.py:63
+msgid "Useful links..."
+msgstr "Liens utiles..."
+
+#: src/pyams_content/component/links/zmi/container.py:99
+msgid "Useful links list"
+msgstr "Liste des liens utiles"
+
+#: src/pyams_content/component/links/zmi/container.py:156
+msgid "Link target"
+msgstr "Cible du lien"
+
+#: src/pyams_content/component/links/zmi/container.py:199
+msgid "Edit useful links links"
+msgstr "LIens utiles associés"
+
+#: src/pyams_content/component/links/zmi/container.py:136
+msgid "No currently defined link."
+msgstr "Aucun lien utile asocié à ce contenu."
+
+#: src/pyams_content/component/links/zmi/reverse.py:55
+msgid "Reverse links"
+msgstr "Liens amont"
+
+#: src/pyams_content/component/links/zmi/reverse.py:64
+msgid "Content's internal links"
+msgstr "Liens internes vers ce contenu"
+
+#: src/pyams_content/component/links/zmi/templates/widget-input.pt:12
+msgid "Add internal link..."
+msgstr "Ajouter un lien interne..."
+
+#: src/pyams_content/component/links/zmi/templates/widget-input.pt:19
+msgid "Add external link..."
+msgstr "Ajouter un lien externe..."
+
+#: src/pyams_content/component/links/interfaces/__init__.py:43
+msgid "Link title, as shown in front-office"
+msgstr "Titre du lien, tel qu'affiché en front-office"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:47
+msgid "Link description displayed by front-office template"
+msgstr "Description du lien, affichée en front-office"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:60
+msgid "Internal reference"
+msgstr "Référence interne"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:61
+msgid ""
+"Internal link target reference. You can search a reference using '+' "
+"followed by internal number, of by entering text matching content title."
+msgstr ""
+"Référence interne vers la cible du lien. Vous pouvez rechercher une "
+"référence en utilisant le '+' suivi du numéro interne, ou en indiquant des "
+"mots contenus dans son titre..."
+
+#: src/pyams_content/component/links/interfaces/__init__.py:73
+msgid "Target URL"
+msgstr "URL cible"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:74
+msgid "URL used to access external resource"
+msgstr "URL utilisée pour accéder à cette ressource externe"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:77
+msgid "Language"
+msgstr "Langue"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:78
+msgid "Language used in this remote resource"
+msgstr "Langue utilisée pour cette ressource extene"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:96
+msgid "Contained links"
+msgstr "Liens utiles"
+
+#: src/pyams_content/component/links/interfaces/__init__.py:97
+msgid "List of internal or external links linked to this object"
+msgstr "Liste des liens internes ou externes associés à cet objet"
+
+#: src/pyams_content/shared/common/zmi/search.py:73
+msgid "Quick search results"
+msgstr "Résultats de la recherche rapide"
+
+#: src/pyams_content/shared/common/zmi/search.py:143
+msgid "Advanced search"
+msgstr "Recherche avancée"
+
+#: src/pyams_content/shared/common/zmi/search.py:225
+msgid "Advanced search results"
+msgstr "Résultats de la recherche avancée"
+
+#: src/pyams_content/shared/common/zmi/search.py:118
+#: src/pyams_content/shared/common/zmi/dashboard.py:191
+msgid "Owner"
+msgstr "Propriétaire"
+
+#: src/pyams_content/shared/common/zmi/search.py:121
+#: src/pyams_content/shared/common/zmi/dashboard.py:153
+msgid "Status"
+msgstr "Statut"
+
+#: src/pyams_content/shared/common/zmi/search.py:125
+msgid "Created after..."
+msgstr "Créé entre le"
+
+#: src/pyams_content/shared/common/zmi/search.py:128
+msgid "Created before..."
+msgstr "et le"
+
+#: src/pyams_content/shared/common/zmi/search.py:131
+msgid "Modified after..."
+msgstr "Modifié entre le"
+
+#: src/pyams_content/shared/common/zmi/search.py:134
+msgid "Modified before..."
+msgstr "et le"
+
+#: src/pyams_content/shared/common/zmi/properties.py:54
+msgid "Composition"
+msgstr "Composition"
+
+#: src/pyams_content/shared/common/zmi/properties.py:64
+#: src/pyams_content/shared/common/zmi/manager.py:78
+msgid "Properties"
+msgstr "Propriétés"
+
+#: src/pyams_content/shared/common/zmi/properties.py:75
+msgid "Content properties"
+msgstr "Propriétés du contenu"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:66
+msgid "Workflow"
+msgstr "Workflow"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:164
+#: src/pyams_content/shared/common/zmi/workflow.py:235
+#: src/pyams_content/shared/common/zmi/workflow.py:280
+#: src/pyams_content/shared/common/zmi/workflow.py:338
+#: src/pyams_content/shared/common/zmi/workflow.py:411
+#: src/pyams_content/shared/common/zmi/workflow.py:471
+#: src/pyams_content/shared/common/zmi/workflow.py:516
+#: src/pyams_content/shared/common/zmi/workflow.py:562
+#: src/pyams_content/shared/common/zmi/workflow.py:622
+#: src/pyams_content/shared/common/zmi/workflow.py:667
+#: src/pyams_content/shared/common/zmi/workflow.py:713
+#: src/pyams_content/shared/common/zmi/workflow.py:765
+#: src/pyams_content/shared/common/zmi/__init__.py:225
+#: src/pyams_content/shared/common/zmi/owner.py:74
+msgid "Cancel"
+msgstr "Annuler"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:165
+msgid "Request publication"
+msgstr "Demander la publication"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:236
+#: src/pyams_content/workflow/__init__.py:251
+msgid "Cancel publication request"
+msgstr "Annuler la demande de publication"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:281
+msgid "Refuse publication request"
+msgstr "Refuser la demande de publication"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:339
+msgid "Publish"
+msgstr "Publier"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:412
+msgid "Request retire"
+msgstr "Demander le retrait"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:472
+msgid "Cancel retire request"
+msgstr "Annuler la demande de retrait"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:517
+msgid "Retire"
+msgstr "Retirer"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:563
+#: src/pyams_content/workflow/__init__.py:351
+msgid "Request archive"
+msgstr "Demander l'archivage"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:623
+msgid "Cancel archive request"
+msgstr "Annuler la demande d'archivage"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:668
+msgid "Archive"
+msgstr "Archiver"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:714
+#: src/pyams_content/workflow/__init__.py:410
+#: src/pyams_content/workflow/__init__.py:422
+#: src/pyams_content/workflow/__init__.py:434
+#: src/pyams_content/workflow/__init__.py:446
+#: src/pyams_content/workflow/__init__.py:458
+msgid "Create new version"
+msgstr "Créer une nouvelle version"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:766
+#: src/pyams_content/workflow/__init__.py:470
+msgid "Delete version"
+msgstr "Supprimer cette version"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:203
+#: src/pyams_content/shared/common/zmi/workflow.py:381
+msgid "Publication start date is required"
+msgstr "La date de début de publication est obligatoire"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:206
+#: src/pyams_content/shared/common/zmi/workflow.py:308
+#: src/pyams_content/shared/common/zmi/workflow.py:442
+#: src/pyams_content/shared/common/zmi/workflow.py:593
+msgid "A comment is required"
+msgstr "Le commentaire est obligatoire"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:130
+#, python-format
+msgid "{state} {date}"
+msgstr "{state} {date}"
+
+#: src/pyams_content/shared/common/zmi/workflow.py:127
+#, python-format
+msgid "{state} by {principal}"
+msgstr "{state} par {principal}"
+
+#: src/pyams_content/shared/common/zmi/__init__.py:174
+msgid "Manage this content"
+msgstr "Gérer ce contenu"
+
+#: src/pyams_content/shared/common/zmi/__init__.py:215
+msgid "Duplicate content..."
+msgstr "Dupliquer le contenu..."
+
+#: src/pyams_content/shared/common/zmi/__init__.py:234
+#: src/pyams_content/shared/common/zmi/__init__.py:226
+msgid "Duplicate content"
+msgstr "Duplication d'un contenu"
+
+#: src/pyams_content/shared/common/zmi/__init__.py:273
+#, python-format
+msgid "Duplicate content ({oid})"
+msgstr "Contenu dupliqué ({oid})"
+
+#: src/pyams_content/shared/common/zmi/__init__.py:318
+msgid "Created or modified in this version"
+msgstr "Créé ou modifié dans cette version"
+
+#: src/pyams_content/shared/common/zmi/summary.py:57
+msgid "Summary"
+msgstr "Récapitulatif"
+
+#: src/pyams_content/shared/common/zmi/summary.py:67
+msgid "Display content summary"
+msgstr "Récapitulatif des propriétés du contenu"
+
+#: src/pyams_content/shared/common/zmi/summary.py:91
+msgid "Identity card"
+msgstr "Carte d'identité"
+
+#: src/pyams_content/shared/common/zmi/manager.py:64
+msgid "Tool management"
+msgstr "Gérer l'outil partagé"
+
+#: src/pyams_content/shared/common/zmi/manager.py:88
+msgid "Shared tool properties"
+msgstr "Propriétés de l'outil"
+
+#: src/pyams_content/shared/common/zmi/manager.py:107
+msgid "WARNING"
+msgstr "ATTENTION"
+
+#: src/pyams_content/shared/common/zmi/manager.py:109
+msgid ""
+"Workflow shouldn't be modified if this tool already contains any shared "
+"content!"
+msgstr ""
+"Le workflow ne devrait pas être modifié si cet outil renferme déjà des "
+"contenus partagés !"
+
+#: src/pyams_content/shared/common/zmi/manager.py:132
+msgid "Content languages"
+msgstr "Langues proposées"
+
+#: src/pyams_content/shared/common/zmi/manager.py:149
+msgid ""
+"Tool languages are used to translate own tool properties, and newly created "
+"contents will propose these languages by default"
+msgstr ""
+"Les langues sont utilisées pour traduire les propriétés de l'outil.\n"
+"\n"
+"Les nouveaux contenus proposeront également ces langues par défaut."
+
+#: src/pyams_content/shared/common/zmi/owner.py:51
+msgid "Change owner..."
+msgstr "Changer de propriétaire..."
+
+#: src/pyams_content/shared/common/zmi/owner.py:83
+msgid "Change content's owner"
+msgstr "Changement de propriétaire"
+
+#: src/pyams_content/shared/common/zmi/owner.py:126
+msgid ""
+"All versions of this content which are not archived will be transferred to "
+"newly selected owner"
+msgstr ""
+"Toutes les versions non archivées de ce contenu seront transférées au "
+"nouveau propriétaire sélectionné"
+
+#: src/pyams_content/shared/common/zmi/owner.py:61
+msgid "New owner"
+msgstr "Nouveau propriétaire"
+
+#: src/pyams_content/shared/common/zmi/owner.py:62
+msgid "The selected user will become the new content's owner"
+msgstr "L'utilisateur sélectionné deviendra le nouveau propriétaire du contenu"
+
+#: src/pyams_content/shared/common/zmi/owner.py:64
+msgid "Keep previous owner as contributor"
+msgstr "L'ancien propriétaire reste contributeur"
+
+#: src/pyams_content/shared/common/zmi/owner.py:65
+msgid "If 'yes', the previous owner will still be able to modify this content"
+msgstr ""
+"Si 'oui', l'actuel propriétaire du contenu en restera contributeur et pourra "
+"donc continuer à le mettre à jour"
+
+#: src/pyams_content/shared/common/zmi/owner.py:75
+msgid "Change owner"
+msgstr "Changer le propriétaire"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:108
+msgid "Unique ID"
+msgstr "N° IN"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:121
+msgid "Version"
+msgstr "Version"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:133
+msgid "Urgent request !"
+msgstr "Sollicitation urgente !"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:166
+msgid "Status date"
+msgstr "En date du"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:178
+msgid "Status principal"
+msgstr "Intervenant"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:203
+msgid "Last modification"
+msgstr "Dernière modification"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:221
+#: src/pyams_content/root/zmi/__init__.py:75
+msgid "Dashboard"
+msgstr "Tableau de bord"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:232
+msgid "Contents dashboard"
+msgstr "Tableau de bord des contenus"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:266
+#: src/pyams_content/root/zmi/__init__.py:121
+#, python-format
+msgid "MANAGER - {0} content(s) waiting for your action"
+msgstr "RESPONSABLE - {0} contenu(s) en attente de votre intervention"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:307
+#: src/pyams_content/root/zmi/__init__.py:165
+#, python-format
+msgid "CONTRIBUTOR - Your {0} content(s) waiting for action"
+msgstr "CONTRIBUTEUR - {0} contenu(s) soumi(s) à un responsable"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:337
+#: src/pyams_content/root/zmi/__init__.py:198
+#, python-format
+msgid "CONTRIBUTOR - Your last modified contents (limited to {0})"
+msgstr "CONTRIBUTEUR - Vos derniers contenus modifiés (dans la limite de {0})"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:369
+#: src/pyams_content/root/zmi/__init__.py:232
+msgid "My contents"
+msgstr "Tous mes contenus"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:384
+#: src/pyams_content/root/zmi/__init__.py:247
+msgid "My preparations"
+msgstr "Mes préparations"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:393
+#: src/pyams_content/root/zmi/__init__.py:256
+#, python-format
+msgid "CONTRIBUTOR - Your {0} prepared contents"
+msgstr "CONTRIBUTEUR - {0} contenu(s) en préparation"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:432
+#: src/pyams_content/root/zmi/__init__.py:293
+msgid "Your prepared contents"
+msgstr "Mes contenus en préparation"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:445
+#: src/pyams_content/root/zmi/__init__.py:306
+msgid "My publications"
+msgstr "Mes publications"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:454
+#: src/pyams_content/root/zmi/__init__.py:315
+#, python-format
+msgid "CONTRIBUTOR - Your {0} published contents"
+msgstr "CONTRIBUTEUR - {0} contenu(s) publié(s)"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:493
+#: src/pyams_content/root/zmi/__init__.py:352
+msgid "Your published contents"
+msgstr "Mes contenus publiés"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:506
+#: src/pyams_content/root/zmi/__init__.py:365
+msgid "My retired contents"
+msgstr "Mes contenus retirés"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:515
+#: src/pyams_content/root/zmi/__init__.py:374
+#, python-format
+msgid "CONTRIBUTOR - Your {0} retired contents"
+msgstr "CONTRIBUTEUR - {0} contenu(s) retiré(s)"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:555
+#: src/pyams_content/root/zmi/__init__.py:412
+msgid "Your retired contents"
+msgstr "Mes contenus retirés"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:568
+#: src/pyams_content/root/zmi/__init__.py:425
+msgid "My archived contents"
+msgstr "Mes contenus archivés"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:577
+#: src/pyams_content/root/zmi/__init__.py:434
+#, python-format
+msgid "CONTRIBUTOR - Your {0} archived contents"
+msgstr "CONTRIBUTEUR - {0} contenu(s) archivé(s)"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:623
+#: src/pyams_content/root/zmi/__init__.py:478
+msgid "Your archived contents"
+msgstr "Mes contenus archivés"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:637
+#: src/pyams_content/root/zmi/__init__.py:492
+msgid "Other interventions"
+msgstr "Les autres interventions"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:652
+#: src/pyams_content/root/zmi/__init__.py:507
+msgid "Last publications"
+msgstr "Dernières publications"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:661
+#: src/pyams_content/root/zmi/__init__.py:516
+msgid "CONTRIBUTORS - Last published contents (in the limit of 50)"
+msgstr ""
+"CONTRIBUTEURS - Dernières publications tous contributeurs confondus (dans la "
+"limite de 50)"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:700
+#: src/pyams_content/root/zmi/__init__.py:553
+msgid "Last published contents"
+msgstr "Derniers contenus publiés"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:713
+#: src/pyams_content/root/zmi/__init__.py:566
+msgid "Last updates"
+msgstr "Dernières mises à jour"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:722
+#: src/pyams_content/root/zmi/__init__.py:575
+msgid "CONTRIBUTORS - Last updated contents (in the limit of 50)"
+msgstr "CONTRIBUTEURS - Derniers contenus modifiés (dans la limite de 50)"
+
+#: src/pyams_content/shared/common/zmi/dashboard.py:759
+#: src/pyams_content/root/zmi/__init__.py:610
+msgid "Last updated contents"
+msgstr "Derniers contenus modifiés"
+
+#: src/pyams_content/shared/common/zmi/security.py:61
+msgid "Managers restrictions"
+msgstr "Restrictions des responsables"
+
+#: src/pyams_content/shared/common/zmi/security.py:70
+msgid "Content managers restrictions"
+msgstr "Liste des responsables"
+
+#: src/pyams_content/shared/common/zmi/security.py:102
+msgid "Manager name"
+msgstr "Nom du responsable"
+
+#: src/pyams_content/shared/common/zmi/security.py:142
+#, python-format
+msgid "Edit manager restrictions for « {0} »"
+msgstr "Gérer les restrictions d'accès pour « {0} »"
+
+#: src/pyams_content/shared/common/zmi/security.py:179
+msgid "Apply contents restrictions"
+msgstr "Appliquer des restrictions d'accès"
+
+#: src/pyams_content/shared/common/zmi/security.py:181
+msgid ""
+"You can specify which contents this manager will be able to manage. If you "
+"specify several criteria, the manager will be able to manage contents for "
+"which at least one criteria is matching."
+msgstr ""
+"Vous pouvez indiquer les propriétés des contenus que ce responsable sera "
+"autorisé à gérer. Si vous indiquez plusieurs critères, il pourra gérer les "
+"contenus pour lesquels au moins l'un des critères correspond."
+
+#: src/pyams_content/shared/common/zmi/header.py:80
+#, python-format
+msgid "since {date}"
+msgstr "depuis {date}"
+
+#: src/pyams_content/shared/common/zmi/header.py:88
+msgid "access new version"
+msgstr "accéder à la nouvelle version en préparation"
+
+#: src/pyams_content/shared/common/zmi/header.py:68
+#, python-format
+msgid "{state} by {{principal}}"
+msgstr "{state} par {{principal}}"
+
+#: src/pyams_content/shared/common/zmi/header.py:97
+msgid "access published version"
+msgstr "accéder à la version en ligne"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt:2
+msgid ""
+"You considerate that the currently published version should no more be "
+"publicly visible."
+msgstr ""
+"Vous considérez que la version actuellement en ligne ne doit plus être "
+"consultable."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt:3
+msgid ""
+"WARNING: the content will remain visible until a manager validate the "
+"request."
+msgstr ""
+"ATTENTION : ce contenu restera visible jusqu'à ce qu'un responsable prenne "
+"en charge votre demande."
+
+#: src/pyams_content/shared/common/zmi/templates/header.pt:4
+msgid "Back to previous page"
+msgstr "Revenir à la page précédente"
+
+#: src/pyams_content/shared/common/zmi/templates/header.pt:18
+msgid "by ${owner}"
+msgstr "de ${owner}"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt:2
+msgid "As a manager, you considerate that this content must be archived."
+msgstr ""
+"En tant que responsable, vous considérez que ce contenu doit être archivé."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt:3
+#: src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt:3
+msgid ""
+"After archiving, it will be backed up but you will not be able to publish it "
+"again except by creating a new version."
+msgstr ""
+"Après l'archivage, il sera conservé mais vous ne pourrez plus le publier à "
+"nouveau, sauf en créant une nouvelle version."
+
+#: src/pyams_content/shared/common/zmi/templates/dashboard.pt:18
+msgid "Quick search..."
+msgstr "Recherche rapide..."
+
+#: src/pyams_content/shared/common/zmi/templates/dashboard.pt:21
+msgid "Advanced search..."
+msgstr "Recherche avancée..."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt:2
+msgid ""
+"As a manager, you considerate that this content is complete and can be "
+"published 'as is'."
+msgstr ""
+"En tant que responsable, vous considérez que ce contenu peut être publié en "
+"l'état."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt:4
+msgid ""
+"This operation will make the content publicly available (except if "
+"restricted access has been set)."
+msgstr ""
+"Cette opération va rendre le contenu visible de tous, sauf si des "
+"restrictions d'accès lui ont été appliquées."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-create-message.pt:2
+msgid ""
+"This new content is going to be created in 'draft' mode, so that you can "
+"complete it before publication."
+msgstr ""
+"Ce nouveau contenu va être créé en statut 'Brouillon', pour vous permettre "
+"de le préparer."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-create-message.pt:4
+msgid ""
+"A unique number is also going to be assigned to it. This number will be "
+"shared by all content's versions."
+msgstr ""
+"Un numéro unique lui sera également attribué ; ce numéro sera conservé "
+"pendant toute la vie du contenu, quelle que soit la version."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-operator-warning.pt:1
+msgid ""
+"WARNING: this request was made by a contributor which is not the owner of "
+"this content."
+msgstr ""
+"ATTENTION : cette demande a été effectuée par un contributeur qui n'est pas "
+"le propriétaire de ce contenu !"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt:2
+msgid "You considerate that the currently published must evolve."
+msgstr ""
+"Vous considérez que la version actuellement en ligne de ce contenu doit "
+"évoluer."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt:3
+msgid ""
+"By creating a new version, you can update it's content without impacting the "
+"currently published one."
+msgstr ""
+"En créant une nouvelle version, vous pourrez effectuer des modifications "
+"sans impacter la version actuellement publiée."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt:5
+msgid ""
+"When the new version will be complete, you will be able to make a new "
+"publication request to replace the currently published version (which will "
+"be archived automatically)."
+msgstr ""
+"Lorsque la nouvelle version sera prête, vous pourrez effectuer une nouvelle "
+"demande de publication pour remplacer l'ancienne version publiée (qui sera "
+"archivée automatiquement)."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-propose-message.pt:1
+msgid ""
+"This publication request is going to be transmitted to a content manager."
+msgstr "Cette demande de publication va être soumise à un responsable."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt:2
+msgid "You are going to duplicate a whole content."
+msgstr ""
+"Vous vous apprêtez à dupliquer une version de ce contenu pour en créer un "
+"nouveau."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt:3
+msgid ""
+"The new copy is going to be created in 'draft' mode, so that you can modify "
+"it before publication."
+msgstr ""
+"Ce nouveau contenu va être créé en statut 'Brouillon' pour vous permettre de "
+"le préparer."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt:5
+msgid ""
+"A new unique number is also going to be assigned to it. This number will be "
+"shared by all content's versions."
+msgstr ""
+"Un numéro unique lui sera également attribué ; ce numéro sera conservé "
+"pendant toute la vie du contenu, quelle que soit la version."
+
+#: src/pyams_content/shared/common/zmi/templates/advanced-search.pt:127
+msgid "Created between"
+msgstr "Créé entre le"
+
+#: src/pyams_content/shared/common/zmi/templates/advanced-search.pt:139
+#: src/pyams_content/shared/common/zmi/templates/advanced-search.pt:165
+msgid "and"
+msgstr "et le"
+
+#: src/pyams_content/shared/common/zmi/templates/advanced-search.pt:153
+msgid "Modified between"
+msgstr "Modifié entre le"
+
+#: src/pyams_content/shared/common/zmi/templates/advanced-search.pt:201
+msgid "Tab label"
+msgstr "Libellé de l'onglet"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt:2
+msgid ""
+"As a content manager, you considerate that this content can't be published "
+"'as is'."
+msgstr ""
+"En tant que responsable, vous considérez que ce contenu ne peut pas être "
+"publié en l'état."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt:4
+msgid ""
+"The contributor will be notified of this and will be able to update the "
+"content before doing a new publication request."
+msgstr ""
+"Le contributeur qui vous a sollicité va être notifié de ce refus ; il pourra "
+"alors à nouveau le modifier avant d'effectuer une nouvelle demande de "
+"publication."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-cancel-archiving-message.pt:1
+msgid ""
+"After cancelling this request, the content will return to it's previous "
+"retired state."
+msgstr "En annulant cette demande, ce contenu va retourner en statut 'Retiré'."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-cancel-retiring-message.pt:1
+msgid ""
+"After cancelling this request, the content will return to it's normal "
+"published state."
+msgstr "En annulant cette demande, ce contenu va retourner en statut 'Publié'."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt:2
+msgid ""
+"As a content manager, you considerate that this content should no longer be "
+"published."
+msgstr ""
+"En tant que responsable, vous considérez que ce contenu ne doit plus être "
+"publié."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt:4
+msgid ""
+"Retired content won't be visible anymore, but it can be updated and "
+"published again, or archived."
+msgstr ""
+"Après ce retrait, il ne sera plus visible des internautes. Il pourra par "
+"contre être modifié, pour être publié à nouveau, ou archivé."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-cancel-propose-message.pt:1
+msgid ""
+"After canceling the request, you will be able to update the content again."
+msgstr "En annulant cette demande, ce contenu pourra à nouveau être modifié."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-delete-message.pt:1
+msgid ""
+"The content version is going to be definitely deleted. Will only remain the "
+"currently published version."
+msgstr ""
+"Cette version de ce contenu va être définitivement supprimée. Seule la "
+"version actuellement publiée sera conservée."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-owner-warning.pt:1
+msgid ""
+"RECALL: you are not the owner of the content on which you are intervening."
+msgstr ""
+"RAPPEL : vous intervenez sur un contenu dont vous n'êtes pas le propriétaire."
+
+#: src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:2
+msgid "FOR YOUR INFORMATION"
+msgstr "POUR VOTRE INFORMATION"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:3
+msgid "Previous step:"
+msgstr "Étape précédente :"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:6
+msgid "With this comment:"
+msgstr "Avec ce commentaire :"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:13
+msgid "Next step:"
+msgstr "Étape suivante :"
+
+#: src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt:2
+msgid "This content is already retired and not visible."
+msgstr "Ce contenu est déjà retiré et n'est plus visible des internautes."
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:51
+msgid "Workflow name"
+msgstr "Nom du workflow"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:52
+msgid "Name of workflow utility used to manage tool contents"
+msgstr "Nom du workflow qui gère le cycle de vie des contenus de cet outil"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:60
+#: src/pyams_content/root/interfaces/__init__.py:40
+msgid "Webmasters"
+msgstr "Webmestres"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:61
+msgid "Webmasters can handle all contents, including published ones"
+msgstr ""
+"Les webmestres peuvent modifier et gérer tous les contenus, y compris ceux "
+"qui sont publiés"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:65
+msgid "Pilots"
+msgstr "Pilotes"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:66
+msgid ""
+"Pilots can handle tool configuration, manage access rules, grant users roles "
+"and manage managers restrictions"
+msgstr ""
+"Les pilotes sont autorisés à gérer la configuration des outils, désignent "
+"les responsables et les contributeurs, et peuvent gérer les restrictions "
+"d'accès des contributeurs"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:71
+#: src/pyams_content/shared/common/interfaces/__init__.py:128
+msgid "Managers"
+msgstr "Responsables"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:72
+#: src/pyams_content/shared/common/interfaces/__init__.py:129
+msgid ""
+"Managers can handle main operations in tool's workflow, like publish or "
+"retire contents"
+msgstr ""
+"Les responsables peuvent intervenir sur les étapes importantes du workflow "
+"(comme la publication ou le retrait des contenus), dans la limite des "
+"restrictions qui leur sont imposées"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:77
+#: src/pyams_content/shared/common/interfaces/__init__.py:134
+msgid "Contributors"
+msgstr "Contributeurs"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:78
+msgid "Contributors are users which are allowed to create new contents"
+msgstr "Les contributeurs sont autorisés à créer de nouveaux contenus"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:89
+msgid "Version creator"
+msgstr "Créateur de cette version"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:90
+msgid ""
+"Name of content's version creator. The creator of the first version is also "
+"it's owner."
+msgstr ""
+"Nom du créateur de cette version. Le créateur de la première version d'un "
+"contenu est aussi son propriétaire."
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:94
+msgid "First owner"
+msgstr "Premier propriétaire"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:95
+msgid "Name of content's first version owner"
+msgstr "Nom de l'utilisateur ayant créé la première version"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:99
+msgid "Version modifiers"
+msgstr "Intervenants"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:100
+msgid "List of principals who modified this content"
+msgstr "Liste des utilisateurs qui sont intervenus sur ce contenu"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:104
+msgid ""
+"The content's description is 'hidden' into HTML's page headers; but it can "
+"be seen, for example, in some search engines results as content's description"
+msgstr ""
+"La description du contenu est 'masquée' dans les en-têtes des pages HTML ; "
+"mais on peut la retrouver, par exemple, dans les listes de résultats des "
+"moteurs de recherche comme Google"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:109
+msgid "Keywords"
+msgstr "Mots-clés"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:110
+msgid "They will be included into HTML pages metadata"
+msgstr "Ces mots-clés seront intégrés dans les métadonnées des pages HTML"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:113
+msgid "Notepad"
+msgstr "Bloc-notes"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:114
+msgid "Internal information to be known about this content"
+msgstr ""
+"Pour prendre note d'informations internes utiles ou importantes à propos de "
+"ce contenu"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:121
+msgid "Content owner"
+msgstr "Propriétaire"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:122
+msgid ""
+"The owner is the creator of content's first version, except if it was "
+"transferred afterwards to another owner"
+msgstr ""
+"Le propriétaire est le créateur de la première version d'un contenu, sauf "
+"lorsque cette propriété a été transférée à un autre utilisateur après coup. "
+"Les contenus archivés ne sont plus transférables."
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:135
+msgid ""
+"Contributors are users which are allowed to update this content in addition "
+"to it's owner"
+msgstr ""
+"Les contributeurs sont autorisés, en plus du propriétaire, à modifier ce "
+"contenu"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:140
+msgid "Readers"
+msgstr "Relecteurs"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:141
+msgid ""
+"Readers are users which are asked to verify and comment contents before they "
+"are published"
+msgstr ""
+"Les relecteurs sont des utilisateurs qui sont sollicités pour vérifier et "
+"commenter un contenu avant sa publication"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:146
+msgid "Guests"
+msgstr "Invités"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:147
+msgid ""
+"Guests are users which are allowed to view contents with restricted access"
+msgstr ""
+"Les invités sont autorisés à consulter des contenus dont l'accès a été "
+"restreint"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:166
+msgid "Principal ID"
+msgstr "ID utilisateur"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:171
+msgid "Restricted contents"
+msgstr "Accès restreints"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:172
+msgid ""
+"If 'yes', this manager will get restricted access to manage contents based "
+"on selected settings"
+msgstr ""
+"Si 'oui', ce responsable n'aura qu'un accès restreint à certains contenus en "
+"fonction de paramètres spécifiques"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:177
+msgid "Selected owners"
+msgstr "Propriétaires"
+
+#: src/pyams_content/shared/common/interfaces/__init__.py:178
+msgid "Manager will have access to contents owned by these principals"
+msgstr ""
+"Le responsable n'aura accès qu'aux contenus dont ces utilisateurs sont "
+"propriétaires"
+
+#: src/pyams_content/shared/news/zmi/properties.py:40
+msgid "Publication settings"
+msgstr "Paramètres de publication"
+
+#: src/pyams_content/shared/news/zmi/__init__.py:44
+msgid "This news topic"
+msgstr "Cette brève"
+
+#: src/pyams_content/shared/news/zmi/__init__.py:63
+msgid "Add news topic"
+msgstr "Ajouter une brève"
+
+#: src/pyams_content/shared/news/zmi/__init__.py:73
+msgid "Add new news topic"
+msgstr "Ajout d'une brève"
+
+#: src/pyams_content/shared/news/zmi/__init__.py:54
+#, python-format
+msgid "News topic « {title} »"
+msgstr "Brève « {title} »"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:30
+msgid "News topic"
+msgstr "Brève"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:36
+msgid "Display first version date"
+msgstr "Date de publication de la première version"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:37
+msgid "Display current version date"
+msgstr "Date de publication de cette version"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:50
+msgid "Displayed publication date"
+msgstr "Date de publication affichée"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:51
+msgid "The matching date will be displayed in front-office"
+msgstr "La date correspondate sera affichée en front-office"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:58
+msgid "Push end date"
+msgstr "Date de retrait"
+
+#: src/pyams_content/shared/news/interfaces/__init__.py:59
+msgid ""
+"Some contents can be pushed by components to front-office pages; if you set "
+"a date here, this content will not be pushed anymore passed this date, but "
+"will still be available via the search engine"
+msgstr ""
+"Certains composants peuvent 'pousser' des informations vers les pages du "
+"front-office ; si vous indiquez une date ici, ce contenu ne sera plus poussé "
+"au-delà de cette date, mais restera accessible via le moteur de recherche (à "
+"la différence des contenus retirés ou archivés)"
+
+#: src/pyams_content/profile/zmi/__init__.py:39
+msgid "Admin. profile"
+msgstr "Profil d'admin."
+
+#: src/pyams_content/profile/interfaces/__init__.py:33
+msgid "Default table length"
+msgstr "Longueur des tableaux"
+
+#: src/pyams_content/profile/interfaces/__init__.py:34
+msgid "Default length used for inner tables and dashboards"
+msgstr "Longueur par défaut des tableaux internes et des tableaux de bord"
+
+#: src/pyams_content/root/zmi/__init__.py:110
+msgid "Your contents dashboard"
+msgstr "Tableau de bord des contenus qui vous concernent"
+
+#: src/pyams_content/root/zmi/__init__.py:621
+msgid "Content"
+msgstr "Contenu"
+
+#: src/pyams_content/root/interfaces/__init__.py:36
+msgid "Site managers"
+msgstr "Administrateurs"
+
+#: src/pyams_content/root/interfaces/__init__.py:44
+msgid "Operators group"
+msgstr "Groupe des opérateurs"
+
+#: src/pyams_content/root/interfaces/__init__.py:45
+msgid "Name of group containing all roles owners"
+msgstr ""
+"Tous les utilisateurs auxquels sera attribué un rôle seront placés dans ce "
+"groupe"
+
+#: src/pyams_content/zmi/viewlet/toplinks/__init__.py:45
+msgid "Shared contents"
+msgstr "Contenus partagés"
+
+#: src/pyams_content/zmi/viewlet/toplinks/__init__.py:63
+msgid "My roles"
+msgstr "Mes rôles"
+
+#: src/pyams_content/zmi/viewlet/toplinks/templates/user-addings.pt:7
+msgid "Create new content"
+msgstr "Créer un nouveau contenu"
+
+#: src/pyams_content/workflow/__init__.py:82
+msgid "Draft"
+msgstr "Brouillon"
+
+#: src/pyams_content/workflow/__init__.py:83
+msgid "Proposed"
+msgstr "Publication demandée"
+
+#: src/pyams_content/workflow/__init__.py:84
+msgid "Canceled"
+msgstr "Annulé"
+
+#: src/pyams_content/workflow/__init__.py:85
+msgid "Refused"
+msgstr "Refusé"
+
+#: src/pyams_content/workflow/__init__.py:86
+msgid "Published"
+msgstr "Publié"
+
+#: src/pyams_content/workflow/__init__.py:87
+msgid "Retiring"
+msgstr "Retrait demandé"
+
+#: src/pyams_content/workflow/__init__.py:88
+msgid "Retired"
+msgstr "Retiré"
+
+#: src/pyams_content/workflow/__init__.py:89
+msgid "Archiving"
+msgstr "Archivage demandé"
+
+#: src/pyams_content/workflow/__init__.py:90
+msgid "Archived"
+msgstr "Archivé"
+
+#: src/pyams_content/workflow/__init__.py:91
+msgid "Deleted"
+msgstr "Supprimé"
+
+#: src/pyams_content/workflow/__init__.py:93
+#, python-format
+msgid "draft created by {principal}"
+msgstr "brouillon créé par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:94
+#, python-format
+msgid "publication requested by {principal}"
+msgstr "publication demandée par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:95
+#, python-format
+msgid "published by {principal}"
+msgstr "publié par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:96
+#, python-format
+msgid "retiring requested by {principal}"
+msgstr "retrait demandé par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:97
+#, python-format
+msgid "retired by {principal}"
+msgstr "retiré par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:98
+#, python-format
+msgid "archiving requested by {principal}"
+msgstr "archivage demandé par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:99
+#, python-format
+msgid "archived by {principal}"
+msgstr "archivé par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:219
+msgid "Initialize"
+msgstr "Création"
+
+#: src/pyams_content/workflow/__init__.py:222
+msgid "Draft creation"
+msgstr "Création du brouillon"
+
+#: src/pyams_content/workflow/__init__.py:225
+#: src/pyams_content/workflow/__init__.py:238
+msgid "Propose publication"
+msgstr "Demander la publication"
+
+#: src/pyams_content/workflow/__init__.py:232
+#: src/pyams_content/workflow/__init__.py:245
+msgid "Publication request"
+msgstr "Demande de publication"
+
+#: src/pyams_content/workflow/__init__.py:233
+#: src/pyams_content/workflow/__init__.py:246
+#: src/pyams_content/workflow/__init__.py:324
+#: src/pyams_content/workflow/__init__.py:358
+msgid ""
+"content managers authorized to take charge of your content are going to be "
+"notified of your request."
+msgstr ""
+"les responsables habilités à prendre en charge votre demande vont être "
+"sollicités."
+
+#: src/pyams_content/workflow/__init__.py:258
+msgid "Publication request canceled"
+msgstr "Annulation de la demande de publication"
+
+#: src/pyams_content/workflow/__init__.py:262
+msgid "Reset canceled publication to draft"
+msgstr "Retour automatique en statut 'brouillon'"
+
+#: src/pyams_content/workflow/__init__.py:266
+#: src/pyams_content/workflow/__init__.py:293
+msgid "State reset to 'draft' (automatic)"
+msgstr "Retour automatique en statut 'brouillon'"
+
+#: src/pyams_content/workflow/__init__.py:270
+msgid "Reset canceled publication to retired"
+msgstr "Retour automatique en statut 'retiré'"
+
+#: src/pyams_content/workflow/__init__.py:274
+msgid "State reset to 'retired' (automatic)"
+msgstr "Retour automatique en statut 'retiré'"
+
+#: src/pyams_content/workflow/__init__.py:278
+msgid "Refuse publication"
+msgstr "Refuser la publication"
+
+#: src/pyams_content/workflow/__init__.py:285
+msgid "Publication refused"
+msgstr "Refus de publication"
+
+#: src/pyams_content/workflow/__init__.py:289
+msgid "Reset refused publication to draft"
+msgstr "Publication refusée"
+
+#: src/pyams_content/workflow/__init__.py:297
+msgid "Reset refused publication to retired"
+msgstr "Publication refusée"
+
+#: src/pyams_content/workflow/__init__.py:301
+msgid "State reset to 'refused' (automatic)"
+msgstr "Retour automatique en status 'refusé'"
+
+#: src/pyams_content/workflow/__init__.py:313
+msgid "Content published"
+msgstr "Publication"
+
+#: src/pyams_content/workflow/__init__.py:317
+msgid "Request retiring"
+msgstr "Demander le retrait"
+
+#: src/pyams_content/workflow/__init__.py:323
+msgid "Retire request"
+msgstr "Demande de retrait"
+
+#: src/pyams_content/workflow/__init__.py:329
+msgid "Cancel retiring request"
+msgstr "Annuler la demande de retrait"
+
+#: src/pyams_content/workflow/__init__.py:336
+msgid "Retire request canceled"
+msgstr "Annulation de la demande de retrait"
+
+#: src/pyams_content/workflow/__init__.py:340
+msgid "Retire content"
+msgstr "Retirer"
+
+#: src/pyams_content/workflow/__init__.py:347
+msgid "Content retired"
+msgstr "Retrait"
+
+#: src/pyams_content/workflow/__init__.py:357
+msgid "Archive request"
+msgstr "Demande d'archivage"
+
+#: src/pyams_content/workflow/__init__.py:363
+msgid "Cancel archiving request"
+msgstr "Annuler la demande d'archivage"
+
+#: src/pyams_content/workflow/__init__.py:370
+msgid "Archive request canceled"
+msgstr "Annulation de la demande d'archivage"
+
+#: src/pyams_content/workflow/__init__.py:374
+msgid "Archive content"
+msgstr "Archiver"
+
+#: src/pyams_content/workflow/__init__.py:382
+msgid "Content archived"
+msgstr "Archivage"
+
+#: src/pyams_content/workflow/__init__.py:386
+msgid "Archive published content"
+msgstr "Archivage automatique d'un contenu publié"
+
+#: src/pyams_content/workflow/__init__.py:390
+#: src/pyams_content/workflow/__init__.py:398
+#: src/pyams_content/workflow/__init__.py:406
+msgid "Content archived after version publication"
+msgstr "Archivage automatique après publication"
+
+#: src/pyams_content/workflow/__init__.py:394
+msgid "Archive retiring content"
+msgstr "Archivage automatique d'un contenu en attente de retrait"
+
+#: src/pyams_content/workflow/__init__.py:402
+msgid "Archive retired content"
+msgstr "Archivage automatique d'un contenu retiré"
+
+#: src/pyams_content/workflow/__init__.py:418
+#: src/pyams_content/workflow/__init__.py:430
+#: src/pyams_content/workflow/__init__.py:442
+#: src/pyams_content/workflow/__init__.py:454
+#: src/pyams_content/workflow/__init__.py:466
+msgid "New version created"
+msgstr "Création d'une nouvelle version"
+
+#: src/pyams_content/workflow/__init__.py:478
+msgid "Version deleted"
+msgstr "Version supprimée"
+
+#: src/pyams_content/workflow/__init__.py:544
+#, python-format
+msgid "publication refused by {principal}"
+msgstr "publication refusée par {principal}"
+
+#: src/pyams_content/workflow/__init__.py:189
+#, python-format
+msgid "Published version {0}"
+msgstr "Version {0} publiée"
+
+#: src/pyams_content/interfaces/__init__.py:50
+msgid "Unique key"
+msgstr "Clé unique"
+
+#: src/pyams_content/interfaces/__init__.py:51
+msgid "WARNING: this key can't be modified after creation!!!"
+msgstr "ATTENTION : cette clé ne pourra plus être modifiée !!!"
+
+#: src/pyams_content/interfaces/__init__.py:55
+msgid "Visible label used to display content"
+msgstr "Le titre affiché en front-office pourra être modifié ultérieurement"
+
+#: src/pyams_content/interfaces/__init__.py:58
+msgid "Short name"
+msgstr "Titre court"
+
+#: src/pyams_content/interfaces/__init__.py:59
+msgid "Short name used in breadcrumbs"
+msgstr ""
+"Affiché lorsque le contenu est consulté depuis son site d'origine (s'il a "
+"été identifié)"
+
+#: src/pyams_content/interfaces/__init__.py:66
+msgid "Creation date"
+msgstr "Date de création"
+
+#: src/pyams_content/interfaces/__init__.py:70
+msgid "Modification date"
+msgstr "Date de modification"
+
+#~ msgid "Close"
+#~ msgstr "Annuler"
+
+#~ msgid "Base info."
+#~ msgstr "Infos de base"
+
+#~ msgid "Last modified contents (limited to 30)"
+#~ msgstr "Derniers contenus modifiés (dans la limite de 30)"
+
+#~ msgid ""
+#~ "As a manager, you considerate that this content must be archived.<dynamic "
+#~ "element> After archiving, it will be backed up but you will not be able "
+#~ "to publish it again except by creating a new version."
+#~ msgstr ""
+#~ "En tant que responsable, vous considérez que ce contenu doit être archivé."
+#~ "<br />Après archivage, il sera sauvegardé mais ne pourra plus être ni "
+#~ "modifié ni publié, sauf en créant une nouvelle version."
+
+#~ msgid ""
+#~ "You are going to duplicate a whole content.<dynamic element> The new copy "
+#~ "is going to be created in 'draft' mode, so that you can modify it before "
+#~ "publication. <dynamic element> A new unique number is also going to be "
+#~ "assigned to it. This number will be shared by all content's versions."
+#~ msgstr ""
+#~ "Vous êtes sur le point de dupliquer un contenu dans sa globalité.<br />La "
+#~ "nouvelle copie va être créée en statut 'Brouillon', de façon à ce que "
+#~ "vous pouissiez la modifier jusqu'à sa publication. Un nouveau numéro "
+#~ "unique va également lui être attribué."
+
+#~ msgid "{label} ({ext})"
+#~ msgstr "{label} ({ext})"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/locales/pyams_content.pot	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1719 @@
+# 
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-10-07 15:42+0200\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"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+
+#: ./src/pyams_content/__init__.py:33
+msgid "Manage site root"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:35
+msgid "Manage site"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:37
+msgid "Manage tool"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:39
+msgid "Create content"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:41
+msgid "Manage content"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:43
+msgid "Comment content"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:45
+#: ./src/pyams_content/workflow/__init__.py:305
+msgid "Publish content"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:49
+msgid "Webmaster (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:57
+msgid "Pilot (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:65
+msgid "Manager (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:72
+msgid "Creator (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:77
+msgid "Contributor (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:85
+msgid "Reader (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:91
+msgid "Operator (role)"
+msgstr ""
+
+#: ./src/pyams_content/__init__.py:95
+msgid "Guest user (role)"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/__init__.py:55
+#: ./src/pyams_content/component/gallery/zmi/templates/widget-input.pt:5
+msgid "Add gallery"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/__init__.py:66
+msgid "Add new images gallery"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/__init__.py:159
+msgid "Update gallery properties"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:60
+msgid "Images galleries..."
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:76
+msgid "Galleries list"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:121
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:46
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:86
+#: ./src/pyams_content/component/extfile/zmi/container.py:167
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:41
+#: ./src/pyams_content/component/paragraph/zmi/container.py:200
+#: ./src/pyams_content/component/paragraph/interfaces/__init__.py:41
+#: ./src/pyams_content/component/links/zmi/container.py:144
+#: ./src/pyams_content/component/links/interfaces/__init__.py:42
+#: ./src/pyams_content/shared/common/zmi/templates/advanced-search.pt:187
+#: ./src/pyams_content/interfaces/__init__.py:54
+msgid "Title"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:133
+#: ./src/pyams_content/component/extfile/zmi/container.py:112
+msgid "Images"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:146
+msgid "Display gallery contents"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:186
+msgid "Edit galleries links"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/container.py:113
+msgid "No currently defined gallery."
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:56
+msgid "Update gallery contents"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:67
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:78
+msgid "Add image(s)"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:183
+#: ./src/pyams_content/component/extfile/zmi/__init__.py:186
+msgid "Update image properties"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:220
+msgid "Remove image..."
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:235
+msgid "No provided object_name argument!"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/gallery.py:239
+msgid "Given image name doesn't exist!"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/interfaces.py:32
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:52
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:49
+msgid "Author"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/interfaces.py:35
+msgid "Author comments"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/interfaces.py:36
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:56
+msgid "Comments relatives to author's rights management"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/interfaces.py:39
+msgid "Images data"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/interfaces.py:40
+msgid "You can upload a single file or choose to upload a whole ZIP archive"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/templates/gallery-images.pt:20
+msgid "Hidden image"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/zmi/templates/gallery-images.pt:36
+msgid "Download"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:49
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:90
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:45
+#: ./src/pyams_content/component/links/interfaces/__init__.py:46
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:103
+msgid "Description"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:55
+msgid "Author's comments"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:59
+msgid "Audio data"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:60
+msgid "Sound file associated with the current media"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:63
+msgid "Sound title"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:64
+msgid "Title of associated sound file"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:67
+msgid "Sound description"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:68
+msgid "Short description of associated sound file"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:71
+msgid "PIF number"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:72
+msgid "Number used to identify media into national library database"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:75
+msgid "Visible image?"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:76
+msgid "If 'no', this image won't be displayed in front office"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:87
+msgid "Gallery title, as shown in front-office"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:91
+msgid "Gallery description displayed by front-office template"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:94
+msgid "Visible gallery?"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:95
+msgid "If 'no', this gallery won't be displayed in front office"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:122
+msgid "Contained galleries"
+msgstr ""
+
+#: ./src/pyams_content/component/gallery/interfaces/__init__.py:123
+msgid "List of images galleries linked to this object"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/__init__.py:111
+msgid "Standard file"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/__init__.py:120
+msgid "Image"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/__init__.py:129
+msgid "Video"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/__init__.py:138
+msgid "Audio file"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/__init__.py:66
+#: ./src/pyams_content/component/extfile/zmi/templates/widget-input.pt:5
+msgid "Add external file"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/__init__.py:77
+msgid "Add new external file"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/__init__.py:153
+msgid "Update file properties"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/__init__.py:55
+msgid "External file type"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:63
+msgid "External files..."
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:107
+msgid "External files list"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:179
+msgid "Filename"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:195
+msgid "Size"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:241
+msgid "Edit external files links"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:111
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:101
+#: ./src/pyams_content/component/paragraph/zmi/container.py:144
+msgid "External files"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:113
+msgid "Videos"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:114
+msgid "Sounds"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/zmi/container.py:157
+msgid "No currently stored external file."
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:42
+msgid "File title, as shown in front-office"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:46
+msgid "File description displayed by front-office template"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:50
+msgid "Name of document's author"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:57
+msgid "File data"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:58
+msgid "File content"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:69
+#: ./src/pyams_content/component/paragraph/interfaces/__init__.py:86
+msgid "Image data"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:70
+msgid "Image content"
+msgstr ""
+
+#: ./src/pyams_content/component/extfile/interfaces/__init__.py:102
+msgid "List of external files linked to this object"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/summary.py:46
+msgid "Paragraphs"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/container.py:58
+msgid "Paragraphs..."
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/container.py:73
+msgid "Paragraphs list"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/container.py:130
+msgid "Paragraph properties"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/container.py:163
+msgid "Useful links"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/container.py:182
+msgid "Images galleries"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/container.py:120
+msgid "No currently defined paragraph."
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/illustration.py:54
+msgid "Add illustration..."
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/illustration.py:65
+msgid "Add new illustration"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/illustration.py:100
+msgid "Edit illustration properties"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/illustration.py:173
+msgid "Centered illustration"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/illustration.py:181
+msgid "Small illustration on the left with zoom"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/illustration.py:189
+msgid "Small illustration on the right with zoom"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/html.py:54
+msgid "Add HTML paragraph..."
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/html.py:65
+msgid "Add new HTML paragraph"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/html.py:106
+msgid "Edit paragraph properties"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/zmi/templates/summary.pt:7
+#: ./src/pyams_content/component/paragraph/zmi/templates/summary.pt:30
+msgid "This content doesn't contain any paragraph."
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/__init__.py:42
+msgid "Paragraph title"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/__init__.py:69
+msgid "Body"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/__init__.py:89
+msgid "Legend"
+msgstr ""
+
+#: ./src/pyams_content/component/paragraph/interfaces/__init__.py:92
+msgid "Image style"
+msgstr ""
+
+#: ./src/pyams_content/component/theme/zmi/__init__.py:52
+#: ./src/pyams_content/component/theme/zmi/manager.py:45
+msgid "Themes..."
+msgstr ""
+
+#: ./src/pyams_content/component/theme/zmi/__init__.py:63
+msgid "Content themes"
+msgstr ""
+
+#: ./src/pyams_content/component/theme/zmi/manager.py:56
+msgid "Selected themes"
+msgstr ""
+
+#: ./src/pyams_content/component/theme/interfaces/__init__.py:43
+msgid "Terms"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/__init__.py:52
+msgid "Add internal link"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/__init__.py:64
+msgid "Add new internal link"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/__init__.py:122
+#: ./src/pyams_content/component/links/zmi/__init__.py:227
+msgid "Edit link properties"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/__init__.py:157
+msgid "Add external link"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/__init__.py:169
+msgid "Add new External link"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/container.py:63
+msgid "Useful links..."
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/container.py:99
+msgid "Useful links list"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/container.py:156
+msgid "Link target"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/container.py:199
+msgid "Edit useful links links"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/container.py:136
+msgid "No currently defined link."
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/reverse.py:55
+msgid "Reverse links"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/reverse.py:64
+msgid "Content's internal links"
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/templates/widget-input.pt:12
+msgid "Add internal link..."
+msgstr ""
+
+#: ./src/pyams_content/component/links/zmi/templates/widget-input.pt:19
+msgid "Add external link..."
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:43
+msgid "Link title, as shown in front-office"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:47
+msgid "Link description displayed by front-office template"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:60
+msgid "Internal reference"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:61
+msgid ""
+"Internal link target reference. You can search a reference using '+' followed"
+" by internal number, of by entering text matching content title."
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:73
+msgid "Target URL"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:74
+msgid "URL used to access external resource"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:77
+msgid "Language"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:78
+msgid "Language used in this remote resource"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:96
+msgid "Contained links"
+msgstr ""
+
+#: ./src/pyams_content/component/links/interfaces/__init__.py:97
+msgid "List of internal or external links linked to this object"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:73
+msgid "Quick search results"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:143
+msgid "Advanced search"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:225
+msgid "Advanced search results"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:118
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:191
+msgid "Owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:121
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:153
+msgid "Status"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:125
+msgid "Created after..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:128
+msgid "Created before..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:131
+msgid "Modified after..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/search.py:134
+msgid "Modified before..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/properties.py:54
+msgid "Composition"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/properties.py:64
+#: ./src/pyams_content/shared/common/zmi/manager.py:78
+msgid "Properties"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/properties.py:75
+msgid "Content properties"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:66
+msgid "Workflow"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:164
+#: ./src/pyams_content/shared/common/zmi/workflow.py:235
+#: ./src/pyams_content/shared/common/zmi/workflow.py:280
+#: ./src/pyams_content/shared/common/zmi/workflow.py:338
+#: ./src/pyams_content/shared/common/zmi/workflow.py:411
+#: ./src/pyams_content/shared/common/zmi/workflow.py:471
+#: ./src/pyams_content/shared/common/zmi/workflow.py:516
+#: ./src/pyams_content/shared/common/zmi/workflow.py:562
+#: ./src/pyams_content/shared/common/zmi/workflow.py:622
+#: ./src/pyams_content/shared/common/zmi/workflow.py:667
+#: ./src/pyams_content/shared/common/zmi/workflow.py:713
+#: ./src/pyams_content/shared/common/zmi/workflow.py:765
+#: ./src/pyams_content/shared/common/zmi/__init__.py:225
+#: ./src/pyams_content/shared/common/zmi/owner.py:74
+msgid "Cancel"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:165
+msgid "Request publication"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:236
+#: ./src/pyams_content/workflow/__init__.py:251
+msgid "Cancel publication request"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:281
+msgid "Refuse publication request"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:339
+msgid "Publish"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:412
+msgid "Request retire"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:472
+msgid "Cancel retire request"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:517
+msgid "Retire"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:563
+#: ./src/pyams_content/workflow/__init__.py:351
+msgid "Request archive"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:623
+msgid "Cancel archive request"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:668
+msgid "Archive"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:714
+#: ./src/pyams_content/workflow/__init__.py:410
+#: ./src/pyams_content/workflow/__init__.py:422
+#: ./src/pyams_content/workflow/__init__.py:434
+#: ./src/pyams_content/workflow/__init__.py:446
+#: ./src/pyams_content/workflow/__init__.py:458
+msgid "Create new version"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:766
+#: ./src/pyams_content/workflow/__init__.py:470
+msgid "Delete version"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:203
+#: ./src/pyams_content/shared/common/zmi/workflow.py:381
+msgid "Publication start date is required"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:206
+#: ./src/pyams_content/shared/common/zmi/workflow.py:308
+#: ./src/pyams_content/shared/common/zmi/workflow.py:442
+#: ./src/pyams_content/shared/common/zmi/workflow.py:593
+msgid "A comment is required"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:130
+#, python-format
+msgid "{state} {date}"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/workflow.py:127
+#, python-format
+msgid "{state} by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/__init__.py:174
+msgid "Manage this content"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/__init__.py:215
+msgid "Duplicate content..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/__init__.py:234
+#: ./src/pyams_content/shared/common/zmi/__init__.py:226
+msgid "Duplicate content"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/__init__.py:273
+#, python-format
+msgid "Duplicate content ({oid})"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/__init__.py:318
+msgid "Created or modified in this version"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/summary.py:57
+msgid "Summary"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/summary.py:67
+msgid "Display content summary"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/summary.py:91
+msgid "Identity card"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/manager.py:64
+msgid "Tool management"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/manager.py:88
+msgid "Shared tool properties"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/manager.py:107
+msgid "WARNING"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/manager.py:109
+msgid ""
+"Workflow shouldn't be modified if this tool already contains any shared "
+"content!"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/manager.py:132
+msgid "Content languages"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/manager.py:149
+msgid ""
+"Tool languages are used to translate own tool properties, and newly created "
+"contents will propose these languages by default"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:51
+msgid "Change owner..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:83
+msgid "Change content's owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:126
+msgid ""
+"All versions of this content which are not archived will be transferred to "
+"newly selected owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:61
+msgid "New owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:62
+msgid "The selected user will become the new content's owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:64
+msgid "Keep previous owner as contributor"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:65
+msgid "If 'yes', the previous owner will still be able to modify this content"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/owner.py:75
+msgid "Change owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:108
+msgid "Unique ID"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:121
+msgid "Version"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:133
+msgid "Urgent request !"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:166
+msgid "Status date"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:178
+msgid "Status principal"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:203
+msgid "Last modification"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:221
+#: ./src/pyams_content/root/zmi/__init__.py:75
+msgid "Dashboard"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:232
+msgid "Contents dashboard"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:266
+#: ./src/pyams_content/root/zmi/__init__.py:121
+#, python-format
+msgid "MANAGER - {0} content(s) waiting for your action"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:307
+#: ./src/pyams_content/root/zmi/__init__.py:165
+#, python-format
+msgid "CONTRIBUTOR - Your {0} content(s) waiting for action"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:337
+#: ./src/pyams_content/root/zmi/__init__.py:198
+#, python-format
+msgid "CONTRIBUTOR - Your last modified contents (limited to {0})"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:369
+#: ./src/pyams_content/root/zmi/__init__.py:232
+msgid "My contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:384
+#: ./src/pyams_content/root/zmi/__init__.py:247
+msgid "My preparations"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:393
+#: ./src/pyams_content/root/zmi/__init__.py:256
+#, python-format
+msgid "CONTRIBUTOR - Your {0} prepared contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:432
+#: ./src/pyams_content/root/zmi/__init__.py:293
+msgid "Your prepared contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:445
+#: ./src/pyams_content/root/zmi/__init__.py:306
+msgid "My publications"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:454
+#: ./src/pyams_content/root/zmi/__init__.py:315
+#, python-format
+msgid "CONTRIBUTOR - Your {0} published contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:493
+#: ./src/pyams_content/root/zmi/__init__.py:352
+msgid "Your published contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:506
+#: ./src/pyams_content/root/zmi/__init__.py:365
+msgid "My retired contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:515
+#: ./src/pyams_content/root/zmi/__init__.py:374
+#, python-format
+msgid "CONTRIBUTOR - Your {0} retired contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:555
+#: ./src/pyams_content/root/zmi/__init__.py:412
+msgid "Your retired contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:568
+#: ./src/pyams_content/root/zmi/__init__.py:425
+msgid "My archived contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:577
+#: ./src/pyams_content/root/zmi/__init__.py:434
+#, python-format
+msgid "CONTRIBUTOR - Your {0} archived contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:623
+#: ./src/pyams_content/root/zmi/__init__.py:478
+msgid "Your archived contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:637
+#: ./src/pyams_content/root/zmi/__init__.py:492
+msgid "Other interventions"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:652
+#: ./src/pyams_content/root/zmi/__init__.py:507
+msgid "Last publications"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:661
+#: ./src/pyams_content/root/zmi/__init__.py:516
+msgid "CONTRIBUTORS - Last published contents (in the limit of 50)"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:700
+#: ./src/pyams_content/root/zmi/__init__.py:553
+msgid "Last published contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:713
+#: ./src/pyams_content/root/zmi/__init__.py:566
+msgid "Last updates"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:722
+#: ./src/pyams_content/root/zmi/__init__.py:575
+msgid "CONTRIBUTORS - Last updated contents (in the limit of 50)"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/dashboard.py:759
+#: ./src/pyams_content/root/zmi/__init__.py:610
+msgid "Last updated contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/security.py:61
+msgid "Managers restrictions"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/security.py:70
+msgid "Content managers restrictions"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/security.py:102
+msgid "Manager name"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/security.py:142
+#, python-format
+msgid "Edit manager restrictions for « {0} »"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/security.py:179
+msgid "Apply contents restrictions"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/security.py:181
+msgid ""
+"You can specify which contents this manager will be able to manage. If you "
+"specify several criteria, the manager will be able to manage contents for "
+"which at least one criteria is matching."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/header.py:80
+#, python-format
+msgid "since {date}"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/header.py:88
+msgid "access new version"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/header.py:68
+#, python-format
+msgid "{state} by {{principal}}"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/header.py:97
+msgid "access published version"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt:2
+msgid ""
+"You considerate that the currently published version should no more be "
+"publicly visible."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt:3
+msgid ""
+"WARNING: the content will remain visible until a manager validate the "
+"request."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/header.pt:4
+msgid "Back to previous page"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/header.pt:18
+msgid "by ${owner}"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt:2
+msgid "As a manager, you considerate that this content must be archived."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt:3
+#: ./src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt:3
+msgid ""
+"After archiving, it will be backed up but you will not be able to publish it "
+"again except by creating a new version."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/dashboard.pt:18
+msgid "Quick search..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/dashboard.pt:21
+msgid "Advanced search..."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt:2
+msgid ""
+"As a manager, you considerate that this content is complete and can be "
+"published 'as is'."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt:4
+msgid ""
+"This operation will make the content publicly available (except if restricted"
+" access has been set)."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-create-message.pt:2
+msgid ""
+"This new content is going to be created in 'draft' mode, so that you can "
+"complete it before publication."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-create-message.pt:4
+msgid ""
+"A unique number is also going to be assigned to it. This number will be "
+"shared by all content's versions."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-operator-warning.pt:1
+msgid ""
+"WARNING: this request was made by a contributor which is not the owner of "
+"this content."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt:2
+msgid "You considerate that the currently published must evolve."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt:3
+msgid ""
+"By creating a new version, you can update it's content without impacting the "
+"currently published one."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt:5
+msgid ""
+"When the new version will be complete, you will be able to make a new "
+"publication request to replace the currently published version (which will be"
+" archived automatically)."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-propose-message.pt:1
+msgid ""
+"This publication request is going to be transmitted to a content manager."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt:2
+msgid "You are going to duplicate a whole content."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt:3
+msgid ""
+"The new copy is going to be created in 'draft' mode, so that you can modify "
+"it before publication."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt:5
+msgid ""
+"A new unique number is also going to be assigned to it. This number will be "
+"shared by all content's versions."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/advanced-search.pt:127
+msgid "Created between"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/advanced-search.pt:139
+#: ./src/pyams_content/shared/common/zmi/templates/advanced-search.pt:165
+msgid "and"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/advanced-search.pt:153
+msgid "Modified between"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/advanced-search.pt:201
+msgid "Tab label"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt:2
+msgid ""
+"As a content manager, you considerate that this content can't be published "
+"'as is'."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt:4
+msgid ""
+"The contributor will be notified of this and will be able to update the "
+"content before doing a new publication request."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-cancel-archiving-message.pt:1
+msgid ""
+"After cancelling this request, the content will return to it's previous "
+"retired state."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-cancel-retiring-message.pt:1
+msgid ""
+"After cancelling this request, the content will return to it's normal "
+"published state."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt:2
+msgid ""
+"As a content manager, you considerate that this content should no longer be "
+"published."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt:4
+msgid ""
+"Retired content won't be visible anymore, but it can be updated and published"
+" again, or archived."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-cancel-propose-message.pt:1
+msgid ""
+"After canceling the request, you will be able to update the content again."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-delete-message.pt:1
+msgid ""
+"The content version is going to be definitely deleted. Will only remain the "
+"currently published version."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-owner-warning.pt:1
+msgid ""
+"RECALL: you are not the owner of the content on which you are intervening."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:2
+msgid "FOR YOUR INFORMATION"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:3
+msgid "Previous step:"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:6
+msgid "With this comment:"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt:13
+msgid "Next step:"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt:2
+msgid "This content is already retired and not visible."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:51
+msgid "Workflow name"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:52
+msgid "Name of workflow utility used to manage tool contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:60
+#: ./src/pyams_content/root/interfaces/__init__.py:40
+msgid "Webmasters"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:61
+msgid "Webmasters can handle all contents, including published ones"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:65
+msgid "Pilots"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:66
+msgid ""
+"Pilots can handle tool configuration, manage access rules, grant users roles "
+"and manage managers restrictions"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:71
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:128
+msgid "Managers"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:72
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:129
+msgid ""
+"Managers can handle main operations in tool's workflow, like publish or "
+"retire contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:77
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:134
+msgid "Contributors"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:78
+msgid "Contributors are users which are allowed to create new contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:89
+msgid "Version creator"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:90
+msgid ""
+"Name of content's version creator. The creator of the first version is also "
+"it's owner."
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:94
+msgid "First owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:95
+msgid "Name of content's first version owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:99
+msgid "Version modifiers"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:100
+msgid "List of principals who modified this content"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:104
+msgid ""
+"The content's description is 'hidden' into HTML's page headers; but it can be"
+" seen, for example, in some search engines results as content's description"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:109
+msgid "Keywords"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:110
+msgid "They will be included into HTML pages metadata"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:113
+msgid "Notepad"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:114
+msgid "Internal information to be known about this content"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:121
+msgid "Content owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:122
+msgid ""
+"The owner is the creator of content's first version, except if it was "
+"transferred afterwards to another owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:135
+msgid ""
+"Contributors are users which are allowed to update this content in addition "
+"to it's owner"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:140
+msgid "Readers"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:141
+msgid ""
+"Readers are users which are asked to verify and comment contents before they "
+"are published"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:146
+msgid "Guests"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:147
+msgid ""
+"Guests are users which are allowed to view contents with restricted access"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:166
+msgid "Principal ID"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:171
+msgid "Restricted contents"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:172
+msgid ""
+"If 'yes', this manager will get restricted access to manage contents based on"
+" selected settings"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:177
+msgid "Selected owners"
+msgstr ""
+
+#: ./src/pyams_content/shared/common/interfaces/__init__.py:178
+msgid "Manager will have access to contents owned by these principals"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/zmi/properties.py:40
+msgid "Publication settings"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/zmi/__init__.py:44
+msgid "This news topic"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/zmi/__init__.py:63
+msgid "Add news topic"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/zmi/__init__.py:73
+msgid "Add new news topic"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/zmi/__init__.py:54
+#, python-format
+msgid "News topic « {title} »"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:30
+msgid "News topic"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:36
+msgid "Display first version date"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:37
+msgid "Display current version date"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:50
+msgid "Displayed publication date"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:51
+msgid "The matching date will be displayed in front-office"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:58
+msgid "Push end date"
+msgstr ""
+
+#: ./src/pyams_content/shared/news/interfaces/__init__.py:59
+msgid ""
+"Some contents can be pushed by components to front-office pages; if you set a"
+" date here, this content will not be pushed anymore passed this date, but "
+"will still be available via the search engine"
+msgstr ""
+
+#: ./src/pyams_content/profile/zmi/__init__.py:39
+msgid "Admin. profile"
+msgstr ""
+
+#: ./src/pyams_content/profile/interfaces/__init__.py:33
+msgid "Default table length"
+msgstr ""
+
+#: ./src/pyams_content/profile/interfaces/__init__.py:34
+msgid "Default length used for inner tables and dashboards"
+msgstr ""
+
+#: ./src/pyams_content/root/zmi/__init__.py:110
+msgid "Your contents dashboard"
+msgstr ""
+
+#: ./src/pyams_content/root/zmi/__init__.py:621
+msgid "Content"
+msgstr ""
+
+#: ./src/pyams_content/root/interfaces/__init__.py:36
+msgid "Site managers"
+msgstr ""
+
+#: ./src/pyams_content/root/interfaces/__init__.py:44
+msgid "Operators group"
+msgstr ""
+
+#: ./src/pyams_content/root/interfaces/__init__.py:45
+msgid "Name of group containing all roles owners"
+msgstr ""
+
+#: ./src/pyams_content/zmi/viewlet/toplinks/__init__.py:45
+msgid "Shared contents"
+msgstr ""
+
+#: ./src/pyams_content/zmi/viewlet/toplinks/__init__.py:63
+msgid "My roles"
+msgstr ""
+
+#: ./src/pyams_content/zmi/viewlet/toplinks/templates/user-addings.pt:7
+msgid "Create new content"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:82
+msgid "Draft"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:83
+msgid "Proposed"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:84
+msgid "Canceled"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:85
+msgid "Refused"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:86
+msgid "Published"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:87
+msgid "Retiring"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:88
+msgid "Retired"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:89
+msgid "Archiving"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:90
+msgid "Archived"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:91
+msgid "Deleted"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:93
+#, python-format
+msgid "draft created by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:94
+#, python-format
+msgid "publication requested by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:95
+#, python-format
+msgid "published by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:96
+#, python-format
+msgid "retiring requested by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:97
+#, python-format
+msgid "retired by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:98
+#, python-format
+msgid "archiving requested by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:99
+#, python-format
+msgid "archived by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:219
+msgid "Initialize"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:222
+msgid "Draft creation"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:225
+#: ./src/pyams_content/workflow/__init__.py:238
+msgid "Propose publication"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:232
+#: ./src/pyams_content/workflow/__init__.py:245
+msgid "Publication request"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:233
+#: ./src/pyams_content/workflow/__init__.py:246
+#: ./src/pyams_content/workflow/__init__.py:324
+#: ./src/pyams_content/workflow/__init__.py:358
+msgid ""
+"content managers authorized to take charge of your content are going to be "
+"notified of your request."
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:258
+msgid "Publication request canceled"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:262
+msgid "Reset canceled publication to draft"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:266
+#: ./src/pyams_content/workflow/__init__.py:293
+msgid "State reset to 'draft' (automatic)"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:270
+msgid "Reset canceled publication to retired"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:274
+msgid "State reset to 'retired' (automatic)"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:278
+msgid "Refuse publication"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:285
+msgid "Publication refused"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:289
+msgid "Reset refused publication to draft"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:297
+msgid "Reset refused publication to retired"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:301
+msgid "State reset to 'refused' (automatic)"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:313
+msgid "Content published"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:317
+msgid "Request retiring"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:323
+msgid "Retire request"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:329
+msgid "Cancel retiring request"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:336
+msgid "Retire request canceled"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:340
+msgid "Retire content"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:347
+msgid "Content retired"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:357
+msgid "Archive request"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:363
+msgid "Cancel archiving request"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:370
+msgid "Archive request canceled"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:374
+msgid "Archive content"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:382
+msgid "Content archived"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:386
+msgid "Archive published content"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:390
+#: ./src/pyams_content/workflow/__init__.py:398
+#: ./src/pyams_content/workflow/__init__.py:406
+msgid "Content archived after version publication"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:394
+msgid "Archive retiring content"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:402
+msgid "Archive retired content"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:418
+#: ./src/pyams_content/workflow/__init__.py:430
+#: ./src/pyams_content/workflow/__init__.py:442
+#: ./src/pyams_content/workflow/__init__.py:454
+#: ./src/pyams_content/workflow/__init__.py:466
+msgid "New version created"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:478
+msgid "Version deleted"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:544
+#, python-format
+msgid "publication refused by {principal}"
+msgstr ""
+
+#: ./src/pyams_content/workflow/__init__.py:189
+#, python-format
+msgid "Published version {0}"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:50
+msgid "Unique key"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:51
+msgid "WARNING: this key can't be modified after creation!!!"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:55
+msgid "Visible label used to display content"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:58
+msgid "Short name"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:59
+msgid "Short name used in breadcrumbs"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:66
+msgid "Creation date"
+msgstr ""
+
+#: ./src/pyams_content/interfaces/__init__.py:70
+msgid "Modification date"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/profile/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/profile/admin.py	Thu Oct 08 13:37:29 2015 +0200
@@ -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 pyams_content.profile.interfaces import IAdminProfile, ADMIN_PROFILE_KEY
+from pyams_security.interfaces import IPrincipalInfo
+from zope.annotation.interfaces import IAnnotations, IAttributeAnnotatable
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config
+from pyams_utils.request import check_request
+from pyramid.threadlocal import get_current_registry
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.interface import implementer, Interface
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IAdminProfile, IAttributeAnnotatable)
+class AdminProfile(Persistent):
+    """Admin profile persistent class"""
+
+    table_page_length = FieldProperty(IAdminProfile['table_page_length'])
+
+
+@adapter_config(context=Interface, provides=IAdminProfile)
+def AdminProfileFactory(context):
+    request = check_request()
+    return IAdminProfile(request.principal)
+
+
+@adapter_config(context=IPrincipalInfo, provides=IAdminProfile)
+def PrincipalAdminProfileFactory(principal):
+    """Principal admin profile factory adapter"""
+    annotations = IAnnotations(principal)
+    profile = annotations.get(ADMIN_PROFILE_KEY)
+    if profile is None:
+        profile = annotations[ADMIN_PROFILE_KEY] = AdminProfile()
+        get_current_registry().notify(ObjectCreatedEvent(profile))
+    return profile
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/profile/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+from zope.schema import Choice
+
+from pyams_content import _
+
+
+ADMIN_PROFILE_KEY = 'pyams_content.admin_profile'
+
+
+class IAdminProfile(Interface):
+    """User admin profile preferences"""
+
+    table_page_length = Choice(title=_("Default table length"),
+                               description=_("Default length used for inner tables and dashboards"),
+                               values=(10, 25, 50, 100),
+                               default=10)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/profile/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.profile.interfaces import IAdminProfile
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_form.form import InnerEditForm
+from pyams_security.zmi.profile import UserProfileEditForm
+from pyams_utils.adapter import adapter_config
+from z3c.form import field
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@adapter_config(name='admin_profile',
+                context=(Interface, IPyAMSLayer, UserProfileEditForm),
+                provides=IInnerTabForm)
+class AdminProfileTabForm(InnerEditForm):
+    """Admin profile tab form"""
+
+    tab_label = _("Admin. profile")
+    legend = None
+
+    fields = field.Fields(IAdminProfile)
+    edit_permission = None
+
+    label_css_class = 'control-label col-md-4'
+    input_css_class = 'col-md-8'
+
+    weight = 20
+
+    def getContent(self):
+        return IAdminProfile(self.request.principal)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/root/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,125 @@
+#
+# -*- encoding: utf-8 -*-
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.root.interfaces import ISiteRootRoles, ISiteRootConfiguration, ISiteRoot, \
+    ISiteRootToolsConfiguration, ISiteRootBackOfficeConfiguration
+from pyams_portal.interfaces import IPortalContext
+from pyams_security.interfaces import IDefaultProtectionPolicy, IGrantedRoleEvent, ISecurityManager
+from pyams_skin.interfaces.configuration import IStaticConfiguration
+from pyams_utils.interfaces.site import IConfigurationFactory, IBackOfficeConfigurationFactory
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from persistent import Persistent
+from pyams_security.property import RolePrincipalsFieldProperty
+from pyams_security.security import ProtectedObject
+from pyams_skin.configuration import Configuration, StaticConfiguration, BackOfficeConfiguration
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import get_utility
+from pyams_utils.site import BaseSiteRoot
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from zope.interface import implementer, Interface
+
+
+@adapter_config(context=(ISiteRoot, Interface, Interface), provides=IStaticConfiguration)
+class SiteRootStaticConfiguration(StaticConfiguration):
+    """Site root static configuration"""
+
+    application_package = 'pyams_content'
+    application_name = 'PyAMS CMS'
+
+    include_reload_button = False
+
+
+@implementer(IDefaultProtectionPolicy, ISiteRoot, ISiteRootRoles, IPortalContext)
+class SiteRoot(ProtectedObject, BaseSiteRoot):
+    """Main site root"""
+
+    __roles__ = ('system.Manager', 'pyams.Webmaster', 'pyams.Operator')
+
+    roles_interface = ISiteRootRoles
+
+    managers = RolePrincipalsFieldProperty(ISiteRootRoles['managers'])
+    webmasters = RolePrincipalsFieldProperty(ISiteRootRoles['webmasters'])
+    operators = RolePrincipalsFieldProperty(ISiteRootRoles['operators'])
+
+
+@implementer(ISiteRootConfiguration)
+class SiteRootConfiguration(Configuration):
+    """Site root configuration"""
+
+
+@adapter_config(context=ISiteRoot, provides=IConfigurationFactory)
+def SiteRootConfigurationFactory(context):
+    return SiteRootConfiguration
+
+
+@implementer(ISiteRootBackOfficeConfiguration)
+class SiteRootBackOfficeConfiguration(BackOfficeConfiguration):
+    """Site root back-office configuration"""
+
+
+@adapter_config(context=ISiteRoot, provides=IBackOfficeConfigurationFactory)
+def SiteRootBackOfficeConfigurationFactory(context):
+    return SiteRootBackOfficeConfiguration
+
+
+@subscriber(IGrantedRoleEvent)
+def handle_granted_role(event):
+    """Add principals to operators group when a role is granted"""
+    role_id = event.role_id
+    if (role_id == 'pyams.Operator') or (not role_id.startswith('pyams.')):
+        return
+    root = get_parent(event.object, ISiteRoot)
+    if not root.operators:
+        return
+    security = get_utility(ISecurityManager)
+    for principal_id in root.operators:
+        if not principal_id:
+            continue
+        group = security.get_principal(principal_id, info=False)
+        if event.principal_id not in group.principals:
+            group.principals = group.principals | {event.principal_id}
+
+
+#
+# Tools configuration
+#
+
+@implementer(ISiteRootToolsConfiguration)
+class SiteRootToolsConfiguration(Persistent):
+    """Site root tools configuration"""
+
+    tools_name = None
+    news_tool_name = None
+
+
+SITEROOT_TOOLS_CONFIGURATION_KEY = 'pyams_config.tools.configuration'
+
+
+@adapter_config(context=ISiteRoot, provides=ISiteRootToolsConfiguration)
+def site_root_tools_configuration_factory(context):
+    """Site root tools configuration factory"""
+    annotations = IAnnotations(context)
+    config = annotations.get(SITEROOT_TOOLS_CONFIGURATION_KEY)
+    if config is None:
+        config = annotations[SITEROOT_TOOLS_CONFIGURATION_KEY] = SiteRootToolsConfiguration()
+    return config
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/root/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,62 @@
+#
+# 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_skin.interfaces.configuration import IConfiguration, IBackOfficeConfiguration
+from pyams_utils.interfaces.site import ISiteRoot as ISiteRootBase
+
+# import packages
+from pyams_security.schema import PrincipalsSet, Principal
+from zope.interface import Interface, Attribute
+
+from pyams_content import _
+
+
+class ISiteRoot(ISiteRootBase):
+    """Main site root interface"""
+
+
+class ISiteRootRoles(Interface):
+    """Main site roles"""
+
+    managers = PrincipalsSet(title=_("Site managers"),
+                             role_id='system.Manager',
+                             required=False)
+
+    webmasters = PrincipalsSet(title=_("Webmasters"),
+                               role_id='pyams.Webmaster',
+                               required=False)
+
+    operators = Principal(title=_("Operators group"),
+                          description=_("Name of group containing all roles owners"),
+                          role_id='pyams.Operator',
+                          required=False)
+
+
+class ISiteRootConfiguration(IConfiguration):
+    """Site root configuration interface"""
+
+
+class ISiteRootToolsConfiguration(Interface):
+    """Site root tools configuration interface"""
+
+    tools_name = Attribute("Tools name")
+    news_tool_name = Attribute("News tool name")
+
+
+class ISiteRootBackOfficeConfiguration(IBackOfficeConfiguration):
+    """Site root back-office configuration interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/root/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,625 @@
+#
+# 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 hypatia.interfaces import ICatalog
+from pyams_content.interfaces import PUBLISH_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION
+from pyams_content.profile.interfaces import IAdminProfile
+from pyams_content.root.interfaces import ISiteRoot
+from pyams_content.shared.common.interfaces import ISharedTool, IManagerRestrictions
+from pyams_content.shared.common.interfaces.zmi import ISiteRootDashboardTable
+from pyams_content.zmi.interfaces import IDashboardMenu, IMyDashboardMenu, IAllContentsMenu
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowState, IWorkflow
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues, IColumn
+from zope.dublincore.interfaces import IZopeDublinCore
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import And, Or, Any, Eq
+from pyams_catalog.query import CatalogResultSet
+from pyams_content.shared.common.zmi.dashboard import BaseDashboardTable as BaseDashboardTableBase
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import I18nColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.list import unique
+from pyams_utils.registry import get_utility, get_utilities_for
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.view import AdminView
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@implementer(ISiteRootDashboardTable)
+class BaseDashboardTable(BaseDashboardTableBase):
+    """Base dashboard table"""
+
+
+#
+# Main dashboard menu
+#
+
+@viewlet_config(name='dashboard.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='dashboard.menu', layer=IAdminLayer, provides=IDashboardMenu)
+@implementer(IDashboardMenu)
+class SiteRootDashboardMenu(MenuItem):
+    """Site root dashboard menu"""
+
+    label = _("Dashboard")
+    icon_class = 'fa-line-chart'
+    url = '#dashboard.html'
+
+
+@pagelet_config(name='dashboard.html', context=ISiteRoot, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/dashboard.pt', layer=IAdminLayer)
+@implementer(IInnerPage)
+class SiteRootDashboardView(AdminView):
+    """Site root dashboard view"""
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    def __init__(self, context, request):
+        super(SiteRootDashboardView, self).__init__(context, request)
+        self.tables = []
+        self.tables.append(SiteRootDashboardManagerWaitingTable(self.context, self.request))
+        self.tables.append(SiteRootDashboardOwnerWaitingTable(self.context, self.request))
+        self.tables.append(SiteRootDashboardOwnerModifiedTable(self.context, self.request))
+        for table in self.tables:
+            table.hide_toolbar = True
+
+    def update(self):
+        super(SiteRootDashboardView, self).update()
+        [table.update() for table in self.tables]
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootDashboardView), provides=IPageHeader)
+class SiteRootDashboardHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root properties header adapter"""
+
+    icon_class = 'fa fa-fw fa-line-chart'
+
+    title = _("Your contents dashboard")
+
+
+#
+# Contents waiting for manager action
+#
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootDashboardManagerWaitingTable(BaseDashboardTable):
+    """Site root dashboard manager waiting contents table"""
+
+    _title = _("MANAGER - {0} content(s) waiting for your action")
+
+    dt_sort_order = 'asc'
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootDashboardManagerWaitingTable), provides=IValues)
+class SiteRootDashboardManagerWaitingValues(ContextRequestViewAdapter):
+    """Site root dashboard manager waiting contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = Eq(catalog['content_type'], tool.shared_content_type) & \
+                    Any(catalog['workflow_state'], workflow.waiting_states)
+            params = params | query if params else query
+        return filter(self.check_access,
+                      unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(IWorkflowState(x).state),
+                                                  key=lambda y: IZopeDublinCore(y).modified, reverse=True)[0],
+                                 CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                              sort_index='modified_date')))))
+
+    def check_access(self, content):
+        if self.request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context=content):
+            return True
+        if self.request.principal.id in content.managers:
+            return True
+        restrictions = IManagerRestrictions(content).get_restrictions(self.request.principal)
+        if restrictions is not None:
+            return restrictions.check_access(content, PUBLISH_CONTENT_PERMISSION, self.request)
+        else:
+            return False
+
+
+#
+# Last owned contents waiting for action
+#
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootDashboardOwnerWaitingTable(BaseDashboardTable):
+    """Site root dashboard waiting owned contents table"""
+
+    _title = _("CONTRIBUTOR - Your {0} content(s) waiting for action")
+
+    dt_sort_order = 'asc'
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootDashboardOwnerWaitingTable), provides=IValues)
+class SiteRootDashboardOwnerWaitingValues(ContextRequestViewAdapter):
+    """Site root dashboard waiting owned contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = Eq(catalog['content_type'], tool.shared_content_type) & \
+                    Any(catalog['workflow_state'], workflow.waiting_states) & \
+                    Eq(catalog['workflow_principal'], self.request.principal.id)
+            params = params | query if params else query
+        return unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(IWorkflowState(x).state),
+                                           key=lambda y: IZopeDublinCore(y).modified, reverse=True)[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       sort_index='modified_date'))))
+
+
+#
+# Last owned modified contents
+#
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootDashboardOwnerModifiedTable(BaseDashboardTable):
+    """Site root dashboard modified contents table"""
+
+    _title = _("CONTRIBUTOR - Your last modified contents (limited to {0})")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootDashboardOwnerModifiedTable), provides=IValues)
+class SiteRootDashboardOwnerModifiedValues(ContextRequestViewAdapter):
+    """Site root dashboard modified contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            query = And(Eq(catalog['content_type'], tool.shared_content_type),
+                        Or(Eq(catalog['role:owner'], self.request.principal.id),
+                           Eq(catalog['role:contributor'], self.request.principal.id)))
+            params = params | query if params else query
+        return unique(map(lambda x: IWorkflowVersions(x).get_last_versions()[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       limit=IAdminProfile(self.request.principal).table_page_length,
+                                                                       sort_index='modified_date',
+                                                                       reverse=True))))
+
+
+#
+# All my contents menu
+#
+
+@viewlet_config(name='my-contents.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=5)
+@viewletmanager_config(name='my-contents.menu', layer=IAdminLayer, provides=IMyDashboardMenu)
+@implementer(IMyDashboardMenu)
+class SiteRootMyDashboardMenu(MenuItem):
+    """Site root 'my contents' dashboard menu"""
+
+    label = _("My contents")
+    icon_class = 'fa-user'
+    url = '#'
+
+
+#
+# My preparations
+# Dashboard of owned and modified contents which can be updated
+#
+
+@viewlet_config(name='my-preparations.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=5)
+class SiteRootPreparationsMenu(MenuItem):
+    """Site root preparations dashboard menu"""
+
+    label = _("My preparations")
+    icon_class = None
+    url = '#my-preparations.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootPreparationsTable(BaseDashboardTable):
+    """Site root preparations table"""
+
+    _title = _("CONTRIBUTOR - Your {0} prepared contents")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootPreparationsTable), provides=IValues)
+class SiteRootPreparationsValues(ContextRequestViewAdapter):
+    """Site root preparations values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = And(Eq(catalog['content_type'], tool.shared_content_type),
+                        Or(Eq(catalog['role:owner'], self.request.principal.id),
+                           Eq(catalog['role:contributor'], self.request.principal.id)),
+                        Any(catalog['workflow_state'], workflow.update_states))
+            params = params | query if params else query
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='my-preparations.html', context=ISiteRoot, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteRootPreparationsView(AdminView, ContainerView):
+    """Site root preparations view"""
+
+    table_class = SiteRootPreparationsTable
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootPreparationsView), provides=IPageHeader)
+class SiteRootPreparationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root preparations header adapter"""
+
+    icon_class = 'fa fa-fw fa-user'
+
+    title = _("Your prepared contents")
+
+
+#
+# My publications
+# Dashboard of owned and modified contents which are published
+#
+
+@viewlet_config(name='my-publications.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+class SiteRootPublicationsMenu(MenuItem):
+    """Site root publications dashboard menu"""
+
+    label = _("My publications")
+    icon_class = None
+    url = '#my-publications.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootPublicationsTable(BaseDashboardTable):
+    """Site root publications table"""
+
+    _title = _("CONTRIBUTOR - Your {0} published contents")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootPublicationsTable), provides=IValues)
+class SiteRootPublicationsValues(ContextRequestViewAdapter):
+    """Site root publications values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = And(Eq(catalog['content_type'], tool.shared_content_type),
+                        Or(Eq(catalog['role:owner'], self.request.principal.id),
+                           Eq(catalog['role:contributor'], self.request.principal.id)),
+                        Any(catalog['workflow_state'], workflow.published_states))
+            params = params | query if params else query
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='my-publications.html', context=ISiteRoot, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteRootPublicationsView(AdminView, ContainerView):
+    """Site root publications view"""
+
+    table_class = SiteRootPublicationsTable
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootPublicationsView), provides=IPageHeader)
+class SiteRootPublicationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root publications header adapter"""
+
+    icon_class = 'fa fa-fw fa-user'
+
+    title = _("Your published contents")
+
+
+#
+# My retired contents
+# Dashboard of owned and modified contents which are retired
+#
+
+@viewlet_config(name='my-retired-contents.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=15)
+class SiteRootRetiredMenu(MenuItem):
+    """Site root retired contents dashboard menu"""
+
+    label = _("My retired contents")
+    icon_class = None
+    url = '#my-retired-contents.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootRetiredContentsTable(BaseDashboardTable):
+    """Site root retired contents table"""
+
+    _title = _("CONTRIBUTOR - Your {0} retired contents")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootRetiredContentsTable), provides=IValues)
+class SiteRootRetiredContentsValues(ContextRequestViewAdapter):
+    """Site root retired contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = And(Eq(catalog['content_type'], tool.shared_content_type),
+                        Or(Eq(catalog['role:owner'], self.request.principal.id),
+                           Eq(catalog['role:contributor'], self.request.principal.id)),
+                        Any(catalog['workflow_state'], workflow.retired_states))
+            params = params | query if params else query
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='my-retired-contents.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteRootRetiredContentsView(AdminView, ContainerView):
+    """Site root retired contents view"""
+
+    table_class = SiteRootRetiredContentsTable
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootRetiredContentsView), provides=IPageHeader)
+class SiteRootRetiredContentsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root retired contents header adapter"""
+
+    icon_class = 'fa fa-fw fa-user'
+
+    title = _("Your retired contents")
+
+
+#
+# My archived contents
+# Dashboard of owned and modified contents which are archived
+#
+
+@viewlet_config(name='my-archived-contents.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=20)
+class SiteRootArchivedMenu(MenuItem):
+    """Site root archived contents dashboard menu"""
+
+    label = _("My archived contents")
+    icon_class = None
+    url = '#my-archived-contents.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootArchivedContentsTable(BaseDashboardTable):
+    """Site root archived contents table"""
+
+    _title = _("CONTRIBUTOR - Your {0} archived contents")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootArchivedContentsTable), provides=IValues)
+class SiteRootArchivedContentsValues(ContextRequestViewAdapter):
+    """Site root archived contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        principal_id = self.request.principal.id
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = And(Eq(catalog['content_type'], tool.shared_content_type),
+                        Or(Eq(catalog['role:owner'], principal_id),
+                           Eq(catalog['role:contributor'], principal_id)),
+                        Any(catalog['workflow_state'], workflow.readonly_states))
+            params = params | query if params else query
+        return unique(map(lambda x: sorted((version for version in
+                                            IWorkflowVersions(x).get_versions(IWorkflow(x).readonly_states)
+                                            if principal_id in (version.owner | version.contributors)),
+                                           key=lambda x: IWorkflowState(x).version_id,
+                                           reverse=True)[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       sort_index='modified_date',
+                                                                       reverse=True))))
+
+
+@pagelet_config(name='my-archived-contents.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteRootArchivedContentsView(AdminView, ContainerView):
+    """Site root archived contents view"""
+
+    table_class = SiteRootArchivedContentsTable
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootArchivedContentsView), provides=IPageHeader)
+class SiteRootArchivedContentsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root archived contents header adapter"""
+
+    icon_class = 'fa fa-fw fa-user'
+
+    title = _("Your archived contents")
+
+
+#
+# All interventions
+#
+
+@viewlet_config(name='all-interventions.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+@viewletmanager_config(name='all-interventions.menu', layer=IAdminLayer, provides=IAllContentsMenu)
+@implementer(IAllContentsMenu)
+class SiteRootAllContentsMenu(MenuItem):
+    """Site root 'all contents' dashboard menu"""
+
+    label = _("Other interventions")
+    icon_class = 'fa-pencil-square'
+    url = '#'
+
+
+#
+# Last publications
+# Dashboard of all published contents
+#
+
+@viewlet_config(name='all-publications.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IAllContentsMenu, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+class SiteRootAllPublicationsMenu(MenuItem):
+    """Site root published contents dashboard menu"""
+
+    label = _("Last publications")
+    icon_class = None
+    url = '#all-publications.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootAllPublicationsTable(BaseDashboardTable):
+    """Site root published contents table"""
+
+    _title = _("CONTRIBUTORS - Last published contents (in the limit of 50)")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAllPublicationsTable), provides=IValues)
+class SiteRootAllPublicationsValues(ContextRequestViewAdapter):
+    """Site root published contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            workflow = get_utility(IWorkflow, name=tool.shared_content_workflow)
+            query = And(Eq(catalog['content_type'], tool.shared_content_type),
+                        Any(catalog['workflow_state'], workflow.published_states))
+            params = params | query if params else query
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   limit=50,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='all-publications.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteRootAllPublicationsView(AdminView, ContainerView):
+    """Site root published contents view"""
+
+    table_class = SiteRootAllPublicationsTable
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAllPublicationsView), provides=IPageHeader)
+class SiteRootAllPublicationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root published contents header adapter"""
+
+    icon_class = 'fa fa-fw fa-pencil-square'
+
+    title = _("Last published contents")
+
+
+#
+# Last updates
+# Dashboard of all updated contents
+#
+
+@viewlet_config(name='all-updates.menu', context=ISiteRoot, layer=IAdminLayer,
+                manager=IAllContentsMenu, permission=VIEW_SYSTEM_PERMISSION, weight=20)
+class SiteRootAllUpdatesMenu(MenuItem):
+    """Site root updated contents dashboard menu"""
+
+    label = _("Last updates")
+    icon_class = None
+    url = '#all-updates.html'
+
+
+@implementer(ISiteRootDashboardTable)
+class SiteRootAllUpdatesTable(BaseDashboardTable):
+    """Site root updated contents table"""
+
+    _title = _("CONTRIBUTORS - Last updated contents (in the limit of 50)")
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAllUpdatesTable), provides=IValues)
+class SiteRootAllUpdatesValues(ContextRequestViewAdapter):
+    """Site root updated contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = None
+        for name, tool in get_utilities_for(ISharedTool):
+            query = Eq(catalog['content_type'], tool.shared_content_type)
+            params = params | query if params else query
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   limit=50,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='all-updates.html', context=ISiteRoot, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SiteRootAllUpdatesView(AdminView, ContainerView):
+    """Site root updated contents view"""
+
+    table_class = SiteRootAllUpdatesTable
+
+
+@adapter_config(context=(ISiteRoot, IPyAMSLayer, SiteRootAllUpdatesView), provides=IPageHeader)
+class SiteRootAllUpdatesHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root updated contents header adapter"""
+
+    icon_class = 'fa fa-fw fa-pencil-square'
+
+    title = _("Last updated contents")
+
+
+#
+# Custom columns
+#
+
+@adapter_config(name='tool', context=(Interface, IPyAMSLayer, ISiteRootDashboardTable), provides=IColumn)
+class SiteRootDashboardContentTypeColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard content type column"""
+
+    _header = _("Content")
+    weight = 1
+
+    def getValue(self, obj):
+        return self.request.localizer.translate(obj.content_name)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/root/zmi/search.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/root/zmi/templates/dashboard.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,4 @@
+<tal:loop repeat="table view.tables">
+	<tal:if condition="tuple(table.values)"
+			content="structure table.render()" />
+</tal:loop>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,190 @@
+#
+# 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_i18n.content import I18nManagerMixin
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from hypatia.interfaces import ICatalog
+from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles, ISharedContent, ISharedTool
+from pyams_content.interfaces import IBaseContentInfo
+from pyams_i18n.interfaces import II18nManager
+from pyams_security.interfaces import IDefaultProtectionPolicy
+from pyams_sequence.interfaces import ISequentialIdTarget, ISequentialIdInfo
+from pyams_utils.interfaces import VIEW_PERMISSION
+from pyams_workflow.interfaces import IWorkflowPublicationSupport, IWorkflow, IObjectClonedEvent, IWorkflowVersions
+from zope.dublincore.interfaces import IZopeDublinCore
+from zope.intid.interfaces import IIntIds
+from zope.lifecycleevent.interfaces import IObjectModifiedEvent
+
+# import packages
+from persistent import Persistent
+from pyams_security.property import RolePrincipalsFieldProperty
+from pyams_security.security import ProtectedObject
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
+from pyams_utils.request import query_request
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+#
+# Content types management
+#
+
+CONTENT_TYPES = {}
+
+
+def register_content_type(content):
+    """Register a new content type"""
+    CONTENT_TYPES[content.content_type] = content
+
+
+#
+# Workflow shared content class and adapters
+#
+
+@implementer(IDefaultProtectionPolicy, IWfSharedContent, IWfSharedContentRoles, IWorkflowPublicationSupport)
+class WfSharedContent(ProtectedObject, Persistent, Contained, I18nManagerMixin):
+    """Shared data content class"""
+
+    __roles__ = ('pyams.Owner', 'pyams.Manager', 'pyams.Contributors', 'pyams.Reader', 'pyams.Guest')
+    roles_interface = IWfSharedContentRoles
+
+    owner = RolePrincipalsFieldProperty(IWfSharedContentRoles['owner'])
+    managers = RolePrincipalsFieldProperty(IWfSharedContentRoles['managers'])
+    contributors = RolePrincipalsFieldProperty(IWfSharedContentRoles['contributors'])
+    readers = RolePrincipalsFieldProperty(IWfSharedContentRoles['readers'])
+    guests = RolePrincipalsFieldProperty(IWfSharedContentRoles['guests'])
+
+    content_type = None
+
+    title = FieldProperty(IWfSharedContent['title'])
+    short_name = FieldProperty(IWfSharedContent['short_name'])
+    creator = FieldProperty(IWfSharedContent['creator'])
+    modifiers = FieldProperty(IWfSharedContent['modifiers'])
+    description = FieldProperty(IWfSharedContent['description'])
+    keywords = FieldProperty(IWfSharedContent['keywords'])
+    notepad = FieldProperty(IWfSharedContent['notepad'])
+
+    @property
+    def first_owner(self):
+        versions = IWorkflowVersions(self)
+        return versions.get_version(1).creator
+
+
+@subscriber(IObjectModifiedEvent, context_selector=IWfSharedContent)
+def handle_modified_shared_content(event):
+    """Define content's modifiers when content is modified"""
+    request = query_request()
+    if request is not None:
+        content = event.object
+        principal_id = request.principal.id
+        modifiers = content.modifiers or set()
+        if principal_id not in modifiers:
+            modifiers.add(principal_id)
+            content.modifiers = modifiers
+            catalog = query_utility(ICatalog)
+            intids = query_utility(IIntIds)
+            catalog['modifiers'].reindex_doc(intids.register(content), content)
+
+
+@subscriber(IObjectClonedEvent, context_selector=IWfSharedContent)
+def handle_cloned_shared_content(event):
+    """Handle cloned object when a new version is created
+
+    Current principal is set as version creator, and is added to version
+    contributors if he is not the original content's owner
+    """
+    request = query_request()
+    principal_id = request.principal.id
+    content = event.object
+    content.creator = principal_id
+    if principal_id != content.owner:
+        # creation of new versions doesn't change owner
+        # but new creators are added to contributors list
+        contributors = content.contributors or set()
+        contributors.add(principal_id)
+        content.contributors = contributors
+    # reset modifiers
+    content.modifiers = set()
+
+
+@adapter_config(context=IWfSharedContent, provides=ISequentialIdInfo)
+def WfSharedContentSequenceAdapter(context):
+    """Shared content sequence adapter"""
+    parent = get_parent(context, ISharedContent)
+    return ISequentialIdInfo(parent)
+
+
+@adapter_config(context=IWfSharedContent, provides=IBaseContentInfo)
+class WfSharedContentInfoAdapter(ContextAdapter):
+    """Shared content base info adapter"""
+
+    @property
+    def created_date(self):
+        return IZopeDublinCore(self.context).created
+
+    @property
+    def modified_date(self):
+        return IZopeDublinCore(self.context).modified
+
+
+@adapter_config(context=IWfSharedContent, provides=IWorkflow)
+def WfSharedContentWorkflowAdapter(context):
+    """Shared content workflow adapter"""
+    parent = get_parent(context, ISharedTool)
+    return query_utility(IWorkflow, name=parent.shared_content_workflow)
+
+
+#
+# Main shared content class and adapters
+#
+
+@implementer(ISharedContent, ISequentialIdTarget)
+class SharedContent(Persistent, Contained):
+    """Workflow managed shared data"""
+
+    view_permission = VIEW_PERMISSION
+
+    sequence_name = ''  # use default sequence generator
+    sequence_prefix = ''
+
+    @property
+    def workflow_name(self):
+        return get_parent(self, ISharedTool).shared_content_workflow
+
+
+@adapter_config(context=ISharedContent, provides=IBaseContentInfo)
+class SharedContentInfoAdapter(ContextAdapter):
+    """Shared content base info adapter"""
+
+    @property
+    def created_date(self):
+        return IZopeDublinCore(self.context).created
+
+    @property
+    def modified_date(self):
+        return IZopeDublinCore(self.context).modified
+
+
+@adapter_config(context=ISharedContent, provides=IWorkflow)
+def SharedContentWorkflowAdapter(context):
+    """Shared content workflow adapter"""
+    parent = get_parent(context, ISharedTool)
+    return query_utility(IWorkflow, name=parent.shared_content_workflow)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,196 @@
+#
+# 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.schema import TextLineListField
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import IBaseContent, MANAGE_CONTENT_PERMISSION
+from pyams_content.root.interfaces import ISiteRoot
+from pyams_workflow.interfaces import IWorkflowManagedContent
+from zope.container.interfaces import IContainer
+
+# import packages
+from pyams_i18n.schema import I18nTextField
+from pyams_security.schema import Principal, PrincipalsSet
+from zope.container.constraints import containers, contains
+from zope.interface import Interface, Attribute
+from zope.schema import Choice, Bool, Text, Datetime
+
+from pyams_content import _
+
+
+class ISharedToolContainer(IBaseContent, IContainer):
+    """Shared tools container"""
+
+    containers(ISiteRoot)
+    contains('.ISharedTool')
+
+
+class ISharedTool(IBaseContent, IContainer):
+    """Shared tool interface"""
+
+    containers(ISharedToolContainer)
+    contains('.ISharedData')
+
+    shared_content_type = Attribute("Shared data content type")
+    shared_content_factory = Attribute("Shared data factory")
+
+    shared_content_workflow = Choice(title=_("Workflow name"),
+                                     description=_("Name of workflow utility used to manage tool contents"),
+                                     vocabulary="PyAMS workflows",
+                                     default="PyAMS default workflow")
+
+
+class ISharedToolRoles(Interface):
+    """Shared tool roles interface"""
+
+    webmasters = PrincipalsSet(title=_("Webmasters"),
+                               description=_("Webmasters can handle all contents, including published ones"),
+                               role_id='pyams.Webmaster',
+                               required=False)
+
+    pilots = PrincipalsSet(title=_("Pilots"),
+                           description=_("Pilots can handle tool configuration, manage access rules, grant users "
+                                         "roles and manage managers restrictions"),
+                           role_id='pyams.Pilot',
+                           required=False)
+
+    managers = PrincipalsSet(title=_("Managers"),
+                             description=_("Managers can handle main operations in tool's workflow, like publish "
+                                           "or retire contents"),
+                             role_id='pyams.Manager',
+                             required=False)
+
+    contributors = PrincipalsSet(title=_("Contributors"),
+                                 description=_("Contributors are users which are allowed to create new contents"),
+                                 role_id='pyams.Contributor',
+                                 required=False)
+
+
+class IWfSharedContent(IBaseContent):
+    """Shared content interface"""
+
+    content_type = Attribute("Content data type")
+    content_name = Attribute("Content name")
+
+    creator = Principal(title=_("Version creator"),
+                        description=_("Name of content's version creator. "
+                                      "The creator of the first version is also it's owner."),
+                        required=True)
+
+    first_owner = Principal(title=_("First owner"),
+                            description=_("Name of content's first version owner"),
+                            required=True,
+                            readonly=True)
+
+    modifiers = PrincipalsSet(title=_("Version modifiers"),
+                              description=_("List of principals who modified this content"),
+                              required=False)
+
+    description = I18nTextField(title=_("Description"),
+                                description=_("The content's description is 'hidden' into HTML's page headers; but it "
+                                              "can be seen, for example, in some search engines results as content's "
+                                              "description"),
+                                required=False)
+
+    keywords = TextLineListField(title=_("Keywords"),
+                                 description=_("They will be included into HTML pages metadata"),
+                                 required=False)
+
+    notepad = Text(title=_("Notepad"),
+                   description=_("Internal information to be known about this content"),
+                   required=False)
+
+
+class IWfSharedContentRoles(Interface):
+    """Shared content roles"""
+
+    owner = PrincipalsSet(title=_("Content owner"),
+                          description=_("The owner is the creator of content's first version, except if it was "
+                                        "transferred afterwards to another owner"),
+                          role_id='pyams.Owner',
+                          required=True,
+                          max_length=1)
+
+    managers = PrincipalsSet(title=_("Managers"),
+                             description=_("Managers can handle main operations in tool's workflow, like publish "
+                                           "or retire contents"),
+                             role_id='pyams.Manager',
+                             required=False)
+
+    contributors = PrincipalsSet(title=_("Contributors"),
+                                 description=_("Contributors are users which are allowed to update this content in "
+                                               "addition to it's owner"),
+                                 role_id='pyams.Contributor',
+                                 required=False)
+
+    readers = PrincipalsSet(title=_("Readers"),
+                            description=_("Readers are users which are asked to verify and comment contents before "
+                                          "they are published"),
+                            role_id='pyams.Reader',
+                            required=False)
+
+    guests = PrincipalsSet(title=_("Guests"),
+                           description=_("Guests are users which are allowed to view contents with restricted access"),
+                           role_id='pyams.Guest',
+                           required=False)
+
+
+class ISharedContent(IWorkflowManagedContent):
+    """Workflow managed shared content interface"""
+
+
+#
+# Shared tool manager security restrictions
+#
+
+MANAGER_RESTRICTIONS_KEY = 'pyams_content.manager.restrictions'
+
+
+class IManagerRestrictionInfo(Interface):
+    """Shared content manager restrictions"""
+
+    principal_id = Principal(title=_("Principal ID"),
+                             required=True)
+
+    restriction_interface = Attribute("Restrictions interface")
+
+    restricted_contents = Bool(title=_("Restricted contents"),
+                               description=_("If 'yes', this manager will get restricted access to manage contents "
+                                             "based on selected settings"),
+                               required=False,
+                               default=True)
+
+    owners = PrincipalsSet(title=_("Selected owners"),
+                           description=_("Manager will have access to contents owned by these principals"),
+                           required=False)
+
+    def check_access(self, context, permission=MANAGE_CONTENT_PERMISSION, request=None):
+        """Check if principal is granted access to given content"""
+
+
+class IManagerRestrictionsFactory(Interface):
+    """Manager restrictions factory interface"""
+
+
+class IManagerRestrictions(Interface):
+    """Manager restrictions"""
+
+    def get_restrictions(self, principal):
+        """Get manager restrictions for given principal"""
+
+    def set_restrictions(self, principal, restrictions):
+        """Set manager restrictions for given principal"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/interfaces/templates/summary-layout.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+<tal:var content="structure provider:pagelet" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/interfaces/zmi.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,37 @@
+#
+# 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_skin.interfaces.container import ITable
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_template.template import layout_config
+from zope.interface import Interface
+
+
+class ISharedToolDashboardTable(ITable):
+    """Shared tool dashboard table marker interface"""
+
+
+class ISiteRootDashboardTable(ISharedToolDashboardTable):
+    """Site root dashboard table marker interface"""
+
+
+@layout_config(template='templates/summary-layout.pt', layer=IPyAMSLayer)
+class IInnerSummaryView(Interface):
+    """Inner summary view marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/manager.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,69 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.common.interfaces import ISharedToolContainer, ISharedTool, ISharedToolRoles
+from pyams_i18n.interfaces import II18nManager
+from pyams_security.interfaces import IDefaultProtectionPolicy
+from pyams_workflow.interfaces import IWorkflow
+from zope.annotation.interfaces import IAttributeAnnotatable
+
+# import packages
+from pyams_security.property import RolePrincipalsFieldProperty
+from pyams_security.security import ProtectedObject
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility
+from zope.container.folder import Folder
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(ISharedToolContainer, IAttributeAnnotatable)
+class SharedToolContainer(Folder):
+    """Shared tools container"""
+
+    title = FieldProperty(ISharedToolContainer['title'])
+    short_name = FieldProperty(ISharedToolContainer['short_name'])
+
+
+@implementer(IDefaultProtectionPolicy, ISharedTool, ISharedToolRoles, IAttributeAnnotatable, II18nManager)
+class SharedTool(ProtectedObject, Folder):
+    """Shared tool"""
+
+    __roles__ = ('pyams.Webmaster', 'pyams.Pilot', 'pyams.Manager', 'pyams.Contributor')
+
+    roles_interface = ISharedToolRoles
+
+    webmasters = RolePrincipalsFieldProperty(ISharedToolRoles['webmasters'])
+    pilots = RolePrincipalsFieldProperty(ISharedToolRoles['pilots'])
+    managers = RolePrincipalsFieldProperty(ISharedToolRoles['managers'])
+    contributors = RolePrincipalsFieldProperty(ISharedToolRoles['contributors'])
+
+    title = FieldProperty(ISharedTool['title'])
+    short_name = FieldProperty(ISharedTool['short_name'])
+
+    shared_content_type = None
+    shared_content_factory = None
+    shared_content_workflow = FieldProperty(ISharedTool['shared_content_workflow'])
+
+    languages = FieldProperty(II18nManager['languages'])
+
+
+@adapter_config(context=ISharedTool, provides=IWorkflow)
+def SharedToolWorkflowAdapter(context):
+    """Shared tool workflow adapter"""
+    return query_utility(IWorkflow, name=context.shared_content_workflow)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/security.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,97 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent, IManagerRestrictions, MANAGER_RESTRICTIONS_KEY, \
+    IManagerRestrictionsFactory, ISharedTool, IManagerRestrictionInfo
+
+# import packages
+from persistent import Persistent
+from pyams_security.interfaces import IPrincipalInfo
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.request import check_request
+from pyams_utils.traversing import get_parent
+from zope.annotation.interfaces import IAnnotations
+from zope.container.folder import Folder
+from zope.interface import implementer
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IManagerRestrictionInfo)
+class SharedToolManagerRestrictionInfo(Persistent):
+    """Shared tool manager restriction info"""
+
+    restriction_interface = IManagerRestrictionInfo
+
+    principal_id = FieldProperty(IManagerRestrictionInfo['principal_id'])
+    restricted_contents = FieldProperty(IManagerRestrictionInfo['restricted_contents'])
+    owners = FieldProperty(IManagerRestrictionInfo['owners'])
+
+    def __init__(self, principal_id):
+        self.principal_id = principal_id
+
+    def check_access(self, context, permission=MANAGE_CONTENT_PERMISSION, request=None):
+        if request is None:
+            request = check_request()
+        if not request.has_permission(permission, context):  # check permission
+            return False
+        if not self.restricted_contents:  # get access if no restriction
+            return True
+        if context.owner & (self.owners or set()):  # check if owners are matching
+            return True
+        return False
+
+
+@adapter_config(context=ISharedTool, provides=IManagerRestrictions)
+class SharedToolManagerRestrictions(ContextAdapter):
+    """Shared tool manager restrictions"""
+
+    def get_restrictions(self, principal):
+        annotations = IAnnotations(self.context)
+        restrictions_folder = annotations.get(MANAGER_RESTRICTIONS_KEY)
+        if restrictions_folder is None:
+            restrictions_folder = annotations[MANAGER_RESTRICTIONS_KEY] = Folder()
+            locate(restrictions_folder, self.context)
+        if IPrincipalInfo.providedBy(principal):
+            principal = principal.id
+        return restrictions_folder.get(principal)
+
+    def set_restrictions(self, principal, restrictions):
+        annotations = IAnnotations(self.context)
+        restrictions_folder = annotations.get(MANAGER_RESTRICTIONS_KEY)
+        if restrictions_folder is None:
+            restrictions_folder = annotations[MANAGER_RESTRICTIONS_KEY] = Folder()
+            locate(restrictions_folder, self.context)
+        if IPrincipalInfo.providedBy(principal):
+            principal = principal.id
+        restrictions_folder[principal] = restrictions
+
+
+@adapter_config(context=IWfSharedContent, provides=IManagerRestrictions)
+def SharedContentManagerRestrictions(context):
+    """Shared tool manager restrictions"""
+    tool = get_parent(context, ISharedTool)
+    if tool is not None:
+        return IManagerRestrictions(tool)
+
+
+@adapter_config(context=ISharedTool, provides=IManagerRestrictionsFactory)
+def SharedToolManagerRestrictionsFactory(context):
+    """Default shared tool manager restrictions factory"""
+    return SharedToolManagerRestrictionInfo
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,319 @@
+#
+# 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
+from uuid import uuid4
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, MANAGE_CONTENT_PERMISSION, CREATE_CONTENT_PERMISSION, \
+    PUBLISH_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent, ISharedContent, ISharedTool, IManagerRestrictions
+from pyams_form.interfaces.form import IFormContextPermissionChecker, IWidgetsPrefixViewletsManager
+from pyams_i18n.interfaces import II18n, II18nManager
+from pyams_sequence.interfaces import ISequentialIntIds, ISequentialIdInfo
+from pyams_skin.interfaces import IContentTitle
+from pyams_skin.interfaces.container import ITable, ITableElementEditor
+from pyams_skin.interfaces.viewlet import IContextActions, IMenuHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import FORBIDDEN_PERMISSION
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowInfo, IWorkflowState, IWorkflowCommentInfo, IWorkflow
+from pyams_zmi.interfaces.menu import ISiteManagementMenu
+from zope.dublincore.interfaces import IZopeDublinCore
+
+# import packages
+from pyams_form.form import AJAXAddForm
+from pyams_form.schema import CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import DefaultElementEditorAdapter
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter, ContextRequestAdapter
+from pyams_utils.registry import get_utility
+from pyams_utils.request import check_request
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_workflow.versions import WorkflowHistoryItem
+from pyams_zmi.form import AdminDialogAddForm
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.copy import copy
+from zope.interface import Interface
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+
+from pyams_content import _
+
+
+class SharedContentAddForm(AdminDialogAddForm):
+    """Shared content add form"""
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    icon_css_class = 'fa fa-fw fa-plus'
+    fields = field.Fields(IWfSharedContent).select('title', 'description')
+
+    ajax_handler = 'add-shared-content.json'
+    edit_permission = CREATE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(SharedContentAddForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'textarea'
+
+    def create(self, data):
+        return self.context.shared_content_factory.content_class()
+
+    def update_content(self, content, data):
+        # generic content update
+        changes = super(SharedContentAddForm, self).update_content(content, data)
+        content.creator = self.request.principal.id
+        content.owner = self.request.principal.id
+        content.short_name = content.title.copy()
+        # init content languages
+        languages = II18nManager(self.context).languages
+        if languages:
+            II18nManager(content).languages = languages.copy()
+        return changes
+
+    def add(self, wf_content):
+        content = self.context.shared_content_factory()
+        self.request.registry.notify(ObjectCreatedEvent(content))
+        uuid = self.__uuid = str(uuid4())
+        self.context[uuid] = content
+        IWorkflowVersions(content).add_version(wf_content, None)
+        IWorkflowInfo(wf_content).fire_transition('init')
+
+    def nextURL(self):
+        return absolute_url(self.context, self.request, '{0}/++versions++/1/@@admin.html'.format(self.__uuid))
+
+
+class SharedContentAJAXAddForm(AJAXAddForm):
+    """Shared event add form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'redirect',
+                'location': self.nextURL()}
+
+
+@viewlet_config(name='wf-create-message', context=Interface, layer=IPyAMSLayer, view=SharedContentAddForm,
+                manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-create-message.pt')
+class SharedContentAddFormMessage(Viewlet):
+    """Shared content add form info message"""
+
+
+#
+# Edit adapters and views
+#
+
+@adapter_config(context=IWfSharedContent, provides=IFormContextPermissionChecker)
+class WfSharedContentPermissionChecker(ContextAdapter):
+    """Shared content form permission checker"""
+
+    @property
+    def edit_permission(self):
+        workflow = IWorkflow(self.context)
+        state = IWorkflowState(self.context).state
+        if state in workflow.readonly_states:  # access forbidden to all for archived contents
+            return FORBIDDEN_PERMISSION
+        elif state in workflow.protected_states:  # webmaster can update published contents
+            return MANAGE_SITE_ROOT_PERMISSION
+        else:
+            request = check_request()
+            if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, self.context):  # webmaster access
+                return MANAGE_SITE_ROOT_PERMISSION
+            if state in workflow.manager_states:  # restricted manager access
+                if request.principal.id in self.context.managers:
+                    return PUBLISH_CONTENT_PERMISSION
+                restrictions = IManagerRestrictions(self.context).get_restrictions(request.principal)
+                if restrictions and restrictions.check_access(self.context,
+                                                              permission=PUBLISH_CONTENT_PERMISSION,
+                                                              request=request):
+                    return PUBLISH_CONTENT_PERMISSION
+            else:
+                if request.principal.id in self.context.owner | self.context.contributors | self.context.managers:
+                    return MANAGE_CONTENT_PERMISSION
+                restrictions = IManagerRestrictions(self.context).get_restrictions(request.principal)
+                if restrictions and restrictions.check_access(self.context,
+                                                              permission=MANAGE_CONTENT_PERMISSION,
+                                                              request=request):
+                    return MANAGE_CONTENT_PERMISSION
+        return FORBIDDEN_PERMISSION
+
+
+class WfSharedContentPermissionMixin(object):
+    """Shared content permission checker"""
+
+    @property
+    def permission(self):
+        content = get_parent(self.context, IWfSharedContent)
+        if content is not None:
+            return IFormContextPermissionChecker(content).edit_permission
+
+
+@adapter_config(context=(IWfSharedContent, ISiteManagementMenu), provides=IMenuHeader)
+class WfSharedContentSiteManagementMenuHeader(ContextRequestAdapter):
+    """Shared content site management menu header adapter"""
+
+    header = _("Manage this content")
+
+
+@adapter_config(context=(IWfSharedContent, IPyAMSLayer, Interface), provides=IContentTitle)
+class WfSharedContentTitleAdapter(ContextRequestViewAdapter):
+    """Shared content title adapter"""
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+
+class WfSharedContentHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared content header adapter"""
+
+    @property
+    def back_url(self):
+        shared_tool = get_parent(self.context, ISharedTool)
+        return absolute_url(shared_tool, self.request, 'admin.html#dashboard.html')
+
+    back_target = None
+    icon_class = 'fa fa-fw fa-edit'
+
+
+@adapter_config(context=(IWfSharedContent, IPyAMSLayer, ITable), provides=ITableElementEditor)
+class WfSharedContentElementEditor(DefaultElementEditorAdapter):
+    """Shared content element editor"""
+
+    view_name = 'admin.html'
+    modal_target = False
+
+
+#
+# Duplication menus and views
+#
+
+@viewlet_config(name='duplication.menu', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=1)
+class WfSharedContentDuplicateMenu(ToolbarMenuItem):
+    """Shared content duplication menu item"""
+
+    label = _("Duplicate content...")
+    label_css_class = 'fa fa-fw fa-files-o'
+
+    url = 'duplicate.html'
+    modal_target = True
+
+
+class ISharedContentDuplicateButtons(Interface):
+    """Shared content duplication form buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    duplicate = button.Button(name='duplicate', title=_("Duplicate content"))
+
+
+@pagelet_config(name='duplicate.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission=CREATE_CONTENT_PERMISSION)
+class WfSharedContentDuplicateForm(AdminDialogAddForm):
+    """Shared content duplicate form"""
+
+    legend = _("Duplicate content")
+    fields = field.Fields(IWorkflowCommentInfo)
+    buttons = button.Buttons(ISharedContentDuplicateButtons)
+
+    ajax_handler = 'duplicate.json'
+    edit_permission = CREATE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(WfSharedContentDuplicateForm, self).updateWidgets(prefix)
+        self.widgets['comment'].label_css_class = 'textarea'
+
+    def updateActions(self):
+        super(WfSharedContentDuplicateForm, self).updateActions()
+        if 'duplicate' in self.actions:
+            self.actions['duplicate'].addClass('btn-primary')
+
+    def createAndAdd(self, data):
+        registry = self.request.registry
+        # initialize new content
+        content = get_parent(self.context, ISharedContent)
+        new_content = content.__class__()
+        registry.notify(ObjectCreatedEvent(new_content))
+        container = get_parent(content, ISharedTool)
+        container[str(uuid4())] = new_content
+        # initialize new version
+        new_version = copy(self.context)
+        registry.notify(ObjectCreatedEvent(new_version))
+        locate(new_version, self.context.__parent__)  # locate new version for traversing to work...
+        new_version.creator = self.request.principal.id
+        new_version.owner = self.request.principal.id
+        new_version.modifiers = set()
+        # store new version
+        translate = self.request.localizer.translate
+        workflow = get_utility(IWorkflow, name=new_content.workflow_name)
+        sequence = get_utility(ISequentialIntIds, name=new_content.sequence_name)
+        IWorkflowVersions(new_content).add_version(new_version, workflow.initial_state)
+        history = WorkflowHistoryItem(date=datetime.utcnow(),
+                                      source_state=translate(workflow.states.getTerm(IWorkflowState(
+                                          self.context).state).title),
+                                      transition=translate(_("Duplicate content ({oid})")).format(
+                                          oid=sequence.get_short_oid(ISequentialIdInfo(content).oid,
+                                                                     content.sequence_prefix)),
+                                      target_state=translate(workflow.states.getTerm(workflow.initial_state).title),
+                                      principal=self.request.principal.id,
+                                      comment=data.get('comment'))
+        state = IWorkflowState(new_version)
+        state.history.clear()
+        state.history.append(history)
+        return new_version
+
+
+@view_config(name='duplicate.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission=CREATE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class WfSharedContentDuplicateAJAXForm(AJAXAddForm, WfSharedContentDuplicateForm):
+    """Shared content duplicate form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'redirect',
+                'location': absolute_url(changes, self.request, 'admin.html')}
+
+
+@viewlet_config(name='wf-duplicate-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=WfSharedContentDuplicateForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-duplicate-message.pt')
+class WfSharedContentDuplicateFormMessage(Viewlet):
+    """Shared content add form info message"""
+
+
+#
+# Custom columns mixins
+#
+
+class WfModifiedContentColumnMixin(object):
+    """Shared content modified column mixin"""
+
+    def renderCell(self, item):
+        value = self.getValue(item)
+        content = get_parent(item, IWfSharedContent)
+        if content is not None:
+            if IWorkflowState(content).version_id > 1:
+                item_dc = IZopeDublinCore(item)
+                if item_dc.modified and (item_dc.modified > IZopeDublinCore(content).created):
+                    translate = self.request.localizer.translate
+                    value += '<i class="fa fa-fw fa-circle txt-color-orange pull-right hint" title="{0}" ' \
+                             'data-ams-hint-gravity="e"></i>'.format(translate(_("Created or modified in this version")))
+        return value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/dashboard.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,759 @@
+#
+# 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 hypatia.interfaces import ICatalog
+from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, PUBLISH_CONTENT_PERMISSION
+from pyams_content.profile.interfaces import IAdminProfile
+from pyams_content.shared.common.interfaces import ISharedTool, IWfSharedContent, IManagerRestrictions
+from pyams_content.shared.common.interfaces.zmi import ISharedToolDashboardTable
+from pyams_content.zmi.interfaces import IDashboardMenu, IMyDashboardMenu, IAllContentsMenu
+from pyams_i18n.interfaces import II18n
+from pyams_security.interfaces import ISecurityManager
+from pyams_sequence.interfaces import ISequentialIdInfo, ISequentialIdTarget, ISequentialIntIds
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.container import ITableElementName
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_workflow.interfaces import IWorkflowState, IWorkflow, IWorkflowVersions
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues, IColumn
+from zope.dublincore.interfaces import IZopeDublinCore
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import And, Or, Eq, Any
+from pyams_catalog.query import CatalogResultSet
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, I18nColumn, ActionColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.date import format_datetime, SH_DATETIME_FORMAT
+from pyams_utils.list import unique
+from pyams_utils.property import cached_property
+from pyams_utils.registry import get_utility
+from pyams_utils.timezone import tztime
+from pyams_utils.traversing import get_parent
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.view import AdminView
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+#
+# Shared tools common adapters
+#
+
+@implementer(ISharedToolDashboardTable)
+class BaseDashboardTable(BaseTable):
+    """Base dashboard table"""
+
+    _title = '{0} contents'
+
+    sortOn = None
+    dt_sort_order = 'desc'
+
+    @property
+    def title(self):
+        return self.request.localizer.translate(self._title).format(len(self.values))
+
+    @property
+    def data_attributes(self):
+        attributes = super(BaseDashboardTable, self).data_attributes
+        attributes['table'] = {'data-ams-datatable-sorting': "{0},{1}".format(len(self.columns)-1,
+                                                                              self.dt_sort_order),
+                               'data-ams-datatable-display-length':
+                                   IAdminProfile(self.request.principal).table_page_length}
+        return attributes
+
+    @cached_property
+    def values(self):
+        return tuple(super(BaseDashboardTable, self).values)
+
+
+@adapter_config(context=(IWfSharedContent, IPyAMSLayer, ISharedToolDashboardTable), provides=ITableElementName)
+class SharedToolDashboardContentNameAdapter(ContextRequestViewAdapter):
+    """Shared tool dashboard content name adapter"""
+
+    @property
+    def name(self):
+        return II18n(self.context).query_attribute('short_name', request=self.request)
+
+
+@adapter_config(name='sequence', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardSequenceColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard sequence ID column"""
+
+    _header = _("Unique ID")
+    weight = 14
+
+    def getValue(self, obj):
+        target = get_parent(obj, ISequentialIdTarget)
+        sequence = get_utility(ISequentialIntIds, name=target.sequence_name)
+        return sequence.get_short_oid(ISequentialIdInfo(obj).oid, target.sequence_prefix)
+
+
+@adapter_config(name='version', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardVersionColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard version column"""
+
+    _header = _("Version")
+    weight = 15
+
+    def getValue(self, obj):
+        return str(IWorkflowState(obj).version_id)
+
+
+@adapter_config(name='urgency', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardUrgencyColumn(ActionColumn):
+    """Shared tool dashboard urgency column"""
+
+    icon_class = 'fa fa-fw fa-exclamation-triangle txt-color-red'
+    icon_hint = _("Urgent request !")
+
+    url = '#'
+    weight = 19
+
+    def renderCell(self, item):
+        state = IWorkflowState(item)
+        if not state.state_urgency:
+            return ''
+        else:
+            return super(SharedToolDashboardUrgencyColumn, self).renderCell(item)
+
+    def get_url(self, item):
+        return self.url
+
+
+@adapter_config(name='status', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardStatusColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard status column"""
+
+    _header = _("Status")
+    weight = 20
+
+    def getValue(self, obj):
+        workflow = IWorkflow(obj)
+        state = IWorkflowState(obj)
+        return self.request.localizer.translate(workflow.get_state_label(state.state))
+
+
+@adapter_config(name='status_date', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardStatusDateColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard status date column"""
+
+    _header = _("Status date")
+    weight = 21
+
+    def getValue(self, obj):
+        state = IWorkflowState(obj)
+        return format_datetime(state.state_date, SH_DATETIME_FORMAT, request=self.request)
+
+
+@adapter_config(name='status_principal', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardStatusPrincipalColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard status principal column"""
+
+    _header = _("Status principal")
+    weight = 22
+
+    def getValue(self, obj):
+        state = IWorkflowState(obj)
+        manager = get_utility(ISecurityManager)
+        return manager.get_principal(state.state_principal).title
+
+
+@adapter_config(name='owner', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardOwnerColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard owner column"""
+
+    _header = _("Owner")
+    weight = 30
+
+    def getValue(self, obj):
+        manager = get_utility(ISecurityManager)
+        return manager.get_principal(next(iter(obj.owner))).title
+
+
+@adapter_config(name='modified', context=(Interface, IPyAMSLayer, ISharedToolDashboardTable), provides=IColumn)
+class SharedToolDashboardModifiedColumn(I18nColumn, GetAttrColumn):
+    """Shared tool dashboard modified column"""
+
+    _header = _("Last modification")
+    weight = 40
+
+    def getValue(self, obj):
+        return format_datetime(tztime(IZopeDublinCore(obj).modified), SH_DATETIME_FORMAT, request=self.request)
+
+
+#
+# Shared tool control panel
+#
+
+@viewlet_config(name='dashboard.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='dashboard.menu', layer=IAdminLayer, provides=IDashboardMenu)
+@implementer(IDashboardMenu)
+class SharedToolDashboardMenu(MenuItem):
+    """Shared tool dashboard menu"""
+
+    label = _("Dashboard")
+    icon_class = 'fa-line-chart'
+    url = '#dashboard.html'
+
+
+@pagelet_config(name='dashboard.html', context=ISharedTool, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@template_config(template='templates/dashboard.pt', layer=IAdminLayer)
+@implementer(IInnerPage)
+class SharedToolDashboardView(AdminView):
+    """Shared tool dashboard view"""
+
+    title = _("Contents dashboard")
+
+    def __init__(self, context, request):
+        super(SharedToolDashboardView, self).__init__(context, request)
+        self.tables = []
+        self.tables.append(SharedToolDashboardManagerWaitingTable(self.context, self.request))
+        self.tables.append(SharedToolDashboardOwnerWaitingTable(self.context, self.request))
+        self.tables.append(SharedToolDashboardOwnerModifiedTable(self.context, self.request))
+        for table in self.tables:
+            table.hide_toolbar = True
+
+    def update(self):
+        super(SharedToolDashboardView, self).update()
+        [table.update() for table in self.tables]
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolDashboardView), provides=IPageHeader)
+class SharedToolDashboardHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool properties header adapter"""
+
+    back_url = '/admin.html#dashboard.html'
+    back_target = None
+
+    icon_class = 'fa fa-fw fa-line-chart'
+
+
+#
+# Contents waiting for manager action
+#
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolDashboardManagerWaitingTable(BaseDashboardTable):
+    """Shared tool dashboard waiting table"""
+
+    _title = _("MANAGER - {0} content(s) waiting for your action")
+
+    dt_sort_order = 'asc'
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolDashboardManagerWaitingTable), provides=IValues)
+class SharedToolDashboardManagerWaitingValues(ContextRequestViewAdapter):
+    """Shared tool dashboard waiting values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = Eq(catalog['content_type'], self.context.shared_content_type) & \
+                 Any(catalog['workflow_state'], workflow.waiting_states)
+        return filter(self.check_access,
+                      unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(IWorkflowState(x).state),
+                                                  key=lambda y: IZopeDublinCore(y).modified, reverse=True)[0],
+                                 CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                              sort_index='modified_date')))))
+
+    def check_access(self, content):
+        if self.request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context=content):
+            return True
+        if self.request.principal.id in content.managers:
+            return True
+        restrictions = IManagerRestrictions(content).get_restrictions(self.request.principal)
+        if restrictions is not None:
+            return restrictions.check_access(content, PUBLISH_CONTENT_PERMISSION, self.request)
+        else:
+            return False
+
+
+#
+# Last owned contents waiting for action
+#
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolDashboardOwnerWaitingTable(BaseDashboardTable):
+    """Shared tool dashboard waiting owned contents table"""
+
+    _title = _("CONTRIBUTOR - Your {0} content(s) waiting for action")
+
+    dt_sort_order = 'asc'
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolDashboardOwnerWaitingTable), provides=IValues)
+class SharedToolDashboardOwnerWaitingValues(ContextRequestViewAdapter):
+    """Shared tool dashboard waiting owned contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = Eq(catalog['content_type'], self.context.shared_content_type) & \
+                 Any(catalog['workflow_state'], workflow.waiting_states) & \
+                 Eq(catalog['workflow_principal'], self.request.principal.id)
+        return unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(IWorkflowState(x).state),
+                                           key=lambda y: IZopeDublinCore(y).modified, reverse=True)[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       sort_index='modified_date'))))
+
+
+#
+# Last owned modified contents
+#
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolDashboardOwnerModifiedTable(BaseDashboardTable):
+    """Shared tool dashboard owned modified contents table"""
+
+    _title = _("CONTRIBUTOR - Your last modified contents (limited to {0})")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolDashboardOwnerModifiedTable), provides=IValues)
+class SharedToolDashboardOwnerModifiedValues(ContextRequestViewAdapter):
+    """Shared tool dashboard waiting owned contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = And(Eq(catalog['content_type'], self.context.shared_content_type),
+                        Or(Eq(catalog['role:owner'], self.request.principal.id),
+                           Eq(catalog['role:contributor'], self.request.principal.id)))
+        return unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(IWorkflowState(x).state),
+                                           key=lambda y: IZopeDublinCore(y).modified, reverse=True)[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       limit=IAdminProfile(self.request.principal).table_page_length,
+                                                                       sort_index='modified_date',
+                                                                       reverse=True))))
+
+
+#
+# All my contents menu
+#
+
+@viewlet_config(name='my-contents.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=5)
+@viewletmanager_config(name='my-contents.menu', layer=IAdminLayer, provides=IMyDashboardMenu)
+@implementer(IMyDashboardMenu)
+class SharedToolMyDashboardMenu(MenuItem):
+    """Shared tool 'my contents' dashboard menu"""
+
+    label = _("My contents")
+    icon_class = 'fa-user'
+    url = '#'
+    
+    
+#
+# My preparations
+# Dashboard of owned and modified contents which can be updated
+#
+
+@viewlet_config(name='my-preparations.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=5)
+class SharedToolPreparationsMenu(MenuItem):
+    """Site root preparations dashboard menu"""
+
+    label = _("My preparations")
+    icon_class = None
+    url = '#my-preparations.html'
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolPreparationsTable(BaseDashboardTable):
+    """Site root preparations table"""
+
+    _title = _("CONTRIBUTOR - Your {0} prepared contents")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolPreparationsTable), provides=IValues)
+class SharedToolPreparationsValues(ContextRequestViewAdapter):
+    """Site root preparations values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = And(Eq(catalog['content_type'], self.context.shared_content_type),
+                     Or(Eq(catalog['role:owner'], self.request.principal.id),
+                        Eq(catalog['role:contributor'], self.request.principal.id)),
+                     Any(catalog['workflow_state'], workflow.update_states))
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='my-preparations.html', context=ISharedTool, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolPreparationsView(AdminView, ContainerView):
+    """Site root preparations view"""
+
+    table_class = SharedToolPreparationsTable
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolPreparationsView), provides=IPageHeader)
+class SharedToolPreparationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Site root preparations header adapter"""
+
+    back_url = '#dashboard.html'
+    icon_class = 'fa fa-fw fa-user'
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    subtitle = _("Your prepared contents")
+
+
+#
+# My publications
+# Dashboard of owned and modified contents which are published
+#
+
+@viewlet_config(name='my-publications.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+class SharedToolPublicationsMenu(MenuItem):
+    """Shared tool publications dashboard menu"""
+
+    label = _("My publications")
+    icon_class = None
+    url = '#my-publications.html'
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolPublicationsTable(BaseDashboardTable):
+    """Shared tool publications table"""
+
+    _title = _("CONTRIBUTOR - Your {0} published contents")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolPublicationsTable), provides=IValues)
+class SharedToolPublicationsValues(ContextRequestViewAdapter):
+    """Shared tool publications values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = And(Eq(catalog['content_type'], self.context.shared_content_type),
+                     Or(Eq(catalog['role:owner'], self.request.principal.id),
+                        Eq(catalog['role:contributor'], self.request.principal.id)),
+                     Any(catalog['workflow_state'], workflow.published_states))
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='my-publications.html', context=ISharedTool, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolPublicationsView(AdminView, ContainerView):
+    """Shared tool publications view"""
+
+    table_class = SharedToolPublicationsTable
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolPublicationsView), provides=IPageHeader)
+class SharedToolPublicationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool publications header adapter"""
+
+    back_url = '#dashboard.html'
+    icon_class = 'fa fa-fw fa-user'
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    subtitle = _("Your published contents")
+
+
+#
+# My retired contents
+# Dashboard of owned and modified contents which are retired
+#
+
+@viewlet_config(name='my-retired-contents.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=15)
+class SharedToolRetiredMenu(MenuItem):
+    """Shared tool retired contents dashboard menu"""
+
+    label = _("My retired contents")
+    icon_class = None
+    url = '#my-retired-contents.html'
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolRetiredContentsTable(BaseDashboardTable):
+    """Shared tool retired contents table"""
+
+    _title = _("CONTRIBUTOR - Your {0} retired contents")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolRetiredContentsTable), provides=IValues)
+class SharedToolRetiredContentsValues(ContextRequestViewAdapter):
+    """Shared tool retired contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = And(Eq(catalog['content_type'], self.context.shared_content_type),
+                     Or(Eq(catalog['role:owner'], self.request.principal.id),
+                        Eq(catalog['role:contributor'], self.request.principal.id)),
+                     Any(catalog['workflow_state'], workflow.retired_states))
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='my-retired-contents.html', context=ISharedTool, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolRetiredContentsView(AdminView, ContainerView):
+    """Shared tool retired contents view"""
+
+    table_class = SharedToolRetiredContentsTable
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolRetiredContentsView), provides=IPageHeader)
+class SharedToolRetiredContentsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool retired contents header adapter"""
+
+    back_url = '#dashboard.html'
+    icon_class = 'fa fa-fw fa-user'
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    subtitle = _("Your retired contents")
+
+
+#
+# My archived contents
+# Dashboard of owned and modified contents which are archived
+#
+
+@viewlet_config(name='my-archived-contents.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IMyDashboardMenu, permission=VIEW_SYSTEM_PERMISSION, weight=20)
+class SharedToolArchivedMenu(MenuItem):
+    """Shared tool archived contents dashboard menu"""
+
+    label = _("My archived contents")
+    icon_class = None
+    url = '#my-archived-contents.html'
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolArchivedContentsTable(BaseDashboardTable):
+    """Shared tool archived contents table"""
+
+    _title = _("CONTRIBUTOR - Your {0} archived contents")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolArchivedContentsTable), provides=IValues)
+class SharedToolArchivedContentsValues(ContextRequestViewAdapter):
+    """Shared tool archived contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        principal_id = self.request.principal.id
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = And(Eq(catalog['content_type'], self.context.shared_content_type),
+                     Or(Eq(catalog['role:owner'], principal_id),
+                        Eq(catalog['role:contributor'], principal_id)),
+                     Any(catalog['workflow_state'], workflow.readonly_states))
+        return unique(map(lambda x: sorted((version for version in
+                                            IWorkflowVersions(x).get_versions(IWorkflow(x).readonly_states)
+                                            if principal_id in (version.owner | version.contributors)),
+                                           key=lambda x: IWorkflowState(x).version_id,
+                                           reverse=True)[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       sort_index='modified_date',
+                                                                       reverse=True))))
+
+
+@pagelet_config(name='my-archived-contents.html', context=ISharedTool, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolArchivedContentsView(AdminView, ContainerView):
+    """Shared tool archived contents view"""
+
+    table_class = SharedToolArchivedContentsTable
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolArchivedContentsView), provides=IPageHeader)
+class SharedToolArchivedContentsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool archived contents header adapter"""
+
+    back_url = '#dashboard.html'
+    icon_class = 'fa fa-fw fa-user'
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    subtitle = _("Your archived contents")
+
+
+#
+# All interventions
+#
+
+@viewlet_config(name='all-interventions.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+@viewletmanager_config(name='all-interventions.menu', layer=IAdminLayer, provides=IAllContentsMenu)
+@implementer(IAllContentsMenu)
+class SharedToolAllContentsMenu(MenuItem):
+    """Shared tool 'all contents' dashboard menu"""
+
+    label = _("Other interventions")
+    icon_class = 'fa-pencil-square'
+    url = '#'
+
+
+#
+# Last publications
+# Dashboard of all published contents
+#
+
+@viewlet_config(name='all-publications.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IAllContentsMenu, permission=VIEW_SYSTEM_PERMISSION, weight=10)
+class SharedToolAllPublicationsMenu(MenuItem):
+    """Shared tool published contents dashboard menu"""
+
+    label = _("Last publications")
+    icon_class = None
+    url = '#all-publications.html'
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolAllPublicationsTable(BaseDashboardTable):
+    """Shared tool published contents table"""
+
+    _title = _("CONTRIBUTORS - Last published contents (in the limit of 50)")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAllPublicationsTable), provides=IValues)
+class SharedToolAllPublicationsValues(ContextRequestViewAdapter):
+    """Shared tool published contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        workflow = get_utility(IWorkflow, name=self.context.shared_content_workflow)
+        params = And(Eq(catalog['content_type'], self.context.shared_content_type),
+                     Any(catalog['workflow_state'], workflow.published_states))
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   limit=50,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='all-publications.html', context=ISharedTool, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolAllPublicationsView(AdminView, ContainerView):
+    """Shared tool published contents view"""
+
+    table_class = SharedToolAllPublicationsTable
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAllPublicationsView), provides=IPageHeader)
+class SharedToolAllPublicationsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool published contents header adapter"""
+
+    back_url = '#dashboard.html'
+    icon_class = 'fa fa-fw fa-pencil-square'
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    subtitle = _("Last published contents")
+
+
+#
+# Last updates
+# Dashboard of all updated contents
+#
+
+@viewlet_config(name='all-updates.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IAllContentsMenu, permission=VIEW_SYSTEM_PERMISSION, weight=20)
+class SharedToolAllUpdatesMenu(MenuItem):
+    """Shared tool updated contents dashboard menu"""
+
+    label = _("Last updates")
+    icon_class = None
+    url = '#all-updates.html'
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolAllUpdatesTable(BaseDashboardTable):
+    """Shared tool updated contents table"""
+
+    _title = _("CONTRIBUTORS - Last updated contents (in the limit of 50)")
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAllUpdatesTable), provides=IValues)
+class SharedToolAllUpdatesValues(ContextRequestViewAdapter):
+    """Shared tool updated contents values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = Eq(catalog['content_type'], self.context.shared_content_type)
+        return unique(CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                   limit=50,
+                                                                   sort_index='modified_date',
+                                                                   reverse=True)))
+
+
+@pagelet_config(name='all-updates.html', context=ISharedTool, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolAllUpdatesView(AdminView, ContainerView):
+    """Shared tool updated contents view"""
+
+    table_class = SharedToolAllUpdatesTable
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAllUpdatesView), provides=IPageHeader)
+class SharedToolAllUpdatesHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool updated contents header adapter"""
+
+    back_url = '#dashboard.html'
+    icon_class = 'fa fa-fw fa-pencil-square'
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+    subtitle = _("Last updated contents")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/header.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,116 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.common.interfaces import IWfSharedContent, ISharedTool
+from pyams_form.interfaces.form import IInnerTabForm
+from pyams_i18n.interfaces import II18n
+from pyams_security.interfaces import ISecurityManager
+from pyams_sequence.interfaces import ISequentialIntIds, ISequentialIdInfo
+from pyams_skin.layer import IPyAMSLayer
+from pyams_workflow.interfaces import IWorkflowState, IWorkflow, IWorkflowStateLabel, IWorkflowVersions
+
+# import packages
+from pyams_template.template import template_config
+from pyams_utils.date import format_datetime
+from pyams_utils.registry import get_utility
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import contentprovider_config
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@contentprovider_config(name='content_header', context=IWfSharedContent, view=Interface, layer=IPyAMSLayer)
+@template_config(template='templates/header.pt', layer=IPyAMSLayer)
+class SharedContentHeaderContentProvider(object):
+    """Header for shared contents"""
+
+    back_url = '#summary.html'
+    back_target = None
+
+    icon_class = ''
+
+    def __init__(self, context, request, view):
+        super(SharedContentHeaderContentProvider, self).__init__(context, request, view)
+        # sequence
+        sequence = get_utility(ISequentialIntIds)
+        self.oid = sequence.get_short_oid(ISequentialIdInfo(context).oid)
+        # security
+        security = get_utility(ISecurityManager)
+        owner = next(iter(context.owner))
+        self.owner = security.get_principal(owner).title
+        # workflow
+        translate = request.localizer.translate
+        workflow = IWorkflow(context)
+        versions = IWorkflowVersions(context)
+        state = IWorkflowState(context)
+        self.version_id = state.version_id
+        state_label = request.registry.queryAdapter(workflow, IWorkflowStateLabel, name=state.state)
+        if state_label is None:
+            state_label = request.registry.queryAdapter(workflow, IWorkflowStateLabel)
+        if state_label is None:
+            self.state = translate(_("{state} by {{principal}}")).format(
+                state=translate(workflow.get_state_label(state.state)))
+        else:
+            self.state = state_label.get_label(context, request, format=False)
+        principal_class = 'text-danger' if state.state_principal != owner else 'txt-color-text'
+        self.state = self.state.replace('{principal}',
+                                        '<span class="{0}">{{principal}}</span>'.format(principal_class))
+        state_class = 'text-danger' if state.state in workflow.update_states else None
+        if state_class:
+            self.state = '<span class="{state_class}">{state}</span>'.format(state_class=state_class,
+                                                                             state=self.state)
+        self.state = self.state.format(principal=security.get_principal(state.state_principal).title)
+        self.state_date = translate(_("since {date}")).format(date=format_datetime(state.state_date, request=request))
+        if state.state not in workflow.update_states and versions.has_version(workflow.update_states):
+            target = sorted(versions.get_versions(workflow.update_states),
+                            key=lambda x: IWorkflowState(x).version_id,
+                            reverse=True)[-1]
+            self.version_link = {
+                'css_class': 'text-danger',
+                'href': absolute_url(target, request, 'admin.html'),
+                'title': translate(_("access new version"))
+            }
+        elif state.state not in workflow.published_states and versions.has_version(workflow.published_states):
+            target = sorted(versions.get_versions(workflow.published_states),
+                            key=lambda x: IWorkflowState(x).version_id,
+                            reverse=True)[-1]
+            self.version_link = {
+                'css_class': 'txt-color-text',
+                'href': absolute_url(target, request, 'admin.html'),
+                'title': translate(_("access published version"))
+            }
+        else:
+            self.version_link = None
+
+    @property
+    def title(self):
+        tool = get_parent(self.context, ISharedTool)
+        return II18n(tool).query_attribute('title', request=self.request)
+
+
+@contentprovider_config(name='content_header', context=IWfSharedContent, view=IInnerTabForm, layer=IPyAMSLayer)
+class SharedContentInnerPageHeaderContentProvider(object):
+    """Inner page header content provider"""
+
+    def update(self):
+        pass
+
+    def render(self):
+        return ''
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/manager.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,151 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
+from pyams_content.shared.common.interfaces import ISharedTool
+from pyams_form.interfaces.form import IWidgetForm, IFormHelp
+from pyams_i18n.interfaces import II18n, II18nManager
+from pyams_skin.interfaces import IInnerPage, IPageHeader, IContentTitle
+from pyams_skin.interfaces.viewlet import IMenuHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu, ISiteManagementMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_form.help import FormHelp
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextRequestAdapter
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+#
+# Shared tools common adapters
+#
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, Interface), provides=IContentTitle)
+class SharedToolTitleAdapter(ContextRequestViewAdapter):
+    """Shared tool title adapter"""
+
+    @property
+    def title(self):
+        return II18n(self.context).query_attribute('title', request=self.request)
+
+
+@adapter_config(context=(ISharedTool, ISiteManagementMenu), provides=IMenuHeader)
+class SharedToolSiteManagementMenuHeader(ContextRequestAdapter):
+    """Shared tool site management menu header adapter"""
+
+    header = _("Tool management")
+
+
+#
+# Shared tool properties
+#
+
+@viewlet_config(name='properties.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=ISiteManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='properties.menu', layer=IAdminLayer, provides=IPropertiesMenu)
+@implementer(IPropertiesMenu)
+class SharedToolPropertiesMenu(MenuItem):
+    """Shared tool properties menu"""
+
+    label = _("Properties")
+    icon_class = 'fa-edit'
+    url = '#properties.html'
+
+
+@pagelet_config(name='properties.html', context=ISharedTool, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class SharedToolPropertiesEditForm(AdminEditForm):
+    """Shared tool properties edit form"""
+
+    legend = _("Shared tool properties")
+
+    fields = field.Fields(ISharedTool).omit('__parent__', '__name__')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+
+@view_config(name='properties.json', context=ISharedTool, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class SharedToolPropertiesAJAXEditForm(AJAXEditForm, SharedToolPropertiesEditForm):
+    """Shared tool properties edit form, JSON renderer"""
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolPropertiesEditForm), provides=IFormHelp)
+class SharedToolPropertiesHelpAdapter(FormHelp):
+    """Shared tool properties help adapter"""
+
+    permission = MANAGE_TOOL_PERMISSION
+
+    header = _("WARNING")
+    status = 'danger'
+    message = _("""Workflow shouldn't be modified if this tool already contains any shared content!""")
+    message_format = 'rest'
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, Interface), provides=IPageHeader)
+class SharedToolPropertiesHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool properties header adapter"""
+
+    back_url = '/admin.html#properties.html'
+    back_target = None
+
+    icon_class = 'fa fa-fw fa-edit'
+
+
+#
+# Shared tool languages
+#
+
+@pagelet_config(name='languages.html', context=ISharedTool, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerPage, IWidgetForm)
+class SharedToolLanguagesEditForm(AdminEditForm):
+    """Shared tool languages edit form"""
+
+    legend = _("Content languages")
+
+    fields = field.Fields(II18nManager)
+    ajax_handler = 'languages.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+
+@view_config(name='languages.json', context=ISharedTool, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class SharedToolLanguagesAJAXEditForm(AJAXEditForm, SharedToolLanguagesEditForm):
+    """Shared tool languages edit form, JSON renderer"""
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolLanguagesEditForm), provides=IFormHelp)
+class SharedToolLanguagesEditFormHelp(FormHelp):
+    """Shared tool languages edit form help"""
+
+    message = _("Tool languages are used to translate own tool properties, and newly created contents will propose "
+                "these languages by default")
+    message_format = 'rest'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/owner.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,127 @@
+#
+# 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 pyramid.view import view_config
+from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
+from zope.lifecycleevent import ObjectModifiedEvent
+from pyams_content.interfaces import MANAGE_SITE_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles
+from pyams_form.form import AJAXAddForm
+from pyams_form.help import FormHelp
+from pyams_form.interfaces.form import IFormHelp
+from pyams_form.schema import CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.schema import Principal
+from pyams_security.zmi.interfaces import IObjectSecurityMenu
+from pyams_skin.layer import IPyAMSLayer
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflow, IWorkflowState
+from pyams_zmi.form import AdminDialogAddForm
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from z3c.form import field, button
+from zope.interface import Interface
+from zope.schema import Bool
+
+from pyams_content import _
+
+
+@viewlet_config(name='change-owner.menu', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=Interface, manager=IObjectSecurityMenu, permission=MANAGE_SITE_PERMISSION, weight=10)
+class WfSharedContentOwnerChangeMenu(MenuItem):
+    """Shared content owner change menu"""
+
+    label = _("Change owner...")
+    icon_class = 'fa fa-fw fa-user'
+
+    url = 'change-owner.html'
+    modal_target = True
+
+
+class IWfSharedContentOwnerChangeInfo(Interface):
+    """Shared content owner change form fields"""
+
+    new_owner = Principal(title=_("New owner"),
+                          description=_("The selected user will become the new content's owner"))
+
+    keep_owner_as_contributor = Bool(title=_("Keep previous owner as contributor"),
+                                     description=_("If 'yes', the previous owner will still be able to modify this "
+                                                   "content"),
+                                     required=False,
+                                     default=False)
+
+
+class IWfSharedContentOwnerChangeButtons(Interface):
+    """Shared content owner change form buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    change = button.Button(name='change', title=_("Change owner"))
+
+
+@pagelet_config(name='change-owner.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission=MANAGE_SITE_PERMISSION)
+class WfSharedContentOwnerChangeForm(AdminDialogAddForm):
+    """Shared content owner change form"""
+
+    legend = _("Change content's owner")
+
+    fields = field.Fields(IWfSharedContentOwnerChangeInfo)
+    fields['keep_owner_as_contributor'].widgetFactory = SingleCheckBoxFieldWidget
+    buttons = button.Buttons(IWfSharedContentOwnerChangeButtons)
+
+    ajax_handler = 'change-owner.json'
+    edit_permission = MANAGE_SITE_PERMISSION
+
+    def updateActions(self):
+        super(WfSharedContentOwnerChangeForm, self).updateActions()
+        if 'change' in self.actions:
+            self.actions['change'].addClass('btn-primary')
+
+    def createAndAdd(self, data):
+        new_owner = data.get('new_owner')
+        workflow = IWorkflow(self.context)
+        for version in IWorkflowVersions(self.context).get_versions():
+            if IWorkflowState(version).state in workflow.readonly_states:
+                continue
+            roles = IWfSharedContentRoles(version)
+            previous_owner = next(iter(roles.owner))
+            roles.owner = {new_owner}
+            contributors = roles.contributors
+            if (previous_owner in contributors) and not data.get('keep_owner_as_contributor'):
+                contributors.remove(previous_owner)
+            contributors.add(new_owner)
+            self.request.registry.notify(ObjectModifiedEvent(version))
+
+
+@view_config(name='change-owner.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission=MANAGE_SITE_PERMISSION, renderer='json', xhr=True)
+class WfSharedContentOwnerChangeAJAXForm(AJAXAddForm,WfSharedContentOwnerChangeForm):
+    """Shared content owner change form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'reload'}
+
+
+@adapter_config(context=(IWfSharedContent, IPyAMSLayer, WfSharedContentOwnerChangeForm), provides=IFormHelp)
+class WfSharedContentOwnerChangeFormHelp(FormHelp):
+    """Shared content owner change form help"""
+
+    message = _("All versions of this content which are not archived will be transferred to newly selected owner")
+    message_format = 'rest'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/properties.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,98 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_form.interfaces.form import IWidgetForm
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IContentManagementMenu, IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_content.shared.common.zmi import WfSharedContentHeaderAdapter
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminEditForm
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+#
+# Content properties
+#
+
+@viewlet_config(name='properties.menu', context=IWfSharedContent, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=MANAGE_CONTENT_PERMISSION, weight=10)
+@viewletmanager_config(name='properties.menu', layer=IAdminLayer, provides=IPropertiesMenu)
+@implementer(IPropertiesMenu)
+class SharedContentCompositionMenu(MenuItem):
+    """Shared content properties menu"""
+
+    label = _("Composition")
+    icon_class = 'fa-dropbox'
+    url = '#'
+
+
+@viewlet_config(name='properties.submenu', context=IWfSharedContent, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=MANAGE_CONTENT_PERMISSION, weight=10)
+class SharedContentPropertiesMenu(MenuItem):
+    """Shared content properties menu"""
+
+    label = _("Properties")
+    icon_class = 'fa-edit'
+    url = '#properties.html'
+
+
+@pagelet_config(name='properties.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class SharedContentPropertiesEditForm(AdminEditForm):
+    """Shared content properties edit form"""
+
+    legend = _("Content properties")
+
+    fields = field.Fields(IWfSharedContent).omit('__parent__', '__name__', 'creator', 'first_owner', 'modifiers')
+    ajax_handler = 'properties.json'
+
+    def updateWidgets(self, prefix=None):
+        super(SharedContentPropertiesEditForm, self).updateWidgets(prefix)
+        if 'description' in self.widgets:
+            self.widgets['description'].label_css_class = 'textarea'
+        if 'notepad' in self.widgets:
+            self.widgets['notepad'].label_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class SharedContentPropertiesAJAXEditForm(AJAXEditForm, SharedContentPropertiesEditForm):
+    """Shared content properties edit form, JSON renderer"""
+
+
+@adapter_config(context=(IWfSharedContent, IAdminLayer, SharedContentPropertiesEditForm), provides=IPageHeader)
+class SharedContentPropertiesHeaderAdapter(WfSharedContentHeaderAdapter):
+    """Shared content properties header adapter"""
+
+    icon_class = 'fa fa-fw fa-edit'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/search.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,250 @@
+#
+# 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 hypatia.interfaces import ICatalog
+from pyams_content.profile.interfaces import IAdminProfile
+from pyams_content.shared.common.interfaces import ISharedTool
+from pyams_content.shared.common.interfaces.zmi import ISharedToolDashboardTable
+from pyams_pagelet.interfaces import PageletCreatedEvent
+from pyams_sequence.interfaces import ISequentialIntIds
+from pyams_skin.interfaces import IPageHeader, IContentSearch
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_workflow.interfaces import IWorkflowVersions, IWorkflow
+from pyams_zmi.interfaces import IAdminView
+from z3c.table.interfaces import IValues
+from zope.dublincore.interfaces import IZopeDublinCore
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Eq, Contains, Ge, Le
+from pyams_content.workflow import STATES_VOCABULARY
+from pyams_catalog.query import CatalogResultSet
+from pyams_form.search import SearchView, SearchForm, SearchResultsView, ISearchFields
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.schema import Principal
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.list import unique
+from pyams_utils.registry import get_utility
+from pyams_zmi.view import AdminView
+from pyramid.response import Response
+from pyramid.view import view_config
+from z3c.form import field
+from zope.interface import implementer
+from zope.schema import Date, Choice
+
+from pyams_content import _
+
+
+#
+# Quick search adapters
+#
+
+@view_config(name='quick-search.html', context=ISharedTool, request_type=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+def shared_tool_quick_search_view(request):
+    """Shared tool quick search view"""
+    results = SharedToolQuickSearchResults(request.context, request)
+    results.update()
+    return Response(results.render())
+
+
+@implementer(ISharedToolDashboardTable)
+class SharedToolQuickSearchResults(BaseTable):
+    """Shared tool quick search results table"""
+
+    title = _("Quick search results")
+
+    sortOn = None
+
+    @property
+    def data_attributes(self):
+        attributes = super(SharedToolQuickSearchResults, self).data_attributes
+        attributes['table'] = {'data-ams-datatable-sorting': '[]',
+                               'data-ams-datatable-display-length':
+                                   IAdminProfile(self.request.principal).table_page_length}
+        return attributes
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolQuickSearchResults), provides=IValues)
+class SharedToolQuickSearchValues(ContextRequestViewAdapter):
+    """Shared tool quick search results view values adapter"""
+
+    @property
+    def values(self):
+        catalog = get_utility(ICatalog)
+        params = Eq(catalog['content_type'], self.context.shared_content_type)
+        query = self.request.params.get('query')
+        if query:
+            sequence = get_utility(ISequentialIntIds)
+            if query.startswith('+'):
+                params &= Eq(catalog['oid'], sequence.get_full_oid(query))
+            else:
+                query_params = Eq(catalog['oid'], sequence.get_full_oid(query))
+                index = catalog['title:' + self.request.registry.settings.get('pyramid.default_locale_name', 'en')]
+                if index.check_query(query):
+                    query_params |= Contains(index, ' and '.join((w+'*' for w in query.split())))
+                params &= query_params
+        return unique(map(lambda x: IWorkflowVersions(x).get_last_versions()[0],
+                          CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                       sort_index='modified_date',
+                                                                       reverse=True))))
+
+
+#
+# Advanced search adapters
+#
+
+class ISharedToolAdvancedSearchFields(ISearchFields):
+    """Shared tool advanced search fields"""
+
+    owner = Principal(title=_("Owner"),
+                      required=False)
+
+    status = Choice(title=_("Status"),
+                    vocabulary=STATES_VOCABULARY,
+                    required=False)
+
+    created_after = Date(title=_("Created after..."),
+                         required=False)
+
+    created_before = Date(title=_("Created before..."),
+                          required=False)
+
+    modified_after = Date(title=_("Modified after..."),
+                          required=False)
+
+    modified_before = Date(title=_("Modified before..."),
+                           required=False)
+
+
+@template_config(template='templates/advanced-search.pt', layer=IPyAMSLayer)
+@implementer(IAdminView)
+class SharedToolAdvancedSearchForm(SearchForm):
+    """Shared tool advanced search form"""
+
+    legend = _("Advanced search")
+
+    ajax_handler = 'advanced-search-results.html'
+
+    def __init__(self, context, request):
+        super(SharedToolAdvancedSearchForm, self).__init__(context, request)
+        request.registry.notify(PageletCreatedEvent(self))
+
+    @property
+    def fields(self):
+        workflow = IWorkflow(self.context)
+        fields = field.Fields(ISharedToolAdvancedSearchFields)
+        fields['status'].vocabulary = workflow.states
+        return fields
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAdvancedSearchForm), provides=IContentSearch)
+class SharedToolAdvancedSearchFormSearchAdapter(ContextRequestViewAdapter):
+    """Shared tool adavanced search form search adapter"""
+
+    def get_search_results(self, data):
+        catalog = get_utility(ICatalog)
+        params = Eq(catalog['content_type'], self.context.shared_content_type)
+        query = data.get('query')
+        if query:
+            sequence = get_utility(ISequentialIntIds)
+            if query.startswith('+'):
+                params &= Eq(catalog['oid'], sequence.get_full_oid(query))
+            else:
+                query_params = Eq(catalog['oid'], sequence.get_full_oid(query))
+                index = catalog['title:' + self.request.registry.settings.get('pyramid.default_locale_name', 'en')]
+                if index.check_query(query):
+                    query_params |= Contains(index, ' and '.join((w+'*' for w in query.split())))
+                params &= query_params
+        if data.get('owner'):
+            params &= Eq(catalog['owner'], data['owner'])
+        if data.get('status'):
+            params &= Eq(catalog['workflow_state'], data['status'])
+        if data.get('created_after'):
+            params &= Ge(catalog['created_date'], data['created_after'])
+        if data.get('created_before'):
+            params &= Le(catalog['created_date'], data['created_before'])
+        if data.get('modified_after'):
+            params &= Ge(catalog['modified_date'], data['modified_after'])
+        if data.get('modified_before'):
+            params &= Le(catalog['modified_date'], data['modified_before'])
+        if data.get('status'):
+            return unique(map(lambda x: sorted(IWorkflowVersions(x).get_versions(data['status']),
+                                               key=lambda y: IZopeDublinCore(y).modified)[0],
+                              CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                           sort_index='modified_date',
+                                                                           reverse=True))))
+        else:
+            return unique(map(lambda x: IWorkflowVersions(x).get_last_versions()[0],
+                              CatalogResultSet(CatalogQuery(catalog).query(params,
+                                                                           sort_index='modified_date',
+                                                                           reverse=True))))
+
+
+@pagelet_config(name='advanced-search.html', context=ISharedTool, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+class SharedToolAdvancedSearchView(AdminView, SearchView):
+    """Shared tool advanced search view"""
+
+    search_form_factory = SharedToolAdvancedSearchForm
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAdvancedSearchView), provides=IPageHeader)
+class SharedToolAdvancedSearchHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool advanced search header adapter"""
+
+    back_url = '#dashboard.html'
+    back_target = None
+
+    icon_class = 'fa fa-fw fa-search'
+
+
+@view_config(name='advanced-search-results.html', context=ISharedTool, request_type=IPyAMSLayer,
+             permission=VIEW_SYSTEM_PERMISSION)
+@implementer(ISharedToolDashboardTable)
+class SharedToolAdvancedSearchResultsView(AdminView, SearchResultsView):
+    """Shared tool advanced search results view"""
+
+    title = _("Advanced search results")
+    search_form_factory = SharedToolAdvancedSearchForm
+
+    sortOn = None
+
+    def __init__(self, context, request):
+        super(SharedToolAdvancedSearchResultsView, self).__init__(context, request)
+        request.registry.notify(PageletCreatedEvent(self))
+
+    @property
+    def data_attributes(self):
+        attributes = super(SharedToolAdvancedSearchResultsView, self).data_attributes
+        attributes['table'] = {'data-ams-datatable-sorting': '[]',
+                               'data-ams-datatable-display-length':
+                                   IAdminProfile(self.request.principal).table_page_length}
+        return attributes
+
+
+@adapter_config(context=(ISharedTool, IPyAMSLayer, SharedToolAdvancedSearchResultsView), provides=IValues)
+class SearchResultsViewValuesAdapter(ContextRequestViewAdapter):
+    """Search results view values adapter"""
+
+    @property
+    def values(self):
+        form = self.view.search_form_factory(self.context, self.request)
+        return form.get_search_results()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/security.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,197 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_TOOL_PERMISSION
+from pyams_content.shared.common.interfaces import ISharedTool, IManagerRestrictions, IManagerRestrictionsFactory
+from pyams_security.interfaces import ISecurityManager, IPrincipalInfo
+from pyams_security.zmi.interfaces import IObjectSecurityMenu
+from pyams_skin.interfaces import IInnerPage, IPageHeader
+from pyams_skin.interfaces.container import ITableElementEditor
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.layer import IAdminLayer
+from z3c.table.interfaces import IValues, IColumn
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_form.group import NamedWidgetsGroup
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.container import ContainerView
+from pyams_skin.page import DefaultPageHeaderAdapter
+from pyams_skin.table import BaseTable, I18nColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.property import cached_property
+from pyams_utils.registry import get_utility
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyams_zmi.view import AdminView
+from pyramid.exceptions import NotFound
+from pyramid.url import resource_url
+from pyramid.view import view_config
+from z3c.form import field
+from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
+from z3c.form.interfaces import HIDDEN_MODE
+from z3c.table.column import GetAttrColumn
+from zope.interface import implementer
+from zope.schema import getFieldNamesInOrder
+
+from pyams_content import _
+
+
+@viewlet_config(name='managers-restrictions.menu', context=ISharedTool, layer=IAdminLayer,
+                manager=IObjectSecurityMenu, permission=MANAGE_TOOL_PERMISSION, weight=910)
+class SharedToolManagersRestrictionsMenu(MenuItem):
+    """Shared tool managers restrictions menu"""
+
+    label = _("Managers restrictions")
+    icon_class = 'fa-lock'
+    url = '#managers-restrictions.html'
+
+
+class SharedToolManagersRestrictionsTable(BaseTable):
+    """Shared tool manager restrictions table"""
+
+    id = 'security_manager_restrictions'
+    title = _("Content managers restrictions")
+
+
+@adapter_config(context=(ISharedTool, IAdminLayer, SharedToolManagersRestrictionsTable), provides=IValues)
+class SharedToolManagerRestrictionsValuesAdapter(ContextRequestViewAdapter):
+    """Shared tool manager restrictions values adapter"""
+
+    @property
+    def values(self):
+        manager = get_utility(ISecurityManager)
+        return sorted([manager.get_principal(principal_id) for principal_id in self.context.managers],
+                      key=lambda x: x.title)
+
+
+@adapter_config(context=(IPrincipalInfo, IAdminLayer, SharedToolManagersRestrictionsTable),
+                provides=ITableElementEditor)
+class PrincipalInfoElementEditor(ContextRequestViewAdapter):
+    """Principal info element editor"""
+
+    view_name = 'manager-restrictions.html'
+
+    @property
+    def url(self):
+        return resource_url(self.view.context, self.request, self.view_name, query={'principal_id': self.context.id})
+
+    modal_target = True
+
+
+@adapter_config(name='name', context=(ISharedTool, IAdminLayer, SharedToolManagersRestrictionsTable), provides=IColumn)
+class SharedToolManagerRestrictionsNameColumn(I18nColumn, GetAttrColumn):
+    """Shared tool manager restrictions name column"""
+
+    _header = _("Manager name")
+    attrName = 'title'
+    weight = 10
+
+
+@pagelet_config(name='managers-restrictions.html', context=ISharedTool, layer=IPyAMSLayer,
+                permission=MANAGE_TOOL_PERMISSION)
+@implementer(IInnerPage)
+class SharedToolManagersRestrictionsView(AdminView, ContainerView):
+    """Shared tool managers restrictions view"""
+
+    table_class = SharedToolManagersRestrictionsTable
+
+
+@adapter_config(context=(ISharedTool, IAdminLayer, SharedToolManagersRestrictionsView), provides=IPageHeader)
+class SharedToolManagersRestrictionsHeaderAdapter(DefaultPageHeaderAdapter):
+    """Shared tool managers restrictions header adapter"""
+
+    back_url = 'admin.html#protected-object-roles.html'
+    back_target = None
+
+    icon_class = 'fa fa-fw fa-lock'
+
+
+#
+# Manager restrictions edit form
+#
+
+@pagelet_config(name='manager-restrictions.html', context=ISharedTool, layer=IPyAMSLayer,
+                permission=MANAGE_TOOL_PERMISSION)
+class SharedToolManagerRestrictionsEditForm(AdminDialogEditForm):
+    """Shared tool manager restrictions edit form"""
+
+    icon_css_class = 'fa fa-fw fa-lock'
+
+    ajax_handler = 'manager-restrictions.json'
+    edit_permission = MANAGE_TOOL_PERMISSION
+
+    @property
+    def legend(self):
+        return self.request.localizer.translate(_("Edit manager restrictions for « {0} »")).format(self.principal.title)
+
+    @cached_property
+    def interface(self):
+        restrictions = self.getContent()
+        return restrictions.restriction_interface
+
+    @property
+    def fields(self):
+        fields = field.Fields(self.interface)
+        fields['restricted_contents'].widgetFactory = SingleCheckBoxFieldWidget
+        return fields
+
+    @cached_property
+    def principal_id(self):
+        principal_id = self.request.params.get('principal_id') or self.request.params.get('form.widgets.principal_id')
+        if not principal_id:
+            raise NotFound
+        return principal_id
+
+    @cached_property
+    def principal(self):
+        manager = get_utility(ISecurityManager)
+        return manager.get_principal(self.principal_id)
+
+    def getContent(self):
+        manager_restrictions = IManagerRestrictions(self.context)
+        restrictions = manager_restrictions.get_restrictions(self.principal_id)
+        if restrictions is None:
+            restrictions = IManagerRestrictionsFactory(self.context)(self.principal_id)
+            manager_restrictions.set_restrictions(self.principal_id, restrictions)
+        return restrictions
+
+    def update(self):
+        super(SharedToolManagerRestrictionsEditForm, self).update()
+        names = getFieldNamesInOrder(self.interface)
+        self.add_group(NamedWidgetsGroup(self, 'restricted_access', self.widgets, names,
+                                         legend=_("Apply contents restrictions"),
+                                         css_class='inner',
+                                         help=_("You can specify which contents this manager will be able to manage. "
+                                                "If you specify several criteria, the manager will be able to manage "
+                                                "contents for which at least one criteria is matching."),
+                                         switch=True,
+                                         checkbox_switch=True,
+                                         checkbox_field=self.interface['restricted_contents']))
+
+    def updateWidgets(self, prefix=None):
+        super(SharedToolManagerRestrictionsEditForm, self).updateWidgets(prefix)
+        self.widgets['principal_id'].value = self.principal
+        self.widgets['principal_id'].mode = HIDDEN_MODE
+
+
+@view_config(name='manager-restrictions.json', context=ISharedTool, request_type=IPyAMSLayer,
+             permission=MANAGE_TOOL_PERMISSION, renderer='json', xhr=True)
+class SharedToolManagerRestrictionsAJAXEditForm(AJAXEditForm, SharedToolManagerRestrictionsEditForm):
+    """Shared tool manager restrictions edit form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/summary.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,110 @@
+#
+# 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_workflow.interfaces import IWorkflowState, IWorkflowVersions
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import IBaseContentInfo
+from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles, ISharedTool
+from pyams_content.zmi.interfaces import ISummaryMenu
+from pyams_form.interfaces.form import IWidgetForm, IInnerTabForm
+from pyams_sequence.interfaces import ISequentialIdInfo
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_content.shared.common.zmi.header import SharedContentHeaderContentProvider
+from pyams_form.form import InnerDisplayForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_utils.adapter import adapter_config
+from pyams_utils.date import format_datetime
+from pyams_utils.timezone import tztime
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config, contentprovider_config
+from pyams_zmi.form import AdminDisplayForm
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='summary.menu', context=IWfSharedContent, layer=IAdminLayer,
+                manager=IContentManagementMenu, permission=VIEW_SYSTEM_PERMISSION, weight=1)
+@viewletmanager_config(name='summary.menu', layer=IAdminLayer, provides=ISummaryMenu)
+@implementer(ISummaryMenu)
+class SharedContentSummaryMenu(MenuItem):
+    """Shared content summary menu"""
+
+    label = _("Summary")
+    icon_class = 'fa-info-circle'
+    url = '#summary.html'
+
+
+@pagelet_config(name='summary.html', context=IWfSharedContent, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IWidgetForm, IInnerPage)
+class SharedContentSummaryForm(AdminDisplayForm):
+    """Shared content summary form"""
+
+    legend = _("Display content summary")
+    css_class = 'ams-form form-horizontal form-tight'
+
+    fields = field.Fields(IWfSharedContent).select('title', 'short_name', 'description', 'keywords')
+
+
+@contentprovider_config(name='content_header', context=IWfSharedContent, view=SharedContentSummaryForm,
+                        layer=IPyAMSLayer)
+class SharedContentSummaryFormHeaderProvider(SharedContentHeaderContentProvider):
+    """Shared content summary form header provider"""
+
+    @property
+    def back_url(self):
+        tool = get_parent(self.context, ISharedTool)
+        return absolute_url(tool, self.request, 'admin.html')
+
+
+@adapter_config(name='dublincore-summary',
+                context=(IWfSharedContent, IPyAMSLayer, SharedContentSummaryForm),
+                provides=IInnerTabForm)
+class SharedContentDublinCoreSummary(InnerDisplayForm):
+    """Shared content DublinCore summary"""
+
+    weight = 1
+    tab_label = _("Identity card")
+    css_class = 'form-tight'
+
+    @property
+    def fields(self):
+        fields = field.Fields(ISequentialIdInfo).select('hex_oid') + \
+                 field.Fields(IWfSharedContentRoles).select('owner') + \
+                 field.Fields(IWfSharedContent).select('creator', 'first_owner') + \
+                 field.Fields(IBaseContentInfo) + \
+                 field.Fields(IWfSharedContent).select('modifiers', 'notepad')
+        state = IWorkflowState(self.context)
+        if state.version_id == 1:
+            fields = fields.omit('first_owner')
+        return fields
+
+    def updateWidgets(self, prefix=None):
+        super(SharedContentDublinCoreSummary, self).updateWidgets(prefix)
+        info = IBaseContentInfo(self.context)
+        self.widgets['created_date'].value = format_datetime(tztime(info.created_date))
+        self.widgets['modified_date'].value = format_datetime(tztime(info.modified_date))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/advanced-search.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,222 @@
+<div class="ams-widget" i18n:domain="pyams_content">
+	<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.legend"></h2>
+		<tal:var content="structure provider:pyams.toolbar" />
+	</header>
+	<div class="widget-body no-padding">
+		<div tal:define="prefix provider:form_prefix"
+			 tal:replace="structure prefix">Form prefix</div>
+		<tal:var content="structure provider:content_help" />
+		<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>
+					<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-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=""
+									 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>
+							<div class="form-group" tal:define="widget view.widgets['query']">
+								<label class="control-label col-md-3">
+									<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 class="col-md-9">
+									<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>
+							<div class="form-group">
+								<tal:var define="widget view.widgets['owner']">
+									<label class="control-label col-md-3">
+										<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 class="col-md-4">
+										<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>
+								</tal:var>
+								<tal:var define="widget view.widgets['status']">
+									<label class="control-label col-md-1">
+										<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 class="col-md-4">
+										<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>
+								</tal:var>
+							</div>
+							<div class="form-group">
+								<label class="control-label col-md-3">
+									<span i18n:translate="">Created between</span>
+								</label>
+								<div class="col-md-4">
+									<label class="input"
+											tal:define="widget view.widgets['created_after']"
+										   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 class="control-label col-md-1 text-align-center">
+									<i18n:var translate=""> and </i18n:var>
+								</div>
+								<div class="col-md-4">
+									<label class="input"
+											tal:define="widget view.widgets['created_before']"
+										   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>
+							<div class="form-group">
+								<label class="control-label col-md-3">
+									<span i18n:translate="">Modified between</span>
+								</label>
+								<div class="col-md-4">
+									<label class="input"
+											tal:define="widget view.widgets['modified_after']"
+										   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 class="control-label col-md-1 text-align-center">
+									<i18n:var translate=""> and </i18n:var>
+								</div>
+								<div class="col-md-4">
+									<label class="input"
+											tal:define="widget view.widgets['modified_before']"
+										   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>
+						</fieldset>
+					</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}"
+								   tal:content="tabform.tab_label" i18n:translate="">Tab label</a>
+							</li>
+						</ul>
+						<div class="tab-content">
+							<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>
+			</div>
+			<footer>
+				<button tal:repeat="action view.actions.values()"
+						tal:replace="structure action.render()">Action</button>
+			</footer>
+		</form>
+		<div tal:define="prefix provider:form_suffix"
+			 tal:replace="structure prefix">Form suffix</div>
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/dashboard.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,32 @@
+<div class="ams-widget" i18n:domain="pyams_content">
+	<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">
+		<form class="ams-form clearfix margin-bottom-10" method="post" action="quick-search.html"
+			  data-async data-ams-form-submit-target="#search_results">
+			<div class="form-group">
+				<div class="col-md-push-6 col-md-4">
+					<label class="input">
+						<button type="submit" class="icon-append fa fa-fw fa-search no-border no-padding"
+								data-ams-form-hide-loading="true"></button>
+						<input type="text" name="query" placeholder="Quick search..." i18n:attributes="placeholder" />
+					</label>
+					<a class="nowrap btn-sm col-md-2" href="#advanced-search.html"
+					   i18n:translate="">Advanced search...</a>
+				</div>
+			</div>
+		</form>
+		<div id="search_results">
+			<tal:loop repeat="table view.tables">
+				<tal:if condition="tuple(table.values)"
+						content="structure table.render()" />
+			</tal:loop>
+		</div>
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/header.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,28 @@
+<tal:block i18n:domain="pyams_content">
+	<h1 class="page-title margin-bottom-5" tal:define="back_url view.back_url">
+		<a class="font-xs hint" data-ams-target="#content"
+		   title="Back to previous page" i18n:attributes="title"
+		   tal:attributes="href back_url;
+						   data-ams-target view.back_target;">
+			<i class="fa fa-chevron-left padding-right-10"></i>
+		</a>
+		<tal:var define="config extension:back_configuration"
+				 condition="config.display_content_icon">
+			<i tal:attributes="class view.icon_class"></i>
+		</tal:var>
+		<tal:var content="view.title" />
+	</h1>
+	<div class="margin-left-10 margin-bottom-5 padding-left-20">
+		<span class="bold content-oid" tal:content="view.oid">OID</span> |
+		<span class="bold content-title" tal:content="i18n:context.title">Title</span> |
+		<span class="content-owner" i18n:translate="">by <i18n:var name="owner" tal:content="view.owner" /></span><br />
+		<span class="content-version" tal:content="string:V${view.version_id}">Version</span> |
+		<span class="content-state" tal:content="structure view.state">state</span> |
+		<span class="content-state-date" tal:content="view.state_date">state date</span>
+		<tal:if define="state_link view.version_link" condition="state_link">|
+			<a tal:attributes="class state_link['css_class'];
+							   href state_link['href'];"
+			   tal:content="state_link['title']">link</a>
+		</tal:if>
+	</div>
+</tal:block>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,5 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">As a manager, you considerate that this content must be archived.</i18n:var><br />
+	<i18n:var translate="">After archiving, it will be backed up but you will not be able to publish it again except
+						   by creating a new version.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,5 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">This content is already retired and not visible.</i18n:var><br />
+	<i18n:var translate="">After archiving, it will be backed up but you will not be able to publish it again except
+						   by creating a new version.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-cancel-archiving-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	After cancelling this request, the content will return to it's previous retired state.
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-cancel-propose-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	After canceling the request, you will be able to update the content again.
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-cancel-retiring-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	After cancelling this request, the content will return to it's normal published state.
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,7 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">You considerate that the currently published must evolve.</i18n:var><br />
+	<i18n:var translate="">By creating a new version, you can update it's content without impacting the currently
+						   published one.</i18n:var><br />
+	<i18n:var translate="">When the new version will be complete, you will be able to make a new publication request
+						   to replace the currently published version (which will be archived automatically).</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-create-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,6 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">This new content is going to be created in 'draft' mode, so that you can complete it
+						   before publication.</i18n:var><br />
+	<i18n:var translate="">A unique number is also going to be assigned to it. This number will be shared by all
+						   content's versions.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-delete-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	The content version is going to be definitely deleted. Will only remain the currently published version.
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,7 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">You are going to duplicate a whole content.</i18n:var><br />
+	<i18n:var translate="">The new copy is going to be created in 'draft' mode, so that you can modify it before
+						   publication.</i18n:var><br />
+	<i18n:var translate="">A new unique number is also going to be assigned to it. This number will be shared by all
+						   content's versions.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-operator-warning.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<div class="alert alert-danger margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	WARNING: this request was made by a contributor which is not the owner of this content.
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-owner-warning.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<div class="alert alert-danger margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	RECALL: you are not the owner of the content on which you are intervening.
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-propose-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,3 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content" i18n:translate="">
+	This publication request is going to be transmitted to a content manager.
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,6 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">As a manager, you considerate that this content is complete and can be published 'as is'.
+						   </i18n:var><br />
+	<i18n:var translate="">This operation will make the content publicly available (except if restricted access has
+						   been set).</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,6 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">As a content manager, you considerate that this content can't be published 'as is'.
+						   </i18n:var><br />
+	<i18n:var translate="">The contributor will be notified of this and will be able to update the content before
+						   doing a new publication request.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,6 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">As a content manager, you considerate that this content should no longer be published.
+						   </i18n:var><br />
+	<i18n:var translate="">Retired content won't be visible anymore, but it can be updated and published again, or
+						   archived.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,4 @@
+<p class="alert alert-info padding-5 margin-bottom-5" i18n:domain="pyams_content">
+	<i18n:var translate="">You considerate that the currently published version should no more be publicly visible.</i18n:var><br />
+	<i18n:var translate="">WARNING: the content will remain visible until a manager validate the request.</i18n:var>
+</p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,15 @@
+<div class="alert alert-warning padding-5" i18n:domain="pyams_content">
+	<strong i18n:translate="">FOR YOUR INFORMATION</strong><br />
+	<u i18n:translate="">Previous step:</u> <tal:var content="view.previous_step" />
+	<tal:var define="message view.previous_message">
+		<tal:if	condition="message"><br />
+			<span class="small" i18n:translate="">With this comment:</span>
+			<p class="small padding-x-20 padding-y-5" tal:content="structure message"></p>
+		</tal:if>
+		<br tal:condition="not:message" />
+	</tal:var>
+	<tal:if define="next_step view.next_step"
+			condition="next_step">
+		<u i18n:translate="">Next step:</u> <tal:var content="next_step" />
+	</tal:if>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/common/zmi/workflow.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,813 @@
+#
+# 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.text import text_to_html
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from datetime import datetime
+
+# import interfaces
+from pyams_content.shared.common.interfaces import IWfSharedContent, ISharedTool, ISharedContent
+from pyams_form.interfaces.form import IInnerTabForm, IFormPrefixViewletsManager, IWidgetsPrefixViewletsManager, \
+    IFormSuffixViewletsManager
+from pyams_security.interfaces import ISecurityManager
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_workflow.interfaces import IWorkflowInfo, IWorkflowTransitionInfo, IWorkflowPublicationInfo, \
+    IWorkflowCommentInfo, IWorkflowVersions, IWorkflowState, IWorkflowManagedContent, IWorkflow, IWorkflowStateLabel, \
+    IWorkflowRequestUrgencyInfo, SYSTEM, MANUAL
+from z3c.form.interfaces import IDataExtractedEvent
+
+# import packages
+from pyams_content.shared.common.zmi.summary import SharedContentSummaryForm
+from pyams_content.workflow import DRAFT, DELETED
+from pyams_form.form import AJAXAddForm, InnerDisplayForm
+from pyams_form.schema import CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_template.template import template_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.date import format_datetime
+from pyams_utils.registry import get_utility
+from pyams_utils.timezone import tztime
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_workflow.zmi.transition import WorkflowContentTransitionForm, WorkflowContentTransitionAJAXForm
+from pyramid.events import subscriber
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import implementer, Interface, Invalid
+
+from pyams_content import _
+
+
+#
+# Workflow summary view
+#
+
+@adapter_config(name='workflow-summary',
+                context=(IWfSharedContent, IPyAMSLayer, SharedContentSummaryForm),
+                provides=IInnerTabForm)
+class WfSharedContentWorkflowSummary(InnerDisplayForm):
+    """Shared content workflow summary"""
+
+    weight = 10
+    tab_label = _("Workflow")
+    tab_target = 'workflow-summary.html'
+
+    fields = field.Fields(Interface)
+
+
+@pagelet_config(name='workflow-summary.html', context=IWfSharedContent, layer=IPyAMSLayer)
+@implementer(IInnerPage, IInnerTabForm)
+class WorkflowSummaryDisplayForm(InnerDisplayForm):
+    """Workflow summary display form"""
+
+    @property
+    def fields(self):
+        fields = field.Fields(IWorkflowState).omit('history') + \
+                 field.Fields(IWorkflowPublicationInfo)
+        workflow = IWorkflow(self.context)
+        if IWorkflowState(self.context).state not in workflow.waiting_states:
+            fields = fields.omit('state_urgency')
+        return fields
+
+    def updateWidgets(self, prefix=None):
+        super(WorkflowSummaryDisplayForm, self).updateWidgets(prefix)
+        state = IWorkflowState(self.context)
+        content = get_parent(self.context, IWorkflowManagedContent)
+        workflow = get_utility(IWorkflow, name=content.workflow_name)
+        self.widgets['state'].value = self.request.localizer.translate(workflow.get_state_label(state.state))
+        self.widgets['state_date'].value = format_datetime(tztime(state.state_date))
+        info = IWorkflowPublicationInfo(self.context)
+        if info.publication_date:
+            self.widgets['publication_date'].value = format_datetime(tztime(info.publication_date))
+        if info.first_publication_date:
+            self.widgets['first_publication_date'].value = format_datetime(tztime(info.first_publication_date))
+        if info.publication_effective_date:
+            self.widgets['publication_effective_date'].value = format_datetime(tztime(info.publication_effective_date))
+        if info.publication_expiration_date:
+            self.widgets['publication_expiration_date'].value = format_datetime((tztime(info.publication_expiration_date)))
+
+
+#
+# Generic transition info
+#
+
+@viewlet_config(name='wf-transition-info', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=WorkflowContentTransitionForm, manager=IFormSuffixViewletsManager, weight=10)
+@template_config(template='templates/wf-transition-info.pt')
+class WorkflowContentTransitionFormInfo(Viewlet):
+    """Publication request form info message"""
+
+    @property
+    def previous_step(self):
+        translate = self.request.localizer.translate
+        workflow = IWorkflow(self.context)
+        state = IWorkflowState(self.context)
+        adapter = self.request.registry.queryAdapter(workflow, IWorkflowStateLabel,
+                                                     name=state.state)
+        if adapter is None:
+            adapter = self.request.registry.queryAdapter(workflow, IWorkflowStateLabel)
+        if adapter is not None:
+            state_label = adapter.get_label(self.context, request=self.request)
+        else:
+            security = get_utility(ISecurityManager)
+            state_label = translate(_("{state} by {principal}")).format(
+                state=translate(workflow.get_state_label(state.state)),
+                principal=security.get_principal(state.state_principal).title)
+        return translate(_("{state} {date}")).format(state=state_label,
+                                                     date=format_datetime(state.state_date, request=self.request))
+
+    @property
+    def previous_message(self):
+        workflow = IWorkflow(self.context)
+        state = IWorkflowState(self.context)
+        position = 0
+        history_item = None
+        trigger = SYSTEM
+        while trigger != MANUAL:
+            position -= 1
+            history_item = state.history[position]
+            if history_item.transition_id:
+                trigger = workflow.get_transition_by_id(history_item.transition_id).trigger
+            else:
+                break
+        if history_item:
+            return text_to_html((history_item.comment or '').strip())
+
+    @property
+    def next_step(self):
+        transition = self.__parent__.transition
+        return self.request.localizer.translate(transition.user_data.get('next_step')) \
+            if 'next_step' in transition.user_data else None
+
+
+#
+# Request publication form
+#
+
+class IPublicationRequestButtons(Interface):
+    """Shared content publication request buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Request publication"))
+
+
+@pagelet_config(name='wf-propose.html', context=IWfSharedContent, layer=IPyAMSLayer, permission='pyams.ManageContent')
+class PublicationRequestForm(WorkflowContentTransitionForm):
+    """Shared content publication request form"""
+
+    fields = field.Fields(IWorkflowTransitionInfo) + \
+             field.Fields(IWorkflowPublicationInfo).select('publication_effective_date',
+                                                           'publication_expiration_date') + \
+             field.Fields(IWorkflowRequestUrgencyInfo) + \
+             field.Fields(IWorkflowCommentInfo)
+    buttons = button.Buttons(IPublicationRequestButtons)
+    ajax_handler = 'wf-propose.json'
+
+    def updateWidgets(self, prefix=None):
+        super(PublicationRequestForm, self).updateWidgets(prefix)
+        self.widgets['publication_effective_date'].required = True
+        self.widgets['publication_effective_date'].value = datetime.now().strftime('%d/%m/%y %H:%M')
+        self.widgets['comment'].required = True
+
+    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')
+        return super(PublicationRequestForm, self).createAndAdd(data)
+
+
+@view_config(name='wf-propose.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class PublicationRequestAJAXForm(WorkflowContentTransitionAJAXForm, PublicationRequestForm):
+    """Shared content publication request form, JSON renderer"""
+
+
+@subscriber(IDataExtractedEvent, form_selector=PublicationRequestForm)
+def handle_publication_request_form_data_extraction(event):
+    """Handle publication request form data extraction"""
+    if not event.data.get('publication_effective_date'):
+        event.form.widgets.errors += (Invalid(_("Publication start date is required")), )
+    comment = (event.data.get('comment') or '').strip()
+    if not comment:
+        event.form.widgets.errors += (Invalid(_("A comment is required")), )
+
+
+@viewlet_config(name='wf-propose-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRequestForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class PublicationRequestFormWarning(Viewlet):
+    """Publication request form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-propose-message', context=IWfSharedContent, layer=IPyAMSLayer, view=PublicationRequestForm,
+                manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-propose-message.pt')
+class PublicationRequestFormMessage(Viewlet):
+    """Publication request form info message"""
+
+
+#
+# Cancel publication request form
+#
+
+class IPublicationRequestCancelButtons(Interface):
+    """Shared content publication request cancel buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Cancel publication request"))
+
+
+@pagelet_config(name='wf-cancel-propose.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.ManageContent')
+class PublicationRequestCancelForm(WorkflowContentTransitionForm):
+    """Shared content publication request cancel form"""
+
+    buttons = button.Buttons(IPublicationRequestCancelButtons)
+    ajax_handler = 'wf-cancel-propose.json'
+
+
+@view_config(name='wf-cancel-propose.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class PublicationRequestCancelAJAXForm(WorkflowContentTransitionAJAXForm, PublicationRequestCancelForm):
+    """Shared content publication request cancel form, JSON renderer"""
+
+
+@viewlet_config(name='wf-cancel-propose-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRequestCancelForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class PublicationRequestCancelFormWarning(Viewlet):
+    """Publication request cancel form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-cancel-propose-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRequestCancelForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-cancel-propose-message.pt')
+class PublicationRequestCancelFormMessage(Viewlet):
+    """Publication request cancel form info message"""
+
+
+#
+# Refuse publication form
+#
+
+class IPublicationRequestRefuseButtons(Interface):
+    """Shared content publication request refuse buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Refuse publication request"))
+
+
+@pagelet_config(name='wf-refuse.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.PublishContent')
+class PublicationRequestRefuseForm(WorkflowContentTransitionForm):
+    """Shared content publication request refuse form"""
+
+    buttons = button.Buttons(IPublicationRequestRefuseButtons)
+    ajax_handler = 'wf-refuse.json'
+
+    def updateWidgets(self, prefix=None):
+        super(PublicationRequestRefuseForm, self).updateWidgets(prefix)
+        self.widgets['comment'].required = True
+
+
+@view_config(name='wf-refuse.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.PublishContent', renderer='json', xhr=True)
+class PublicationRequestRefuseAJAXForm(WorkflowContentTransitionAJAXForm, PublicationRequestRefuseForm):
+    """Shared content publication request refuse form, JSON renderer"""
+
+
+@subscriber(IDataExtractedEvent, form_selector=PublicationRequestRefuseForm)
+def handle_publication_request_refuse_form_data_extraction(event):
+    """Handle publication request refuse form data extraction"""
+    comment = (event.data.get('comment') or '').strip()
+    if not comment:
+        event.form.widgets.errors += (Invalid(_("A comment is required")), )
+
+
+@viewlet_config(name='wf-refuse-operator-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRequestRefuseForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-operator-warning.pt')
+class PublicationRequestRefuseFormWarning(Viewlet):
+    """Publication request refuse form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        state = IWorkflowState(context)
+        if state.state_principal in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-refuse-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRequestRefuseForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-refuse-propose-message.pt')
+class PublicationRequestRefuseFormMessage(Viewlet):
+    """Publication request refuse form info message"""
+
+
+#
+# Publish form
+#
+
+class IPublicationButtons(Interface):
+    """Shared content publication buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Publish"))
+
+
+@pagelet_config(name='wf-publish.html', context=IWfSharedContent, layer=IPyAMSLayer, permission='pyams.PublishContent')
+class PublicationForm(WorkflowContentTransitionForm):
+    """Shared content publication form"""
+
+    fields = field.Fields(IWorkflowTransitionInfo) + \
+             field.Fields(IWorkflowPublicationInfo).select('publication_effective_date',
+                                                           'publication_expiration_date') + \
+             field.Fields(IWorkflowCommentInfo)
+    buttons = button.Buttons(IPublicationButtons)
+    ajax_handler = 'wf-publish.json'
+
+    def updateWidgets(self, prefix=None):
+        super(PublicationForm, self).updateWidgets(prefix)
+        pub_info = IWorkflowPublicationInfo(self.context)
+        self.widgets['publication_effective_date'].required = True
+        if ('publication_effective_date' in self.widgets) and pub_info.publication_effective_date:
+            self.widgets['publication_effective_date'].value = \
+                tztime(pub_info.publication_effective_date).strftime('%d/%m/%y %H:%M')
+        if ('publication_expiration_date' in self.widgets) and pub_info.publication_expiration_date:
+            self.widgets['publication_expiration_date'].value = \
+                tztime(pub_info.publication_expiration_date).strftime('%d/%m/%y %H:%M')
+
+    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')
+        return super(PublicationForm, self).createAndAdd(data)
+
+
+@view_config(name='wf-publish.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.PublishContent', renderer='json', xhr=True)
+class PublicationAJAXForm(WorkflowContentTransitionAJAXForm, PublicationForm):
+    """Shared content publication form, JSON renderer"""
+
+
+@subscriber(IDataExtractedEvent, form_selector=PublicationForm)
+def handle_publication_form_data_extraction(event):
+    """Handle publication form data extraction"""
+    if not event.data.get('publication_effective_date'):
+        event.form.widgets.errors += (Invalid(_("Publication start date is required")), )
+
+
+@viewlet_config(name='wf-publish-operator-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-operator-warning.pt')
+class PublicationFormWarning(Viewlet):
+    """Shared content publication form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        state = IWorkflowState(context)
+        if state.state_principal in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-publish-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-publish-message.pt')
+class PublicationFormMessage(Viewlet):
+    """Shared content publication form info message"""
+
+
+#
+# Publication retire request form
+#
+
+class IPublicationRetireRequestButtons(Interface):
+    """Shared content publication retire request buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Request retire"))
+
+
+@pagelet_config(name='wf-retiring.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.ManageContent')
+class PublicationRetireRequestForm(WorkflowContentTransitionForm):
+    """Shared content publication request refuse form"""
+
+    fields = field.Fields(IWorkflowTransitionInfo) + \
+             field.Fields(IWorkflowRequestUrgencyInfo) + \
+             field.Fields(IWorkflowCommentInfo)
+    buttons = button.Buttons(IPublicationRetireRequestButtons)
+    ajax_handler = 'wf-retiring.json'
+
+    def updateWidgets(self, prefix=None):
+        super(PublicationRetireRequestForm, self).updateWidgets(prefix)
+        self.widgets['comment'].required = True
+
+
+@view_config(name='wf-retiring.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class PublicationRetireRequestAJAXForm(WorkflowContentTransitionAJAXForm, PublicationRetireRequestForm):
+    """Shared content publication retire request form, JSON renderer"""
+
+
+@subscriber(IDataExtractedEvent, form_selector=PublicationRetireRequestForm)
+def handle_publication_retire_request_form_data_extraction(event):
+    """Handle publication retire request form data extraction"""
+    comment = (event.data.get('comment') or '').strip()
+    if not comment:
+        event.form.widgets.errors += (Invalid(_("A comment is required")), )
+
+
+@viewlet_config(name='wf-retiring-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRetireRequestForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class PublicationRetireRequestFormWarning(Viewlet):
+    """Publication retire request form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-retiring-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRetireRequestForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-retiring-message.pt')
+class PublicationRetireRequestFormMessage(Viewlet):
+    """Publication retire request form info message"""
+
+
+#
+# Publication retire cancel form
+#
+
+class IPublicationRetireCancelButtons(Interface):
+    """Shared content publication retire request cancel buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Cancel retire request"))
+
+
+@pagelet_config(name='wf-cancel-retiring.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.ManageContent')
+class PublicationRetireCancelForm(WorkflowContentTransitionForm):
+    """Shared content publication retire request cancel form"""
+
+    buttons = button.Buttons(IPublicationRetireCancelButtons)
+    ajax_handler = 'wf-cancel-retiring.json'
+
+
+@view_config(name='wf-cancel-retiring.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class PublicationRetireCancelAJAXForm(WorkflowContentTransitionAJAXForm, PublicationRetireCancelForm):
+    """Shared content publication retire request cancel form, JSON renderer"""
+
+
+@viewlet_config(name='wf-cancel-retiring-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRetireCancelForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class PublicationRetireCancelFormWarning(Viewlet):
+    """Publication retire request cancel form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-cancel-retiring-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRetireCancelForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-cancel-retiring-message.pt')
+class PublicationRetireCancelFormMessage(Viewlet):
+    """Publication retire request form info message"""
+
+
+#
+# Publication retire form
+#
+
+class IPublicationRetireButtons(Interface):
+    """Shared content publication retire buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Retire"))
+
+
+@pagelet_config(name='wf-retire.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.PublishContent')
+class PublicationRetireForm(WorkflowContentTransitionForm):
+    """Shared content publication retire form"""
+
+    buttons = button.Buttons(IPublicationRetireButtons)
+    ajax_handler = 'wf-retire.json'
+
+
+@view_config(name='wf-retire.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.PublishContent', renderer='json', xhr=True)
+class PublicationRetireAJAXForm(WorkflowContentTransitionAJAXForm, PublicationRetireForm):
+    """Shared content publication retire form, JSON renderer"""
+
+
+@viewlet_config(name='wf-retire-operator-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRetireForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-operator-warning.pt')
+class PublicationRetireFormWarning(Viewlet):
+    """Publication retire form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        state = IWorkflowState(context)
+        if state.state_principal in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-retire-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationRetireForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-retire-message.pt')
+class PublicationRetireFormMessage(Viewlet):
+    """Publication retire form info message"""
+
+
+#
+# Publication archive request form
+#
+
+class IPublicationArchiveRequestButtons(Interface):
+    """Shared content publication archive request buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Request archive"))
+
+
+@pagelet_config(name='wf-archiving.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.ManageContent')
+class PublicationArchiveRequestForm(WorkflowContentTransitionForm):
+    """Shared content publication request archive form"""
+
+    fields = field.Fields(IWorkflowTransitionInfo) + \
+             field.Fields(IWorkflowRequestUrgencyInfo) + \
+             field.Fields(IWorkflowCommentInfo)
+    buttons = button.Buttons(IPublicationArchiveRequestButtons)
+    ajax_handler = 'wf-archiving.json'
+
+    def updateWidgets(self, prefix=None):
+        super(PublicationArchiveRequestForm, self).updateWidgets(prefix)
+        self.widgets['comment'].required = True
+
+
+@view_config(name='wf-archiving.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class PublicationArchiveRequestAJAXForm(WorkflowContentTransitionAJAXForm, PublicationArchiveRequestForm):
+    """Shared content publication archive request form, JSON renderer"""
+
+
+@subscriber(IDataExtractedEvent, form_selector=PublicationArchiveRequestForm)
+def handle_archive_request_form_data_extraction(event):
+    """Handle archive request form data extraction"""
+    comment = (event.data.get('comment') or '').strip()
+    if not comment:
+        event.form.widgets.errors += (Invalid(_("A comment is required")), )
+
+
+@viewlet_config(name='wf-archiving-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationArchiveRequestForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class PublicationArchiveRequestFormWarning(Viewlet):
+    """Publication archive request form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-archiving-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationArchiveRequestForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-archiving-message.pt')
+class PublicationArchiveRequestFormMessage(Viewlet):
+    """Publication archive request form info message"""
+
+
+#
+# Publication archive cancel form
+#
+
+class IPublicationArchiveCancelButtons(Interface):
+    """Shared content publication archive request cancel buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Cancel archive request"))
+
+
+@pagelet_config(name='wf-cancel-archiving.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.ManageContent')
+class PublicationArchiveCancelForm(WorkflowContentTransitionForm):
+    """Shared content publication archive request cancel form"""
+
+    buttons = button.Buttons(IPublicationArchiveCancelButtons)
+    ajax_handler = 'wf-cancel-archiving.json'
+
+
+@view_config(name='wf-cancel-archiving.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class PublicationArchiveCancelAJAXForm(WorkflowContentTransitionAJAXForm, PublicationArchiveCancelForm):
+    """Shared content publication archive request cancel form, JSON renderer"""
+
+
+@viewlet_config(name='wf-cancel-archiving-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationArchiveCancelForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class PublicationArchiveCancelFormWarning(Viewlet):
+    """Publication archive cancel form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-cancel-archiving-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationArchiveCancelForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-cancel-archiving-message.pt')
+class PublicationArchiveCancelFormMessage(Viewlet):
+    """Publication archive cancel form info message"""
+
+
+#
+# Publication archive form
+#
+
+class IPublicationArchiveButtons(Interface):
+    """Shared content publication archive buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Archive"))
+
+
+@pagelet_config(name='wf-archive.html', context=IWfSharedContent, layer=IPyAMSLayer,
+                permission='pyams.PublishContent')
+class PublicationArchiveForm(WorkflowContentTransitionForm):
+    """Shared content publication archive form"""
+
+    buttons = button.Buttons(IPublicationArchiveButtons)
+    ajax_handler = 'wf-archive.json'
+
+
+@view_config(name='wf-archive.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.PublishContent', renderer='json', xhr=True)
+class PublicationArchiveAJAXForm(WorkflowContentTransitionAJAXForm, PublicationArchiveForm):
+    """Shared content publication archive form, JSON renderer"""
+
+
+@viewlet_config(name='wf-archive-operator-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationArchiveForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-operator-warning.pt')
+class PublicationArchiveFormWarning(Viewlet):
+    """Publication archive form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        state = IWorkflowState(context)
+        if state.state_principal in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-archive-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=PublicationArchiveForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-archive-message.pt')
+class PublicationArchiveFormMessage(Viewlet):
+    """Publication archive form info message"""
+
+
+#
+# Clone form
+#
+
+class ISharedContentCloneButtons(Interface):
+    """Shared content clone buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Create new version"))
+
+
+@pagelet_config(name='wf-clone.html', context=IWfSharedContent, layer=IPyAMSLayer, permission='pyams.CreateContent')
+class SharedContentCloneForm(WorkflowContentTransitionForm):
+    """Shared content clone form"""
+
+    buttons = button.Buttons(ISharedContentCloneButtons)
+    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=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.CreateContent', renderer='json', xhr=True)
+class SharedContentCloneAJAXForm(AJAXAddForm, SharedContentCloneForm):
+    """Shared content clone form, JSON rendener"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'redirect',
+                'location': absolute_url(changes, self.request, 'admin.html#properties.html')}
+
+
+@viewlet_config(name='wf-clone-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=SharedContentCloneForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class SharedContentCloneFormWarning(Viewlet):
+    """Shared content clone form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-clone-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=SharedContentCloneForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-clone-message.pt')
+class SharedContentCloneFormMessage(Viewlet):
+    """Shared content clone form info message"""
+
+
+#
+# Delete form
+#
+
+class ISharedContentDeleteButtons(Interface):
+    """Shared content delete form buttons"""
+
+    close = CloseButton(name='close', title=_("Cancel"))
+    action = button.Button(name='action', title=_("Delete version"))
+
+
+@pagelet_config(name='wf-delete.html', context=IWfSharedContent, layer=IPyAMSLayer, permission='pyams.ManageContent')
+class SharedContentDeleteForm(WorkflowContentTransitionForm):
+    """Shared content delete form"""
+
+    buttons = button.Buttons(ISharedContentDeleteButtons)
+    ajax_handler = 'wf-delete.json'
+
+    def createAndAdd(self, data):
+        state = IWorkflowState(self.context)
+        if state.version_id == 1:  # remove the first and only version => remove all
+            content = get_parent(self.context, ISharedContent)
+            self.__target = get_parent(content, ISharedTool)
+            del content.__parent__[content.__name__]
+        else:
+            self.__target = get_parent(self.context, ISharedTool)
+            IWorkflowVersions(self.context).remove_version(state.version_id, state=DELETED, comment=data.get('comment'))
+
+
+@view_config(name='wf-delete.json', context=IWfSharedContent, request_type=IPyAMSLayer,
+             permission='pyams.ManageContent', renderer='json', xhr=True)
+class SharedContentDeleteAJAXForm(AJAXAddForm, SharedContentDeleteForm):
+    """Shared content delete form, JSON rendener"""
+
+    def get_ajax_output(self, changes):
+        return {'status': 'redirect',
+                'location': absolute_url(self._SharedContentDeleteForm__target, self.request, 'admin.html')}
+
+
+@viewlet_config(name='wf-delete-owner-warning', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=SharedContentDeleteForm, manager=IFormPrefixViewletsManager, weight=10)
+@template_config(template='templates/wf-owner-warning.pt')
+class SharedContentDeleteFormWarning(Viewlet):
+    """Shared content delete form warning message"""
+
+    def __new__(cls, context, request, view, manager):
+        if request.principal.id in context.owner:
+            return None
+        return Viewlet.__new__(cls)
+
+
+@viewlet_config(name='wf-delete-message', context=IWfSharedContent, layer=IPyAMSLayer,
+                view=SharedContentDeleteForm, manager=IWidgetsPrefixViewletsManager, weight=20)
+@template_config(template='templates/wf-delete-message.pt')
+class SharedContentDeleteFormMessage(Viewlet):
+    """Shared content delete form info message"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/news/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,50 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.gallery.interfaces import IGalleryContainerTarget
+from pyams_content.component.extfile.interfaces import IExtFileContainerTarget
+from pyams_content.component.links.interfaces import ILinkContainerTarget
+from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget
+from pyams_content.component.theme.interfaces import IThemesTarget
+from pyams_content.shared.news.interfaces import INewsEvent, IWfNewsEvent, NEWS_CONTENT_TYPE, NEWS_CONTENT_NAME
+
+# import packages
+from pyams_content.shared.common import SharedContent, WfSharedContent, register_content_type
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IWfNewsEvent, IParagraphContainerTarget, IThemesTarget, IExtFileContainerTarget, ILinkContainerTarget,
+             IGalleryContainerTarget)
+class WfNewsEvent(WfSharedContent):
+    """Base news event"""
+
+    content_type = NEWS_CONTENT_TYPE
+    content_name = NEWS_CONTENT_NAME
+
+    displayed_publication_date = FieldProperty(IWfNewsEvent['displayed_publication_date'])
+    push_end_date = FieldProperty(IWfNewsEvent['push_end_date'])
+
+register_content_type(WfNewsEvent)
+
+
+@implementer(INewsEvent)
+class NewsEvent(SharedContent):
+    """Workflow managed news event class"""
+
+    content_class = WfNewsEvent
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/news/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,66 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.common.interfaces import ISharedTool, IWfSharedContent, ISharedContent
+
+# import packages
+from zope.interface import Attribute
+from zope.schema import Choice, Datetime
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+from pyams_content import _
+
+
+NEWS_CONTENT_TYPE = 'news'
+NEWS_CONTENT_NAME = _("News topic")
+
+
+DISPLAY_FIRST_VERSION = 'first'
+DISPLAY_CURRENT_VERSION = 'current'
+
+VERSION_DISPLAY = {DISPLAY_FIRST_VERSION: _("Display first version date"),
+                   DISPLAY_CURRENT_VERSION: _("Display current version date")}
+
+VERSION_DISPLAY_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t)
+                                               for v, t in VERSION_DISPLAY.items()])
+
+
+class INewsManager(ISharedTool):
+    """News manager interface"""
+
+
+class IWfNewsEvent(IWfSharedContent):
+    """News event interface"""
+
+    displayed_publication_date = Choice(title=_("Displayed publication date"),
+                                        description=_("The matching date will be displayed in front-office"),
+                                        vocabulary=VERSION_DISPLAY_VOCABULARY,
+                                        default=DISPLAY_FIRST_VERSION,
+                                        required=True)
+
+    publication_date = Attribute("Publication date")
+
+    push_end_date = Datetime(title=_("Push end date"),
+                             description=_("Some contents can be pushed by components to front-office pages; if you "
+                                           "set a date here, this content will not be pushed anymore passed this "
+                                           "date, but will still be available via the search engine"),
+                             required=False)
+
+
+class INewsEvent(ISharedContent):
+    """Workflow managed news event interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/news/manager.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,47 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.component.theme.interfaces import IThemesManagerTarget
+from pyams_content.shared.news.interfaces import INewsManager, NEWS_CONTENT_TYPE
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.component.interfaces import ISite
+from zope.lifecycleevent.interfaces import IObjectAddedEvent
+
+# import packages
+from pyams_content.shared.common.manager import SharedTool
+from pyams_content.shared.news import NewsEvent
+from pyams_utils.traversing import get_parent
+from pyramid.events import subscriber
+from zope.interface import implementer
+
+
+@implementer(INewsManager, IThemesManagerTarget, IAttributeAnnotatable)
+class NewsManager(SharedTool):
+    """News manager class"""
+
+    shared_content_type = NEWS_CONTENT_TYPE
+    shared_content_factory = NewsEvent
+
+
+@subscriber(IObjectAddedEvent, context_selector=INewsManager)
+def handle_added_news_manager(event):
+    """Register news manager when added"""
+    site = get_parent(event.newParent, ISite)
+    registry = site.getSiteManager()
+    if registry is not None:
+        registry.registerUtility(event.object, INewsManager)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/news/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,79 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import CREATE_CONTENT_PERMISSION
+from pyams_content.shared.news.interfaces import INewsManager, IWfNewsEvent
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager, IMenuHeader
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_content.shared.common.zmi import SharedContentAddForm, SharedContentAJAXAddForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.interfaces import IContentTitle
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextRequestAdapter
+from pyams_viewlet.viewlet import viewlet_config
+from pyramid.view import view_config
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@adapter_config(context=(IWfNewsEvent, IContentManagementMenu), provides=IMenuHeader)
+class NewsEventContentMenuHeader(ContextRequestAdapter):
+    """News event content menu header adapter"""
+
+    header = _("This news topic")
+
+
+@adapter_config(context=(IWfNewsEvent, IPyAMSLayer, Interface), provides=IContentTitle)
+class NewsEventTitleAdapter(ContextRequestViewAdapter):
+    """News event title adapter"""
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        return translate(_("News topic « {title} »")).format(
+            title=II18n(self.context).query_attribute('short_name', request=self.request))
+
+
+@viewlet_config(name='add-shared-content.action', context=INewsManager, layer=IAdminLayer, view=Interface,
+                manager=IWidgetTitleViewletManager, permission=CREATE_CONTENT_PERMISSION, weight=1)
+class NewsEventAddAction(ToolbarAction):
+    """News event adding action"""
+
+    label = _("Add news topic")
+    url = 'add-shared-content.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-shared-content.html', context=INewsManager, layer=IPyAMSLayer,
+                permission='pyams.CreateContent')
+class NewsEventAddForm(SharedContentAddForm):
+    """News event add form"""
+
+    legend = _("Add new news topic")
+
+
+@view_config(name='add-shared-content.json', context=INewsManager, request_type=IPyAMSLayer,
+             permission=CREATE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class NewsEventAJAXAddForm(SharedContentAJAXAddForm, NewsEventAddForm):
+    """News event add form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/news/zmi/properties.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,43 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.news.interfaces import IWfNewsEvent
+from pyams_form.interfaces.form import IInnerSubForm
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.shared.common.zmi.properties import SharedContentPropertiesEditForm
+from pyams_form.form import InnerEditForm
+from pyams_utils.adapter import adapter_config
+from z3c.form import field
+from zope.interface import implementer
+
+from pyams_content import _
+
+
+@adapter_config(name='publication',
+                context=(IWfNewsEvent, IPyAMSLayer, SharedContentPropertiesEditForm),
+                provides=IInnerSubForm)
+@implementer(IInnerSubForm)
+class NewsEventPropertiesEditForm(InnerEditForm):
+    """News event properties edit form extension"""
+
+    legend = _("Publication settings")
+    fieldset_class = 'bordered no-x-margin margin-y-10'
+
+    fields = field.Fields(IWfNewsEvent).select('displayed_publication_date', 'push_end_date')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/site.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,48 @@
+#
+# 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_utils.interfaces.site import ISiteGenerations
+from zope.intid.interfaces import IIntIds
+from zope.site.interfaces import INewLocalSite
+
+# import packages
+from pyams_utils.registry import utility_config
+from pyams_utils.site import check_required_utilities
+from pyramid.events import subscriber
+from zope.intid import IntIds
+
+
+REQUIRED_UTILITIES = ((IIntIds, '', IntIds, 'Internal IDs'),)
+
+
+@subscriber(INewLocalSite)
+def handle_new_local_site(event):
+    """Create a new IntIds when a site is created"""
+    site = event.manager.__parent__
+    check_required_utilities(site, REQUIRED_UTILITIES)
+
+
+@utility_config(name='PyAMS base', provides=ISiteGenerations)
+class BaseGenerationsChecker(object):
+    """PyAMS base generations checker"""
+
+    generation = 1
+
+    def evolve(self, site, current=None):
+        """Check for required utilities"""
+        check_required_utilities(site, REQUIRED_UTILITIES)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/site/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.site.interfaces import ISite
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from pyams_utils.container import BTreeOrderedContainer
+from zope.componentvocabulary.vocabulary import UtilityVocabulary
+from zope.interface import provider, implementer
+from zope.schema.vocabulary import getVocabularyRegistry
+
+
+@implementer(ISite)
+class Site(BTreeOrderedContainer):
+    """Site persistent class"""
+
+
+@provider(IVocabularyFactory)
+class SiteVocabulary(UtilityVocabulary):
+    """Sites vocabulary"""
+
+    interface = ISite
+    nameOnly = True
+
+getVocabularyRegistry().register('PyAMS content sites', SiteVocabulary)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/site/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -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
+
+# import packages
+from zope.interface import Interface
+
+
+class ISite(Interface):
+    """Site interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import __interfaces
+
+# import packages
+from fanstatic import Library
+
+
+library = Library('pyams_content', 'resources')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/resources/js/pyams_content.js	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,164 @@
+(function($) {
+
+	PyAMS_content = {
+
+		/**
+		 * TinyMCE editor configuration
+		 */
+		TinyMCE: {
+
+			initEditor: function(settings) {
+				settings.image_list = PyAMS_content.TinyMCE.getImagesList;
+				settings.link_list = PyAMS_content.TinyMCE.getLinksList;
+				return settings;
+			},
+
+			getImagesList: function(callback) {
+				return MyAMS.ajax.post('get-images-list.json', {}, callback);
+			},
+
+			getLinksList: function(callback) {
+				return MyAMS.ajax.post('get-links-list.json', {}, callback);
+			}
+		},
+
+		/**
+		 * External files management
+		 */
+		extfiles: {
+
+			refresh: function(options) {
+				if (typeof(options) == 'string')
+					options = JSON.parse(options);
+				var select = $('select[name="form.widgets.files:list"]');
+				var plugin = select.data('select2');
+				$('<option></option>').attr('value', options.new_file.id)
+									  .attr('selected', 'selected')
+									  .text(options.new_file.text)
+									  .appendTo(select);
+				var data = select.select2('data');
+				data.push(options.new_file);
+				select.select2('data', data);
+				plugin.results.empty();
+				plugin.opts.populateResults.call(plugin, plugin.results, options.files, {term: ''});
+			}
+		},
+
+
+		/**
+		 * Links management
+		 */
+		links: {
+
+			refresh: function(options) {
+				if (typeof(options) == 'string')
+					options = JSON.parse(options);
+				var select = $('select[name="form.widgets.links:list"]');
+				var plugin = select.data('select2');
+				$('<option></option>').attr('value', options.new_link.id)
+									  .attr('selected', 'selected')
+									  .text(options.new_link.text)
+									  .appendTo(select);
+				var data = select.select2('data');
+				data.push(options.new_link);
+				select.select2('data', data);
+				plugin.results.empty();
+				plugin.opts.populateResults.call(plugin, plugin.results, options.links, {term: ''});
+			}
+		},
+
+
+		/**
+		 * Galleries management
+		 */
+		galleries: {
+
+			refresh: function(options) {
+				if (typeof(options) == 'string')
+					options = JSON.parse(options);
+				var select = $('select[name="form.widgets.galleries:list"]');
+				var plugin = select.data('select2');
+				$('<option></option>').attr('value', options.new_gallery.id)
+									  .attr('selected', 'selected')
+									  .text(options.new_gallery.text)
+									  .appendTo(select);
+				var data = select.select2('data');
+				data.push(options.new_gallery);
+				select.select2('data', data);
+				plugin.results.empty();
+				plugin.opts.populateResults.call(plugin, plugin.results, options.galleries, {term: ''});
+			},
+
+			setOrder: function(event, ui) {
+				if (ui && ui.item.hasClass('already-dropped'))
+					return;
+				var gallery = ui.item.parents('.gallery');
+				var ids = $('.image', gallery).listattr('data-ams-element-name');
+				MyAMS.ajax.post(gallery.data('ams-location') + '/set-images-order.json',
+								{images: JSON.stringify(ids)});
+			},
+
+			removeFile: function(element) {
+				return function() {
+					var link = $(this);
+					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(button) {
+						if (button == MyAMS.i18n.BTN_OK) {
+							var gallery = link.parents('.gallery');
+							var location = gallery.data('ams-location');
+							var image = link.parents('.image');
+							var object_name = image.data('ams-element-name');
+							MyAMS.ajax.post(location + '/delete-element.json', {'object_name': object_name}, function(result, status) {
+								image.remove();
+							});
+						}
+					});
+				}
+			}
+		},
+
+
+		/**
+		 * Themes management
+		 */
+		themes: {
+
+			initExtracts: function(element) {
+				var thesaurus = $('select[name="form.widgets.thesaurus_name:list"]', element);
+				var thesaurus_name = thesaurus.val();
+				var extract = $('select[name="form.widgets.extract_name:list"]', element);
+				var extract_name = extract.val();
+				if (thesaurus_name) {
+					MyAMS.jsonrpc.post('getExtracts', {thesaurus_name: thesaurus_name}, {url: '/api/thesaurus/json'}, function(data) {
+						extract.empty();
+						$(data.result).each(function() {
+							$('<option></option>').attr('value', this.id)
+												  .attr('selected', this.id == extract_name)
+												  .text(this.text)
+												  .appendTo(extract);
+						});
+					});
+				}
+				extract.attr('data-ams-events-handlers', '{"select2-open": "PyAMS_content.themes.getExtracts"}');
+			},
+
+			getExtracts: function(event) {
+				var select = $(event.currentTarget);
+				var form = select.parents('form');
+				var thesaurus_name = $('select[name="form.widgets.thesaurus_name:list"]', form).val();
+				if (thesaurus_name) {
+					MyAMS.jsonrpc.post('getExtracts', {thesaurus_name: thesaurus_name}, {url: '/api/thesaurus/json'}, function(data) {
+						var extract = $('select[name="form.widgets.extract_name:list"]', form);
+						var plugin = extract.data('select2');
+						plugin.results.empty();
+						plugin.opts.populateResults.call(plugin, plugin.results, data.result, {term: ''});
+					});
+				}
+			}
+		}
+	}
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/resources/js/pyams_content.min.js	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+(function(a){if(window.ONF===undefined){window.ONF={}}ONF.Skin={extfiles:{refresh:function(c){if(typeof(c)=="string"){c=JSON.parse(c)}var b=a('select[name="form.widgets.files:list"]');var d=b.data("select2");a("<option></option>").attr("value",c.new_file.id).attr("selected","selected").text(c.new_file.text).appendTo(b);var e=b.select2("data");e.push(c.new_file);b.select2("data",e);d.results.empty();d.opts.populateResults.call(d,d.results,c.files,{term:""})}},links:{refresh:function(c){if(typeof(c)=="string"){c=JSON.parse(c)}var b=a('select[name="form.widgets.links:list"]');var d=b.data("select2");a("<option></option>").attr("value",c.new_link.id).attr("selected","selected").text(c.new_link.text).appendTo(b);var e=b.select2("data");e.push(c.new_link);b.select2("data",e);d.results.empty();d.opts.populateResults.call(d,d.results,c.links,{term:""})}},galleries:{refresh:function(c){if(typeof(c)=="string"){c=JSON.parse(c)}var b=a('select[name="form.widgets.galleries:list"]');var d=b.data("select2");a("<option></option>").attr("value",c.new_gallery.id).attr("selected","selected").text(c.new_gallery.text).appendTo(b);var e=b.select2("data");e.push(c.new_gallery);b.select2("data",e);d.results.empty();d.opts.populateResults.call(d,d.results,c.galleries,{term:""})},setOrder:function(d,e){if(e&&e.item.hasClass("already-dropped")){return}var b=e.item.parents(".gallery");var c=a(".image",b).listattr("data-ams-element-name");MyAMS.ajax.post(b.data("ams-location")+"/set-images-order.json",{images:JSON.stringify(c)})},removeFile:function(b){return function(){var c=a(this);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(g){if(g==MyAMS.i18n.BTN_OK){var f=c.parents(".gallery");var e=f.data("ams-location");var h=c.parents(".image");var d=h.data("ams-element-name");MyAMS.ajax.post(e+"/delete-element.json",{object_name:d},function(i,j){h.remove()})}})}}},themes:{initExtracts:function(d){var c=a('select[name="form.widgets.thesaurus_name:list"]',d);var b=c.val();var f=a('select[name="form.widgets.extract_name:list"]',d);var e=f.val();if(b){MyAMS.jsonrpc.post("getExtracts",{thesaurus_name:b},{url:"/api/thesaurus/json"},function(g){f.empty();a(g.result).each(function(){a("<option></option>").attr("value",this.id).attr("selected",this.id==e).text(this.text).appendTo(f)})})}f.attr("data-ams-events-handlers",'{"select2-open": "ONF.Skin.themes.getExtracts"}')},getExtracts:function(e){var b=a(e.currentTarget);var d=b.parents("form");var c=a('select[name="form.widgets.thesaurus_name:list"]',d).val();if(c){MyAMS.jsonrpc.post("getExtracts",{thesaurus_name:c},{url:"/api/thesaurus/json"},function(h){var g=a('select[name="form.widgets.extract_name:list"]',d);var f=g.data("select2");f.results.empty();f.opts.populateResults.call(f,f.results,h.result,{term:""})})}}}}})(jQuery);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.js	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,5 @@
+tinyMCE.addI18n('fr', {
+	"Insert internal link": "Insérer un lien interne",
+	"Linktitle": "Texte du lien",
+	"Internal number": "N° interne"
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.min.js	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+tinyMCE.addI18n("fr",{"Insert internal link":"Insérer un lien interne",Linktitle:"Texte du lien","Internal number":"N° interne"});
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+tinymce.PluginManager.add('onflinks', function(editor, url) {
+
+	editor.addButton('onflinks', {
+		icon: 'cloud-check',
+		tooltip: "Insert internal link",
+		image: '/--static--/pyams_content/img/external.png',
+		onclick: function() {
+			editor.windowManager.open({
+				title: "Insert internal link",
+				body: [
+					{type: 'textbox', name: 'title', label: 'Link title', value: editor.selection.getContent()},
+					{type: 'textbox', name: 'oid', label:'Internal number'}
+				],
+				onsubmit: function(e) {
+					editor.insertContent('<a href="oid://' + e.data.oid + '">' + e.data.title + '</a>');
+				}
+			});
+		}
+	})
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+tinymce.PluginManager.add("onflinks",function(b,a){b.addButton("onflinks",{icon:"cloud-check",tooltip:"Insert internal link",image:"/--static--/onf_website/img/external.png",onclick:function(){b.windowManager.open({title:"Insert internal link",body:[{type:"textbox",name:"title",label:"Link title",value:b.selection.getContent()},{type:"textbox",name:"oid",label:"Internal number"}],onsubmit:function(c){b.insertContent('<a href="oid://'+c.data.oid+'">'+c.data.title+"</a>")}})}})});
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/skin/routes.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,57 @@
+#
+# 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 hypatia.interfaces import ICatalog
+from pyams_sequence.interfaces import ISequentialIntIds
+from pyams_workflow.interfaces import IWorkflowVersions
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Eq, Any
+from pyams_catalog.query import CatalogResultSet
+from pyams_content.workflow import VISIBLE_STATES
+from pyams_utils.registry import get_utility
+from pyams_utils.url import absolute_url
+from pyramid.exceptions import NotFound
+from pyramid.response import Response
+from pyramid.view import view_config
+
+
+@view_config(route_name='oid_access')
+def get_oid_access(request):
+    """Get direct access to given OID"""
+    oid = request.matchdict.get('oid')
+    if oid:
+        view_name = request.matchdict.get('view')
+        sequence = get_utility(ISequentialIntIds)
+        hex_oid = sequence.get_full_oid(oid)
+        catalog = get_utility(ICatalog)
+        params = Eq(catalog['oid'], hex_oid)
+        if not view_name:
+            params &= Any(catalog['workflow_state'], VISIBLE_STATES)
+        results = list(CatalogResultSet(CatalogQuery(catalog).query(params)))
+        if results:
+            if view_name:  # back-office access => last version
+                version = IWorkflowVersions(results[0]).get_last_versions()[0]
+            else:
+                version = results[0]
+            response = Response()
+            response.status_code = 302
+            response.location = absolute_url(version, request, '/'.join(view_name))
+            return response
+    raise NotFound()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/tests/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/tests/test_utilsdocs.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,59 @@
+#
+# 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.
+#
+
+"""
+Generic Test case for pyams_content doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.dirname(__file__)
+
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
+    """Returns a test suite, based on doctests found in /doctest."""
+    suite = []
+    if globs is None:
+        globs = globals()
+
+    flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+             doctest.REPORT_ONLY_FIRST_FAILURE)
+
+    package_dir = os.path.split(test_dir)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+
+    doctest_dir = os.path.join(package_dir, 'doctests')
+
+    # filtering files on extension
+    docs = [os.path.join(doctest_dir, doc) for doc in
+            os.listdir(doctest_dir) if doc.endswith('.txt')]
+
+    for test in docs:
+        suite.append(doctest.DocFileSuite(test, optionflags=flags,
+                                          globs=globs, setUp=setUp,
+                                          tearDown=tearDown,
+                                          module_relative=False))
+
+    return unittest.TestSuite(suite)
+
+def test_suite():
+    """returns the test suite"""
+    return doc_suite(current_dir)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/tests/test_utilsdocstrings.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+
+"""
+Generic Test case for pyams_content doc strings
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+
+def doc_suite(test_dir, globs=None):
+    """Returns a test suite, based on doc tests strings found in /*.py"""
+    suite = []
+    if globs is None:
+        globs = globals()
+
+    flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+             doctest.REPORT_ONLY_FIRST_FAILURE)
+
+    package_dir = os.path.split(test_dir)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+
+    # filtering files on extension
+    docs = [doc for doc in
+            os.listdir(package_dir) if doc.endswith('.py')]
+    docs = [doc for doc in docs if not doc.startswith('__')]
+
+    for test in docs:
+        fd = open(os.path.join(package_dir, test))
+        content = fd.read()
+        fd.close()
+        if '>>> ' not in content:
+            continue
+        test = test.replace('.py', '')
+        location = 'pyams_content.%s' % test
+        suite.append(doctest.DocTestSuite(location, optionflags=flags,
+                                          globs=globs))
+
+    return unittest.TestSuite(suite)
+
+def test_suite():
+    """returns the test suite"""
+    return doc_suite(current_dir)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/workflow/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,567 @@
+#
+# 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_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, MANAGE_CONTENT_PERMISSION, PUBLISH_CONTENT_PERMISSION, \
+    CREATE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContentRoles, IManagerRestrictions
+from pyams_content.workflow.interfaces import IContentWorkflow
+from pyams_security.interfaces import IRoleProtectedObject, ISecurityManager
+from pyams_workflow.interfaces import IWorkflow, AUTOMATIC, IWorkflowPublicationInfo, SYSTEM, IWorkflowVersions, \
+    IWorkflowState, ObjectClonedEvent, IWorkflowInfo, IWorkflowStateLabel
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import utility_config, get_utility
+from pyams_utils.request import check_request
+from pyams_workflow.workflow import Transition, Workflow
+from pyramid.threadlocal import get_current_registry
+from zope.copy import copy
+from zope.interface import implementer
+from zope.location import locate
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+from pyams_content import _
+
+
+#
+# Workflow states
+#
+
+DRAFT = 'draft'
+PROPOSED = 'proposed'
+CANCELED = 'canceled'
+REFUSED = 'refused'
+PUBLISHED = 'published'
+RETIRING = 'retiring'
+RETIRED = 'retired'
+ARCHIVING = 'archiving'
+ARCHIVED = 'archived'
+DELETED = 'deleted'
+
+STATES_IDS = (DRAFT,
+              PROPOSED,
+              CANCELED,
+              REFUSED,
+              PUBLISHED,
+              RETIRING,
+              RETIRED,
+              ARCHIVING,
+              ARCHIVED,
+              DELETED)
+
+UPDATE_STATES = (DRAFT, RETIRED)
+
+READONLY_STATES = (ARCHIVED, DELETED)
+
+PROTECTED_STATES = (PUBLISHED, RETIRING, ARCHIVING)
+
+MANAGER_STATES = (PROPOSED, )
+
+VISIBLE_STATES = PUBLISHED_STATES = (PUBLISHED, RETIRING)
+
+WAITING_STATES = (PROPOSED, RETIRING, ARCHIVING)
+
+RETIRED_STATES = (RETIRED, ARCHIVING)
+
+STATES_LABELS = (_("Draft"),
+                 _("Proposed"),
+                 _("Canceled"),
+                 _("Refused"),
+                 _("Published"),
+                 _("Retiring"),
+                 _("Retired"),
+                 _("Archiving"),
+                 _("Archived"),
+                 _("Deleted"))
+
+STATES_HEADERS = {DRAFT: _("draft created by {principal}"),
+                  PROPOSED: _("publication requested by {principal}"),
+                  PUBLISHED: _("published by {principal}"),
+                  RETIRING: _("retiring requested by {principal}"),
+                  RETIRED: _("retired by {principal}"),
+                  ARCHIVING: _("archiving requested by {principal}"),
+                  ARCHIVED: _("archived by {principal}")}
+
+STATES_VOCABULARY = SimpleVocabulary([SimpleTerm(STATES_IDS[i], title=t)
+                                      for i, t in enumerate(STATES_LABELS)])
+
+
+#
+# Workflow conditions
+#
+
+def can_propose_content(wf, context):
+    """Check if a content can be proposed"""
+    versions = IWorkflowVersions(context)
+    if versions.has_version(PROPOSED):
+        return False
+    request = check_request()
+    if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
+        return True
+    if request.principal.id in context.owner | {context.creator} | context.contributors:
+        return True
+    return False
+
+
+def can_backdraft_content(wf, context):
+    """Check if content can return to DRAFT state"""
+    return IWorkflowPublicationInfo(context).publication_date is None
+
+
+def can_retire_content(wf, context):
+    """Check if already published content can return to RETIRED state"""
+    return IWorkflowPublicationInfo(context).publication_date is not None
+
+
+def can_create_new_version(wf, context):
+    """Check if we can create a new version"""
+    versions = IWorkflowVersions(context)
+    if (versions.has_version(DRAFT) or
+        versions.has_version(PROPOSED) or
+        versions.has_version(CANCELED) or
+        versions.has_version(REFUSED)):
+        return False
+    request = check_request()
+    if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
+        return True
+    if request.principal.id in context.owner | {context.creator} | context.contributors:
+        return True
+    return False
+
+
+def can_delete_version(wf, context):
+    """Check if we can delete a draft version"""
+    request = check_request()
+    if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
+        return True
+    return request.principal.id in context.owner | {context.creator} | context.contributors
+
+
+def can_manage_content(wf, context):
+    """Check if a manager can handle content"""
+    request = check_request()
+    if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
+        return True
+    if request.principal.id in context.managers:
+        return True
+    restrictions = IManagerRestrictions(context).get_restrictions(request.principal.id)
+    return restrictions and restrictions.check_access(context, permission=PUBLISH_CONTENT_PERMISSION, request=request)
+
+
+def can_cancel_operation(wf, context):
+    """Check if we can cancel a request"""
+    request = check_request()
+    if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context):
+        return True
+    if request.principal.id in context.owner | {context.creator} | context.contributors:
+        return True
+    return request.principal.id == IWorkflowState(context).state_principal
+
+
+#
+# Workflow actions
+#
+
+def publish_action(wf, context):
+    """Publish version"""
+    IWorkflowPublicationInfo(context).publication_date = datetime.utcnow()
+    translate = check_request().localizer.translate
+    version_id = IWorkflowState(context).version_id
+    for version in IWorkflowVersions(context).get_versions((PUBLISHED, RETIRING, RETIRED, ARCHIVING)):
+        if version is not context:
+            IWorkflowInfo(version).fire_transition_toward('archived',
+                                                          comment=translate(_("Published version {0}")).format(version_id))
+
+
+def archive_action(wf, context):
+    """Remove readers when a content is archived"""
+    roles = IWfSharedContentRoles(context, None)
+    if roles is not None:
+        IRoleProtectedObject(context).revoke_role('pyams.Reader', roles.readers)
+
+
+def clone_action(wf, context):
+    """Create new version"""
+    result = copy(context)
+    locate(result, context.__parent__)
+    registry = get_current_registry()
+    registry.notify(ObjectClonedEvent(result, context))
+    return result
+
+
+def delete_action(wf, context):
+    """Delete draft version, and parent if single version"""
+    versions = IWorkflowVersions(context)
+    versions.remove_version(IWorkflowState(context).version_id)
+
+
+#
+# Workflow transitions
+#
+
+init = Transition(transition_id='init',
+                  title=_("Initialize"),
+                  source=None,
+                  destination=DRAFT,
+                  history_label=_("Draft creation"))
+
+draft_to_proposed = Transition(transition_id='draft_to_proposed',
+                               title=_("Propose publication"),
+                               source=DRAFT,
+                               destination=PROPOSED,
+                               permission=MANAGE_CONTENT_PERMISSION,
+                               condition=can_propose_content,
+                               menu_css_class='fa fa-fw fa-question',
+                               view_name='wf-propose.html',
+                               history_label=_("Publication request"),
+                               next_step=_("content managers authorized to take charge of your content are going to "
+                                           "be notified of your request."),
+                               order=1)
+
+retired_to_proposed = Transition(transition_id='retired_to_proposed',
+                                 title=_("Propose publication"),
+                                 source=RETIRED,
+                                 destination=PROPOSED,
+                                 permission=MANAGE_CONTENT_PERMISSION,
+                                 condition=can_propose_content,
+                                 menu_css_class='fa fa-fw fa-question',
+                                 view_name='wf-propose.html',
+                                 history_label=_("Publication request"),
+                                 next_step=_("content managers authorized to take charge of your content are going to "
+                                             "be notified of your request."),
+                                 order=1)
+
+proposed_to_canceled = Transition(transition_id='proposed_to_canceled',
+                                  title=_("Cancel publication request"),
+                                  source=PROPOSED,
+                                  destination=CANCELED,
+                                  permission=MANAGE_CONTENT_PERMISSION,
+                                  condition=can_cancel_operation,
+                                  menu_css_class='fa fa-fw fa-mail-reply',
+                                  view_name='wf-cancel-propose.html',
+                                  history_label=_("Publication request canceled"),
+                                  order=2)
+
+canceled_to_draft = Transition(transition_id='canceled_to_draft',
+                               title=_("Reset canceled publication to draft"),
+                               source=CANCELED,
+                               destination=DRAFT,
+                               trigger=AUTOMATIC,
+                               history_label=_("State reset to 'draft' (automatic)"),
+                               condition=can_backdraft_content)
+
+canceled_to_retired = Transition(transition_id='canceled_to_retired',
+                                 title=_("Reset canceled publication to retired"),
+                                 source=CANCELED,
+                                 destination=RETIRED,
+                                 trigger=AUTOMATIC,
+                                 history_label=_("State reset to 'retired' (automatic)"),
+                                 condition=can_retire_content)
+
+proposed_to_refused = Transition(transition_id='proposed_to_refused',
+                                 title=_("Refuse publication"),
+                                 source=PROPOSED,
+                                 destination=REFUSED,
+                                 permission=PUBLISH_CONTENT_PERMISSION,
+                                 condition=can_manage_content,
+                                 menu_css_class='fa fa-fw fa-thumbs-o-down',
+                                 view_name='wf-refuse.html',
+                                 history_label=_("Publication refused"),
+                                 order=3)
+
+refused_to_draft = Transition(transition_id='refused_to_draft',
+                              title=_("Reset refused publication to draft"),
+                              source=REFUSED,
+                              destination=DRAFT,
+                              trigger=AUTOMATIC,
+                              history_label=_("State reset to 'draft' (automatic)"),
+                              condition=can_backdraft_content)
+
+refused_to_retired = Transition(transition_id='refused_to_retired',
+                                title=_("Reset refused publication to retired"),
+                                source=REFUSED,
+                                destination=RETIRED,
+                                trigger=AUTOMATIC,
+                                history_label=_("State reset to 'refused' (automatic)"),
+                                condition=can_retire_content)
+
+proposed_to_published = Transition(transition_id='proposed_to_published',
+                                   title=_("Publish content"),
+                                   source=PROPOSED,
+                                   destination=PUBLISHED,
+                                   permission=PUBLISH_CONTENT_PERMISSION,
+                                   condition=can_manage_content,
+                                   action=publish_action,
+                                   menu_css_class='fa fa-fw fa-thumbs-o-up',
+                                   view_name='wf-publish.html',
+                                   history_label=_("Content published"),
+                                   order=4)
+
+published_to_retiring = Transition(transition_id='published_to_retiring',
+                                   title=_("Request retiring"),
+                                   source=PUBLISHED,
+                                   destination=RETIRING,
+                                   permission=MANAGE_CONTENT_PERMISSION,
+                                   menu_css_class='fa fa-fw fa-pause',
+                                   view_name='wf-retiring.html',
+                                   history_label=_("Retire request"),
+                                   next_step=_("content managers authorized to take charge of your content are going "
+                                               "to be notified of your request."),
+                                   order=7)
+
+retiring_to_published = Transition(transition_id='retiring_to_published',
+                                   title=_("Cancel retiring request"),
+                                   source=RETIRING,
+                                   destination=PUBLISHED,
+                                   permission=MANAGE_CONTENT_PERMISSION,
+                                   condition=can_cancel_operation,
+                                   menu_css_class='fa fa-fw fa-mail-reply',
+                                   view_name='wf-cancel-retiring.html',
+                                   history_label=_("Retire request canceled"),
+                                   order=8)
+
+retiring_to_retired = Transition(transition_id='retiring_to_retired',
+                                 title=_("Retire content"),
+                                 source=RETIRING,
+                                 destination=RETIRED,
+                                 permission=PUBLISH_CONTENT_PERMISSION,
+                                 condition=can_manage_content,
+                                 menu_css_class='fa fa-fw fa-stop',
+                                 view_name='wf-retire.html',
+                                 history_label=_("Content retired"),
+                                 order=9)
+
+retired_to_archiving = Transition(transition_id='retired_to_archiving',
+                                  title=_("Request archive"),
+                                  source=RETIRED,
+                                  destination=ARCHIVING,
+                                  permission=MANAGE_CONTENT_PERMISSION,
+                                  menu_css_class='fa fa-fw fa-archive',
+                                  view_name='wf-archiving.html',
+                                  history_label=_("Archive request"),
+                                  next_step=_("content managers authorized to take charge of your content are going to "
+                                              "be notified of your request."),
+                                  order=10)
+
+archiving_to_retired = Transition(transition_id='archiving_to_retired',
+                                  title=_("Cancel archiving request"),
+                                  source=ARCHIVING,
+                                  destination=RETIRED,
+                                  permission=MANAGE_CONTENT_PERMISSION,
+                                  condition=can_cancel_operation,
+                                  menu_css_class='fa fa-fw fa-mail-reply',
+                                  view_name='wf-cancel-archiving.html',
+                                  history_label=_("Archive request canceled"),
+                                  order=11)
+
+archiving_to_archived = Transition(transition_id='archiving_to_archived',
+                                   title=_("Archive content"),
+                                   source=ARCHIVING,
+                                   destination=ARCHIVED,
+                                   permission=PUBLISH_CONTENT_PERMISSION,
+                                   condition=can_manage_content,
+                                   action=archive_action,
+                                   menu_css_class='fa fa-fw fa-archive',
+                                   view_name='wf-archive.html',
+                                   history_label=_("Content archived"),
+                                   order=12)
+
+published_to_archived = Transition(transition_id='published_to_archived',
+                                   title=_("Archive published content"),
+                                   source=PUBLISHED,
+                                   destination=ARCHIVED,
+                                   trigger=SYSTEM,
+                                   history_label=_("Content archived after version publication"),
+                                   action=archive_action)
+
+retiring_to_archived = Transition(transition_id='retiring_to_archived',
+                                  title=_("Archive retiring content"),
+                                  source=RETIRING,
+                                  destination=ARCHIVED,
+                                  trigger=SYSTEM,
+                                  history_label=_("Content archived after version publication"),
+                                  action=archive_action)
+
+retired_to_archived = Transition(transition_id='retired_to_archived',
+                                 title=_("Archive retired content"),
+                                 source=RETIRED,
+                                 destination=ARCHIVED,
+                                 trigger=SYSTEM,
+                                 history_label=_("Content archived after version publication"),
+                                 action=archive_action)
+
+published_to_draft = Transition(transition_id='published_to_draft',
+                                title=_("Create new version"),
+                                source=PUBLISHED,
+                                destination=DRAFT,
+                                permission=CREATE_CONTENT_PERMISSION,
+                                condition=can_create_new_version,
+                                action=clone_action,
+                                menu_css_class='fa fa-fw fa-file-o',
+                                view_name='wf-clone.html',
+                                history_label=_("New version created"),
+                                order=13)
+
+retiring_to_draft = Transition(transition_id='retiring_to_draft',
+                               title=_("Create new version"),
+                               source=RETIRING,
+                               destination=DRAFT,
+                               permission=CREATE_CONTENT_PERMISSION,
+                               condition=can_create_new_version,
+                               action=clone_action,
+                               menu_css_class='fa fa-fw fa-file-o',
+                               view_name='wf-clone.html',
+                               history_label=_("New version created"),
+                               order=14)
+
+retired_to_draft = Transition(transition_id='retired_to_draft',
+                              title=_("Create new version"),
+                              source=RETIRED,
+                              destination=DRAFT,
+                              permission=CREATE_CONTENT_PERMISSION,
+                              condition=can_create_new_version,
+                              action=clone_action,
+                              menu_css_class='fa fa-fw fa-file-o',
+                              view_name='wf-clone.html',
+                              history_label=_("New version created"),
+                              order=15)
+
+archiving_to_draft = Transition(transition_id='archiving_to_draft',
+                                title=_("Create new version"),
+                                source=ARCHIVING,
+                                destination=DRAFT,
+                                permission=CREATE_CONTENT_PERMISSION,
+                                condition=can_create_new_version,
+                                action=clone_action,
+                                menu_css_class='fa fa-fw fa-file-o',
+                                view_name='wf-clone.html',
+                                history_label=_("New version created"),
+                                order=16)
+
+archived_to_draft = Transition(transition_id='archived_to_draft',
+                               title=_("Create new version"),
+                               source=ARCHIVED,
+                               destination=DRAFT,
+                               permission=CREATE_CONTENT_PERMISSION,
+                               condition=can_create_new_version,
+                               action=clone_action,
+                               menu_css_class='fa fa-fw fa-file-o',
+                               view_name='wf-clone.html',
+                               history_label=_("New version created"),
+                               order=17)
+
+delete = Transition(transition_id='delete',
+                    title=_("Delete version"),
+                    source=DRAFT,
+                    destination=DELETED,
+                    permission=MANAGE_CONTENT_PERMISSION,
+                    condition=can_delete_version,
+                    action=delete_action,
+                    menu_css_class='fa fa-fw fa-trash',
+                    view_name='wf-delete.html',
+                    history_label=_("Version deleted"),
+                    order=18)
+
+wf_transitions = [init,
+                  draft_to_proposed,
+                  retired_to_proposed,
+                  proposed_to_canceled,
+                  canceled_to_draft,
+                  canceled_to_retired,
+                  proposed_to_refused,
+                  refused_to_draft,
+                  refused_to_retired,
+                  proposed_to_published,
+                  published_to_retiring,
+                  retiring_to_published,
+                  retiring_to_retired,
+                  retired_to_archiving,
+                  archiving_to_retired,
+                  published_to_archived,
+                  retiring_to_archived,
+                  retired_to_archived,
+                  archiving_to_archived,
+                  published_to_draft,
+                  retiring_to_draft,
+                  retired_to_draft,
+                  archiving_to_draft,
+                  archived_to_draft,
+                  delete]
+
+
+@implementer(IContentWorkflow)
+class ContentWorkflow(Workflow):
+    """PyAMS default content workflow"""
+
+
+@adapter_config(context=IContentWorkflow, provides=IWorkflowStateLabel)
+class WorkflowStateLabelAdapter(ContextAdapter):
+    """Generic state label adapter"""
+
+    @staticmethod
+    def get_label(content, request=None, format=True):
+        if request is None:
+            request = check_request()
+        translate = request.localizer.translate
+        security = get_utility(ISecurityManager)
+        state = IWorkflowState(content)
+        state_label = translate(STATES_HEADERS[state.state])
+        if format:
+            state_label = state_label.format(principal=security.get_principal(state.state_principal).title)
+        return state_label
+
+
+@adapter_config(name=DRAFT, context=IContentWorkflow, provides=IWorkflowStateLabel)
+class DraftWorkflowStateLabelAdapter(ContextAdapter):
+    """Draft state label adapter"""
+
+    @staticmethod
+    def get_label(content, request=None, format=True):
+        if request is None:
+            request = check_request()
+        translate = request.localizer.translate
+        security = get_utility(ISecurityManager)
+        state = IWorkflowState(content)
+        if len(state.history) == 1:
+            state_label = translate(STATES_HEADERS[state.state])
+        else:
+            state_label = translate(_('publication refused by {principal}'))
+        if format:
+            state_label = state_label.format(principal=security.get_principal(state.state_principal).title)
+        return state_label
+
+
+wf = ContentWorkflow(wf_transitions,
+                     states=STATES_VOCABULARY,
+                     initial_state=DRAFT,
+                     update_states=UPDATE_STATES,
+                     readonly_states=READONLY_STATES,
+                     protected_states=PROTECTED_STATES,
+                     manager_states=MANAGER_STATES,
+                     published_states=VISIBLE_STATES,
+                     waiting_states=WAITING_STATES,
+                     retired_states=RETIRED_STATES)
+
+
+@utility_config(name='PyAMS default workflow', provides=IWorkflow)
+class WorkflowUtility(object):
+    """PyAMS default workflow utility"""
+
+    def __new__(cls):
+        return wf
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/workflow/interfaces.py	Thu Oct 08 13:37:29 2015 +0200
@@ -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_workflow.interfaces import IWorkflow
+
+# import packages
+
+
+class IContentWorkflow(IWorkflow):
+    """PyAMS default content workflow marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/zmi/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/zmi/interfaces/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,37 @@
+#
+# 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_skin.interfaces.viewlet import IMenuItem
+
+# import packages
+
+
+class IDashboardMenu(IMenuItem):
+    """Dashboard menu"""
+
+
+class IMyDashboardMenu(IMenuItem):
+    """My contents dashboard menu"""
+
+
+class IAllContentsMenu(IMenuItem):
+    """Dashboard menu for all contents"""
+
+
+class ISummaryMenu(IMenuItem):
+    """Summary menu"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/zmi/tinymce.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_form.interfaces.form import IForm
+from pyams_skin.interfaces.tinymce import ITinyMCEConfiguration
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter
+
+
+@adapter_config(context=(IForm, IPyAMSLayer), provides=ITinyMCEConfiguration)
+class TinyMCEEditorConfiguration(ContextRequestAdapter):
+    """TinyMCE editor configuration"""
+
+    @property
+    def configuration(self):
+        return {'ams-plugins': 'pyams_content',
+                'ams-plugin-pyams_content-src': '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
+                'ams-plugin-pyams_content-async': 'false',
+                'ams-tinymce-init-callback': 'PyAMS_content.TinyMCE.initEditor'}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/zmi/viewlet/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/zmi/viewlet/toplinks/__init__.py	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,100 @@
+#
+# 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_template.template import template_config
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from hypatia.interfaces import ICatalog
+from pyams_content.shared.common.interfaces import ISharedTool
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import ITopLinksViewletManager
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from hypatia.catalog import CatalogQuery
+from hypatia.query import Any, And, Or, NotEq
+from pyams_catalog.query import CatalogResultSet
+from pyams_skin.viewlet.toplinks import TopLinksViewlet, TopLinksMenu
+from pyams_utils.list import unique
+from pyams_utils.registry import get_local_registry, get_utility
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+
+from pyams_content import _
+
+
+@viewlet_config(name='shared-tools.menu', layer=IAdminLayer, manager=ITopLinksViewletManager, weight=30)
+class SharedToolsMenu(TopLinksViewlet):
+    """Shared tools menu"""
+
+    label = ''
+    css_class = 'top-menu bordered margin-top-10'
+    dropdown_label = _("Shared contents")
+
+    def update(self):
+        super(SharedToolsMenu, self).update()
+        registry = get_local_registry()
+        for name, tool in registry.getUtilitiesFor(ISharedTool):
+            menu = TopLinksMenu(self.context, self.request, self.__parent__, self)
+            menu.label = II18n(tool).query_attribute('short_name', request=self.request) or tool.__name__
+            menu.url = absolute_url(tool, self.request, 'admin.html#dashboard.html')
+            self.viewlets.append(menu)
+
+
+@viewlet_config(name='user-roles.menu', layer=IAdminLayer, manager=ITopLinksViewletManager, weight=90)
+class UserRolesMenu(TopLinksViewlet):
+    """User roles menu"""
+
+    label = ''
+    css_class = 'top-menu bordered margin-top-10'
+    dropdown_label = _("My roles")
+
+    def update(self):
+        super(UserRolesMenu, self).update()
+        catalog = get_utility(ICatalog)
+        params = And(Or(Any(catalog['role:contributor'], {self.request.principal.id}),
+                        Any(catalog['role:manager'], {self.request.principal.id}),
+                        Any(catalog['role:pilot'], {self.request.principal.id})),
+                     NotEq(catalog['content_type'], None))
+        for tool in sorted(unique(CatalogResultSet(CatalogQuery(catalog).query(params))),
+                           key=lambda x: II18n(x).query_attribute('title', request=self.request)):
+            menu = TopLinksMenu(self.context, self.request, self.__parent__, self)
+            menu.label = II18n(tool).query_attribute('title', request=self.request) or tool.__name__
+            menu.url = absolute_url(tool, self.request, 'admin.html#dashboard.html')
+            self.viewlets.append(menu)
+
+
+@viewlet_config(name='user-addings.menu', layer=IAdminLayer, manager=ITopLinksViewletManager, weight=95)
+@template_config(template='templates/user-addings.pt', layer=IAdminLayer)
+class UserAddingsMenu(TopLinksViewlet):
+    """User addings menu"""
+
+    label = ''
+    css_class = 'top-menu margin-top-5'
+    dropdown_label = ''
+
+    def update(self):
+        super(UserAddingsMenu, self).update()
+        catalog = get_utility(ICatalog)
+        params = And(Any(catalog['role:contributor'], {self.request.principal.id}),
+                     NotEq(catalog['content_type'], None))
+        for tool in sorted(unique(CatalogResultSet(CatalogQuery(catalog).query(params))),
+                           key=lambda x: II18n(x).query_attribute('title', request=self.request)):
+            menu = TopLinksMenu(self.context, self.request, self.__parent__, self)
+            menu.label = self.request.localizer.translate(tool.shared_content_factory.content_class.content_name)
+            menu.url = absolute_url(tool, self.request, 'add-shared-content.html')
+            menu.data = {'data-toggle': 'modal'}
+            self.viewlets.append(menu)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/zmi/viewlet/toplinks/templates/user-addings.pt	Thu Oct 08 13:37:29 2015 +0200
@@ -0,0 +1,14 @@
+<div class="top-menu" tal:attributes="class view.css_class" i18n:domain="pyams_content">
+	<span class="label" tal:content="view.label | default">Label:</span>
+	<span class="top-selector" class="popover-trigger-element dropdown-toggle"
+		  data-toggle="dropdown">
+		<i class="fa fa-2x fa-plus-square text-success opaque hint"
+		   data-ams-hint-gravity="w"
+		   title="Create new content" i18n:attributes="title"></i>
+	</span>
+	<ul class="dropdown-menu">
+		<tal:loop repeat="menu view.viewlets">
+			<tal:li replace="structure menu.render()" />
+		</tal:loop>
+	</ul>
+</div>