def createWorkItems(self, activity_ids, source, content_object=None): """Creates a new workitem for the activity with the given name. Raises UnknownActivityError if one or more of the given activities do not exist. """ if not activity_ids: return () utils.logger.log( logging.DEBUG, 'Creating workitems %r in %r' % (activity_ids, source)) self._creating += 1 try: passed_activities = self._veto_workitem_creation( activity_ids, source) new_ids, new = self._create_workitems_helper( passed_activities, source, content_object) source.generated_workitems += tuple(new_ids) for wi in new: controller = ILifeCycleController(wi) controller.start( "Work item was created and automatically started.") self.notifyWorkItemStateChange() finally: self._creating -= 1 # We need to explicitly check for completeness here again, because we # blocked the checks during creation. self._check_complete(self.getWorkItems('active')) return new_ids
def _rememberTrigger(self, triggering_workitem, route): mode = self.getActivity().mode all_routes = self._getAllRoutes() closed_routes = self.closed_routes if route in closed_routes: return controller = ILifeCycleController(self) if mode == MULTI_MERGE: closed_routes.append(route) self._doTrigger(triggering_workitem, route) if len(closed_routes) == len(all_routes): self.completing_work_item = triggering_workitem # XXX no test for this controller.complete("Gate '%s' found by route '%s'" % (self.activity_id, route)) elif mode == DISCRIMINATE: if len(closed_routes) == 0: self._doTrigger(triggering_workitem, route) self.completing_work_item = triggering_workitem controller.complete("Gate '%s' found by route '%s'" % (self.activity_id, route)) closed_routes.append(route) elif mode == DELAYED_DISCRIMINATE: self.delayed_discriminator = True elif mode == SYNCHRONIZING_MERGE: closed_routes.append(route) if len(closed_routes) == len(all_routes): self._doTrigger(triggering_workitem, route) self.completing_work_item = triggering_workitem controller.complete("Gate '%s' completed via route '%s'" % (self.activity_id, route)) else: # XXX no tests for this controller.fail("Unknown gate mode: '%s'" % mode) self.closed_routes = closed_routes
def onTermination(self): """Trigger that gets called before the object is terminated.""" for inst in self.objectValues(): controller = ILifeCycleController(inst) if controller.state != 'ended': controller.terminate('Terminated due to termination of %s.' % self.log_name)
def onReset(self): """Trigger that gets called after the object is reset.""" for inst in self.objectValues(): controller = ILifeCycleController(inst) if controller.state != 'ended': controller.terminate('Terminated due to reset of %s.' % self.log_name)
def test_recursive(self): doc = self._init_object('workflows/termination_recursive.alf') instance = doc.getInstance() controller = ILifeCycleController(instance) controller.start("testing") task = instance.getWorkItems()[0] task.complete(exit="complete") self.assertEquals('ended', controller.state) self.assertEquals(True, controller.completed)
def test_object_deletion(self): doc = self._init_object('workflows/termination_delete.alf') instance = doc.getInstance() container = doc.getParentNode() id = doc.getId() assert id in container.objectIds() controller = ILifeCycleController(instance) controller.start("testing") self.assertEquals('ended', controller.state) self.assertEquals(True, controller.completed) self.assert_(id not in container.objectIds())
def test_fallout_startActivity(self): # test for fix of bug #2666 self.loginAsPortalOwner() self._create_test_users() self._import_wf('workflows/fallout_startActivity.alf') doc1 = self.create(self.portal, 'DummyContent', 'doc1') doc1.assignProcess(self.test_process) instance = doc1.getInstance() controller = ILifeCycleController(instance) controller.start('test') self.assertEqual('failed', controller.state) fallout_workitems = instance.getWorkItems('failed') self.assertEqual(1, len(fallout_workitems)) self.assertEqual('become_fallout', fallout_workitems[0].activity_id)
def test_restart(self): self.loginAsPortalOwner() self._import_wf('workflows/task.alf') portal = self.portal doc = self.create(portal, 'DummyContent', 'doc1') doc.assignProcess(self.test_process) instance = doc.getInstance() ILifeCycleController(instance).start('testing') # This was a bug that is explicitly tested for now self.failUnless(instance.aq_base is doc.getInstance().aq_base) ILifeCycleController(instance).reset('more testing') ILifeCycleController(instance).start('more testing') self.failUnless(instance.aq_base is doc.getInstance().aq_base)
class SwitchWorkItem(BaseWorkItem): zope.interface.implements(ISwitchWorkItem) zope.interface.classProvides(IWorkItemClass) security = ClassSecurityInfo() activity_type = "switch" security.declarePrivate('onStart') def onStart(self): "Trigger that gets called after the workitem has been started." super(SwitchWorkItem, self).onStart() activity = self.getActivity() first = activity.mode == "first" for case in activity.getExits(): try: case_result = utils.evaluateTales(case.condition, workitem=self) except Exception, m: ILifeCycleController(self).fail( "Evaluating condition on case %s of activity %s raised an " " exception." % (case.id, activity.title_or_id()), m) return if case_result: self.passCheckpoint(case.id) if first: break ILifeCycleController(self).complete(activity.title_or_id())
def manage_afterAdd(self, item, container): DummyContent.inheritedAttribute('manage_afterAdd')(self, item, container) if len(self.getAllInstances()) > 0: return self.assignProcess(test_process) ILifeCycleController(self.getInstance()).start("testing")
def test_before_start(self): self.loginAsPortalOwner() self._import_wf('workflows/before_start.alf') portal = self.portal wftool = getToolByName(portal, 'workflow_manager') doc = self.create(portal, 'DummyContent', 'doc1') doc.before = 0 doc.after = 0 doc.expr_works = 0 doc.assignProcess(self.test_process) instance = doc.getInstance() ILifeCycleController(instance).start('testing') self.assertEquals(1, doc.expr_works) self.assertEquals(1, doc.before) self.assertEquals(0, doc.after) self.assertEquals([], wftool.processes['test'].current()['some-task'].graphGetStartActivities()) self.assertEquals(1, len(instance.getWorkItems())) task = instance.getWorkItems()[0] self.assertEquals(1, len(task.objectValues())) task_start = task.objectValues()[0] self.assertEquals(1, len(task_start.objectValues())) task_start_expression = task_start.objectValues()[0] self.assert_(isinstance( task_start_expression, Products.AlphaFlow.aspects.expression.ExpressionAspect))
def test_contentretrieve(self): self.loginAsPortalOwner() self._create_test_users() self._import_wf() portal = self.portal fold = self.create(portal, 'DummyFolder', 'f1') doc = self.create(fold, 'DummyContent', 'doc1') doc.setTitle('gaaack') doc.reindexObject() doc.assignProcess(self.test_process) instance = doc.getInstance() ILifeCycleController(instance).start('testing') wi = instance.getWorkItems()[0] def check(ob): self.assertEquals(doc.absolute_url(), ob.getUrl()) self.assertEquals(doc.getPhysicalPath(), ob.getContentObjectPath()) self.assertEquals(doc, ob.getContentObject()) brain = ob.getContentObjectPortalCatalogBrain() self.assertEquals(doc.Title(), brain.Title) self.assertEquals(doc.absolute_url(), brain.getURL()) check(instance) check(wi)
def test_notifyAssigneesChange_content(self): self.loginAsPortalOwner() self._import_wf('workflows/cachetest_multi.alf') self._create_test_users() portal = self.portal alf = getToolByName(portal, 'workflow_manager') portal.invokeFactory('DummyContent', 'doc') doc = portal.doc doc.test_assignees = ['hans'] doc.test_assignees2 = ['hubert'] doc.assignProcess(self.test_process) instance = doc.getInstance() ILifeCycleController(instance).start("Los gehts") self.assertEquals(['ProcessUser'], alf.getDynamicRolesForContent(doc, 'hans')) self.assertEquals(['ProcessUser'], alf.getDynamicRolesForContent(doc, 'hubert')) doc.test_assignees2 = ['karlheinz'] alf.updateCacheByContent(doc) self.assertEquals(['ProcessUser'], alf.getDynamicRolesForContent(doc, 'hans')) self.assertEquals(['ProcessUser'], alf.getDynamicRolesForContent(doc, 'karlheinz')) self.assertRaises(KeyError, alf.getDynamicRolesForContent, doc, 'hubert')
def onStart(self): """Runs the automatic procedure, handles exceptions and moves on.""" try: BaseWorkItem.onStart(self) self.run() except Exception, m: ILifeCycleController(self).fail("Automatic activity failed.", m)
def _is_accepted(self): acc = getattr(self, '_is_accepted_' + self.getActivity().decision_modus)() if not acc: return self.passCheckpoint("accept") ILifeCycleController(self).complete("All assignees accepted.")
def __repr__(self): try: state = ILifeCycleController(self).state except TypeError: state = 'n/a' return '<%s for %r (%s)>' % (self.__class__.__name__, self.activity_id, state)
def _update_cache(self, workitem): """update internal state/workitem cache""" state_to_id = getattr(aq_base(self), '_cache_state_to_id', None) if state_to_id is None: state_to_id = self._cache_state_to_id = {} id_to_state = getattr(aq_base(self), '_cache_id_to_state', None) if id_to_state is None: id_to_state = self._cache_id_to_state = {} to_index = getattr(aq_base(self), '_v_workitems_to_index', None) if to_index is None: to_index = self._v_workitems_to_index = {} id = workitem.getId() new_state = ILifeCycleController(workitem).state old_state = id_to_state.get(id) if new_state != old_state: # remove old state from state_to_id, # NOTE: id_to_state will be overwritten implicitly ids_in_old_state = state_to_id.setdefault(old_state, {}) try: del ids_in_old_state[id] except KeyError: pass # set new state state_to_id.setdefault(new_state, {})[id] = True id_to_state[id] = new_state to_index[id] = 1 self._p_changed = 1
def onRecovery(self): """Trigger that gets called after the object is recovered.""" for inst in self.objectValues(): controller = ILifeCycleController(inst) if controller.state == 'failed': raise ValueError("Can't recover while failed %s are left." % self.log_children_name)
def beforeCreationItems(self, items, parent): vote_no = [] my_parent = self.getParent() # Do the new items belong to a route? route = self._findRouteForWI(parent) if route is None: return [] all_routes = self._getAllRoutes() for wi in items: if wi == self.activity_id: vote_no.append(wi) self._rememberTrigger(parent, route) # XXX This is some special stuff to make the delayed trigger work if self.getActivity().mode == DELAYED_DISCRIMINATE: process = self.getProcess() new_activity = process[wi] if isinstance(new_activity, GateActivity): closed_routes = self.closed_routes if not route in self.closed_routes: closed_routes.append(route) if len(closed_routes) == len(all_routes) and self.delayed_discriminator: self._doTrigger(parent, route) self.completing_work_item = parent ILifeCycleController(self).complete( "Gate '%s' triggered and completed after route " "'%s'" % (self.activity_id, route)) self.delayed_discriminator = False self.closed_routes = closed_routes return vote_no
def accept(self, REQUEST=None): """Accept""" if self.state != "active": raise ValueError, "Can't accept when not active." self.passCheckpoint("accept") ILifeCycleController(self).complete("Accepted.") self.notifyAssigneesChange() self._update_ui_after_action("Review registered.", REQUEST)
def reject(self, REQUEST=None): """Reject""" if self.state != "active": raise ValueError, "Can't reject when not active." self.passCheckpoint("reject") ILifeCycleController(self).complete("Rejected.") self.notifyAssigneesChange() self._update_ui_after_action('Rejected.', REQUEST)
def _veto_workitem_creation(self, activity_ids, source): vetoed = [] for wi in self.getWorkItems(): try: vetoed.extend(wi.beforeCreationItems(activity_ids, source)) except Exception, m: ILifeCycleController(wi).fail("Vetoing failed.", m) return []
def run(self): """Performs the actual automatic activity""" instance = self.getInstance() workitems = instance.getWorkItems() for wi in workitems: if wi is self: continue ILifeCycleController(wi).terminate( "Terminated by %s." % self.getActivity().title_or_id())
def _check_failed(self): """Perform a check whether this instance is failed. This method returns True in two cases: 1. The state of the process instance is `failed` 2. At least one of the work items is failed. """ items_failed = len(self.getWorkItemIds(state="failed")) controller = ILifeCycleController(self) if controller.state == 'failed': return True if items_failed > 0: controller.fail( "Automatically failed because %s work items failed." % items_failed) return True return False
def _create_instance(self): portal = self.portal portal.invokeFactory('DummyContent', 'doc') doc = portal.doc doc.assignProcess(self.test_process) instance = doc.getInstance() f = instance.getField('write_document_assignees') f.set(instance, 'author') ILifeCycleController(instance).start('test') return instance
def test_notifyAssigneesChange(self): self.loginAsPortalOwner() self._import_wf('workflows/cachetest.alf') self._create_test_users() portal = self.portal alf = getToolByName(portal, 'workflow_manager') portal.invokeFactory('DummyContent', 'doc') doc = portal.doc doc.test_assignees = ['hans'] doc.assignProcess(self.test_process) instance = doc.getInstance() ILifeCycleController(instance).start("Los gehts") wi = instance.getWorkItems()[0] self.assertEquals(['Assignee'], alf.getDynamicRolesForWorkItem(wi, 'hans')) self.assertEquals(['ProcessUser'], alf.getDynamicRolesForInstance(instance, 'hans')) doc.test_assignees = ['heinrich'] wi.notifyAssigneesChange() self.assertRaises(KeyError, alf.getDynamicRolesForWorkItem, wi, 'hans') self.assertEquals(['Assignee'], alf.getDynamicRolesForWorkItem(wi, 'heinrich')) self.assertEquals(['ProcessUser'], alf.getDynamicRolesForInstance(instance, 'heinrich')) wi_old = wi assignees = ['Ernie', 'Bert'] doc.test_assignees = assignees wi.complete('complete') wi = instance.getWorkItems()[0] self.assertRaises(KeyError, alf.getDynamicRolesForWorkItem, wi_old, 'heinrich') for assignee in assignees: self.assertEquals(['Assignee'], alf.getDynamicRolesForWorkItem(wi, assignee)) self.assertEquals(['ProcessUser'], alf.getDynamicRolesForInstance(instance, assignee)) self.assertRaises(KeyError, alf.getDynamicRolesForInstance, instance, 'hans') self.assertRaises(KeyError, alf.getDynamicRolesForInstance, instance, 'heinrich') doc.test_assignees = [] wi.complete('complete') wi = instance.getWorkItems()[0] self.assertEquals({}, dict(alf._workitem_role_cache))
def _check_complete(self, workitems): """End instance if only daemons are left. workitems -- list of active workitems (passed for performance reasons) This method is a bit weird, but works. There are two cases: 1. If no active work items are left, we complete the instance. 2. If only daemons are left, we complete those. As a side effect, when the last daemon is completed, we get another trigger for _check_complete and the instance will be completed by case 1. """ if self._creating: # Do not check for completeness while we are still creating work # items. return controller = ILifeCycleController(self) daemons = [ wi for wi in workitems if IDaemonActivity.providedBy(wi.getActivity()) ] items_active = len(workitems) daemons_active = len(daemons) if controller.state != "active": # XXX Missing test # We don't have to check further as we are not active. return if not items_active: # Case 1: No active work items controller.complete( "Automatically completed instance as all work items ended.") if (controller.state == "active" and items_active == daemons_active): # Case 2: Only daemons are active for daemon in daemons: ILifeCycleController(daemon).complete( "Automatically completed daemon.")
def test_grouped_schema(self): self._create_test_users() self._import_wf('workflows/multi_review.alf') portal = self.portal alf = getToolByName(portal, 'workflow_manager') portal.invokeFactory("DummyContent", "doc") doc = portal.doc doc.assignProcess(self.test_process) instance = doc.getInstance() controller = ILifeCycleController(instance) controller.start('test') wis = instance.getWorkItems() self.assertEqual(1, len(wis)) gs = wis[0].getGroupedSchema() act_ids_expected = ['write_document'] act_ids_expected.sort() act_ids_got = [ g.activity_id for g in gs ] act_ids_got.sort() self.assertEquals(act_ids_expected, act_ids_got) self.assertEquals(gs[0].Title(), 'Dokument schreiben')
def notifyWorkItemStateChange(self, workitem): """Check if routes have to be closed.""" # check if workitem is one of our gates if workitem.getId() not in self.generated_workitems: return if workitem.activity_type != "gate": return # check if gate is completed if not ILifeCycleController(workitem).completed: return finished_gate = workitem.activity_id workitems = self.getGeneratedWorkItems() # kill other gates for wi in workitems: if wi.activity_type != "gate": continue if wi.state != "active": continue ILifeCycleController(wi).terminate( "Competing gate '%s' successfully completed." % finished_gate) # kill open routes for wi in workitems: if wi.activity_type == "gate": continue # XXX Do not kill the work item that triggered all this ... # Our assumption is, that the work item is successfull and # either will try to complete in a moment, or may still be # relevant as it was 'successfull'. That's the ignore= flag. killWorkItemRecursively( wi, "Competing route successfully completed at gate '%s'." % finished_gate, ignore=[workitem.completing_work_item]) # complete routing ILifeCycleController(self).complete("Gate %s completed" % workitem.activity_id)
def test_cache(self): zope.component.provideAdapter(LifeCycleControllerFactory, adapts=(WorkitemFake,), provides=ILifeCycleController) portal = self.portal portal.invokeFactory('DummyContent', 'doc1') doc1 = portal.doc1 definition = ProcessDefinitionFake() i = Instance(definition, doc1, 'test') wi = WorkitemFake() ILifeCycleController(wi).state = 'active' i._setObject(wi.id, wi) wi = i._getOb(wi.id) i._update_cache(wi) self.assertEquals([wi], i.getWorkItems()) ILifeCycleController(wi).state = 'complete' i._update_cache(wi) self.assertEquals([], i.getWorkItems()) self.assertEquals([wi], i.getWorkItems('complete'))