--- /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> ' + 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> '+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>