# HG changeset patch # User Thierry Florac # Date 1444304249 -7200 # Node ID 7c0001cacf8e46dee487de05e54ea02793ebd96c Version 0.1.0 diff -r 000000000000 -r 7c0001cacf8e .hgignore --- /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$ diff -r 000000000000 -r 7c0001cacf8e LICENSE --- /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. diff -r 000000000000 -r 7c0001cacf8e MANIFEST.in --- /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 *.*~ diff -r 000000000000 -r 7c0001cacf8e bootstrap.py --- /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) diff -r 000000000000 -r 7c0001cacf8e buildout.cfg --- /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 diff -r 000000000000 -r 7c0001cacf8e docs/HISTORY.txt --- /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 diff -r 000000000000 -r 7c0001cacf8e docs/README.txt diff -r 000000000000 -r 7c0001cacf8e setup.py --- /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 +# 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' + ] + }) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/PKG-INFO --- /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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/SOURCES.txt --- /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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/dependency_links.txt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/entry_points.txt --- /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 + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/namespace_packages.txt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/not-zip-safe --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/requires.txt --- /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] diff -r 000000000000 -r 7c0001cacf8e src/pyams_content.egg-info/top_level.txt --- /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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/__init__.py --- /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 +# 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'}}) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/__init__.py --- /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 +# 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")) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/container.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/zmi/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/zmi/container.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/zmi/templates/container.pt --- /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 @@ +
+
+ + +

+ + +
+
+ + + + +
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/zmi/templates/widget-display.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/zmi/templates/widget-input.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/extfile/zmi/widget.py --- /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 +# 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)) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/__init__.py --- /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 +# 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)) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/container.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/container.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/gallery.py --- /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 +# 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'} diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/interfaces.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/templates/container.pt --- /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 @@ +
+
+ + +

+ + +
+
+ +
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/templates/gallery-images.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/templates/widget-display.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/templates/widget-input.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/gallery/zmi/widget.py --- /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 +# 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)) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/illustration/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/container.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/container.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/reverse.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/templates/container.pt --- /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 @@ +
+
+ + +

+ + +
+
+ +
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/templates/widget-display.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/templates/widget-input.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/links/zmi/widget.py --- /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 +# 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)) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/__init__.py --- /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 +# 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)) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/container.py --- /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 +# 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() diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/html.py --- /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 +# 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']) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/illustration.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/interfaces/__init__.py --- /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 +# 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') diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/container.py --- /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 +# 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'} diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/html.py --- /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 +# 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() diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/illustration.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/summary.py --- /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 +# 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() diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/templates/container.pt --- /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 @@ +
+
+ + +

+ + +
+
+ +
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/templates/html-summary.pt --- /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 @@ +

title

+
body
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/templates/illustration-left.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/templates/illustration-right.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/templates/illustration.pt --- /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 @@ +
+
+ legend +
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/paragraph/zmi/templates/summary.pt --- /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 @@ + + +
+ +

This content doesn't contain any paragraph.

+
+
+ + +
+ +
+ +

This content doesn't contain any paragraph.

+
+
+
+
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/theme/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/theme/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/theme/zmi/__init__.py --- /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 +# 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' diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/theme/zmi/manager.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/component/theme/zmi/templates/themes-info.pt --- /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 @@ +
+
+ + +

+ + +
+
+
Form prefix
+ +
+ +
+ +
+
+
Form suffix
+
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/configure.zcml --- /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 @@ + + + + + + + + + + + + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/doctests/README.txt --- /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 +===================== diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/generations/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/include.py --- /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 +# 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') diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/interfaces/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/interfaces/container.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo Binary file src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.mo has changed diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/locales/fr/LC_MESSAGES/pyams_content.po --- /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 , 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 \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. 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é." +#~ "
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. The new copy " +#~ "is going to be created in 'draft' mode, so that you can modify it before " +#~ "publication. 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é.
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})" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/locales/pyams_content.pot --- /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 , 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 \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 "" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/profile/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/profile/admin.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/profile/interfaces/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/profile/zmi/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/root/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/root/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/root/zmi/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/root/zmi/search.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/root/zmi/templates/dashboard.pt --- /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 @@ + + + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/interfaces/templates/summary-layout.pt --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/interfaces/zmi.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/manager.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/security.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/__init__.py --- /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 +# 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 += ''.format(translate(_("Created or modified in this version"))) + return value diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/dashboard.py --- /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 +# 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") diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/header.py --- /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 +# 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}', + '{{principal}}'.format(principal_class)) + state_class = 'text-danger' if state.state in workflow.update_states else None + if state_class: + self.state = '{state}'.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 '' diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/manager.py --- /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 +# 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' diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/owner.py --- /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 +# 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' diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/properties.py --- /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 +# 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' diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/search.py --- /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 +# 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() diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/security.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/summary.py --- /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 +# 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)) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/advanced-search.pt --- /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 @@ +
+
+ + +

+ +
+
+
Form prefix
+ +
+ +
+ +
+
+
Form suffix
+
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/dashboard.pt --- /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 @@ +
+
+ + +

Title

+ + +
+
+
+
+
+ + Advanced search... +
+
+
+
+ + + +
+
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/header.pt --- /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 @@ + +

+ + + + + + + +

+
+ OID | + Title | + by
+ Version | + state | + state date + | + link + +
+
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-archive-message.pt --- /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 @@ +

+ As a manager, you considerate that this content must be archived.
+ After archiving, it will be backed up but you will not be able to publish it again except + by creating a new version. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-archiving-message.pt --- /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 @@ +

+ This content is already retired and not visible.
+ After archiving, it will be backed up but you will not be able to publish it again except + by creating a new version. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-cancel-archiving-message.pt --- /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 @@ +

+ After cancelling this request, the content will return to it's previous retired state. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-cancel-propose-message.pt --- /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 @@ +

+ After canceling the request, you will be able to update the content again. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-cancel-retiring-message.pt --- /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 @@ +

+ After cancelling this request, the content will return to it's normal published state. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-clone-message.pt --- /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 @@ +

+ You considerate that the currently published must evolve.
+ By creating a new version, you can update it's content without impacting the currently + published one.
+ 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). +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-create-message.pt --- /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 @@ +

+ This new content is going to be created in 'draft' mode, so that you can complete it + before publication.
+ A unique number is also going to be assigned to it. This number will be shared by all + content's versions. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-delete-message.pt --- /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 @@ +

+ The content version is going to be definitely deleted. Will only remain the currently published version. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-duplicate-message.pt --- /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 @@ +

+ You are going to duplicate a whole content.
+ The new copy is going to be created in 'draft' mode, so that you can modify it before + publication.
+ A new unique number is also going to be assigned to it. This number will be shared by all + content's versions. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-operator-warning.pt --- /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 @@ +
+ WARNING: this request was made by a contributor which is not the owner of this content. +
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-owner-warning.pt --- /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 @@ +
+ RECALL: you are not the owner of the content on which you are intervening. +
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-propose-message.pt --- /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 @@ +

+ This publication request is going to be transmitted to a content manager. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-publish-message.pt --- /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 @@ +

+ As a manager, you considerate that this content is complete and can be published 'as is'. +
+ This operation will make the content publicly available (except if restricted access has + been set). +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-refuse-propose-message.pt --- /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 @@ +

+ As a content manager, you considerate that this content can't be published 'as is'. +
+ The contributor will be notified of this and will be able to update the content before + doing a new publication request. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-retire-message.pt --- /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 @@ +

+ As a content manager, you considerate that this content should no longer be published. +
+ Retired content won't be visible anymore, but it can be updated and published again, or + archived. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-retiring-message.pt --- /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 @@ +

+ You considerate that the currently published version should no more be publicly visible.
+ WARNING: the content will remain visible until a manager validate the request. +

diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/templates/wf-transition-info.pt --- /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 @@ +
+ FOR YOUR INFORMATION
+ Previous step: + +
+ With this comment: +

+
+
+
+ + Next step: + +
diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/common/zmi/workflow.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/news/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/news/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/news/manager.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/news/zmi/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/shared/news/zmi/properties.py --- /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 +# 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') diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/site.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/site/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/site/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/__init__.py --- /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 +# 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') diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/resources/js/pyams_content.js --- /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'); + $('').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'); + $('').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'); + $('').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: '  ' + 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() { + $('').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); diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/resources/js/pyams_content.min.js --- /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("").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("").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("").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:'  '+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("").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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.js --- /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" +}); diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/resources/js/tinymce/onflinks/langs/fr.min.js --- /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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.js --- /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('' + e.data.title + ''); + } + }); + } + }) +}); diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/resources/js/tinymce/onflinks/plugin.min.js --- /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(''+c.data.title+"")}})}})}); \ No newline at end of file diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/skin/routes.py --- /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 +# 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() diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/tests/__init__.py --- /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 @@ + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/tests/test_utilsdocs.py --- /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 +# 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') + diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/tests/test_utilsdocstrings.py --- /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 +# 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') diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/workflow/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/workflow/interfaces.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/zmi/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/zmi/interfaces/__init__.py --- /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 +# 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""" diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/zmi/tinymce.py --- /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 +# 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'} diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/zmi/viewlet/__init__.py --- /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 +# 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 diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/zmi/viewlet/toplinks/__init__.py --- /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 +# 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) diff -r 000000000000 -r 7c0001cacf8e src/pyams_content/zmi/viewlet/toplinks/templates/user-addings.pt --- /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 @@ +
+ Label: + + + + +