===============
XPDL-2.1 Import
===============

Here we will demonstrate, that xpdl-2.1 files are also importable and works.
For general description of how to work with XPDL files, refer xpdl.txt.

    >>> from shoobx.wfmc import xpdl
    >>> xpdl.RAISE_ON_DUPLICATE_ERROR = True
    >>> import os
    >>> package = xpdl.read(open(os.path.join(this_directory,
    ...                                       'publication-2.1.xpdl')))


This package contains a single definition:

    >>> package
    {'Publication': ProcessDefinition('Publication')}

    >>> pd = package['Publication']
    >>> from shoobx.wfmc.attributeintegration import AttributeIntegration
    >>> integration = AttributeIntegration()
    >>> pd.integration = integration

Now, having read the process definition, we can use it as we did
before (in "README.txt").  As before, we'll create an event subscriber
so that we can see what's going on:

    >>> def log_workflow(event):
    ...     print (event)

    >>> import zope.event
    >>> zope.event.subscribers.append(log_workflow)

and we'll register the process definition as a utility:

    >>> import zope.component
    >>> from shoobx.wfmc.process import StaticProcessDefinitionFactory
    >>> pdfactory = StaticProcessDefinitionFactory()
    >>> zope.component.provideUtility(pdfactory)
    >>> pdfactory.register(pd)

and we'll define and register participant and application adapters:

    >>> import zope.interface
    >>> from shoobx.wfmc import interfaces

    >>> @zope.interface.implementer(interfaces.IParticipant)
    ... class Participant(object):
    ...     zope.component.adapts(interfaces.IActivity)
    ...
    ...     def __init__(self, activity, process):
    ...         self.activity = activity

    >>> class User:
    ...     def __init__(self):
    ...         self.work_list = []

    >>> authors = {'bob': User(), 'ted': User(), 'sally': User()}

    >>> reviewer = User()
    >>> tech1 = User()
    >>> tech2 = User()

    >>> class Author(Participant):
    ...     def __init__(self, activity, process):
    ...         Participant.__init__(self, activity, process)
    ...         author_name = activity.process.workflowRelevantData.author
    ...         print("Author `%s` selected" % author_name)
    ...         self.user = authors[author_name]

    >>> integration.authorParticipant = Author

    >>> class Reviewer(Participant):
    ...     user = reviewer
    >>> integration.reviewerParticipant = Reviewer

    >>> class Tech1(Participant):
    ...     user = tech1
    >>> integration.tech1Participant = Tech1

    >>> class Tech2(Participant):
    ...     user = tech2
    >>> integration.tech2Participant = Tech2

    >>> integration.SystemParticipant = Participant

    >>> @zope.interface.implementer(interfaces.IWorkItem)
    ... class ApplicationBase(object):
    ...     zope.component.adapts(interfaces.IParticipant)
    ...
    ...     def __init__(self, participant, process, activity):
    ...         self.participant = participant
    ...         self.activity = participant.activity
    ...         participant.user.work_list.append(self)
    ...
    ...     def start(self, args):
    ...         pass
    ...
    ...     def finish(self):
    ...         self.participant.activity.workItemFinished(self)

    >>> class Prepare(ApplicationBase):
    ...
    ...     def summary(self):
    ...         process = self.activity.process
    ...         doc = getattr(process.applicationRelevantData, 'doc', '')
    ...         if doc:
    ...             print('Previous draft:')
    ...             print(doc)
    ...             print('Changes we need to make:')
    ...             for change in process.workflowRelevantData.tech_changes:
    ...                 print(change)
    ...         else:
    ...             print('Please write the initial draft')
    ...
    ...     def finish(self, doc):
    ...         self.activity.process.applicationRelevantData.doc = doc
    ...         super(Prepare, self).finish()

    >>> integration.prepareWorkItem = Prepare

    >>> class TechReview(ApplicationBase):
    ...
    ...     def getDoc(self):
    ...         return self.activity.process.applicationRelevantData.doc
    ...
    ...     def finish(self, decision, changes):
    ...         output = {'publish': decision, 'tech_changes': changes}
    ...         self.activity.workItemFinished(self, output)

    >>> integration.tech_reviewWorkItem = TechReview

    >>> class Review(TechReview):
    ...
    ...     def start(self, args):
    ...         publish1 = args['publish1']
    ...         publish2 = args['publish2']
    ...         changes1 = args['tech_changes1']
    ...         changes2 = args['tech_changes2']
    ...         if not (publish1 and publish2):
    ...             output = {'publish': False,
    ...                       'tech_changes': changes1 + changes2,
    ...                       'ed_changes': ()}
    ...             # Reject if either tech reviewer rejects
    ...             self.activity.workItemFinished(
    ...                 self, output)
    ...
    ...         if changes1 or changes2:
    ...             output = {'publish': True,
    ...                       'tech_changes': changes1 + changes2,
    ...                       'ed_changes': ()}
    ...             # we won't do anyting if there are tech changes
    ...             self.activity.workItemFinished(
    ...                 self, output)
    ...
    ...     def finish(self, ed_changes):
    ...         output = {'publish': True, 'tech_changes': (), 'ed_changes': ed_changes}
    ...         self.activity.workItemFinished(self, output)

    >>> integration.ed_reviewWorkItem = Review

    >>> class Final(ApplicationBase):
    ...
    ...     def summary(self):
    ...         process = self.activity.process
    ...         doc = getattr(process.applicationRelevantData, 'doc', '')
    ...         print('Previous draft:')
    ...         print(self.activity.process.applicationRelevantData.doc)
    ...         print('Changes we need to make:')
    ...         for change in process.workflowRelevantData.ed_changes:
    ...            print(change)
    ...
    ...     def finish(self, doc):
    ...         self.activity.process.applicationRelevantData.doc = doc
    ...         super(Final, self).finish()

    >>> integration.finalWorkItem = Final

    >>> class ReviewFinal(TechReview):
    ...
    ...     def finish(self, ed_changes):
    ...         output = {'publish': True, 'tech_changes': (), 'ed_changes': ed_changes}
    ...         self.activity.workItemFinished(self, output)

    >>> integration.rfinalWorkItem = ReviewFinal


    >>> @zope.interface.implementer(interfaces.IWorkItem)
    ... class Publish:
    ...     zope.component.adapts(interfaces.IParticipant)
    ...
    ...     def __init__(self, participant, process, activity):
    ...         self.participant = participant
    ...
    ...     def start(self, args):
    ...         print("Published")
    ...         self.finish()
    ...
    ...     def finish(self):
    ...         self.participant.activity.workItemFinished(self)


    >>> integration.publishWorkItem = Publish

    >>> class Reject(Publish):
    ...     def start(self):
    ...         print("Rejected")
    ...         self.finish()

    >>> integration.rejectWorkItem = Reject

and a process context, so we can pass parameters:

    >>> @zope.interface.implementer(interfaces.IProcessContext)
    ... class PublicationContext:
    ...     pass


Now, let's try out our process.  We'll follow the same steps we did in
"README.txt", getting the same results:

    >>> context = PublicationContext()
    >>> proc = pd(context)
    >>> proc.start('bob')
    ProcessStarted(Process('Publication'))
    Transition(None, Activity('Publication.start'))
    ActivityStarted(Activity('Publication.start'))
    ActivityFinished(Activity('Publication.start'))
    Author `bob` selected
    Transition(Activity('Publication.start'),
               Activity('Publication.prepare'))
    ActivityStarted(Activity('Publication.prepare'))
    WorkItemStarting('prepare')
    WorkItemStarted('prepare')

    >>> item = authors['bob'].work_list.pop()
    >>> item.finish("I give my pledge, as an American\n"
    ...             "to save, and faithfully to defend from waste\n"
    ...             "the natural resources of my Country.")
    WorkItemFinished('prepare')
    ActivityFinished(Activity('Publication.prepare'))
    Transition(Activity('Publication.prepare'), Activity('Publication.review0'))
    ActivityStarted(Activity('Publication.review0'))
    ActivityFinished(Activity('Publication.review0'))
    Transition(Activity('Publication.review0'),
               Activity('Publication.tech1'))
    ActivityStarted(Activity('Publication.tech1'))
    WorkItemStarting('tech_review')
    WorkItemStarted('tech_review')
    Transition(Activity('Publication.review0'),
               Activity('Publication.tech2'))
    ActivityStarted(Activity('Publication.tech2'))
    WorkItemStarting('tech_review')
    WorkItemStarted('tech_review')

    >>> item = tech1.work_list.pop()
    >>> print(item.getDoc())
    I give my pledge, as an American
    to save, and faithfully to defend from waste
    the natural resources of my Country.

    >>> item.finish(True, ['Change "American" to "human"'])
    WorkItemFinished('tech_review')
    ActivityFinished(Activity('Publication.tech1'))
    Transition(Activity('Publication.tech1'),
               Activity('Publication.review1'))

    >>> item = tech2.work_list.pop()
    >>> item.finish(True, ['Change "Country" to "planet"'])
    WorkItemFinished('tech_review')
    ActivityFinished(Activity('Publication.tech2'))
    Transition(Activity('Publication.tech2'), Activity('Publication.review1'))
    ActivityStarted(Activity('Publication.review1'))
    ActivityFinished(Activity('Publication.review1'))
    Transition(Activity('Publication.review1'),
               Activity('Publication.review'))
    ActivityStarted(Activity('Publication.review'))
    WorkItemStarting('ed_review')
    WorkItemFinished('ed_review')
    ActivityFinished(Activity('Publication.review'))
    Transition(Activity('Publication.review'), Activity('Publication.review2'))
    ActivityStarted(Activity('Publication.review2'))
    ActivityFinished(Activity('Publication.review2'))
    Author `bob` selected
    Transition(Activity('Publication.review2'),
               Activity('Publication.prepare'))
    ActivityStarted(Activity('Publication.prepare'))
    WorkItemStarting('prepare')
    WorkItemStarted('prepare')
    WorkItemStarted('ed_review')

    >>> item = authors['bob'].work_list.pop()
    >>> item.summary()
    Previous draft:
    I give my pledge, as an American
    to save, and faithfully to defend from waste
    the natural resources of my Country.
    Changes we need to make:
    Change "American" to "human"
    Change "Country" to "planet"

    >>> item.finish("I give my pledge, as an human\n"
    ...             "to save, and faithfully to defend from waste\n"
    ...             "the natural resources of my planet.")
    WorkItemFinished('prepare')
    ActivityFinished(Activity('Publication.prepare'))
    Transition(Activity('Publication.prepare'), Activity('Publication.review0'))
    ActivityStarted(Activity('Publication.review0'))
    ActivityFinished(Activity('Publication.review0'))
    Transition(Activity('Publication.review0'),
               Activity('Publication.tech1'))
    ActivityStarted(Activity('Publication.tech1'))
    WorkItemStarting('tech_review')
    WorkItemStarted('tech_review')
    Transition(Activity('Publication.review0'),
               Activity('Publication.tech2'))
    ActivityStarted(Activity('Publication.tech2'))
    WorkItemStarting('tech_review')
    WorkItemStarted('tech_review')

    >>> item = tech1.work_list.pop()
    >>> item.finish(True, [])
    WorkItemFinished('tech_review')
    ActivityFinished(Activity('Publication.tech1'))
    Transition(Activity('Publication.tech1'),
               Activity('Publication.review1'))

    >>> item = tech2.work_list.pop()
    >>> item.finish(True, [])
    WorkItemFinished('tech_review')
    ActivityFinished(Activity('Publication.tech2'))
    Transition(Activity('Publication.tech2'), Activity('Publication.review1'))
    ActivityStarted(Activity('Publication.review1'))
    ActivityFinished(Activity('Publication.review1'))
    Transition(Activity('Publication.review1'),
               Activity('Publication.review'))
    ActivityStarted(Activity('Publication.review'))
    WorkItemStarting('ed_review')
    WorkItemStarted('ed_review')

    >>> item = reviewer.work_list.pop()
    >>> print(item.getDoc())
    I give my pledge, as an human
    to save, and faithfully to defend from waste
    the natural resources of my planet.

    >>> item.finish(['change "an" to "a"'])
    WorkItemFinished('ed_review')
    ActivityFinished(Activity('Publication.review'))
    Transition(Activity('Publication.review'), Activity('Publication.review2'))
    ActivityStarted(Activity('Publication.review2'))
    ActivityFinished(Activity('Publication.review2'))
    Author `bob` selected
    Transition(Activity('Publication.review2'),
               Activity('Publication.final'))
    ActivityStarted(Activity('Publication.final'))
    WorkItemStarting('final')
    WorkItemStarted('final')

    >>> item = authors['bob'].work_list.pop()
    >>> item.summary()
    Previous draft:
    I give my pledge, as an human
    to save, and faithfully to defend from waste
    the natural resources of my planet.
    Changes we need to make:
    change "an" to "a"

    >>> item.finish("I give my pledge, as a human\n"
    ...             "to save, and faithfully to defend from waste\n"
    ...             "the natural resources of my planet.")
    WorkItemFinished('final')
    ActivityFinished(Activity('Publication.final'))
    Transition(Activity('Publication.final'),
               Activity('Publication.rfinal'))
    ActivityStarted(Activity('Publication.rfinal'))
    WorkItemStarting('rfinal')
    WorkItemStarted('rfinal')

    >>> item = reviewer.work_list.pop()
    >>> print(item.getDoc())
    I give my pledge, as a human
    to save, and faithfully to defend from waste
    the natural resources of my planet.

    >>> item.finish([])
    WorkItemFinished('rfinal')
    ActivityFinished(Activity('Publication.rfinal'))
    Transition(Activity('Publication.rfinal'), Activity('Publication.rfinal1'))
    ActivityStarted(Activity('Publication.rfinal1'))
    ActivityFinished(Activity('Publication.rfinal1'))
    Transition(Activity('Publication.rfinal1'),
               Activity('Publication.publish'))
    ActivityStarted(Activity('Publication.publish'))
    WorkItemStarting('publish')
    Published
    WorkItemFinished('publish')
    ActivityFinished(Activity('Publication.publish'))
    Transition(Activity('Publication.publish'), Activity('Publication.finish'))
    ActivityStarted(Activity('Publication.finish'))
    ActivityFinished(Activity('Publication.finish'))
    ProcessFinished(Process('Publication'))
    WorkItemStarted('publish')

    >>> proc.workflowRelevantData.publish
    True


Descriptions
------------

Most process elements can have names and descriptions.

    >>> pd.__name__
    'Publication'

    >>> pd.description
    'This is the sample process'

    >>> pd.applications['prepare'].__name__
    'Prepare'

    >>> pd.applications['prepare'].description
    'Prepare the initial draft'

    >>> pd.activities['tech1'].__name__
    'Technical Review 1'

    >>> pd.activities['tech1'].description
    'This is the first Technical Review.'

    >>> pd.participants['tech1'].__name__
    'Technical Reviewer 1'

    >>> pd.participants['tech1'].description
    'He is a smart guy.'

    >>> sorted([item.__name__ for item in pd.transitions])
    ['Transition', 'Transition', 'Transition', 'Transition',
    'Transition', 'Transition',
    'Transition', 'Transition', 'Transition', 'Transition',
    'Transition', 'Transition', 'Transition', 'Transition',
    'Transition', 'Transition', 'Transition to Tech Review 1',
    'Transition to Tech Review 2']

    >>> descriptions = [item.description for item in pd.transitions if item.description]
    >>> 'Use this transition if there are editorial changes required.' in descriptions
    True


Pools and Lanes
---------------

The package gets the Pools and Lanes from the XPDL

Pools:

    >>> package.pools
    {'Publication_pool1': Pool('Publication', 'Publication')}

Pool.process_def is the reference to the ProcessDefinition itself

    >>> package.pools['Publication_pool1'].process_def
    'Publication'

Lanes:

    >>> sorted(package.pools['Publication_pool1'].lanes.items())
    [('Publication_pool1_lan1', Lane('Author')),
     ('Publication_pool1_lan2', Lane('Technical Reviewer 1')),
     ('Publication_pool1_lan3', Lane('Technical Reviewer 2')),
     ('Publication_pool1_lan4', Lane('Editorial Reviewer')),
     ('Publication_pool1_lan5', Lane('System')),
     ('Publication_pool1_lan6', Lane('Expression lane'))]

A single Lane has the performers:

    >>> package.pools['Publication_pool1'].lanes[
    ...     'Publication_pool1_lan4'].performers
    ('reviewer',)


Extended attributes
-------------------

Extended attributes are stored in ActivityDefinition.attributes dictionary

    >>> author_activity = pd.activities['prepare']
    >>> author_activity.attributes
    OrderedDict([('ParticipantID', 'author'),
                 ('XOffset', '110'),
                 ('YOffset', '20')])

We have extended attributes also on Participants

    >>> pd.participants['System'].attributes
    OrderedDict([('SubSystemID', 'HAL9000')])
    >>> pd.participants['tech1'].attributes
    OrderedDict([('Capability', 'Required')])

Extended attributes can be simple Values or Complex content, but the parser
makes this transparent.

    >>> reject_activity = pd.activities['reject']
    >>> reject_activity.attributes
    OrderedDict([('ParticipantID', 'System'),
                 ('XOffset', '540'),
                 ('YOffset', '80'),
                 ('Comment', 'Push back to <Q&A>')])

Either way XML reserved characters need to be handled right

    >>> publish_activity = pd.activities['publish']
    >>> publish_activity.attributes
    OrderedDict([('ParticipantID', 'System'),
                 ('XOffset', '540'),
                 ('YOffset', '20'),
                 ('Comment', 'Push to <Production>')])

Scripts
-------

An activity can be also a system script:

    >>> fname = 'publication-2.1-script.xpdl'
    >>> package = xpdl.read(open(os.path.join(this_directory, fname)))
    >>> pd = package['Publication']
    >>> publish_activity = pd.activities['publish']
    >>> publish_activity.scripts
    ('from publisher import push\npush(content)',)


Edge cases
-----------

Just in case there are duplicate ExtendedAttribute Names for a single
ExtendedAttributes container, raise an error.
Having two ExtendedAttribute elements is nonsense.

    >>> fname = 'publication-2.1-duplicate-ExtendedAttribute.xpdl'
    >>> package = xpdl.read(open(os.path.join(this_directory, fname))) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    shoobx.wfmc.xpdl.HandlerError: "The Name 'ParticipantID' is already used, uniqueness violated, value: 'system' see: Publication."
    File ".../shoobx/wfmc/publication-2.1-duplicate-ExtendedAttribute.xpdl", line 240. in ExtendedAttribute

Sometimes we can't always raise an exception when this happens so instead
we can record the parse error.
    >>> xpdl.RAISE_ON_DUPLICATE_ERROR = False
    >>> package = xpdl.read(open(os.path.join(this_directory, fname)))
    >>> package.parseErrors
    ["The Name 'ParticipantID' is already used, uniqueness violated, value: 'system' see: Publication."]


Just in case there are duplicate Deadlines for a single
ExtendedAttributes container, raise an error.
Having two ExtendedAttribute elements is nonsense.

    >>> fname = 'publication-2.1-duplicate-Deadline.xpdl'
    >>> package = xpdl.read(open(os.path.join(this_directory, fname))) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    shoobx.wfmc.xpdl.HandlerError: Current implementation only supports one deadline per activity
    File ".../shoobx/wfmc/publication-2.1-duplicate-Deadline.xpdl", line 376. in Deadline
