{ "info": { "author": "Martijn Faassen (at Infrae)", "author_email": "faassen@startifact.com", "bugtrack_url": null, "classifiers": [ "Environment :: Web Environment", "Framework :: Zope3", "Intended Audience :: Developers", "License :: OSI Approved :: Zope Public License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy" ], "description": "hurry.workflow\n==============\n\nA simple but quite nifty workflow system for Zope 3.\n\nCHANGES\n=======\n\n3.0.2 (2018-01-16)\n------------------\n\n- Update rendering of pypi description.\n\n3.0.1 (2018-01-16)\n------------------\n\n- Fix up a brown paper bag release.\n\n3.0.0 (2018-01-16)\n------------------\n\n- Update dependencies not to rely on ZODB 3 anymore.\n\n- Support python3.4, python3.5, python3.6 in addition to python2.7.\n\n0.13.1 (2013-01-17)\n-------------------\n\n- Make the exceptions also display more informative messages.\n\n0.13 (2013-01-17)\n-----------------\n\n- ``NoTransitionAvailableError`` gained a ``source`` and ``destination``\n attribute indicating what transition wasn't available.\n\n- ``AmbiguousTransitionError`` also gained a ``source`` and ``destination``\n attribute indicating what transition was ambiguous.\n\n- ``InvalidTransitionError`` gained a ``source`` attribute indicating\n the source state of the attempted invalid transition.\n\n- Newer ``bootstrap.py``\n\n0.12 (2012-02-10)\n-----------------\n\n- Make the info() and state() functions on the WorkflowInfo class into\n classmethods as they are not of much use otherwise.\n\n- fireTransitionToward already accepted a check_security=False\n argument, but it would not allow a transition that a user didn't\n have the permission for to be fired after all, because the\n transition wouldn't even be found in the first place. Now it works.\n\n0.11 (2010-04-16)\n-----------------\n\n- Do IAnnotations(self.context) only once in WorkflowState.\n\n- An IWorkflowVersions implementation is now optional.\n\n- Added multiple workflows support.\n\n0.10 (2009-11-19)\n-----------------\n\n- Moved to svn.zope.org for development.\n\n- Added a buildout.cfg, bootstrap.py\n\n- Minimized dependencies. Note that ``Workflow`` does not inherit from\n ``Persistent`` and ``zope.container.contained.Contained``\n anymore. If you need persistent workflow, you need to subclass this\n in your own code. This breaks backwards compatibility, as persistent\n workflows would need to be re-initialized.\n\n0.9.2.1 (2007-08-15)\n--------------------\n\n- Oops, the patches in 0.9.2 were not actually applied. Fixed them\n now.\n\n0.9.2 (2007-08-15)\n------------------\n\n- zope.security changes broke imports in hurry.workflow.\n\n- localUtility directive is now deprecated, so don't use it anymore.\n\n0.9.1 (2006-09-22)\n------------------\n\n- first cheesehop release.\n\n0.9 (2006-06-15)\n----------------\n- separate out from hurry package into hurry.workflow\n\n- eggification work\n\n- Zope 3.3 compatibility work\n\n0.8 (2006-05-01)\n----------------\n\nInitial public release.\n\nDetailed Documentation\n======================\n\nHurry Workflow\n==============\n\nThe hurry workflow system is a \"roll my own because I'm in a hurry\"\nframework.\n\nBasic workflow\n--------------\n\nLet's first make a content object that can go into a workflow::\n\n >>> from zope.interface import implementer, Attribute\n\n >>> from zope.annotation.interfaces import IAttributeAnnotatable\n >>> class IDocument(IAttributeAnnotatable):\n ... title = Attribute('Title')\n >>> @implementer(IDocument)\n ... class Document(object):\n ... def __init__(self, title):\n ... self.title = title\n\nAs you can see, such a content object must provide IAnnotatable, as\nthis is used to store the workflow state. The system uses the\nIWorkflowState adapter to get and set an object's workflow state::\n\n >>> from hurry.workflow import interfaces\n >>> document = Document('Foo')\n >>> state = interfaces.IWorkflowState(document)\n >>> print(state.getState())\n None\n\nThe state can be set directly for an object using the IWorkflowState\nadapter as well::\n\n >>> state.setState('foo')\n >>> state.getState()\n 'foo'\n\nBut let's set it back to None again, so we can start again in a\npristine state for this document::\n\n >>> state.setState(None)\n\nIt's not recommended use setState() do this ourselves, though: usually\nwe'll let the workflow system take care of state transitions and the\nsetting of the initial state.\n\nNow let's define a simple workflow transition from 'a' to 'b'. It\nneeds a condition which must return True before the transition is\nallowed to occur::\n\n >>> def NullCondition(wf, context):\n ... return True\n\nand an action that takes place when the transition is taken::\n\n >>> def NullAction(wf, context):\n ... pass\n\nNow let's construct a transition::\n\n >>> from hurry.workflow import workflow\n >>> transition = workflow.Transition(\n ... transition_id='a_to_b',\n ... title='A to B',\n ... source='a',\n ... destination='b',\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL)\n\nThe transition trigger is either MANUAL, AUTOMATIC or SYSTEM. MANUAL\nindicates user action is needed to fire the transition. AUTOMATIC\ntransitions fire automatically. SYSTEM is a workflow transition\ndirectly fired by the system, and not directly by the user.\n\nWe also will introduce an initial transition, that moves an object\ninto the workflow (for instance just after it is created)::\n\n >>> init_transition = workflow.Transition(\n ... transition_id='to_a',\n ... title='Create A',\n ... source=None,\n ... destination='a')\n\nAnd a final transition, when the object moves out of the workflow again\n(for instance just before it is deleted)::\n\n >>> final_transition = workflow.Transition(\n ... transition_id='finalize',\n ... title='Delete',\n ... source='b',\n ... destination=None)\n\nNow let's put the transitions in an workflow utility::\n\n >>> wf = workflow.Workflow([transition, init_transition, final_transition])\n >>> from zope import component\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n\nWe can get the transition from the workflow using ``get_transition``\nshould we need it::\n\n >>> wf.getTransition('a', 'a_to_b') is transition\n True\n\nIf we try to get a transition that doesn't exist, we get an error::\n\n >>> wf.getTransition('b', 'a_to_b')\n Traceback (most recent call last):\n ...\n hurry.workflow.interfaces.InvalidTransitionError: source: \"b\"\n\n >>> from hurry.workflow.interfaces import InvalidTransitionError\n >>> try:\n ... wf.getTransition('b', 'a_to_b')\n ... except InvalidTransitionError as e_:\n ... e = e_\n >>> e.source\n 'b'\n\nWorkflow transitions cause events to be fired; we will put in a simple\nhandler so we can check whether things were successfully fired::\n\n >>> events = []\n >>> def transition_handler(event):\n ... events.append(event)\n >>> component.provideHandler(\n ... transition_handler,\n ... [interfaces.IWorkflowTransitionEvent])\n\nTo get what transitions to other states are possible from an object,\nas well as to fire transitions and set initial state, we use the\nIWorkflowInfo adapter::\n\n >>> info = interfaces.IWorkflowInfo(document)\n\nWe'll initialize the workflow by firing the 'to_a' transition::\n\n >>> info.fireTransition('to_a')\n\nThis should've fired an event::\n\n >>> events[-1].transition.transition_id\n 'to_a'\n >>> events[-1].source is None\n True\n >>> events[-1].destination\n 'a'\n\nThere's only a single transition defined to workflow state 'b'::\n\n >>> info.getManualTransitionIds()\n ['a_to_b']\n\nWe can also get this by asking which manual (or system) transition\nexists that brings us to the desired workflow state::\n\n >>> info.getFireableTransitionIdsToward('b')\n ['a_to_b']\n\nSince this is a manually triggered transition, we can fire this\ntransition::\n\n >>> info.fireTransition('a_to_b')\n\nThe workflow state should now be 'b'::\n\n >>> state.getState()\n 'b'\n\nWe check that the event indeed got fired::\n\n >>> events[-1].transition.transition_id\n 'a_to_b'\n >>> events[-1].source\n 'a'\n >>> events[-1].destination\n 'b'\n\nWe will also try fireTransitionToward here, so we sneak back the\nworkflow to state 'a' again and try that::\n\n >>> state.setState('a')\n\nTry going through a transition we cannot reach first::\n\n >>> info.fireTransitionToward('c')\n Traceback (most recent call last):\n ...\n hurry.workflow.interfaces.NoTransitionAvailableError: source: \"a\" destination: \"c\"\n\nThis error has some information available of what transition was attempted::\n\n >>> from hurry.workflow.interfaces import NoTransitionAvailableError\n >>> try:\n ... info.fireTransitionToward('c')\n ... except NoTransitionAvailableError as e_:\n ... e = e_\n >>> e.source\n 'a'\n >>> e.destination\n 'c'\n\nNow go to 'b' again::\n\n >>> info.fireTransitionToward('b')\n >>> state.getState()\n 'b'\n\nFinally, before forgetting about our document, we finalize the workflow::\n\n >>> info.fireTransition('finalize')\n >>> state.getState() is None\n True\n\nAnd we have another event that was fired::\n\n >>> events[-1].transition.transition_id\n 'finalize'\n >>> events[-1].source\n 'b'\n >>> events[-1].destination is None\n True\n\n\nMultiple workflows\n------------------\n\nWe have previously registered a workflow as a unnamed utility.\nYou can also register a workflow as a named utility to provide\nseveral workflows for a site.\n\nLet's create a, invoice document::\n\n >>> class IInvoiceDocument(IDocument):\n ... title = Attribute('Title')\n\n >>> @implementer(IInvoiceDocument)\n ... class InvoiceDocument(object):\n ... def __init__(self, title, amount):\n ... self.title = title\n ... self.amount = amount\n\nWe define our workflow::\n\n >>> invoice_init = workflow.Transition(\n ... transition_id='init_invoice',\n ... title='Invoice Received',\n ... source=None,\n ... destination='received')\n >>>\n >>> invoice_paid = workflow.Transition(\n ... transition_id='invoice_paid',\n ... title='Invoice Paid',\n ... source='received',\n ... destination='paid')\n\n >>> invoice_wf = workflow.Workflow( [ invoice_init, invoice_paid ] )\n\nWe register a new workflow utility, WorkflowState and WorkflowInfo adapters, all\nnamed \"invoice\"::\n\n >>> from hurry.workflow import workflow\n >>> from zope.annotation import interfaces as annotation_interfaces\n >>> component.provideUtility(invoice_wf, interfaces.IWorkflow, name=\"invoice\")\n >>> class InvoiceWorkflowInfo(workflow.WorkflowInfo):\n ... name=\"invoice\"\n >>> component.provideAdapter(\n ... InvoiceWorkflowInfo,\n ... (annotation_interfaces.IAnnotatable,),\n ... interfaces.IWorkflowInfo,\n ... name=\"invoice\")\n >>> class InvoiceWorkflowState(workflow.WorkflowState):\n ... state_key = \"invoice.state\"\n ... id_key = \"invoice.id\"\n >>> component.provideAdapter(\n ... InvoiceWorkflowState,\n ... (annotation_interfaces.IAnnotatable,),\n ... interfaces.IWorkflowState,\n ... name=\"invoice\")\n\nNow we can utilize the workflow::\n\n >>> invoice = InvoiceDocument('abc', 22)\n\n >>> info = component.getAdapter(invoice, interfaces.IWorkflowInfo, name=\"invoice\")\n >>> info.fireTransition('init_invoice')\n >>> state = component.getAdapter(invoice, interfaces.IWorkflowState, name=\"invoice\")\n >>> state.getState()\n 'received'\n >>> info.fireTransition('invoice_paid')\n >>> state.getState()\n 'paid'\n\nTo make it easier to get the state and info adapters for a particular context\nobject, there are two convenience functions on the workflow info object. The\ninfo object \"knows\" what workflow utility to look for, as they are associated\nby name::\n\n >>> info_ = InvoiceWorkflowInfo.info(invoice)\n >>> interfaces.IWorkflowInfo.providedBy(info_)\n True\n\n >>> state_ = InvoiceWorkflowInfo.state(invoice)\n >>> interfaces.IWorkflowState.providedBy(state_)\n True\n >>> state.getState() is state_.getState()\n True\n\nOf course, this document always have the default unnamed workflow::\n\n >>> info = interfaces.IWorkflowInfo(invoice)\n >>> info.fireTransition('to_a')\n >>> state = interfaces.IWorkflowState(invoice)\n >>> state.getState()\n 'a'\n\nMulti-version workflow\n----------------------\n\nNow let's go for a more complicated scenario where have multiple\nversions of a document. At any one time a document can have an\nUNPUBLISHED version and a PUBLISHED version. There can also be a\nCLOSED version and any number of ARCHIVED versions::\n\n >>> UNPUBLISHED = 'unpublished'\n >>> PUBLISHED = 'published'\n >>> CLOSED = 'closed'\n >>> ARCHIVED = 'archived'\n\nLet's start with a simple initial transition::\n\n >>> init_transition = workflow.Transition(\n ... transition_id='init',\n ... title='Initialize',\n ... source=None,\n ... destination=UNPUBLISHED)\n\nWhen the unpublished version is published, any previously published\nversion is made to be the CLOSED version. To accomplish this secondary\nstate transition, we'll use the system's built-in versioning ability\nwith the 'fireTransitionsForVersions' method, which can be used to\nfire transitions of other versions of the document::\n\n >>> def PublishAction(wf, context):\n ... wf.fireTransitionForVersions(PUBLISHED, 'close')\n\nNow let's build the transition::\n\n >>> publish_transition = workflow.Transition(\n ... transition_id='publish',\n ... title='Publish',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=NullCondition,\n ... action=PublishAction,\n ... trigger=interfaces.MANUAL,\n ... order=1)\n\nNext, we'll define a transition from PUBLISHED to CLOSED, which means\nwe want to archive whatever was closed before::\n\n >>> def CloseAction(wf, context):\n ... wf.fireTransitionForVersions(CLOSED, 'archive')\n >>> close_transition = workflow.Transition(\n ... transition_id='close',\n ... title='Close',\n ... source=PUBLISHED,\n ... destination=CLOSED,\n ... condition=NullCondition,\n ... action=CloseAction,\n ... trigger=interfaces.MANUAL,\n ... order=2)\n\nNote that CloseAction will also be executed automatically whenever\nstate is transitioned from PUBLISHED to CLOSED using\nfireTransitionsForVersions. This means that publishing a document\nresults in the previously closed document being archived.\n\nIf there is a PUBLISHED but no UNPUBLISHED version, we can make a new\ncopy of the PUBLISHED version and make that the UNPUBLISHED version::\n\n >>> def CanCopyCondition(wf, context):\n ... return not wf.hasVersion(UNPUBLISHED)\n\nSince we are actually creating a new content object, the action should\nreturn the newly created object with the new state::\n\n >>> def CopyAction(wf, context):\n ... return Document('copy of %s' % context.title)\n\n >>> copy_transition = workflow.Transition(\n ... transition_id='copy',\n ... title='Copy',\n ... source=PUBLISHED,\n ... destination=UNPUBLISHED,\n ... condition=CanCopyCondition,\n ... action=CopyAction,\n ... trigger=interfaces.MANUAL,\n ... order=3)\n\nA very similar transition applies to the closed version. If we have\nno UNPUBLISHED version and no PUBLISHED version, we can make a new copy\nfrom the CLOSED version::\n\n >>> def CanCopyCondition(wf, context):\n ... return (not wf.hasVersion(UNPUBLISHED) and\n ... not wf.hasVersion(PUBLISHED))\n\n >>> copy_closed_transition = workflow.Transition(\n ... transition_id='copy_closed',\n ... title='Copy',\n ... source=CLOSED,\n ... destination=UNPUBLISHED,\n ... condition=CanCopyCondition,\n ... action=CopyAction,\n ... trigger=interfaces.MANUAL,\n ... order=4)\n\nFinally let's build the archiving transition::\n\n >>> archive_transition = workflow.Transition(\n ... transition_id='archive',\n ... title='Archive',\n ... source=CLOSED,\n ... destination=ARCHIVED,\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL,\n ... order=5)\n\nNow let's build and provide the workflow utility::\n\n >>> wf = workflow.Workflow([init_transition,\n ... publish_transition, close_transition,\n ... copy_transition, copy_closed_transition,\n ... archive_transition])\n\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n\nLet's get the workflow_versions utility which we can use to track\nversions and come up with a new unique id::\n\n >>> workflow_versions = component.getUtility(interfaces.IWorkflowVersions)\n\nAnd let's start with a document::\n\n >>> document = Document('bar')\n >>> info = interfaces.IWorkflowInfo(document)\n >>> info.fireTransition('init')\n\nWe need the document id to compare later when we create a new version::\n\n >>> state = interfaces.IWorkflowState(document)\n >>> document_id = state.getId()\n\nLet's add it to the workflow versions container so we can find it. Note\nthat we're using a private API here; this could be implemented as adding\nit to a folder or any other way, as long as getVersions() works later::\n\n >>> workflow_versions.addVersion(document) # private API\n\nAlso clear out previously recorded events::\n\n >>> del events[:]\n\nWe can publish it::\n\n >>> info.getManualTransitionIds()\n ['publish']\n\nSo let's do that::\n\n >>> info.fireTransition('publish')\n >>> state.getState()\n 'published'\n\nThe last event should be the 'publish' transition::\n\n >>> events[-1].transition.transition_id\n 'publish'\n\nAnd now we can either close or create a new copy of it. Note that the\nnames are sorted using the order of the transitions::\n\n >>> info.getManualTransitionIds()\n ['close', 'copy']\n\nLet's close it::\n\n >>> info.fireTransition('close')\n >>> state.getState()\n 'closed'\n\nWe're going to create a new copy for editing now::\n\n >>> info.getManualTransitionIds()\n ['copy_closed', 'archive']\n >>> document2 = info.fireTransition('copy_closed')\n >>> workflow_versions.addVersion(document2) # private API to track it\n >>> document2.title\n 'copy of bar'\n >>> state = interfaces.IWorkflowState(document2)\n >>> state.getState()\n 'unpublished'\n >>> state.getId() == document_id\n True\n\nThe original version is still there in its original state::\n\n >>> interfaces.IWorkflowState(document).getState()\n 'closed'\n\nLet's also check the last event in some detail::\n\n >>> event = events[-1]\n >>> event.transition.transition_id\n 'copy_closed'\n >>> event.old_object == document\n True\n >>> event.object == document2\n True\n\nNow we are going to publish the new version::\n\n >>> info = interfaces.IWorkflowInfo(document2)\n >>> info.getManualTransitionIds()\n ['publish']\n >>> info.fireTransition('publish')\n >>> interfaces.IWorkflowState(document2).getState()\n 'published'\n\nThe original is still closed::\n\n >>> interfaces.IWorkflowState(document).getState()\n 'closed'\n\nNow let's publish another copy after this::\n\n >>> document3 = info.fireTransition('copy')\n >>> workflow_versions.addVersion(document3)\n >>> interfaces.IWorkflowInfo(document3).fireTransition('publish')\n\nThis copy is now published::\n\n >>> interfaces.IWorkflowState(document3).getState()\n 'published'\n\nAnd the previously published version is now closed::\n\n >>> interfaces.IWorkflowState(document2).getState()\n 'closed'\n\nNote that due to the condition, it's not possible to copy from the\nclosed version, as there is a published version still remaining::\n\n >>> interfaces.IWorkflowInfo(document2).getManualTransitionIds()\n ['archive']\n\nMeanwhile, the original version, previously closed, is now archived::\n\n >>> interfaces.IWorkflowState(document).getState()\n 'archived'\n\nAutomatic transitions\n---------------------\n\nNow let's try a workflow transition that is automatic and time-based.\nWe'll set up a very simple workflow between 'unpublished' and\n'published', and have the 'published' transition be time-based.\n\nTo simulate time, we have moments::\n\n >>> time_moment = 0\n\nWe will only publish if time_moment is greater than 3::\n\n >>> def TimeCondition(wf, context):\n ... return time_moment > 3\n\nSet up the transition using this condition; note that this one is\nautomatic, i.e. it doesn't have to be triggered by humans::\n\n >>> publish_transition = workflow.Transition(\n ... transition_id='publish',\n ... title='Publish',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=TimeCondition,\n ... action=NullAction,\n ... trigger=interfaces.AUTOMATIC)\n\nSet up the workflow using this transition, and reusing the\ninit transition we defined before::\n\n >>> wf = workflow.Workflow([init_transition, publish_transition])\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n\nClear out all versions; this is an private API we just use for\ndemonstration purposes::\n\n >>> workflow_versions.clear()\n\nNow create a document::\n\n >>> document = Document('bar')\n >>> info = interfaces.IWorkflowInfo(document)\n >>> info.fireTransition('init')\n\nPrivate again; do this with the catalog or any way you prefer in your\nown code::\n\n >>> workflow_versions.addVersion(document)\n\nSince this transition is automatic, we should see it like this::\n\n >>> interfaces.IWorkflowInfo(document).getAutomaticTransitionIds()\n ['publish']\n\nNow fire let's any automatic transitions::\n\n >>> workflow_versions.fireAutomatic()\n\nNothing should have happened as we are still at time moment 0::\n\n >>> state = interfaces.IWorkflowState(document)\n >>> state.getState()\n 'unpublished'\n\nWe change the time moment past 3::\n\n >>> time_moment = 4\n\nNow fire any automatic transitions again::\n\n >>> workflow_versions.fireAutomatic()\n\nThe transition has fired, so the state will be 'published'::\n\n >>> state.getState()\n 'published'\n\nSystem transitions\n------------------\n\nLet's try system transitions now. This transition shouldn't show up\nas manual nor as automatic::\n\n >>> publish_transition = workflow.Transition(\n ... transition_id='publish',\n ... title='Publish',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... trigger=interfaces.SYSTEM)\n\nSet up the workflow using this transition, and reusing the\ninit transition we defined before::\n\n >>> wf = workflow.Workflow([init_transition, publish_transition])\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n\nClear out all versions; this is an private API we just use for\ndemonstration purposes::\n\n >>> workflow_versions.clear()\n\nNow create a document::\n\n >>> document = Document('bar')\n >>> info = interfaces.IWorkflowInfo(document)\n >>> info.fireTransition('init')\n\nPrivate again; do this with the catalog or any way you prefer in your\nown code::\n\n >>> workflow_versions.addVersion(document)\n\nWe should see it as a system transition::\n\n >>> info.getSystemTransitionIds()\n ['publish']\n\nbut not as automatic nor manual::\n\n >>> info.getAutomaticTransitionIds()\n []\n >>> info.getManualTransitionIds()\n []\n\nThis transition can be fired::\n\n >>> info.fireTransition('publish')\n >>> interfaces.IWorkflowState(document).getState()\n 'published'\n\nMultiple transitions\n--------------------\n\nIt's possible to have multiple transitions from the source state to\nthe target state, for instance an automatic and a manual one.\n\nLet's set up a workflow with two manual transitions and a single\nautomatic transitions between two states::\n\n >>> publish_1_transition = workflow.Transition(\n ... transition_id='publish 1',\n ... title='Publish 1',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL)\n\n >>> publish_2_transition = workflow.Transition(\n ... transition_id='publish 2',\n ... title='Publish 2',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL)\n\n >>> publish_auto_transition = workflow.Transition(\n ... transition_id='publish auto',\n ... title='Publish Auto',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=TimeCondition,\n ... action=NullAction,\n ... trigger=interfaces.AUTOMATIC)\n\nClear out all versions; this is an private API we just use for\ndemonstration purposes::\n\n >>> workflow_versions.clear()\n\nSince we're using the time condition again, let's make sure\ntime is at 0 again so that the publish_auto_transition doesn't fire::\n\n >>> time_moment = 0\n\nNow set up the workflow using these transitions, plus our\ninit_transition::\n\n >>> wf = workflow.Workflow([init_transition,\n ... publish_1_transition, publish_2_transition,\n ... publish_auto_transition])\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n\nNow create a document::\n\n >>> document = Document('bar')\n >>> info = interfaces.IWorkflowInfo(document)\n >>> info.fireTransition('init')\n\nWe should have two manual transitions::\n\n >>> sorted(interfaces.IWorkflowInfo(document).getManualTransitionIds())\n ['publish 1', 'publish 2']\n\nAnd a single automatic transition::\n\n >>> interfaces.IWorkflowInfo(document).getAutomaticTransitionIds()\n ['publish auto']\n\nProtecting transitions with permissions\n---------------------------------------\n\nTransitions can be (and should be) protected with a permission, so\nthat not everybody can execute them.\n\nLet's set up a workflow with a permission that has a permission::\n\n >>> publish_transition = workflow.Transition(\n ... transition_id='publish',\n ... title='Publish',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL,\n ... permission=\"zope.ManageContent\")\n\nQuickly set up the workflow state again for a document::\n\n >>> workflow_versions.clear()\n >>> wf = workflow.Workflow([init_transition, publish_transition])\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n >>> document = Document('bar')\n >>> info = interfaces.IWorkflowInfo(document)\n >>> info.fireTransition('init')\n\nLet's set up the security context::\n\n >>> from zope.security.interfaces import Unauthorized\n >>> from zope.security.management import newInteraction, endInteraction\n >>> class Principal:\n ... def __init__(self, id):\n ... self.id = id\n ... self.groups = []\n >>> class Participation:\n ... interaction = None\n ... def __init__(self, principal):\n ... self.principal = principal\n >>> endInteraction() # XXX argh, apparently one is already active?\n >>> newInteraction(Participation(Principal('bob')))\n\nWe shouldn't see this permission appear in our list of possible transitions,\nas we do not have access::\n\n >>> info.getManualTransitionIds()\n []\n\nNow let's try firing the transition. It should fail with Unauthorized::\n\n >>> try:\n ... info.fireTransition('publish')\n ... except Unauthorized:\n ... print(\"Got unauthorized\")\n Got unauthorized\n\nIt's also not allowed for ``fireTransitionToward``::\n\n >>> info.fireTransitionToward(PUBLISHED)\n Traceback (most recent call last):\n ...\n hurry.workflow.interfaces.NoTransitionAvailableError: source: \"unpublished\" destination: \"published\"\n\nIn this case, the transition even't even available because the user\ndoesn't have the right permission.\n\nThe system user is however allowed to do it::\n\n >>> from zope.security.management import system_user\n >>> endInteraction()\n >>> newInteraction(Participation(system_user))\n >>> info.fireTransition('publish')\n\nAnd this goes off without a problem.\n\nThere is also a special way to make it happen by passing check_security is\nFalse to fireTransition::\n\n >>> endInteraction()\n >>> newInteraction(Participation(Principal('bob')))\n >>> interfaces.IWorkflowState(document).setState(UNPUBLISHED)\n >>> info.fireTransition('publish', check_security=False)\n\nThis also works with fireTransitionToward::\n\n >>> interfaces.IWorkflowState(document).setState(UNPUBLISHED)\n >>> info.fireTransitionToward(PUBLISHED, check_security=False)\n\n\nSide effects during transitions\n-------------------------------\n\nSometimes we would like something to get executed *before* the\nWorkflowTransitionEvent is fired, but after a (potential) new version\nof the object has been created. If an object is edited during the\nsame request as a workflow transition, the editing should take place\nafter a potential new version has been created, otherwise the old, not\nthe new, version will be edited.\n\nIf something like a history logger hooks into IWorkflowTransitionEvent\nhowever, it would get information about the new copy *before* the\nediting took place. To allow an editing to take place between the\ncreation of the new copy and the firing of the event, a side effect\nfunction can be passed along when a transition is fired.\n\nThe sequence of execution then is:\n\n* firing of transition itself, creating a new version\n\n* executing the side effect function on the new version\n\n* firing the IWorkflowTransitionEvent\n\nLet's set up a very simple workflow:\n\n >>> foo_transition = workflow.Transition(\n ... transition_id='foo',\n ... title='Foo',\n ... source=UNPUBLISHED,\n ... destination=PUBLISHED,\n ... condition=NullCondition,\n ... action=CopyAction,\n ... trigger=interfaces.MANUAL)\n\nQuickly set up the workflow state again for a document::\n\n >>> workflow_versions.clear()\n >>> wf = workflow.Workflow([init_transition, foo_transition])\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n >>> document = Document('bar')\n >>> events = []\n >>> info = interfaces.IWorkflowInfo(document)\n >>> info.fireTransition('init')\n\nNow let's set up a side effect::\n\n >>> def side_effect(context):\n ... context.title = context.title + '!'\n\nNow fire the transition, with a side effect::\n\n >>> new_version = info.fireTransition('foo', side_effect=side_effect)\n\nThe title of the new version should now have a ! at the end::\n\n >>> new_version.title[-1] == '!'\n True\n\nBut the old version doesn't::\n\n >>> document.title[-1] == '!'\n False\n\nThe events list we set up before should contain two events::\n\n >>> len(events)\n 2\n >>> events[1].object.title[-1] == '!'\n True\n\nAmbiguous transitions\n=====================\n\nLet's set up a situation where there are two equivalent transitions from\n``a`` to ``b``::\n\n >>> transition1 = workflow.Transition(\n ... transition_id='a_to_b1',\n ... title='A to B',\n ... source='a',\n ... destination='b',\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL)\n\n >>> transition2 = workflow.Transition(\n ... transition_id='a_to_b2',\n ... title='A to B',\n ... source='a',\n ... destination='b',\n ... condition=NullCondition,\n ... action=NullAction,\n ... trigger=interfaces.MANUAL)\n\n\n >>> wf = workflow.Workflow([transition1, transition2])\n >>> from zope import component\n >>> component.provideUtility(wf, interfaces.IWorkflow)\n >>> info = interfaces.IWorkflowInfo(document)\n >>> state = interfaces.IWorkflowState(document)\n >>> state.setState('a')\n\n``fireTransitionToward`` is ambiguous as two transitions are possible::\n\n >>> info.fireTransitionToward('b')\n Traceback (most recent call last):\n ...\n hurry.workflow.interfaces.AmbiguousTransitionError: source: \"a\" destination: \"b\"\n\n >>> from hurry.workflow.interfaces import AmbiguousTransitionError\n >>> try:\n ... info.fireTransitionToward('b')\n ... except AmbiguousTransitionError as e_:\n ... e = e_\n >>> e.source\n 'a'\n >>> e.destination\n 'b'\n\n\n\nDownload\n========", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "", "keywords": "zope zope3", "license": "ZPL2.1", "maintainer": "", "maintainer_email": "", "name": "hurry.workflow", "package_url": "https://pypi.org/project/hurry.workflow/", "platform": "", "project_url": "https://pypi.org/project/hurry.workflow/", "project_urls": null, "release_url": "https://pypi.org/project/hurry.workflow/3.0.2/", "requires_dist": null, "requires_python": "", "summary": "hurry.workflow is a simple workflow system. It can be used to implement stateful multi-version workflows for Zope Toolkit applications.", "version": "3.0.2" }, "last_serial": 3494073, "releases": { "0.10": [ { "comment_text": "", "digests": { "md5": "87eb0c511d4a8ba6c23114ff44020b83", "sha256": "00daf4a125c70502c2a64fe794b7ba9524db6c29ab2d58645b30cd63dd0116a5" }, "downloads": -1, "filename": "hurry.workflow-0.10.tar.gz", "has_sig": false, "md5_digest": "87eb0c511d4a8ba6c23114ff44020b83", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24884, "upload_time": "2009-11-19T07:42:55", "url": "https://files.pythonhosted.org/packages/e8/17/5ba26130623385eeecc25db5bb047a951f89f82e2a2f9b0cb9ac4870a568/hurry.workflow-0.10.tar.gz" } ], "0.11": [ { "comment_text": "", "digests": { "md5": "afb8fc825ec06cdf8099d85fde332b3a", "sha256": "d2452a79e279f9c812006c1faa095627f560a503c83f6f5fc04a4896e8c46274" }, "downloads": -1, "filename": "hurry.workflow-0.11.tar.gz", "has_sig": false, "md5_digest": "afb8fc825ec06cdf8099d85fde332b3a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24598, "upload_time": "2010-04-16T15:34:29", "url": "https://files.pythonhosted.org/packages/12/3b/5abf4af0b5ef2fd8cb476f38abcdb8b8bd7a4dd91e6a2096dedfd2db1239/hurry.workflow-0.11.tar.gz" } ], "0.12": [ { "comment_text": "", "digests": { "md5": "dd0b4e09df04ecbd509666a98cc1e34a", "sha256": "97f00a78a6c304c92d60683104fa41323db0a8bd0d33a1caaa2f8180c92b1806" }, "downloads": -1, "filename": "hurry.workflow-0.12.tar.gz", "has_sig": false, "md5_digest": "dd0b4e09df04ecbd509666a98cc1e34a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 28981, "upload_time": "2012-02-10T18:00:36", "url": "https://files.pythonhosted.org/packages/a3/c6/fd4d2094fa81d9f49e5b4e102349c8f3fb453c93d818582b4b017112e786/hurry.workflow-0.12.tar.gz" } ], "0.13": [ { "comment_text": "", "digests": { "md5": "cff6a7ba2585f9bc3f86e35faa0700b9", "sha256": "aaeefdd43087f7cc5e803ce2a15e21ce0ad42e4931cf5914bc705f0d3e2d88ae" }, "downloads": -1, "filename": "hurry.workflow-0.13.tar.gz", "has_sig": false, "md5_digest": "cff6a7ba2585f9bc3f86e35faa0700b9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32640, "upload_time": "2013-01-17T15:31:21", "url": "https://files.pythonhosted.org/packages/72/7c/690fb024dff3d91dcc7eb7aae4f2963ca36116f911ff3414df53184a5933/hurry.workflow-0.13.tar.gz" } ], "0.13.1": [ { "comment_text": "", "digests": { "md5": "6320059d03b50d7df8597ee3c41c9c63", "sha256": "bae7eee89b6a6015e22e0c75befc716b28c2f20325e2109274d2a82f5a5a1ae9" }, "downloads": -1, "filename": "hurry.workflow-0.13.1.tar.gz", "has_sig": false, "md5_digest": "6320059d03b50d7df8597ee3c41c9c63", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32999, "upload_time": "2013-01-17T15:54:59", "url": "https://files.pythonhosted.org/packages/f0/9f/71b6a36fa588fcab1f8724cb8234b029e1011f5d3a8e6aae8f93fa721b72/hurry.workflow-0.13.1.tar.gz" } ], "0.9.1": [ { "comment_text": "", "digests": { "md5": "e36d2b2ac5953f5b9c46a999ee332f47", "sha256": "8971d65c58cf1099ac2643cd721e73b01366423b038f831be9d8f36efb5a4844" }, "downloads": -1, "filename": "hurry.workflow-0.9.1-py2.4.egg", "has_sig": false, "md5_digest": "e36d2b2ac5953f5b9c46a999ee332f47", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 22466, "upload_time": "2006-09-22T16:48:36", "url": "https://files.pythonhosted.org/packages/3c/55/7e34102803d022ef7d1be191531a00f89e840be53df83254caf0f5169ac7/hurry.workflow-0.9.1-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "463d74d3a41846f39935aea7ff75fb06", "sha256": "9367759fb54e0821dd5f552cf0487168975baf2adbbf6e795698aa2f112c85e1" }, "downloads": -1, "filename": "hurry.workflow-0.9.1.tar.gz", "has_sig": false, "md5_digest": "463d74d3a41846f39935aea7ff75fb06", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12329, "upload_time": "2006-09-22T16:48:26", "url": "https://files.pythonhosted.org/packages/05/0a/099715c2b3e4b25eb3ab2c54ab71de395efd29bc2ddd05392ede8c9fe9fc/hurry.workflow-0.9.1.tar.gz" } ], "0.9.2": [ { "comment_text": "", "digests": { "md5": "46f43fd26fefd71ae101896e1c501108", "sha256": "6370199cc87eed4d1b84bd63e3401fa53245feeaf982c0a6b852db6463b2ddb8" }, "downloads": -1, "filename": "hurry.workflow-0.9.2-py2.4.egg", "has_sig": false, "md5_digest": "46f43fd26fefd71ae101896e1c501108", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 28336, "upload_time": "2007-08-15T17:50:20", "url": "https://files.pythonhosted.org/packages/d1/54/0e4918b11123080cf22721b0c9ef8a489da5b98c5a1d81672ed32b7910a1/hurry.workflow-0.9.2-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "dd75429844f1714413e5b793a8ed859b", "sha256": "70bc66afa36f14365bdd039919c9547df5fe6731352373a2e03766a8e4676714" }, "downloads": -1, "filename": "hurry.workflow-0.9.2.tar.gz", "has_sig": false, "md5_digest": "dd75429844f1714413e5b793a8ed859b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20604, "upload_time": "2007-08-15T17:50:16", "url": "https://files.pythonhosted.org/packages/6a/42/e36a5d92b0c3e3c69f55034246c3a9b0659169608a846b7bc04dfa015e47/hurry.workflow-0.9.2.tar.gz" } ], "0.9.2.1": [ { "comment_text": "", "digests": { "md5": "3456357cbebd8ded690c13a80f233086", "sha256": "f6ee74cd314cb4c20b1d89f7d5bee63702e9ddc32427c552ad2ee0593797e0f1" }, "downloads": -1, "filename": "hurry.workflow-0.9.2.1-py2.4.egg", "has_sig": false, "md5_digest": "3456357cbebd8ded690c13a80f233086", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 28359, "upload_time": "2007-08-15T19:13:50", "url": "https://files.pythonhosted.org/packages/4f/9b/af664e9737e056198cc99c6aa7903d35247761924f3be54546a66b52b0ea/hurry.workflow-0.9.2.1-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "25aec2589a36eba74f129fc223829b81", "sha256": "9049bb856af39064edc650d2511d86c31244238a79d9f489fab439549394e1b6" }, "downloads": -1, "filename": "hurry.workflow-0.9.2.1.tar.gz", "has_sig": false, "md5_digest": "25aec2589a36eba74f129fc223829b81", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20720, "upload_time": "2007-08-15T19:13:54", "url": "https://files.pythonhosted.org/packages/a8/b4/e6506b51251b9f0261173f4df2752f68fc56dcecd00bb29d8304556a21f6/hurry.workflow-0.9.2.1.tar.gz" } ], "3.0.1": [ { "comment_text": "", "digests": { "md5": "60c4947b28225e194c5c97bef55e5c3d", "sha256": "1aeb3554d6a2f53d205a3f5b951c899b8c582d916a71b5f6dd36e3de62d08ec0" }, "downloads": -1, "filename": "hurry.workflow-3.0.1.tar.gz", "has_sig": false, "md5_digest": "60c4947b28225e194c5c97bef55e5c3d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 33950, "upload_time": "2018-01-16T13:32:24", "url": "https://files.pythonhosted.org/packages/5e/32/f7892312f33f8f265d383bf7a8371d51ca5f610b4799c5817901cd4d2206/hurry.workflow-3.0.1.tar.gz" } ], "3.0.2": [ { "comment_text": "", "digests": { "md5": "4556874091eecdbd846fa16e549f19d3", "sha256": "3fcaee702a133403a4337c39ed6938e37a6130a45c5a8c52b2bcec58b4bc9e6f" }, "downloads": -1, "filename": "hurry.workflow-3.0.2.tar.gz", "has_sig": false, "md5_digest": "4556874091eecdbd846fa16e549f19d3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34016, "upload_time": "2018-01-16T13:48:07", "url": "https://files.pythonhosted.org/packages/4c/1a/b1ac73ae32200d7dcd964a95b1722d51f60d7473f858c0bd4d83b4451d87/hurry.workflow-3.0.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "4556874091eecdbd846fa16e549f19d3", "sha256": "3fcaee702a133403a4337c39ed6938e37a6130a45c5a8c52b2bcec58b4bc9e6f" }, "downloads": -1, "filename": "hurry.workflow-3.0.2.tar.gz", "has_sig": false, "md5_digest": "4556874091eecdbd846fa16e549f19d3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34016, "upload_time": "2018-01-16T13:48:07", "url": "https://files.pythonhosted.org/packages/4c/1a/b1ac73ae32200d7dcd964a95b1722d51f60d7473f858c0bd4d83b4451d87/hurry.workflow-3.0.2.tar.gz" } ] }