# HG changeset patch # User Thierry Florac # Date 1426604835 -3600 # Node ID 73acbfc1357786bb1675ed981751cd3d90597e13 First commit diff -r 000000000000 -r 73acbfc13577 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Tue Mar 17 16:07:15 2015 +0100 @@ -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 73acbfc13577 .installed.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.installed.cfg Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,79 @@ +[buildout] +installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs/pyams-security.egg-link +parts = package i18n pyflakes test + +[package] +__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pyams_upgrade + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pshell + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pserve + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/prequest + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/proutes + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pcreate + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pdistreport + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/ptweens + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pviews +__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.0-py3.4.egg zc.buildout-2.3.1-py3.4.egg +_b = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +_d = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +eggs = pyams_form + pyams_pagelet + pyams_security + pyams_skin + pyams_template + pyams_utils + pyams_viewlet + pyams_workflow + pyramid + zope.component + zope.interface +eggs-directory = /var/local/env/pyams/eggs +recipe = zc.recipe.egg + +[i18n] +__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pybabel + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pot-create + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/polint +__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.0-py3.4.egg zc.buildout-2.3.1-py3.4.egg +_b = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +_d = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +eggs = babel + lingua +eggs-directory = /var/local/env/pyams/eggs +recipe = zc.recipe.egg + +[pyflakes] +__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pyflakes + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/pyflakes +__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.0-py3.4.egg zc.buildout-2.3.1-py3.4.egg +_b = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +_d = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +eggs = pyflakes +eggs-directory = /var/local/env/pyams/eggs +entry-points = pyflakes=pyflakes.scripts.pyflakes:main +initialization = if not sys.argv[1:]: sys.argv[1:] = ["src"] +recipe = zc.recipe.egg +scripts = pyflakes + +[test] +__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/parts/test + /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/test +__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.0-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-1482e89f68d85eea27f4ed7749df2819 +_b = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +_d = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +_e = /var/local/env/pyams/eggs +bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin +develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/develop-eggs +eggs = pyams_workflow [test] +eggs-directory = /var/local/env/pyams/eggs +location = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/parts/test +recipe = zc.recipe.testrunner +script = /home/tflorac/Dropbox/src/PyAMS/pyams_workflow/bin/test diff -r 000000000000 -r 73acbfc13577 LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Tue Mar 17 16:07:15 2015 +0100 @@ -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 73acbfc13577 MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,5 @@ +include *.txt +recursive-include docs * +recursive-include src * +global-exclude *.pyc +global-exclude *.*~ diff -r 000000000000 -r 73acbfc13577 bootstrap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bootstrap.py Tue Mar 17 16:07:15 2015 +0100 @@ -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 73acbfc13577 buildout.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/buildout.cfg Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,76 @@ +[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 = . + ../pyams_form + ../pyams_mail + ../pyams_pagelet + ../pyams_security + ../pyams_skin + ../pyams_template + ../pyams_utils + ../pyams_viewlet + ../pyams_zmi + ../pyams_zmq + +parts = + package + i18n + pyflakes + test + +[package] +recipe = zc.recipe.egg +eggs = + pyams_form + pyams_pagelet + pyams_security + pyams_skin + pyams_template + pyams_utils + pyams_viewlet + pyams_workflow + pyramid + 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_workflow [test] + +[versions] +pyams_workflow = 0.1.0 diff -r 000000000000 -r 73acbfc13577 docs/HISTORY.txt diff -r 000000000000 -r 73acbfc13577 docs/README.txt diff -r 000000000000 -r 73acbfc13577 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Tue Mar 17 16:07:15 2015 +0100 @@ -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. +# + +""" +This module contains pyams_ 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_workflow', + version=version, + description="PyAMS workflow manager", + 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 workflow', + author='Thierry Florac', + author_email='tflorac@ulthar.net', + url='http://hg.ztfy.org/pyams/pyams_workflow', + 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_workflow.tests.test_utilsdocs.test_suite", + tests_require=tests_require, + extras_require=dict(test=tests_require), + install_requires=[ + 'setuptools', + # -*- Extra requirements: -*- + 'pyams_form', + 'pyams_pagelet', + 'pyams_security', + 'pyams_skin', + 'pyams_template', + 'pyams_utils', + 'pyams_viewlet', + 'pyramid', + 'zope.component', + 'zope.copy', + 'zope.interface', + ], + entry_points=""" + # -*- Entry points: -*- + """, + ) diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/PKG-INFO --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/PKG-INFO Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,18 @@ +Metadata-Version: 1.1 +Name: pyams-workflow +Version: 0.1.0 +Summary: PyAMS workflow manager +Home-page: http://hg.ztfy.org/pyams/pyams_workflow +Author: Thierry Florac +Author-email: tflorac@ulthar.net +License: ZPL +Description: + + +Keywords: Pyramid PyAMS workflow +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 73acbfc13577 src/pyams_workflow.egg-info/SOURCES.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/SOURCES.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,26 @@ +MANIFEST.in +setup.py +docs/HISTORY.txt +docs/README.txt +src/pyams_workflow/__init__.py +src/pyams_workflow/configure.zcml +src/pyams_workflow/content.py +src/pyams_workflow/include.py +src/pyams_workflow/versions.py +src/pyams_workflow/workflow.py +src/pyams_workflow.egg-info/PKG-INFO +src/pyams_workflow.egg-info/SOURCES.txt +src/pyams_workflow.egg-info/dependency_links.txt +src/pyams_workflow.egg-info/entry_points.txt +src/pyams_workflow.egg-info/namespace_packages.txt +src/pyams_workflow.egg-info/not-zip-safe +src/pyams_workflow.egg-info/requires.txt +src/pyams_workflow.egg-info/top_level.txt +src/pyams_workflow/doctests/README.txt +src/pyams_workflow/interfaces/__init__.py +src/pyams_workflow/tests/__init__.py +src/pyams_workflow/tests/test_utilsdocs.py +src/pyams_workflow/tests/test_utilsdocstrings.py +src/pyams_workflow/zmi/__init__.py +src/pyams_workflow/zmi/interfaces.py +src/pyams_workflow/zmi/workflow.py \ No newline at end of file diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/dependency_links.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/dependency_links.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/entry_points.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/entry_points.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,3 @@ + + # -*- Entry points: -*- + \ No newline at end of file diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/namespace_packages.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/namespace_packages.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/not-zip-safe --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/not-zip-safe Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/requires.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/requires.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,14 @@ +setuptools +pyams_form +pyams_pagelet +pyams_security +pyams_skin +pyams_template +pyams_utils +pyams_viewlet +pyramid +zope.component +zope.copy +zope.interface + +[test] diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow.egg-info/top_level.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow.egg-info/top_level.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,1 @@ +pyams_workflow diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/__init__.py Tue Mar 17 16:07:15 2015 +0100 @@ -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' + + +from pyramid.i18n import TranslationStringFactory +_ = TranslationStringFactory('pyams_workflow') + + +def includeme(config): + """Pyramid include""" + + from .include import include_package + include_package(config) diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/configure.zcml Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/content.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/content.py Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,159 @@ +# +# 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_security.interfaces import IPrincipalInfo +from pyams_workflow.interfaces import IWorkflowManagedContent, IWorkflowPublicationInfo, IWorkflow, IWorkflowVersions, \ + IWorkflowVersion, IWorkflowPublicationSupport +from zope.annotation.interfaces import IAnnotations + +# import packages +from persistent import Persistent +from pyams_utils.adapter import adapter_config +from pyams_utils.registry import query_utility +from pyams_utils.request import check_request +from pyams_utils.timezone import gmtime +from pyams_utils.traversing import get_parent +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(IWorkflowPublicationInfo) +class WorkflowContentPublicationInfo(Persistent, Contained): + """Workflow content info""" + + _state_date = FieldProperty(IWorkflowPublicationInfo['state_date']) + _state_principal = FieldProperty(IWorkflowPublicationInfo['state_principal']) + _publication_date = FieldProperty(IWorkflowPublicationInfo['publication_date']) + _first_publication_date = FieldProperty(IWorkflowPublicationInfo['first_publication_date']) + _publication_effective_date = FieldProperty(IWorkflowPublicationInfo['publication_effective_date']) + _publication_expiration_date = FieldProperty(IWorkflowPublicationInfo['publication_expiration_date']) + + @property + def state_date(self): + return self._state_date + + @state_date.setter + def state_date(self, value): + self._state_date = gmtime(value) + + @property + def state_principal(self): + return self._state_principal + + @state_principal.setter + def state_principal(self, value): + if IPrincipalInfo.providedBy(value): + value = value.id + self._state_principal = value + + @property + def publication_date(self): + return self._publication_date + + @publication_date.setter + def publication_date(self, value): + self._publication_date = gmtime(value) + + @property + def first_publication_date(self): + return self._first_publication_date + + @property + def publication_effective_date(self): + return self._publication_effective_date + + @publication_effective_date.setter + def publication_effective_date(self, value): + self._publication_effective_date = gmtime(value) + if value and ((self._first_publication_date is None) or + (self._first_publication_date > self._publication_effective_date)): + self._first_publication_date = self._publication_effective_date + + @property + def publication_expiration_date(self): + return self._publication_expiration_date + + @publication_expiration_date.setter + def publication_expiration_date(self, value): + self._publication_expiration_date = gmtime(value) + + def reset(self): + self._publication_date = None + self._first_publication_date = None + self._publication_effective_date = None + self._publication_expiration_date = None + + def is_published(self): + # check is parent is published + parent = get_parent(self.__parent__, IWorkflowPublicationSupport, allow_context=False) + if (parent is not None) and not IWorkflowPublicationInfo(parent).is_published(): + return False + # associated workflow? + wf_name = IWorkflowManagedContent(self.__parent__).workflow_name + if not wf_name: + return True + # published state(s) in workflow? + workflow = query_utility(IWorkflow, name=wf_name) + if (workflow is None) or not workflow.published_states: + return False + # check content versions + versions = IWorkflowVersions(self.__parent__) + if not (versions or versions.get_versions(workflow.published_states)): + return False + now = datetime.utcnow() + return (self.publication_effective_date is not None) and \ + (self.publication_effective_date <= now) and \ + ((self.publication_expiration_date is None) or + (self.publication_expiration_date >= now)) + + def is_visible(self, request=None): + # associated workflow? + wf_name = IWorkflowManagedContent(self.__parent__).workflow_name + if not wf_name: + return True + # check workflow? + workflow = query_utility(IWorkflow, name=wf_name) + if workflow is None: + return False + if workflow.view_permission: + if request is None: + request = check_request() + if not request.has_permission(workflow.view_permission, context=self.__parent__): + return False + return self.is_published() + + +WORKFLOW_CONTENT_KEY = 'pyams_workflow.content_info' + + +@adapter_config(context=IWorkflowPublicationSupport, provides=IWorkflowPublicationInfo) +def WorkflowContentPublicationInfoFactory(context): + """Workflow content info factory""" + annotations = IAnnotations(context) + info = annotations.get(WORKFLOW_CONTENT_KEY) + if info is None: + info = annotations[WORKFLOW_CONTENT_KEY] = WorkflowContentPublicationInfo() + registry = get_current_registry() + registry.notify(ObjectCreatedEvent(info)) + locate(info, context) + return info diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/doctests/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/doctests/README.txt Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,3 @@ +====================== +pyams_workflow package +====================== diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/include.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/include.py Tue Mar 17 16:07:15 2015 +0100 @@ -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_workflow:locales') + + # load registry components + try: + import pyams_zmi + except ImportError: + config.scan(ignore='pyams_workflow.zmi') + else: + config.scan() + + if hasattr(config, 'load_zcml'): + config.load_zcml('configure.zcml') diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/interfaces/__init__.py Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,356 @@ +# +# 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.interface.interfaces import IObjectEvent, ObjectEvent + +# import packages +from pyams_security.schema import Principal +from zope.interface import implementer, invariant, Interface, Attribute, Invalid +from zope.schema import Choice, Datetime, Set, TextLine, Text, List, Object, Int + +from pyams_workflow import _ + + +MANUAL = 0 +AUTOMATIC = 1 +SYSTEM = 2 + + +class InvalidTransitionError(Exception): + """Base transition error""" + + def __init__(self, source): + self.source = source + + def __str__(self): + return 'source: "%s"' % self.source + + +class NoTransitionAvailableError(InvalidTransitionError): + """Exception raised when there is not available transition""" + + def __init__(self, source, destination): + super(NoTransitionAvailableError, self).__init__(source) + self.destination = destination + + def __str__(self): + return 'source: "%s" destination: "%s"' % (self.source, self.destination) + + +class AmbiguousTransitionError(InvalidTransitionError): + """Exception raised when required transition is ambiguous""" + + def __init__(self, source, destination): + super(AmbiguousTransitionError, self).__init__(source) + self.destination = destination + + def __str__(self): + return 'source: "%s" destination: "%s"' % (self.source, self.destination) + + +class VersionError(Exception): + """Versions management error""" + + +class ConditionFailedError(Exception): + """Exception raised when transition condition failed""" + + +class IWorkflowTransitionEvent(IObjectEvent): + """Workflow transition event interface""" + + source = Attribute('Original state or None if initial state') + + destination = Attribute('New state') + + transition = Attribute('Transition that was fired or None if initial state') + + comment = Attribute('Comment that went with state transition') + + +@implementer(IWorkflowTransitionEvent) +class WorkflowTransitionEvent(ObjectEvent): + """Workflow transition event""" + + def __init__(self, object, source, destination, transition, comment): + super(WorkflowTransitionEvent, self).__init__(object) + self.source = source + self.destination = destination + self.transition = transition + self.comment = comment + + +class IWorkflowVersionTransitionEvent(IWorkflowTransitionEvent): + """Workflow version transition event interface""" + + old_object = Attribute('Old version of object') + + +@implementer(IWorkflowVersionTransitionEvent) +class WorkflowVersionTransitionEvent(WorkflowTransitionEvent): + """Workflow version transition event""" + + def __init__(self, object, old_object, source, destination, transition, comment): + super(WorkflowVersionTransitionEvent, self).__init__(object, source, destination, transition, comment) + self.old_object = old_object + + +class IWorkflow(Interface): + """Defines workflow in the form of transition objects. + + Defined as a utility. + """ + + published_states = Set(title="Published states") + + def initialize(self): + """Do any needed initialization. + + Such as initialization with the workflow versions system. + """ + + def refresh(self, transitions): + """Refresh workflow completely with new transitions.""" + + def get_transitions(self, source): + """Get all transitions from source""" + + def get_transition(self, source, transition_id): + """Get transition with transition_id given source state. + + If the transition is invalid from this source state, + an InvalidTransitionError is raised. + """ + + def get_transition_by_id(self, transition_id): + """Get transition with transition_id""" + + +class IWorkflowInfo(Interface): + """Get workflow info about workflowed object, and drive workflow. + + Defined as an adapter. + """ + + def set_initial_state(self, state, comment=None): + """Set initial state for the context object. + + Fires a transition event. + """ + + def fire_transition(self, transition_id, comment=None, side_effect=None, check_security=True): + """Fire a transition for the context object. + + There's an optional comment parameter that contains some + opaque object that offers a comment about the transition. + This is useful for manual transitions where users can motivate + their actions. + + There's also an optional side effect parameter which should + be a callable which receives the object undergoing the transition + as the parameter. This could do an editing action of the newly + transitioned workflow object before an actual transition event is + fired. + + If check_security is set to False, security is not checked + and an application can fire a transition no matter what the + user's permission is. + """ + + def fire_transition_toward(self, state, comment=None, side_effect=None, check_security=True): + """Fire transition toward state. + + Looks up a manual transition that will get to the indicated + state. + + If no such transition is possible, NoTransitionAvailableError will + be raised. + + If more than one manual transitions are possible, + AmbiguousTransitionError will be raised. + """ + + def fire_transition_for_versions(self, state, transition_id, comment=None): + """Fire a transition for all versions in a state""" + + def fire_automatic(self): + """Fire automatic transitions if possible by condition""" + + def has_version(self, state): + """Return true if a version exists in given state""" + + def get_manual_transition_ids(self): + """Returns list of valid manual transitions. + + These transitions have to have a condition that's True. + """ + + def get_manual_transition_ids_toward(self, state): + """Returns list of manual transitions towards state""" + + def get_automatic_transition_ids(self): + """Returns list of possible automatic transitions. + + Condition is not checked. + """ + + def has_automatic_transitions(self): + """Return true if there are possible automatic outgoing transitions. + + Condition is not checked. + """ + + +class IWorkflowStateHistoryItem(Interface): + """Workflow state history item""" + + date = Datetime(title="State change datetime", + required=True) + + source_version = Int(title="Source version ID", + required=False) + + source_state = TextLine(title="Transition source state", + required=False) + + target_state = TextLine(title="Transition target state", + required=True) + + transition = TextLine(title="Transition name", + required=True) + + principal = Principal(title="Transition principal", + required=False) + + comment = Text(title="Transition comment", + required=False) + + +class IWorkflowState(Interface): + """Store state on workflowed objects. + + Defined as an adapter. + """ + + version_id = Attribute("Version ID") + + state = Attribute("Version state") + + history = List(title="Workflow states history", + value_type=Object(schema=IWorkflowStateHistoryItem)) + + +class IWorkflowVersions(Interface): + """Interface to get information about versions of content in workflow""" + + last_version_id = Attribute("Last version ID") + + def get_version(self, version_id): + """Get version matching given id""" + + def get_versions(self, state=None): + """Get all versions of object known for this (optional) state""" + + def add_version(self, content, state): + """Return new unique version id""" + + def set_state(self, version_id, state): + """Set new state for given version""" + + def has_version(self, state): + """Return true if a version exists with the specific workflow state""" + + def remove_version(self, version_id): + """Remove version with given ID""" + + +class IWorkflowManagedContent(IAttributeAnnotatable): + """Workflow managed content""" + + content_class = Attribute("Content class") + + workflow_name = Choice(title=_("Workflow name"), + description=_("Name of workflow utility managing this content"), + required=True, + vocabulary='PyAMS workflows') + + view_permission = Choice(title=_("View permission"), + description=_("This permission will be required to display content"), + vocabulary='PyAMS permissions', + required=False) + + +class IWorkflowPublicationSupport(IAttributeAnnotatable): + """Workflow publication support""" + + +class IWorkflowVersion(IWorkflowPublicationSupport): + """Workflow content version marker interface""" + + +class IWorkflowPublicationInfo(Interface): + """Workflow content publication info""" + + state_date = Datetime(title=_("State date"), + description=_("Date at which the current state was applied"), + readonly=True) + + state_principal = Principal(title=_("State principal"), + description=_("ID of the principal which defined current state")) + + publication_date = Datetime(title=_("Publication date"), + description=_("Last date at which content was accepted for publication"), + required=False) + + first_publication_date = Datetime(title=_("First publication date"), + description=_("First date at which content was accepted for publication"), + required=False) + + publication_effective_date = Datetime(title=_("Publication start date"), + description=_("Date from which content will be visible"), + required=False) + + publication_expiration_date = Datetime(title=_("Publication end date"), + description=_("Date past which content will not be visible"), + required=False) + + @invariant + def check_expiration_date(self): + if self.publication_expiration_date is not None: + if self.publication_effective_date is None: + raise Invalid(_("Can't define publication end date without publication start date!")) + if self.publication_effective_date >= self.publication_expiration_date: + raise Invalid(_("Publication end date must be defined after publication start date!")) + + def reset(self): + """Reset all publication info (used by clone features)""" + + def is_published(self): + """Is the content published?""" + + def is_visible(self, request=None): + """Is the content visible?""" + + +class IWorkflowCommentInfo(Interface): + """Workflow comment info""" + + comment = Text(title=_("Comment"), + description=_("Comment associated with this operation"), + required=False) diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/tests/__init__.py Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/tests/test_utilsdocs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/tests/test_utilsdocs.py Tue Mar 17 16:07:15 2015 +0100 @@ -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_workflow 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 73acbfc13577 src/pyams_workflow/tests/test_utilsdocstrings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/tests/test_utilsdocstrings.py Tue Mar 17 16:07:15 2015 +0100 @@ -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_workflow 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_workflow.%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 73acbfc13577 src/pyams_workflow/versions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/versions.py Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,238 @@ +# +# 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.traversing import get_parent + +__docformat__ = 'restructuredtext' + + +# import standard library +from datetime import datetime + +# import interfaces +from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowManagedContent, VersionError, IWorkflowState, \ + IWorkflowVersion, IWorkflowStateHistoryItem, IWorkflowTransitionEvent, IWorkflowVersionTransitionEvent +from zope.annotation.interfaces import IAnnotations +from zope.traversing.interfaces import ITraversable + +# import packages +from persistent import Persistent +from persistent.list import PersistentList +from persistent.mapping import PersistentMapping +from pyams_utils.adapter import adapter_config, ContextAdapter +from pyams_utils.request import check_request +from pyramid.events import subscriber +from pyramid.threadlocal import get_current_registry +from zope.container.folder import Folder +from zope.interface import implementer, alsoProvides +from zope.lifecycleevent import ObjectCreatedEvent +from zope.location import locate +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IWorkflowStateHistoryItem) +class WorkflowHistoryItem(Persistent): + """Workflow history item""" + + date = FieldProperty(IWorkflowStateHistoryItem['date']) + source_version = FieldProperty(IWorkflowStateHistoryItem['source_version']) + source_state = FieldProperty(IWorkflowStateHistoryItem['source_state']) + target_state = FieldProperty(IWorkflowStateHistoryItem['target_state']) + transition = FieldProperty(IWorkflowStateHistoryItem['transition']) + principal = FieldProperty(IWorkflowStateHistoryItem['principal']) + comment = FieldProperty(IWorkflowStateHistoryItem['comment']) + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +@implementer(IWorkflowState) +class WorkflowVersionState(Persistent): + """Workflow managed content version object""" + + version_id = None + state = None + + def __init__(self): + self.history = PersistentList() + + +@subscriber(IWorkflowTransitionEvent) +def handle_workflow_transition(event): + """Handle workflow transition""" + if IWorkflowVersionTransitionEvent.providedBy(event): + return + request = check_request() + item = WorkflowHistoryItem(date=datetime.utcnow(), + source_state=event.source, + target_state=event.destination, + transition=event.transition.title, + principal=request.principal.id, + comment=event.comment) + IWorkflowState(event.object).history.append(item) + + +@subscriber(IWorkflowVersionTransitionEvent) +def handle_workflow_version_transition(event): + """Handle workflow version transition""" + request = check_request() + item = WorkflowHistoryItem(date=datetime.utcnow(), + source_version=IWorkflowState(event.old_object).version_id, + source_state=event.source, + target_state=event.destination, + transition=event.transition.title, + principal=request.principal.id, + comment=event.comment) + IWorkflowState(event.object).history.append(item) + + +WORKFLOW_VERSION_KEY = 'pyams_workflow.version' + + +@adapter_config(context=IWorkflowVersion, provides=IWorkflowState) +def WorkflowVersionStateFactory(context): + """Workflow content version state factory""" + annotations = IAnnotations(context) + state = annotations.get(WORKFLOW_VERSION_KEY) + if state is None: + state = annotations[WORKFLOW_VERSION_KEY] = WorkflowVersionState() + registry = get_current_registry() + registry.notify(ObjectCreatedEvent(state)) + return state + + +@implementer(IWorkflowVersions) +class WorkflowVersions(Folder): + """Workflow versions adapter""" + + def __init__(self): + super(WorkflowVersions, self).__init__() + self.last_version_id = 0 + self.state_by_version = PersistentMapping() + self.versions_by_state = PersistentMapping() + self.deleted = PersistentMapping() + + def get_version(self, version_id): + if version_id is None: + version_id = self.last_version_id + try: + return self[str(version_id)] + except KeyError: + raise VersionError("Missing given version ID {0}".format(version_id)) + + def get_versions(self, states=None): + if states: + if isinstance(states, str): + states = (states, ) + versions = set() + for state in states: + if state is None: + state = '__none__' + [versions.add(self[str(version)]) for version in self.versions_by_state.get(state, ())] + return versions + else: + return self.values() + + def add_version(self, content, state): + self.last_version_id += 1 + version_id = self.last_version_id + # init version state + alsoProvides(content, IWorkflowVersion) + wf_state = IWorkflowState(content) + wf_state.version_id = version_id + wf_state.state = state + # store new version + if state is None: + state = '__none__' + self[str(version_id)] = content + self.state_by_version[version_id] = state + versions = self.versions_by_state.get(state, []) + versions.append(version_id) + self.versions_by_state[state] = versions + return version_id + + def set_state(self, version_id, state): + # update version state + version = self[str(version_id)] + wf_state = IWorkflowState(version) + wf_state.version_id = version_id + wf_state.state = state + # update versions/states mapping + if state is None: + state = '__none__' + old_state = self.state_by_version[version_id] + versions = self.versions_by_state[old_state] + if version_id in versions: + versions.remove(version_id) + if versions: + self.versions_by_state[old_state] = versions + else: + del self.versions_by_state[old_state] + self.state_by_version[version_id] = state + versions = self.versions_by_state.get(state, []) + versions.append(version_id) + self.versions_by_state[state] = versions + + def has_version(self, state): + if state is None: + state = '__none__' + return bool(self.versions_by_state.get(state, ())) + + def remove_version(self, version_id): + if str(version_id) not in self: + pass + state = self.state_by_version[version_id] + versions = self.versions_by_state[state] + versions.remove(version_id) + if versions: + self.versions_by_state[state] = versions + else: + del self.versions_by_state[state] + del self.state_by_version[version_id] + self.deleted[version_id] = self[str(version_id)] + del self[str(version_id)] + + +WORKFLOW_VERSIONS_KEY = 'pyams_workflow.versions' + + +@adapter_config(context=IWorkflowManagedContent, provides=IWorkflowVersions) +def WorkflowContentVersionsFactory(context): + """Workflow versions factory""" + annotations = IAnnotations(context) + versions = annotations.get(WORKFLOW_VERSIONS_KEY) + if versions is None: + versions = annotations[WORKFLOW_VERSIONS_KEY] = WorkflowVersions() + registry = get_current_registry() + registry.notify(ObjectCreatedEvent(versions)) + locate(versions, context, '++versions++') + return versions + + +@adapter_config(context=IWorkflowVersion, provides=IWorkflowVersions) +def WorkflowVersionVersionsFactory(context): + """Workflow versions factory for version""" + parent = get_parent(context, IWorkflowManagedContent) + if parent is not None: + return IWorkflowVersions(parent) + + +@adapter_config(name='versions', context=IWorkflowManagedContent, provides=ITraversable) +class WorkflowVersionsTraverser(ContextAdapter): + """++versions++ namespace traverser""" + + def traverse(self, name, furtherpath=None): + versions = IWorkflowVersions(self.context) + if name: + return versions[name] + else: + return versions diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/workflow.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/workflow.py Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,249 @@ +# +# 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.traversing import get_parent + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_workflow.interfaces import MANUAL, IWorkflow, InvalidTransitionError, IWorkflowState, IWorkflowVersions, \ + IWorkflowInfo, ConditionFailedError, WorkflowVersionTransitionEvent, WorkflowTransitionEvent, \ + NoTransitionAvailableError, AmbiguousTransitionError, SYSTEM, AUTOMATIC, IWorkflowManagedContent, IWorkflowVersion + +# import packages +from pyams_utils.adapter import adapter_config +from pyams_utils.registry import get_utility +from pyams_utils.request import check_request +from pyramid.httpexceptions import HTTPUnauthorized +from pyramid.threadlocal import get_current_registry +from zope.interface import implementer +from zope.lifecycleevent import ObjectModifiedEvent + + +def NullCondition(wf, context): + """Null condition""" + return True + + +def NullAction(wf, context): + """Null action""" + pass + + +def granted_permission(permission, context): + return True + + +class Transition(object): + """Transition object + + A transition doesn't make anything by itself. + Everything is handled by the workflow utility + """ + + def __init__(self, transition_id, title, source, destination, + condition=NullCondition, + action=NullAction, + trigger=MANUAL, + permission=None, + order=0, + **user_data): + self.transition_id = transition_id + self.title = title + self.source = source + self.destination = destination + self.condition = condition + self.action = action + self.trigger = trigger + self.permission = permission + self.order = order + self.user_data = user_data + + +@implementer(IWorkflow) +class Workflow(object): + """Workflow utility""" + + def __init__(self, transitions, states, published_states=None): + self.refresh(transitions) + self.states = states + self.published_states = published_states or set() + + def _register(self, transition): + transitions = self._sources.setdefault(transition.source, {}) + transitions[transition.transition_id] = transition + self._id_transitions[transition.transition_id] = transition + + def refresh(self, transitions): + self._sources = {} + self._id_transitions = {} + for transition in transitions: + self._register(transition) + + def get_transitions(self, source): + try: + return self._sources[source].values() + except KeyError: + return [] + + def get_transition(self, source, transition_id): + transition = self._id_transitions[transition_id] + if transition.source != source: + raise InvalidTransitionError(source) + return transition + + def get_transition_by_id(self, transition_id): + return self._id_transitions[transition_id] + + +WORKFLOW_STATE_KEY = 'pyams_workflow.state' + + +@adapter_config(context=IWorkflowVersion, provides=IWorkflowInfo) +class WorkflowInfo(object): + """Workflow info adapter""" + + def __init__(self, context): + self.context = context + self.wf = get_utility(IWorkflow, name=self.name) + + @property + def parent(self): + return get_parent(self.context, IWorkflowManagedContent) + + @property + def name(self): + return self.parent.workflow_name + + def fire_transition(self, transition_id, comment=None, side_effect=None, check_security=True): + versions = IWorkflowVersions(self.parent) + state = IWorkflowState(self.context) + # this raises InvalidTransitionError if id is invalid for current state + transition = self.wf.get_transition(state.state, transition_id) + # check whether we may execute this workflow transition + if check_security and transition.permission: + request = check_request() + if not request.has_permission(transition.permission): + raise HTTPUnauthorized() + # now make sure transition can still work in this context + if not transition.condition(self, self.context): + raise ConditionFailedError() + # perform action, return any result as new version + result = transition.action(self, self.context) + if result is not None: + # clear result history + IWorkflowState(result).history.clear() + # stamp it with version + versions.add_version(result, transition.destination) + # execute any side effect: + if side_effect is not None: + side_effect(result) + event = WorkflowVersionTransitionEvent(result, self.context, + transition.source, + transition.destination, + transition, comment) + else: + versions.set_state(state.version_id, transition.destination) + # execute any side effect + if side_effect is not None: + side_effect(self.context) + event = WorkflowTransitionEvent(self.context, + transition.source, + transition.destination, + transition, comment) + # change state of context or new object + registry = get_current_registry() + registry.notify(event) + # send modified event for original or new object + if result is None: + registry.notify(ObjectModifiedEvent(self.context)) + else: + registry.notify(ObjectModifiedEvent(result)) + return result + + def fire_transition_toward(self, state, comment=None, side_effect=None, check_security=True): + transition_ids = self.get_fireable_transition_ids_toward(state, check_security) + if not transition_ids: + raise NoTransitionAvailableError(self.state(self.context).get_state(), state) + if len(transition_ids) != 1: + raise AmbiguousTransitionError(self.state(self.context).get_state(), state) + return self.fire_transition(transition_ids[0], comment, side_effect, check_security) + + def fire_transition_for_versions(self, state, transition_id, comment=None): + versions = IWorkflowVersions(self.parent) + for version in versions.get_versions(state): + IWorkflowInfo(version).fire_transition(transition_id, comment) + + def fire_automatic(self): + for transition_id in self.get_automatic_transition_ids(): + try: + self.fire_transition(transition_id) + except ConditionFailedError: + # if condition failed, that's fine, then we weren't + # ready to fire yet + pass + else: + # if we actually managed to fire a transition, + # we're done with this one now. + return + + def has_version(self, state): + wf_versions = IWorkflowVersions(self.parent) + return wf_versions.has_version(state) + + def get_manual_transition_ids(self, check_security=True): + if check_security: + request = check_request() + permission_checker = request.has_permission + else: + permission_checker = granted_permission + return [transition.transition_id + for transition in sorted(self._get_transitions(MANUAL), + key=lambda x: x.user_data.get('order', 999)) + if transition.condition(self, self.context) and + permission_checker(transition.permission, self.context)] + + def get_system_transition_ids(self): + # ignore permission checks + return [transition.transition_id + for transition in sorted(self._get_transitions(SYSTEM), + key=lambda x: x.user_data.get('order', 999)) + if transition.condition(self, self.context)] + + def get_fireable_transition_ids(self, check_security=True): + return (self.get_manual_transition_ids(check_security) + + self.get_system_transition_ids()) + + def get_fireable_transition_ids_toward(self, state, check_security=True): + result = [] + for transition_id in self.get_fireable_transition_ids(check_security): + transition = self.wf.get_transition_by_id(transition_id) + if transition.destination == state: + result.append(transition_id) + return result + + def get_automatic_transition_ids(self): + return [transition.transition_id for transition in + self._get_transitions(AUTOMATIC)] + + def has_automatic_transitions(self): + return bool(self.get_automatic_transition_ids()) + + def _get_transitions(self, trigger): + # retrieve all possible transitions from workflow utility + state = IWorkflowState(self.context) + transitions = self.wf.get_transitions(state.state) + # now filter these transitions to retrieve all possible + # transitions in this context, and return their ids + return [transition for transition in transitions if transition.trigger == trigger] diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/zmi/__init__.py Tue Mar 17 16:07:15 2015 +0100 @@ -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 73acbfc13577 src/pyams_workflow/zmi/interfaces.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/zmi/interfaces.py Tue Mar 17 16:07:15 2015 +0100 @@ -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 IWorkflowMenu(Interface): + """Workflow menu marker interface""" diff -r 000000000000 -r 73acbfc13577 src/pyams_workflow/zmi/workflow.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_workflow/zmi/workflow.py Tue Mar 17 16:07:15 2015 +0100 @@ -0,0 +1,67 @@ +# +# 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 IMainMenusViewletManager +from pyams_workflow.interfaces import IWorkflow, IWorkflowInfo, IWorkflowVersion, IWorkflowManagedContent +from pyams_workflow.zmi.interfaces import IWorkflowMenu +from pyams_zmi.layer import IAdminLayer + +# import packages +from pyams_skin.viewlet.menu import Menu, MenuItem +from pyams_utils.registry import query_utility +from pyams_utils.traversing import get_parent +from pyams_viewlet.manager import viewletmanager_config +from pyams_viewlet.viewlet import viewlet_config +from zope.interface import implementer + +from pyams_workflow import _ + + +class WorkflowMenuItem(MenuItem): + """Workflow menu item""" + + def __init__(self, context, request, view, manager, transition): + super(WorkflowMenuItem, self).__init__(context, request, view, manager) + self.label = transition.title + self.url = transition.user_data.get('view_name') + self.weight = transition.order + self.modal_target = True + + +@viewlet_config(name='workflow.menu', layer=IAdminLayer, context=IWorkflowVersion, + manager=IMainMenusViewletManager, permission='system.view', weight=200) +@viewletmanager_config(name='workflow.menu', layer=IAdminLayer, provides=IWorkflowMenu) +@implementer(IWorkflowMenu) +class WorkflowMenu(Menu): + """Workflow menu""" + + header = _("Workflow") + + def _get_viewlets(self): + viewlets = [] + parent = get_parent(self.context, IWorkflowManagedContent) + wf = query_utility(IWorkflow, name=parent.workflow_name) + if wf is None: + return viewlets + info = IWorkflowInfo(self.context) + for transition_id in info.get_manual_transition_ids(): + transition = wf.get_transition_by_id(transition_id) + menu = WorkflowMenuItem(self.context, self.request, self.__parent__, self, transition) + viewlets.append((transition_id, menu)) + viewlets.sort(key=lambda x: x[1].weight) + return viewlets